乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 浅析ASP.NET Core中间件,掌控请求处理过程中的关键

什么是中间件

中间件是一种计算机软件,为软件应用程序提供操作系统以外的服务。它可以被描述为"软件胶水"。

image

中间件使软件开发者更容易实现通信和输入/输出,因此他们可以专注于其应用程序的具体目的。它在20世纪80年代得到了普及,作为解决如何将较新的应用程序与较旧的遗留系统联系起来的问题,尽管这个术语从1968年就开始使用了。

中间件的类型

关于中间件的分类,存在许多定义,这些定义要么是关于它的使用领域,要么是关于它所服务的应用模块。在最近的书目中,中间件的主要分类如下:。

  • 事务性的(Transactional)。处理多个同步/异步交易,作为分布式系统(如银行交易或信用卡支付)的相关请求集群。
  • 面向消息的(Message-oriented)。消息队列和消息传递架构,支持同步/异步通信。
  • 程序性的(Procedural)。远程和本地架构,连接、传递和检索异步系统通信的软件响应,如调用操作。
  • 面向对象(Object-oriented)。与程序性中间件相似,然而,这种类型的中间件包含了面向对象的编程设计原则。从分析上看,其软件组件包含了对象引用、异常和通过分布式对象请求的属性继承。

什么是ASP.NET Core中间件

中间件(Middleware)是一种装配到应用管道以处理请求和响应的软件。

每个组件

  • 选择是否将请求传递到管道中的下一个组件
  • 可在管道中的下一个组件前后执行工作

前世今生

Asp.Net Core其实就是仍然基于.Net Full Framework(最低要求Framework 4.6.2)的项目, 但同时保留了.Net Core一些新的设置理念,比如Asp.Net Core默认使用Kestrel作为Http请求的监听器,而不是使用原来庞大的Https.sys。Kestrel不仅仅是微软下一代的跨平台Http请求监听器,同时还提供了比Https.sys更轻量级以及更快速的Http请求处理。

另除此之外,Asp.Net Core与原来的Web设计另一个最大的区别在于Asp.Net Core(及.Net Core)完全抛弃了原来的使用管道模式来接收以及处理HttpRequest。在Asp.Net Core中允许处理中间件(Middleware)来对所有的HttpRequest来进行请求,当请求被接收到时,Asp.Net Core会调用注册的中间件(按照注册的顺序)对HttpRequest进行处理。这样做相比与原来使用HttpApplication的管道方式而言,其优势在于完全由开发人员决定HttpRequest需要执行怎么样的处理,没有多余的其他步骤。而原来的方式既使开发人员不希望对一个HttpRequest进行任何处理,但HttpApplication仍然会按照管道的设置依次创建HttpModel -> 激活HttpHandler -> 处理Session等。

据.Net Core团队给出来的性能测试数据来看,Asp.Net Core(.Net Core)相比与原来的Web(.Net framework 4.6)程序性能提升了2300%.

而.Net Core其实就是保留了上面所说的优势的同时支持跨平台运行。.Net Core的系统是可以真正运行在除Windows以外的其他平台的。轻量级、跨平台、模块化是.Net Core整体的设计理念,同时也是微软产品理念转变的一个体现。.Net Core虽然有千般好,但是我们当前仍然没有直接使用它,因为它现在有一个致使的“缺陷”那就是生态环境,由于.Net Core的API已经完全重写,虽然当前已经提供了.Net framework 90%以上的API,但是仍然会造成一些开发上的不便,当然这还不是最大的问题,最大的问题在于一些第三方Nuget包仍然不支持.Net Core。这样就会造成一些项目无法直接迁移或是迁移成本太高的问题。

工作原理

ASP.NET Core请求管道包含一系列请求委托,依次调用。

每个委托均可在下一个委托前后执行操作。应尽早在管道中调用异常处理委托,这样它们就能捕获在管道的后期阶段发生的异常。

尽可能简单的ASP.NET Core应用设置了处理所有请求的单个请求委托。这种情况不包括实际请求管道。调用单个匿名函数以响应每个HTTP请求。

下图演示了这一概念,沿黑色箭头执行。

image

如果把上诉图顺时针90度旋转,我们会发现,它像一个俄罗斯套娃。

image

