1.中间件的概念

ASP.NET Core的处理流程是一个管道,中间件是组装到应用程序管道中用来处理请求和响应的组件。 每个中间件可以:

  • 选择是否将请求传递给管道中的下一个组件。
  • 可以在调用管道中的下一个组件之前和之后执行业务逻辑。

  中间件是一个请求委托( public delegate Task RequestDelegate(HttpContext context) )的实例,所以中间件的本质就是一个方法,方法的参数是HttpContext,返回Task。传入的HttpContext参数包含了请求和响应信息,我们可以在中间件中对这些信息就行修改。中间件的管道处理流程如下:

  我们知道中间件是配置请求处理管道的组件,那么谁来负责构建管道呢?负责构建管道的角色是ApplicationBuilder。ApplicationBuilder通过Use、Run、Map及MapWhen方法来注册中间件,构建请求管道。我们简单看下这几个方法。

1 Run

  新建一个WebAPI项目,修改StartUp中的Configure方法如下,用Run方法注册的中间件可以叫做终端中间件,即该中间件执行完成后不再执行后续的中间件。

  1. public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  2. {
  3. //第一个中间件
  4. app.Run(async (context) =>
  5. {
  6. context.Response.ContentType = "text/plain;charset=utf-8";//防止中文乱码
  7. await context.Response.WriteAsync("第一个中间件输出你好~");
  8. });
  9. //第二个中间件
  10. app.Run(async (context) =>
  11. {
  12. await context.Response.WriteAsync("第二个中间件输出你好~");
  13. });
  14. }

  运行程序,我们看到只执行了第一个中间件,后边的中间件不会执行。

2 Use

  Use方法的参数是一个委托实例,委托的第一个参数是HttpContext,这是待处理的请求上下文;第二个参数next是下一个中间件,我们可以通过next.Invoke()调用下一个中间件,并且可以在调用下一个中间件之前/之后对HttpContext做一个逻辑处理。

  1. public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  2. {
  3. //第一个中间件
  4. app.Use(async (context, next) =>
  5. {
  6. context.Response.ContentType = "text/plain;charset=utf-8";//防止中文乱码
  7. await context.Response.WriteAsync($"第一个中间件输出你好~{Environment.NewLine}");
  8.  
  9. await context.Response.WriteAsync($"下一个中间件执行前执行===>{Environment.NewLine}");
  10. await next.Invoke();
  11. await context.Response.WriteAsync($"下一个中间件执行后执行<==={Environment.NewLine}");
  12. });
  13. //第二个中间件
  14. app.Use(async (context,next) =>
  15. {
  16. await context.Response.WriteAsync($"第二个中间件输出你好~{Environment.NewLine}");
  17. });
  18. }

  运行程序如下所示。注意如果我们没有调用next.Invoke()方法,会造成管道短路,后续的所有中间件都不再执行。

3 Map

  在业务简单的情况下,使用一个请求处理管道来处理所有的请求就可以了,当业务复杂的时候, 我们可能考虑把不同业务的请求交给不同的管道中处理。 Map 基于给定请求路径的匹配项来创建请求管道分支。 如果请求路径以给定路径开头,则执行分支。看一个栗子,需求是/userinfo开头的请求使用用户分支管道来处理,/product开头的请求使用产品分支管道处理,代码如下:

  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7. public IConfiguration Configuration { get; }
  8.  
  9. // 依赖注入
  10. public void ConfigureServices(IServiceCollection services)
  11. {
  12. services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
  13. }
  14. /// <summary>
  15. /// 配置用户分支管道,处理以url以/userinfo开头的请求
  16. /// </summary>
  17. /// <param name="app"></param>
  18. private static void UserinfoConfigure(IApplicationBuilder app)
  19. {
  20. app.Use(async (context, next) =>
  21. {
  22. await context.Response.WriteAsync($"处理用户业务,{Environment.NewLine}");
  23. await next.Invoke();
  24. });
  25. app.Run(async (context) => { await context.Response.WriteAsync("用户业务处理完成~"); });
  26. }
  27. /// <summary>
  28. /// 配置产品分支管道,处理以url以/product开头的请求
  29. /// </summary>
  30. /// <param name="app"></param>
  31. private static void ProductConfigure(IApplicationBuilder app)
  32. {
  33. app.Use(async (context, next) =>
  34. {
  35. await context.Response.WriteAsync($"处理产品业务");
  36. await next.Invoke();
  37. });
  38. }
  39. // 配置请求处理管道
  40. public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  41. {
  42. //防止中文乱码
  43. app.Use(async (context,next) =>
  44. {
  45. context.Response.ContentType = "text/plain;charset=utf-8";
  46. await next.Invoke();
  47. });
  48.  
  49. app.Map("/userinfo", UserinfoConfigure);
  50.  
  51. app.Map("/product", ProductConfigure);
  52.  
  53. app.Run(async context =>
  54. {
  55. await context.Response.WriteAsync("主管道处理其他业务");
  56. });
  57. }
  58. }

  运行程序执行结果如下:

