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方法注册的中间件可以叫做终端中间件,即该中间件执行完成后不再执行后续的中间件。

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//第一个中间件
app.Run(async (context) =>
{
context.Response.ContentType = "text/plain;charset=utf-8";//防止中文乱码
await context.Response.WriteAsync("第一个中间件输出你好~");
});
//第二个中间件
app.Run(async (context) =>
{
await context.Response.WriteAsync("第二个中间件输出你好~");
});
}

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

2 Use

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

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//第一个中间件
app.Use(async (context, next) =>
{
context.Response.ContentType = "text/plain;charset=utf-8";//防止中文乱码
await context.Response.WriteAsync($"第一个中间件输出你好~{Environment.NewLine}"); await context.Response.WriteAsync($"下一个中间件执行前执行===>{Environment.NewLine}");
await next.Invoke();
await context.Response.WriteAsync($"下一个中间件执行后执行<==={Environment.NewLine}");
});
//第二个中间件
app.Use(async (context,next) =>
{
await context.Response.WriteAsync($"第二个中间件输出你好~{Environment.NewLine}");
});
}

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

3 Map

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

    public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; } // 依赖注入
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
/// <summary>
/// 配置用户分支管道,处理以url以/userinfo开头的请求
/// </summary>
/// <param name="app"></param>
private static void UserinfoConfigure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
await context.Response.WriteAsync($"处理用户业务,{Environment.NewLine}");
await next.Invoke();
});
app.Run(async (context) => { await context.Response.WriteAsync("用户业务处理完成~"); });
}
/// <summary>
/// 配置产品分支管道,处理以url以/product开头的请求
/// </summary>
/// <param name="app"></param>
private static void ProductConfigure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
await context.Response.WriteAsync($"处理产品业务");
await next.Invoke();
});
}
// 配置请求处理管道
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//防止中文乱码
app.Use(async (context,next) =>
{
context.Response.ContentType = "text/plain;charset=utf-8";
await next.Invoke();
}); app.Map("/userinfo", UserinfoConfigure); app.Map("/product", ProductConfigure); app.Run(async context =>
{
await context.Response.WriteAsync("主管道处理其他业务");
});
}
}

  运行程序执行结果如下:

4 MapWhen

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

    public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; } // 依赖注入
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
/// <summary>
/// 配置分支管道,处理以url中有包含/userinfo的请求
/// </summary>
/// <param name="app"></param>
private static void UserinfoConfigure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
await context.Response.WriteAsync($"处理用户业务,{Environment.NewLine}");
await next.Invoke();
});
app.Run(async (context) => { await context.Response.WriteAsync("用户业务处理完成~"); });
}
/// <summary>
/// 配置分支管道,处理以查询参数有name的请求
/// </summary>
/// <param name="app"></param>
private static void HNameConfigure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
await context.Response.WriteAsync($"查询参数包含name,值为:{context.Request.Query["name"]}");
await next.Invoke();
});
}
// 配置请求处理管道
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//防止中文乱码
app.Use(async (context,next) =>
{
context.Response.ContentType = "text/plain;charset=utf-8";
await next.Invoke();
}); app.MapWhen(context => context.Request.Query.ContainsKey("name"), HNameConfigure);
app.MapWhen(context => context.Request.Path.Value.ToString().Contains("/userinfo"), UserinfoConfigure); app.Run(async context =>
{
await context.Response.WriteAsync("主管道处理其他业务");
});
}
}

  程序执行结果如下:

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

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

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

    /// <summary>
/// 自定义的错误处理类
/// </summary>
public class CostomErrorMiddleware
{
private readonly RequestDelegate next;
private readonly ILogger logger;
private IHostingEnvironment environment;
/// <summary>
/// DI,注入logger和环境变量
/// </summary>
/// <param name="next"></param>
/// <param name="logger"></param>
/// <param name="environment"></param>
public CostomErrorMiddleware(RequestDelegate next, ILogger<CostomErrorMiddleware> logger, IHostingEnvironment environment)
{
this.next = next;
this.logger = logger;
this.environment = environment;
}
/// <summary>
/// 实现Invoke方法
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task Invoke(HttpContext context)
{
try
{
await next.Invoke(context);
}
catch (Exception ex)
{
await HandleError(context, ex);
}
}
/// <summary>
/// 错误信息处理方法
/// </summary>
/// <param name="context"></param>
/// <param name="ex"></param>
/// <returns></returns>
private async Task HandleError(HttpContext context, Exception ex)
{
context.Response.StatusCode = ;
context.Response.ContentType = "text/json;charset=utf-8;";
string errorMsg = $"错误消息:{ex.Message}{Environment.NewLine}错误追踪:{ex.StackTrace}";
//无论是否为开发环境都记录错误日志
logger.LogError(errorMsg);
//浏览器在开发环境显示详细错误信息,其他环境隐藏错误信息
if (environment.IsDevelopment())
{
await context.Response.WriteAsync(errorMsg);
}
else
{
await context.Response.WriteAsync("抱歉,服务端出错了");
}
}
}

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

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