其中最早的中间件Middleware1看起来是最大的套娃,然后往下去看,每一个套娃一个个套进去越来越小,每个套娃的处理过程我们看到,有logic和next,next就表示后面的所有的套娃的一个委托,每一层每一层套下去的话,我们可以在任意的中间件来决定,在后面的中间件之前执行什么,或者说在所有中间件执行完执行什么

image

从图中可以看到,RequestDelegate携带着HttpContext一路经过各种ServerHosting等,最终到达了由IApplicationBuilder构建出来的Applicationpipeline这一管道区域,然后再经过各种中间件处理,最终构建出来了我们的Response,而我们的工具箱也正是在这个过程中变得“饱满”起来。

有一个需要知道的知识点就是,中间件是怎么样添加或者叫注册到管道中的呢?又是如何被应用起来的呢?

上面的图可以看到,橙色区域的Applicationpipeline是由IApplicationBuilder构建起来的。也就是说我们可以在IApplicationBuilder做点什么东西来添加我们的中间件。是的IApplicationBuilder暴露出来了一个IApplicationBuilderUse(Func<RequestDelegate,RequestDelegate>middleware);方法来让我们注册中间件,也就是说位于Startup.cs文件中的Configure方法。

那么又是怎么样应用起来的呢?IApplicationBuilderHosting中有一个IApplicationBuilderFactory的对象,Hosting通过这个Factory创建之后就会传递到了HostingApplication对象中,最后由IWebHost对象调用IServer对象的Start同时把HostingApplication传递进去来最终启动服务端。

核心对象

  • IApplicationBuilder
  • RequestDelegate,处理整个请求的委托
  • HttpContext
  • HttpRequest
  • HttpResponse
  • IFeatureCollection

先来看IApplicationBuilder的定义

//
// 摘要:
//     Defines a class that provides the mechanisms to configure an application's request
//     pipeline.
public interface IApplicationBuilder
{
    //
    // 摘要:
    //     Gets or sets the System.IServiceProvider that provides access to the application's
    //     service container.
    IServiceProvider ApplicationServices { get; set; }
    //
    // 摘要:
    //     Gets a key/value collection that can be used to share data between middleware.
    IDictionary<string, object> Properties { get; }
    //
    // 摘要:
    //     Gets the set of HTTP features the application's server provides.
    IFeatureCollection ServerFeatures { get; }

    //
    // 摘要:
    //     Builds the delegate used by this application to process HTTP requests.
    //
    // 返回结果:
    //     The request handling delegate.
    RequestDelegate Build();
    //
    // 摘要:
    //     Creates a new Microsoft.AspNetCore.Builder.IApplicationBuilder that shares the
    //     Microsoft.AspNetCore.Builder.IApplicationBuilder.Properties of this Microsoft.AspNetCore.Builder.IApplicationBuilder.
    //
    // 返回结果:
    //     The new Microsoft.AspNetCore.Builder.IApplicationBuilder.
    IApplicationBuilder New();
    //
    // 摘要:
    //     Adds a middleware delegate to the application's request pipeline.
    //
    // 参数:
    //   middleware:
    //     The middleware delegate.
    //
    // 返回结果:
    //     The Microsoft.AspNetCore.Builder.IApplicationBuilder.
    IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
}

它有一个Use方法,可以让我们去注册中间件。每一个委托的入参也是一个委托。

它还有一个Build方法,可以把所有的中间件串起来之后合并成一个委托方法。

再来看RequestDelegate的定义

//
// 摘要:
//     A function that can process an HTTP request.
//
// 参数:
//   context:
//     The Microsoft.AspNetCore.Http.HttpContext for the request.
//
// 返回结果:
//     A task that represents the completion of request processing.
public delegate Task RequestDelegate(HttpContext context);

这个委托的入参就是一个HttpContext,所有的委托实际上都是对HttpContext的处理。

中间件顺序

image

ASP.NET Core MVC和Razor Pages应用的完整请求处理管道

image

内置中间件