4 MapWhen

  MapWhen和Map的思想比较相似,MapWhen基于自定义条件来创建请求管道分支,并将请求映射到管道的新分支。看一个栗子就明白了,下边栗子的需求是查询参数包含name的请求交给一个分支管道处理,url包含/userinfo的请求交给用户分支来处理,代码如下:

  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7. public IConfiguration Configuration { get; }
  8.  
  9. // 依赖注入
  10. public void ConfigureServices(IServiceCollection services)
  11. {
  12. services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
  13. }
  14. /// <summary>
  15. /// 配置分支管道,处理以url中有包含/userinfo的请求
  16. /// </summary>
  17. /// <param name="app"></param>
  18. private static void UserinfoConfigure(IApplicationBuilder app)
  19. {
  20. app.Use(async (context, next) =>
  21. {
  22. await context.Response.WriteAsync($"处理用户业务,{Environment.NewLine}");
  23. await next.Invoke();
  24. });
  25. app.Run(async (context) => { await context.Response.WriteAsync("用户业务处理完成~"); });
  26. }
  27. /// <summary>
  28. /// 配置分支管道,处理以查询参数有name的请求
  29. /// </summary>
  30. /// <param name="app"></param>
  31. private static void HNameConfigure(IApplicationBuilder app)
  32. {
  33. app.Use(async (context, next) =>
  34. {
  35. await context.Response.WriteAsync($"查询参数包含name,值为:{context.Request.Query["name"]}");
  36. await next.Invoke();
  37. });
  38. }
  39. // 配置请求处理管道
  40. public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  41. {
  42. //防止中文乱码
  43. app.Use(async (context,next) =>
  44. {
  45. context.Response.ContentType = "text/plain;charset=utf-8";
  46. await next.Invoke();
  47. });
  48.  
  49. app.MapWhen(context => context.Request.Query.ContainsKey("name"), HNameConfigure);
  50. app.MapWhen(context => context.Request.Path.Value.ToString().Contains("/userinfo"), UserinfoConfigure);
  51.  
  52. app.Run(async context =>
  53. {
  54. await context.Response.WriteAsync("主管道处理其他业务");
  55. });
  56. }
  57. }

  程序执行结果如下:

  到这里我们对中间件已经有了一个基本的了解,接下了通过一个异常日志 中间件来了解开发中怎么去使用中间件。