nlog.config:

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

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

    [Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private ILogger<ValuesController> _logger;
public ValuesController(ILogger<ValuesController> logger)
{
_logger = logger;
}
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{ return new string[] { "value1", "value2" };
} // GET api/values/5
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
throw new Exception("有一个错误发生了..");
return "value";
}
}

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

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

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

    /// <summary>
/// 扩展方法
/// </summary>
public static class CostomMiddleware
{
public static IApplicationBuilder UseCostomError(this IApplicationBuilder app)
{
return app.UseMiddleware<CostomErrorMiddleware>();
}
}

  然后修改Configure方法即可:

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory factory)
{
//添加nlog
factory.AddNLog();
env.ConfigureNLog("nlog.config");
//使用扩展方法
app.UseCostomError();
app.UseMvc();
}

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

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

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

    /// <summary>
/// 自定义的错误处理过滤器
/// </summary>
public class CustomErrorFilter :Attribute, IExceptionFilter
{
private readonly ILogger _logger;
private IHostingEnvironment _environment; public CustomErrorFilter(ILogger<CustomErrorFilter> logger,IHostingEnvironment environment)
{ _logger = logger;
_environment = environment;
}
public void OnException(ExceptionContext context)
{
Exception ex = context.Exception;
string errorMsg = $"错误消息:{ex.Message}{Environment.NewLine}错误追踪:{ex.StackTrace}";
ContentResult result = new ContentResult
{
ContentType = "text/json;charset=utf-8;",
StatusCode =
};
//无论是否为开发环境都记录错误日志
_logger.LogError(errorMsg);
//浏览器在开发环境显示详细错误信息,其他环境隐藏错误信息
if (_environment.IsDevelopment())
{
result.Content = $"错误消息:{ex.Message}{Environment.NewLine}错误追踪:{ex.StackTrace}";
}
else
{
result.Content = "抱歉,服务端出错了";
}
context.Result = result;
context.ExceptionHandled = true;
}
}

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

    public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
} public IConfiguration Configuration { get; } // 依赖注入
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(
configure =>
{
configure.Filters.Add<CustomErrorFilter>();//全局过滤器,不用添加特性头
}//全局过滤器,不用添加特性头
).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
//services.AddScoped<CustomErrorFilter>();//局部过滤器,需要在Controller/Action添加特性头 [ServiceFilter(typeof(CustomErrorFilter))]
} // 配置管道
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory factory)
{
factory.AddNLog();
env.ConfigureNLog("nlog.config");
app.UseMvc();
}
}

  然后修改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. unity之加载场景

    游戏中的Loading分为:静态Loading和动态Loading. 简单形象的做个比喻: 静态Loading可能就是一张背景图.而动态的Loading就是在读取的同时有一个东西在“转圈”. 1.静态 ...

  2. .Net 基于Memcache集群的分布式Session

    简述 基于Memcache的Session大家都各有各的说法,比方说:当memcached集群发生故障(比如内存溢出)或者维护(比如升级.增加或减少服务器)时,用户会无法登录,或者被踢掉线等等,每种技 ...

  3. liunx安装jdk

    jdk 安装包  https://pan.baidu.com/s/1cKnUQGU2Sk2nsARAzzVAHw [root@localhost ~]# tar -zxvf jdk-8u152-lin ...

  4. 01 Python 基础数据类型

    基础数据类型,有7种类型,存在即合理. 1.int 整数 主要是做运算的 .比如加减乘除,幂,取余  + - * / ** %...2.bool 布尔值 判断真假以及作为条件变量3.str 字符串 存 ...

  5. 利用poi包装一个简单的Excel读取器.一(适配一个Reader并提供readLine方法)

    通常,读文本我们会使用BufferedReader,它装饰或者说管理了InputStreamReader,同时提供readLine()简化了我们对文本行的读取.就像从流水线上获取产品一样,每当取完一件 ...

  6. 【Edu 67】 补题记录

    CF1187D. Subarray Sorting 想要把一个数x换到前面,x一定是小一点的值.由于B串是固定的,A串可调整,我们可以遍历B数组,对于B[i],找到对于在A数组的位子pos,判断1-p ...

  7. Codeforces Round #483 (Div. 2) B. Minesweeper

    题目地址:http://codeforces.com/contest/984/problem/B 题目大意:扫雷游戏,给你一个n*m的地图,如果有炸弹,旁边的八个位置都会+1,问这幅图是不是正确的. ...

  8. android CTS 命令

    > h //help Host:  help: show this message  help all: show the complete tradefed help  exit: grace ...

  9. Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.apache.catalina.connector.CoyoteWriter and no properties discovered to create BeanSerializer

    一.什么是序列化In computer science, in the context of data storage, serialization is the process of transla ...

  10. Spring MVC 配置类 WebMvcConfigurerAdapter

    WebMvcConfigurerAdapter配置类是spring提供的一种配置方式,采用JavaBean的方式替代传统的基于xml的配置来对spring框架进行自定义的配置.因此,在spring b ...