中间件 描述 顺序
身份验证(Authentication) 提供身份验证支持。 在需要HttpContext.User之前。OAuth回叫的终端。
授权(Authorization) 提供身份验证支持。 紧接在身份验证中间件之后。
Cookie策略(Cookie Policy) 跟踪用户是否同意存储个人信息,并强制实施cookie字段(如secure和SameSite)的最低标准。 在发出cookie的中间件之前。示例:身份验证、会话、MVC(TempData)。
CORS 配置跨域资源共享。 在使用CORS的组件之前。由于此错误,UseCors当前必须在UseResponseCaching之前运行。
DeveloperExceptionPage 生成一个页面,其中包含的错误信息仅适用于开发环境。 在生成错误的组件之前。对于开发环境,项目模板会自动将此中间件注册为管道中的第一个中间件。
诊断(Diagnostics) 提供新应用的开发人员异常页、异常处理、状态代码页和默认网页的几个单独的中间件。 在生成错误的组件之前。异常终端或为新应用提供默认网页的终端。
转接头(Forwarded Headers) 将代理标头转发到当前请求。 在使用已更新字段的组件之前。示例:方案、主机、客户端IP、方法。
运行状况检查(Health Check) 检查ASP.NETCore应用及其依赖项的运行状况,如检查数据库可用性。 如果请求与运行状况检查终结点匹配,则为终端。
标头传播(Header Propagation) 将HTTP标头从传入的请求传播到传出的HTTP客户端请求中。
HTTP日志记录(HTTP Logging) 记录HTTP请求和响应。 中间件管道的开头。
HTTP方法重写(HTTP Method Override) 允许传入POST请求重写方法。 在使用已更新方法的组件之前。
HTTPS重定向(HTTPS Redirection) 将所有HTTP请求重定向到HTTPS。 在使用URL的组件之前。
HTTP严格传输安全性(HSTS,HTTP Strict Transport Security) 添加特殊响应标头的安全增强中间件。 在发送响应之前,修改请求的组件之后。示例:转接头、URL重写。
MVC 用MVC/RazorPages处理请求。 如果请求与路由匹配,则为终端。
OWIN 与基于OWIN的应用、服务器和中间件进行互操作。 如果OWIN中间件处理完请求,则为终端。
请求解压缩(Request Decompression) 提供对解压缩请求的支持。 在读取请求正文的组件之前。
响应缓存(Response Caching) 提供对缓存响应的支持。 在需要缓存的组件之前。UseCORS必须在UseResponseCaching之前。
响应压缩(Response Compression) 提供对压缩响应的支持。 在需要压缩的组件之前。
请求本地化(Request Localization) 提供本地化支持。 在对本地化敏感的组件之前。使用RouteDataRequestCultureProvider时,必须在路由中间件之后显示。
终结点路由(Endpoint Routing) 定义和约束请求路由。 用于匹配路由的终端。
SPA 通过返回单页应用程序(SPA)的默认页面,在中间件链中处理来自这个点的所有请求 在链中处于靠后位置,因此其他服务于静态文件、MVC操作等内容的中间件占据优先位置。
会话(Session) 提供对管理用户会话的支持。 在需要会话的组件之前。
静态文件(Static Files) 为提供静态文件和目录浏览提供支持。 如果请求与文件匹配,则为终端。
URL重写(URL Rewrite) 提供对重写URL和重定向请求的支持。 在使用URL的组件之前。
W3CLogging 以W3C扩展日志文件格式生成服务器访问日志。 中间件管道的开头。
WebSockets 启用WebSockets协议。 在接受WebSocket请求所需的组件之前。

自定义中间件

中间件是一种装配到应用管道以处理请求和响应的软件。 ASP.NET Core提供了一组丰富的内置中间件组件,但在某些情况下,你可能需要写入自定义中间件。

使用中间件

https://github.com/TaylorShi/HelloMiddleware

常用用法

  • Run
  • Use
  • Map
  • MapWhen

基本使用(Use)

Startup.csConfigure方法中来注册中间件。

这里先介绍Use的方式,看下定义

//
// 摘要:
//     Extension methods for adding middleware.
public static class UseExtensions
{
    //
    // 摘要:
    //     Adds a middleware delegate defined in-line to the application's request pipeline.
    //
    // 参数:
    //   app:
    //     The Microsoft.AspNetCore.Builder.IApplicationBuilder instance.
    //
    //   middleware:
    //     A function that handles the request or calls the given next function.
    //
    // 返回结果:
    //     The Microsoft.AspNetCore.Builder.IApplicationBuilder instance.
    public static IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, Func<Task>, Task> middleware);
}

自带中间件注册示例

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

从上诉讲到的图来说,中间件注册的顺序是和执行顺序是存在关系的,最早注册的中间件权力最大,它可以越早的发生作用。