2 使用中间件记录错误日志

  这里使用的日志组件为nlog,首先创建一个WebAPI项目,添加一个自定义日志处理中间件CostomErrorMiddleware,当程序出错时会记录日志,同时开发环境下会把异常的详细信息打印在页面上,非开发环境隐藏详细信息,代码如下:

  1. /// <summary>
  2. /// 自定义的错误处理类
  3. /// </summary>
  4. public class CostomErrorMiddleware
  5. {
  6. private readonly RequestDelegate next;
  7. private readonly ILogger logger;
  8. private IHostingEnvironment environment;
  9. /// <summary>
  10. /// DI,注入logger和环境变量
  11. /// </summary>
  12. /// <param name="next"></param>
  13. /// <param name="logger"></param>
  14. /// <param name="environment"></param>
  15. public CostomErrorMiddleware(RequestDelegate next, ILogger<CostomErrorMiddleware> logger, IHostingEnvironment environment)
  16. {
  17. this.next = next;
  18. this.logger = logger;
  19. this.environment = environment;
  20. }
  21. /// <summary>
  22. /// 实现Invoke方法
  23. /// </summary>
  24. /// <param name="context"></param>
  25. /// <returns></returns>
  26. public async Task Invoke(HttpContext context)
  27. {
  28. try
  29. {
  30. await next.Invoke(context);
  31. }
  32. catch (Exception ex)
  33. {
  34. await HandleError(context, ex);
  35. }
  36. }
  37. /// <summary>
  38. /// 错误信息处理方法
  39. /// </summary>
  40. /// <param name="context"></param>
  41. /// <param name="ex"></param>
  42. /// <returns></returns>
  43. private async Task HandleError(HttpContext context, Exception ex)
  44. {
  45. context.Response.StatusCode = ;
  46. context.Response.ContentType = "text/json;charset=utf-8;";
  47. string errorMsg = $"错误消息:{ex.Message}{Environment.NewLine}错误追踪:{ex.StackTrace}";
  48. //无论是否为开发环境都记录错误日志
  49. logger.LogError(errorMsg);
  50. //浏览器在开发环境显示详细错误信息,其他环境隐藏错误信息
  51. if (environment.IsDevelopment())
  52. {
  53. await context.Response.WriteAsync(errorMsg);
  54. }
  55. else
  56. {
  57. await context.Response.WriteAsync("抱歉,服务端出错了");
  58. }
  59. }
  60. }

  修改StartUp类中的Configure方法如下,注入nlog 需要先安装 NLog.Web.AspNetCore ,使用app.UseMiddleware<CostomErrorMiddleware>()注册我们自定义的中间件,代码如下:

  1. /// 配置请求管道
  2. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory factory)
  3. {
  4. //添加nlog
  5. factory.AddNLog();
  6. env.ConfigureNLog("nlog.config");
  7. //泛型方法添加中间件
  8. app.UseMiddleware<CostomErrorMiddleware>();
  9. app.UseMvc();
  10. }

nlog.config:

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. autoReload="true"
  5. internalLogLevel="Info"
  6. internalLogFile="D:\LogDemoOfWebapi\internal-nlog.txt">
  7. <!-- enable asp.net core layout renderers -->
  8. <extensions>
  9. <add assembly="NLog.Web.AspNetCore"/>
  10. </extensions>
  11. <targets>
  12. <target xsi:type="File" name="errorLog" fileName="D:/logs/AT___${shortdate}.log"
  13. layout="----------------日志记录开始----------------${newline}【日志时间】:${longdate} ${newline}【日志级别】:${level:uppercase=true}${newline}【异常相关信息】${newline}${message}${newline}${newline}${newline}" />
  14. </targets>
  15. <rules>
  16. <logger name="*" minlevel="Error" writeTo="errorLog" />
  17. </rules>
  18. </nlog>

  到这里异常处理中间件就注册完成了,修改ValueController自己制造一个异常来测试一下,代码如下:

  1. [Route("api/[controller]")]
  2. [ApiController]
  3. public class ValuesController : ControllerBase
  4. {
  5. private ILogger<ValuesController> _logger;
  6. public ValuesController(ILogger<ValuesController> logger)
  7. {
  8. _logger = logger;
  9. }
  10. // GET api/values
  11. [HttpGet]
  12. public ActionResult<IEnumerable<string>> Get()
  13. {
  14.  
  15. return new string[] { "value1", "value2" };
  16. }
  17.  
  18. // GET api/values/5
  19. [HttpGet("{id}")]
  20. public ActionResult<string> Get(int id)
  21. {
  22. throw new Exception("有一个错误发生了..");
  23. return "value";
  24. }
  25. }

  运行程序,在开发环境下访问/Values/1,显示结果如下,同时这些错误信息也会通过nlog写入到错误日志中:

  非开发环境下,访问/values/1,显示如下:

  如果我们想使用类似app.UseMvc()这样的形式来使用我们自定义的中间件的话,就需要给ApplicationBuilder添加一个扩展方法,首先添加一个静态类CostomMiddleware,代码如下:

  1. /// <summary>
  2. /// 扩展方法
  3. /// </summary>
  4. public static class CostomMiddleware
  5. {
  6. public static IApplicationBuilder UseCostomError(this IApplicationBuilder app)
  7. {
  8. return app.UseMiddleware<CostomErrorMiddleware>();
  9. }
  10. }

  然后修改Configure方法即可:

  1. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory factory)
  2. {
  3. //添加nlog
  4. factory.AddNLog();
  5. env.ConfigureNLog("nlog.config");
  6. //使用扩展方法
  7. app.UseCostomError();
  8. app.UseMvc();
  9. }

  运行程序后和前边的执行效果是一样的。