除了上诉自带的内置中间件,我们还可以通过Use来注册我们自定义的中间件逻辑。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.Use(async (httpcontext, next) =>
    {
        await httpcontext.Response.WriteAsync("Hello Middleware");
    });

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

运行输出

image

我们看到它只是输出了我们定义的值,但是没有执行后续的动作了,因为我们在这个中间里面没有让它继续执行next的操作。

如果要让它执行后续的中间件,那么这里我们需要增加逻辑。

app.Use(async (httpcontext, next) =>
{
    await httpcontext.Response.WriteAsync("Hello Middleware");
    await next();
});

再次运行,虽然往后执行了,但是会报错,因为后面的中间件修改了Header的值。

image

System.InvalidOperationException: Headers are read-only, response has already started.
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.Microsoft.AspNetCore.Http.IHeaderDictionary.set_Item(String key, StringValues value)
   at Microsoft.AspNetCore.Http.DefaultHttpResponse.set_ContentType(String value)
   at Microsoft.AspNetCore.Mvc.Formatters.OutputFormatter.WriteResponseHeaders(OutputFormatterWriteContext context)
   at Microsoft.AspNetCore.Mvc.Formatters.TextOutputFormatter.WriteAsync(OutputFormatterWriteContext context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsyncCore(ActionContext context, ObjectResult result, Type objectType, Object value)

我们可以调整下顺序,先让后面的执行,再来执行我们这个逻辑。

app.Use(async (httpcontext, next) =>
{
    await next();
    await httpcontext.Response.WriteAsync("Hello Middleware");
});

这时候就顺利执行了,而且会发现,Hello Middleware跟在正常输出结果的后面了。

image

基本使用(Map)

Map的用法是可以针对特定路径来注册中间件逻辑的。

先看下Map扩展的定义

//
// 摘要:
//     Extension methods for the Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.
public static class MapExtensions
{
    //
    // 摘要:
    //     Branches the request pipeline based on matches of the given request path. If
    //     the request path starts with the given path, the branch is executed.
    //
    // 参数:
    //   app:
    //     The Microsoft.AspNetCore.Builder.IApplicationBuilder instance.
    //
    //   pathMatch:
    //     The request path to match.
    //
    //   configuration:
    //     The branch to take for positive path matches.
    //
    // 返回结果:
    //     The Microsoft.AspNetCore.Builder.IApplicationBuilder instance.
    public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration);
}

具体使用

app.Map("/abc", applicationBuilder =>
{
    applicationBuilder.Use(async (httpcontext, next) =>
    {
        await next();
        await httpcontext.Response.WriteAsync("Hello Middleware");
    });
});

执行结果

image

可以看到在地址/abc的时候成功执行了我们定义的中间件逻辑。

基本使用(MapWhen)

当我们的判断逻辑不只是简单的路径的时候,我们可以使用MapWhen,它支持判断逻辑是一个委托。

app.MapWhen(httpContext =>
{
    return httpContext.Request.Query.Keys.Contains("abc");
}, applicationBuilder =>
{
    applicationBuilder.Run(async httpcontext =>
    {
        await httpcontext.Response.WriteAsync("Hello Middleware");
    });
});

这里当请求字符串里面包括abc的Key的时候,通过IApplicationBuilderRun方法直接执行输出。和之前的Use相比,Run就是中间件执行的末端。

image

自定义中间件

先定义一个带有Invoke或者InvokeAsync方法的中间件类。

internal class TeslaMiddleware
{
    readonly RequestDelegate _next;
    readonly ILogger _logger;

    public TeslaMiddleware(RequestDelegate next, ILogger<TeslaMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        using (_logger.BeginScope("TraceIdentifier: {TraceIdentifier}", httpContext.TraceIdentifier))
        {
            _logger.LogDebug("Start Action");

            await _next(httpContext);

            _logger.LogDebug("End Action");
        }
    }
}

这里定义了一个RequestDelegate委托来表示衔接在这个中间件之后的中间件,同时,我们通过日志框架在这前后打印了下日志。

这个我们还通过配置文件将这个代码的日志等级降低到Debug,以便可以看到这个日志。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Warning"
    },
    "Console": {
      "LogLevel": {
        "demoForMiddleware31.Middlewares.TeslaMiddleware": "Debug"
      }
    }
  },
  "AllowedHosts": "*"
}