3 使用过滤器记录错误日志

  过滤器大家应该都很熟悉,在ASP.NET Core中过滤器的使用没有太大的变化,这里也实现一个使用过滤器记录错误日志的栗子,直接看代码吧,首先创建一个过滤器,代码如下:

  1. /// <summary>
  2. /// 自定义的错误处理过滤器
  3. /// </summary>
  4. public class CustomErrorFilter :Attribute, IExceptionFilter
  5. {
  6. private readonly ILogger _logger;
  7. private IHostingEnvironment _environment;
  8.  
  9. public CustomErrorFilter(ILogger<CustomErrorFilter> logger,IHostingEnvironment environment)
  10. {
  11.  
  12. _logger = logger;
  13. _environment = environment;
  14. }
  15. public void OnException(ExceptionContext context)
  16. {
  17. Exception ex = context.Exception;
  18. string errorMsg = $"错误消息:{ex.Message}{Environment.NewLine}错误追踪:{ex.StackTrace}";
  19. ContentResult result = new ContentResult
  20. {
  21. ContentType = "text/json;charset=utf-8;",
  22. StatusCode =
  23. };
  24. //无论是否为开发环境都记录错误日志
  25. _logger.LogError(errorMsg);
  26. //浏览器在开发环境显示详细错误信息,其他环境隐藏错误信息
  27. if (_environment.IsDevelopment())
  28. {
  29. result.Content = $"错误消息:{ex.Message}{Environment.NewLine}错误追踪:{ex.StackTrace}";
  30. }
  31. else
  32. {
  33. result.Content = "抱歉,服务端出错了";
  34. }
  35. context.Result = result;
  36. context.ExceptionHandled = true;
  37. }
  38. }

  修改StartUp类,注入nlog,配置全局过滤器,代码如下,其中nlog.config和中间件栗子中一样:

  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7.  
  8. public IConfiguration Configuration { get; }
  9.  
  10. // 依赖注入
  11. public void ConfigureServices(IServiceCollection services)
  12. {
  13. services.AddMvc(
  14. configure =>
  15. {
  16. configure.Filters.Add<CustomErrorFilter>();//全局过滤器,不用添加特性头
  17. }//全局过滤器,不用添加特性头
  18. ).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
  19. //services.AddScoped<CustomErrorFilter>();//局部过滤器,需要在Controller/Action添加特性头 [ServiceFilter(typeof(CustomErrorFilter))]
  20. }
  21.  
  22. // 配置管道
  23. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory factory)
  24. {
  25. factory.AddNLog();
  26. env.ConfigureNLog("nlog.config");
  27. app.UseMvc();
  28. }
  29. }

  然后修改ValuesController,设置错误和上边中间件的栗子一样,运行代码访问/values/1时,在开发环境中显示如下,同时错误信息也会写入错误日志中:

  在生产环境中访问/values/1的话,错误详细也会写入错误日志中,浏览器显示如下:

  本文介绍了中间件的基本使用,同时使用中间件和过滤器两种方式实现了异常日志的记录,如果文中有错误的地方希望大家可以指出,我会及时改正。

参考文章

  【1】https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/logging/?view=aspnetcore-3.0

  【2】https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.0

  

.Net Core中间件和过滤器实现错误日志记录的更多相关文章

  1. EF Core使用SQL调用返回其他类型的查询 ASP.NET Core 2.0 使用NLog实现日志记录 CSS 3D transforms cSharp:use Activator.CreateInstance with an Interface? SqlHelper DBHelper C# Thread.Abort方法真的让线程停止了吗? 注意!你的Thread.Abort方法真

    EF Core使用SQL调用返回其他类型的查询   假设你想要 SQL 本身编写,而不使用 LINQ. 需要运行 SQL 查询中返回实体对象之外的内容. 在 EF Core 中,执行该操作的另一种方法 ...

  2. 将错误日志记录在txt文本里

    引言 对于已经部署的系统一旦出错对于我们开发人员来说是比较痛苦的事情,因为我们不能跟踪到错误信息,不能 很快的定位到我们的错误位置在哪,这时候如果能像开发环境一样记录一些堆栈信息就可以了,这时候我们就 ...

  3. PHP错误日志记录:display_errors与log_errors的区别

    我们所做的东西,无论在开发环境还是在生产环境都可能会出现一些问题. 开发环境下,我们会要求错误尽可能详细的呈现出来,错误提示信息越详细越好,越详细越能帮助开发人员确定问题所在并从根本上解决他们. 生产 ...

  4. .Net Core 3.0 使用 Serilog 把日志记录到 SqlServer

    Serilog简介 Serilog是.net中的诊断日志库,可以在所有的.net平台上面运行.Serilog支持结构化日志记录,对复杂.分布式.异步应用程序的支持非常出色.Serilog可以通过插件的 ...

  5. .net错误日志记录(log4)

    Log4 web.config <!--这段放前面--> <configSections> <section name="log4net" type= ...

  6. PHP错误日志记录文件位置确定

    1.确定web服务器 ( IIS, APACHE, NGINX 等) 以哪一种方式支持PHP,通常是有下面2种方式 通过模块加载的方式, 适用于apache 通过 CGI/fastCGI 模式, 该模 ...

  7. 利用Image对象,建立Javascript前台错误日志记录

    手记:摘自Javascript高级程序设计(第三版),利用Image对象发送请求,确实有很多优点,有时候这也许就是一个创意点,再次做个笔记供自己和大家参考. 原文: 开发 Web 应用程序过程中的一种 ...

  8. wpf 全局异常捕捉+错误日志记录+自动创建桌面图标

    /// /// 创建桌面图标 /// public static void CreateShortcutOnDesktop(string LnkName) { String shortcutPath ...

  9. asp.net Web项目中使用Log4Net进行错误日志记录

      使用log4net可以很方便地为应用添加日志功能.应用Log4net,开发者可以很精确地控制日志信息的输出,减少了多余信息,提高了日志记录性能.同时,通过外部配置文件,用户可以不用重新编译程序就能 ...

随机推荐

  1. HDU 6363

    题意略. 思路: 这里有两个结论需要注意: 1.gcd(a ^ x - 1,a ^ y - 1) = a ^ gcd(x,y) - 1 2.gcd(fib[x],fib[y]) = fib[gcd(x ...

  2. switch语句(上)(转载)

    switch语句是C#中常用的跳转语句,可以根据一个参数的不同取值执行不同的代码.switch语句可以具备多个分支,也就是说,根据参数的N种取值,可以跳转到N个代码段去运行.这不同于if语句,一条单独 ...

  3. OpenCvSharp 通过特征点匹配图片

    现在的手游基本都是重复操作,一个动作要等好久,结束之后继续另一个动作.很麻烦,所以动起了自己写一个游戏辅助的心思. 这个辅助本身没什么难度,就是通过不断的截图,然后从这个截图中找出预先截好的能代表相应 ...

  4. P2774 方格取数问题 网络最大流 割

    P2774 方格取数问题:https://www.luogu.org/problemnew/show/P2774 题意: 给定一个矩阵,取出不相邻的数字,使得数字的和最大. 思路: 可以把方格分成两个 ...

  5. BZOJ 1935: [Shoi2007]Tree 园丁的烦恼 +CDQ分治

    1935: [Shoi2007]Tree 园丁的烦恼 参考与学习:https://www.cnblogs.com/mlystdcall/p/6219421.html 题意 在一个二维平面中有n颗树,有 ...

  6. 牛客多校第五场 E room 二分图匹配 KM算法模板

    链接:https://www.nowcoder.com/acm/contest/143/E来源:牛客网 Nowcoder University has 4n students and n dormit ...

  7. 迷宫问题 POJ - 3984 [kuangbin带你飞]专题一 简单搜索

    定义一个二维数组: int maze[5][5] = { 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, ...

  8. 用C#实现的几种常用数据校验方法整理(CRC校验;LRC校验;BCC校验;累加和校验)

    CRC即循环冗余校验码(Cyclic Redundancy Check):是数据通信领域中最常用的一种查错校验码,其特征是信息字段和校验字段的长度可以任意选定.循环冗余检查(CRC)是一种数据传输检错 ...

  9. 深度递归必须知道的尾调用(Lambda)

    引导语 本文从一个递归栈溢出说起,像大家介绍一下如何使用尾调用解决这个问题,以及尾调用的原理,最后还提供一个解决方案的工具类,大家可以在工作中放心用起来. 递归-发现栈溢出 现在我们有个需求,需要计算 ...

  10. List集合的排序

    最近在写需求时,将某张表中的短信信息拿出,sql写完后,取出来后的结构是List<Map>,在进行到某一步时需要将这个List<Map>进行逆序排序, 当时第一想法便是写一个f ...