运行结果如下

image

我们看到,这时候Controller还是继续成功执行的。

image

因为这里await _next(httpContext)成功的执行了后面的中间件及逻辑。

如果我们注释掉它,那么后面的Controller逻辑就不会被执行。

image

这样就可以实现一个断路器。

注意事项

  • 中间件注入的顺序十分重要,某些中间件起到的是断路器的作用,某些中间件会做一些请求内容的处理。
  • 应用程序一旦开始向Response.Write时,后续的中间件就不能再去操作它的Header了,否则会抛出异常。

判断是否开始向Response输出内容的方式: Response.HasStarted

public async Task InvokeAsync(HttpContext httpContext)
{
    using (_logger.BeginScope("TraceIdentifier: {TraceIdentifier}", httpContext.TraceIdentifier))
    {
        _logger.LogDebug("Start Action");

        await _next(httpContext);

        if (!httpContext.Response.HasStarted)
        {
            await httpContext.Response.WriteAsync("Hello Middleware");
        }

        _logger.LogDebug("End Action");
    }
}

异常处理中间件(UseExceptionHandler)

常见处理异常的方式

ASP.NET Core给我们提供了四种方式

  • 异常处理页
  • 异常处理匿名委托方法
  • 异常处理过滤器(IExceptionFilter)
  • ExceptionFilterAtttribute

开发环境下的异常处理页(UseDeveloperExceptionPage)

默认在Startup.csConfigure方法中,就有一个开发环境使用的UseDeveloperExceptionPage中间件注册。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

主动报一个错

[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
    throw new Exception("Has Error");
    var rng = new Random();
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
    .ToArray();
}

image

这个错误页面会输出很多信息。

自定义错误页(UseExceptionHandler)

我们可以通过UseExceptionHandler并且指定一个错误页来展示我们要展示的错误信息。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    //if (env.IsDevelopment())
    //{
    //    app.UseDeveloperExceptionPage();
    //}

    app.UseExceptionHandler("/error");

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

这里我们需要配合做一个Error页面出来。

先在ConfigureServices里面添加下MVC的服务。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddMvc();
}

接下来我们新增一个Views-Error-Index.cshtml界面来承载。

image

@model demoForMiddleware231.Exceptions.IKnowException
@{
    ViewData["Title"] = "Index";
}

<h1>错误信息</h1>

<div>Message:<label>@Model.Message</label></div>
<div>ErrorCode:<label>@Model.ErrorCode</label></div>

接下来,看看我们定义的IKnowExceptionKnowException定义。

public interface IKnowException
{
    public string Message { get; }

    public int ErrorCode { get; }

    public object[] ErrorData { get; }
}
public class KnowException : IKnowException
{
    public string Message { get; private set; }

    public int ErrorCode { get; private set; }

    public object[] ErrorData { get; private set; }

    public readonly static IKnowException Unknown = new KnowException { Message = "未知错误", ErrorCode = 9999 };

    public static IKnowException FromKnowException(IKnowException knowException)
    {
        return new KnowException { Message = knowException.Message, ErrorCode = knowException.ErrorCode, ErrorData = knowException.ErrorData };
    }
}

接下来新建一个ErrorController来拼装界面和数据。

public class ErrorController : Controller
{
    [Route("/error")]
    public IActionResult Index()
    {
        var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
        var ex = exceptionHandlerPathFeature?.Error;
        IKnowException knowException = ex as IKnowException;
        if(knowException == null)
        {
            var logger = HttpContext.RequestServices.GetService<ILogger<TeslaExceptionFilterAtttribute>>();
            logger.LogError(ex, ex.Message);
            knowException = KnowException.Unknown;
        }
        else
        {
            knowException = KnowException.FromKnowException(knowException);
        }
        return View(knowException);
    }
}

通过HttpContext.Features方法我们可以得到当前上下文的异常信息,如果这个异常符合我们定义的已知异常类型,那么就把它转换为已知异常类型对象。

如果这个异常不符合我们已知异常类型,那么通过日志记录这个异常,并且把异常类型设置为指定的未知类型异常。

我们来运行看看实际效果如何

image

很好,由于我们主动报的异常并不是已知格式的异常,所以它会显示未知异常的类型。

自定义错误输出(UseExceptionHandler)

其次,我们还可以使用代理方法的方式处理异常,并且以Json的格式进行输出。

之前我们提到,在中间件这里我们可以使用Run模式进行断路,借助UseExceptionHandler,我们也可以实现在发生异常的时候,直接断路返回指定格式的异常信息,这种方式更适合接口类型的服务。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseExceptionHandler(applicationBuilder =>
    {
        applicationBuilder.Run(async httpContext =>
        {
            var exceptionHandlerPathFeature = httpContext.Features.Get<IExceptionHandlerPathFeature>();
            var ex = exceptionHandlerPathFeature?.Error;
            IKnowException knowException = ex as IKnowException;
            if (knowException == null)
            {
                var logger = httpContext.RequestServices.GetService<ILogger<TeslaExceptionFilterAtttribute>>();
                logger.LogError(ex, ex.Message);
                knowException = KnowException.Unknown;
                httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
            }
            else
            {
                knowException = KnowException.FromKnowException(knowException);
                httpContext.Response.StatusCode = StatusCodes.Status200OK;
            }

            var jsonOptions = httpContext.RequestServices.GetService<IOptions<JsonOptions>>();
            httpContext.Response.ContentType = "application/json";
            await httpContext.Response.WriteAsync(JsonSerializer.Serialize(knowException, jsonOptions.Value.JsonSerializerOptions));
        });
    });

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

这里我们设计了如果是已知异常,我们还是输出200,如果是未知异常,那就输出500;

image

监控系统会对响应码进行识别,当监控系统发现比较多的500,监控系统会认为服务处于不可用状态。

自定义异常过滤器(ExceptionFilter)

自定义异常过滤器(ExceptionFilter)可以作用于MVC框架体系内的,而不是在中间件的早期去发生作用。

继承IExceptionFilter自定义异常过滤器TeslaExceptionFilter

public class TeslaExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        IKnowException knowException = context.Exception as IKnowException;
        if (knowException == null)
        {
            var logger = context.HttpContext.RequestServices.GetService<ILogger<TeslaExceptionFilterAtttribute>>();
            logger.LogError(context.Exception, context.Exception.Message);
            knowException = KnowException.Unknown;
            context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
        }
        else
        {
            knowException = KnowException.FromKnowException(knowException);
            context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
        }
        context.Result = new JsonResult(knowException)
        {
            ContentType = "application/json"
        };
    }
}

和之前的处理逻辑一样,遇到已知错误就直接输出,不能识别的错误就标记为未知错误。

ConfigureServices这里把它添加进来。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddMvc(mvcOptions =>
    {
        mvcOptions.Filters.Add<TeslaExceptionFilter>();
    }).AddJsonOptions(jsonOptions =>
    {
        jsonOptions.JsonSerializerOptions.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
    });
}

运行后产生的效果和之前自定义错误输出一样。

image

自定义异常属性(ExceptionFilterAttribute)

自定义一个TeslaExceptionFilterAtttribute,继承自ExceptionFilterAttribute,让它可以处理异常的逻辑。

public class TeslaExceptionFilterAtttribute : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        IKnowException knowException = context.Exception as IKnowException;
        if(knowException == null)
        {
            var logger = context.HttpContext.RequestServices.GetService<ILogger<TeslaExceptionFilterAtttribute>>();
            logger.LogError(context.Exception, context.Exception.Message);
            knowException = KnowException.Unknown;
            context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
        }
        else
        {
            knowException = KnowException.FromKnowException(knowException);
            context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
        }
        context.Result = new JsonResult(knowException)
        {
            ContentType = "application/json"
        };
    }
}

在MVC这里我们暂时不需要添加它

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddMvc(mvcOptions =>
    {

    }).AddJsonOptions(jsonOptions =>
    {
        jsonOptions.JsonSerializerOptions.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
    });
}

然后我们在需要进行这个异常处理逻辑的Controller上增加它。

[ApiController]
[Route("[controller]")]
[TeslaExceptionFilterAtttribute]
public class WeatherForecastController : ControllerBase
{

}

这样跑出来的结果和自定义异常过滤器的是一样的。

image

我们看到其实ExceptionFilterAttribute也是继承了IExceptionFilter,所以它可以注册为全局的。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddMvc(mvcOptions =>
    {
        mvcOptions.Filters.Add<TeslaExceptionFilterAtttribute>();
    }).AddJsonOptions(jsonOptions =>
    {
        jsonOptions.JsonSerializerOptions.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
    });
}

这样就可以移除之前Controller那个Atttribute了,因为它已经是全局过滤器。

image

总结异常处理技巧

  • 用特定的异常类或者接口来表示业务逻辑异常
  • 为业务逻辑异常定义全局错误码
  • 为未知异常定义特定的输出信息和错误码
  • 对于已知业务逻辑异常响应HTTP 200(监控系统友好)
  • 对于未预见的异常响应HTTP 500
  • 为所有的异常记录详细的日志

静态文件中间件(UseStaticFiles)

能力

  • 支持指定相对路径
  • 支持目录浏览
  • 支持设置默认文档
  • 支持映射多目录

启用静态文件(UseStaticFiles)

创建一个约定名为wwwroot的文件夹目录,创建之后,它图标会变化成一个网络图标。

image

Startup.csConfigure方法中引入静态文件中间件UseStaticFiles

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

这时候,我们就可以访问wwwroot下面所有静态文件了。

我们弄一点静态文件丢里面

image

启动后,访问https://localhost:5001/Index.html

image

启用默认页面(UseDefaultFiles)

上面,我们发现,如果不指定具体HTML文件路径,它不会自动去找Index.html

image

我们可以使用UseDefaultFiles

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseDefaultFiles();

    app.UseStaticFiles();

根据它的定义,也可以传值进去。

public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, string requestPath);

image

启用目录浏览(UseDirectoryBrowser)

我们可以通过UseDirectoryBrowser来支持。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseDirectoryBrowser();

    app.UseStaticFiles();

同时在ConfigureServices方法中去注册DirectoryBrowser相关的服务。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddDirectoryBrowser();
}

image

自定义静态文件目录

我们还可以添加对非wwwroot目录的静态文件的支持。

image

我们新建一个名为Assets的文件夹,并且内置一个List.html,然后我们在Configure方法中追加对这个文件夹的物理文件支持。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseStaticFiles();

    app.UseStaticFiles(new StaticFileOptions
    {
        FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "Assets"))
    });

接下来我们去请求这个List.html,它会先去wwwroot目录寻找,如果找不到就继续找我们当前追加的这个目录。

image

并且我们还可以让这个目录,配置单独的访问地址,这里可以通过RequestPath来进行配置。

app.UseStaticFiles(new StaticFileOptions
{
    RequestPath = "/Assets",
    FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "Assets"))
});

image

实现前端HTML5 History路由模式的支持

要实现一个,除了我们定义的api接口,其他的地址访问都自动导向index.html去。

首先我们改造下默认的Controller的Route地址,让它携带一个特征/api

[ApiController]
[Route("api/[controller]")]
public class WeatherForecastController : ControllerBase
{

接下来我们使用MapWhen的方法来做个排除法

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.MapWhen(httpContext =>
    {
        return !httpContext.Request.Path.Value.StartsWith("/api");
    }, applicationBuilder =>
    {
        var rewriteOptions = new RewriteOptions();
        rewriteOptions.AddRewrite(".*", "/index.html", true);

        applicationBuilder.UseRewriter(rewriteOptions);
        applicationBuilder.UseStaticFiles();
    });

这里当请求的地址不是以/api开头的,我们都通过UseRewriter重写路由的中间件把请求地址给重写了,重写之后,我们再使用静态中间件。

image

image

image

除了使用UseRewriter中间件,我们还可以采用断路器的方式。

const int BufferSize = 64 * 1024;

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.MapWhen(httpContext =>
    {
        return !httpContext.Request.Path.Value.StartsWith("/api");
    }, applicationBuilder =>
    {
        applicationBuilder.Run(async httpContext =>
        {
            var indexFile = env.WebRootFileProvider.GetFileInfo("index.html");
            httpContext.Response.ContentType = "text/html";
            using (var fileStream = new FileStream(indexFile.PhysicalPath, FileMode.Open, FileAccess.Read))
            {
                await StreamCopyOperation.CopyToAsync(fileStream, httpContext.Response.Body, null, BufferSize, httpContext.RequestAborted);
            }
        });
    });

也可以达到同样的效果,但是这种和前面的区别是,这种无法利用Http的缓存效果。

image

参考

posted @ 2022-10-10 00:05  TaylorShi  阅读(259)  评论(0编辑  收藏  举报