ASP.NET Core错误处理中间件[4]: 响应状态码页面
StatusCodePagesMiddleware中间件与ExceptionHandlerMiddleware中间件类似,它们都是在后续请求处理过程中“出错”的情况下利用一个错误处理器来接收针对当前请求的处理。它们之间的差异在于对“错误”的认定上:ExceptionHandlerMiddleware中间件所谓的错误就是抛出异常;StatusCodePagesMiddleware中间件则将400~599的响应状态码视为错误。更多关于ASP.NET Core的文章请点这里]
目录
一、StatusCodePagesMiddleware
二、阻止处理异常
三、UseStatusCodePages
四、UseStatusCodePagesWithRedirects
五、UseStatusCodePagesWithReExecute
一、StatusCodePagesMiddleware
如下面的代码片段所示,StatusCodePagesMiddleware中间件也采用“标准”的定义方式,针对它的配置选项通过一个对应的对象以Options模式的形式提供给它。
- public class StatusCodePagesMiddleware
- {
- public StatusCodePagesMiddleware(RequestDelegate next, IOptions<StatusCodePagesOptions> options);
- public Task Invoke(HttpContext context);
- }
除了对错误的认定方式,StatusCodePagesMiddleware中间件和ExceptionHandlerMiddleware中间件对错误处理器的表达也不相同。ExceptionHandlerMiddleware中间件的处理器是一个RequestDelegate委托对象,而StatusCodePagesMiddleware中间件的处理器则是一个Func<StatusCodeContext, Task>委托对象。如下面的代码片段所示,配置选项StatusCodePagesOptions的唯一目的就是提供作为处理器的Func<StatusCodeContext, Task>对象。
- public class StatusCodePagesOptions
- {
- public Func<StatusCodeContext, Task> HandleAsync { get; set; }
- }
一个RequestDelegate对象相当于一个Func<HttpContext, Task>类型的委托对象,而一个StatusCodeContext对象也是对一个HttpContext上下文的封装,这两个委托对象并没有本质上的不同。如下面的代码片段所示,除了从StatusCodeContext对象中获取当前HttpContext上下文,我们还可以通过其Next属性得到一个RequestDelegate对象,并利用它将请求再次分发给后续中间件进行处理。StatusCodeContext对象的Options属性返回创建 StatusCodePagesMiddleware中间件时指定的StatusCodePagesOptions对象。
- public class StatusCodeContext
- {
- public HttpContext HttpContext { get; }
- public RequestDelegate Next { get; }
- public StatusCodePagesOptions Options { get; }
- public StatusCodeContext(HttpContext context, StatusCodePagesOptions options, RequestDelegate next);
- }
由于采用了针对响应状态码的错误处理策略,所以实现在StatusCodePagesMiddleware中间件的错误处理操作只会发生在当前响应状态码为400~599的情况下,如下所示的代码片段就体现了这一点。从下面给出的代码片段可以看出,StatusCodePagesMiddleware中间件除了会查看当前响应状态码,还会查看响应内容及媒体类型。如果响应报文已经包含响应内容或者设置了媒体类型,StatusCodePagesMiddleware中间件将不会执行任何操作,因为这正是后续中间件管道希望回复给客户端的响应,该中间件不应该再画蛇添足。
- public class StatusCodePagesMiddleware
- {
- private RequestDelegate _next;
- private StatusCodePagesOptions _options;
- public StatusCodePagesMiddleware(RequestDelegate next, IOptions<StatusCodePagesOptions> options)
- {
- _next = next;
- _options = options.Value;
- }
- public async Task Invoke(HttpContext context)
- {
- await _next(context);
- var response = context.Response;
- if ((response.StatusCode >= 400 && response.StatusCode <= 599) && !response.ContentLength.HasValue && string.IsNullOrEmpty(response.ContentType))
- {
- await _options.HandleAsync(new StatusCodeContext(context, _options, _next));
- }
- }
- }
StatusCodePagesMiddleware中间件对错误的处理非常简单,它只需要从StatusCodePagesOptions对象中提取出作为错误处理器的Func<StatusCodeContext, Task>对象,然后创建一个StatusCodeContext对象作为输入参数调用这个委托对象即可。
二、阻止处理异常
通过《呈现错误信息》的内容我们知道,如果某些内容已经被写入响应的主体部分,或者响应的媒体类型已经被预先设置,StatusCodePagesMiddleware中间件就不会再执行任何错误处理操作。由于应用程序往往具有自身的异常处理策略,它们可能会显式地返回一个状态码为400~599的响应,在此情况下,StatusCodePagesMiddleware中间件是不应该对当前响应做任何干预的。从这个意义上来讲,StatusCodePagesMiddleware中间件仅仅是作为一种后备的错误处理机制而已。
更进一步来讲,如果后续的某个中间件返回了一个状态码为400~599的响应,并且这个响应只有报头集合没有主体(媒体类型自然也不会设置),那么按照我们在上面给出的错误处理逻辑来看,StatusCodePagesMiddleware中间件还是会按照自己的策略来处理并响应请求。为了解决这种情况,我们必须赋予后续中间件能够阻止StatusCodePagesMiddleware中间件进行错误处理的功能。
阻止StatusCodePagesMiddleware中间件进行错误处理的功能是借助一个通过IStatusCodePagesFeature接口表示的特性来实现的。如下面的代码片段所示,IStatusCodePagesFeature接口定义了唯一的Enabled属性,StatusCodePagesFeature类型是对该接口的默认实现,它的Enabled属性默认返回True。
- public interface IStatusCodePagesFeature
- {
- bool Enabled { get; set; }
- }
- public class StatusCodePagesFeature : IStatusCodePagesFeature
- {
- public bool Enabled { get; set; } = true ;
- }
StatusCodePagesMiddleware中间件在将请求交付给后续管道之前,会创建一个StatusCodePagesFeature对象,并将其添加到当前HttpContext上下文的特性集合中。在最终决定是否执行错误处理操作的时候,它还会通过这个特性检验后续的某个中间件是否不希望其进行不必要的错误处理,如下所示的代码片段很好地体现了这一点。
- public class StatusCodePagesMiddleware
- {
- ...
- public async Task Invoke(HttpContext context)
- {
- var feature = new StatusCodePagesFeature();
- context.Features.Set<IStatusCodePagesFeature>(feature);
- await _next(context);
- var response = context.Response;
- if ((response.StatusCode >= 400 && response.StatusCode <= 599) && !response.ContentLength.HasValue && string.IsNullOrEmpty(response.ContentType) && feature.Enabled)
- {
- await _options.HandleAsync(new StatusCodeContext(context, _options, _next));
- }
- }
- }
下面通过一个简单的实例来演示如何利用StatusCodePagesFeature特性来屏蔽StatusCodePagesMiddleware中间件。在如下所示的代码片段中,我们将针对请求的处理定义在ProcessAsync方法中,该方法会返回一个状态码为“401 Unauthorized”的响应。我们通过随机数让这个方法在50%的概率下利用StatusCodePagesFeature特性来阻止StatusCodePagesMiddleware中间件自身对错误的处理。我们通过调用UseStatusCodePages扩展方法注册的StatusCodePagesMiddleware中间件会直接响应一个内容为“Error occurred!”的字符串。
- public class Program
- {
- private static readonly Random _random = new Random();
- public static void Main()
- {
- Host.CreateDefaultBuilder()
- .ConfigureWebHostDefaults(builder => builder.Configure(app => app
- .UseStatusCodePages(HandleAsync)
- .Run(ProcessAsync)))
- .Build()
- .Run();
- static Task HandleAsync(StatusCodeContext context) => context.HttpContext.Response.WriteAsync("Error occurred!");
- static Task ProcessAsync(HttpContext context)
- {
- context.Response.StatusCode = 401;
- if (_random.Next() % 2 == 0)
- {
- context.Features.Get<IStatusCodePagesFeature>().Enabled = false;
- }
- return Task.CompletedTask;
- }
- }
- }
对于针对该应用的请求来说,我们会得到如下两种不同的响应。没有主体内容的响应是通过ProcessAsync方法产生的,这种情况发生在StatusCodePagesMiddleware中间件通过StatusCodePagesFeature特性被屏蔽的时候。有主体内容的响应则是ProcessAsync方法和StatusCodePagesMiddleware中间件共同作用的结果。
- HTTP/1.1 401 Unauthorized
- Date: Sat, 21 Sep 2019 13:37:31 GMT
- Server: Kestrel
- Content-Length: 15
- Error occurred!
- HTTP/1.1 401 Unauthorized
- Date: Sat, 21 Sep 2019 13:37:36 GMT
- Server: Kestrel
- Content-Length: 0
我们在大部分情况下都会调用IApplicationBuilder接口相应的扩展方法来注册StatusCodePagesMiddleware中间件。对于StatusCodePagesMiddleware中间件的注册来说,除了UseStatusCodePages方法,还有其他方法可供选择。
三、UseStatusCodePages
我们可以调用如下所示的3个UseStatusCodePages扩展方法重载来注册StatusCodePagesMiddleware中间件。不论调用哪个重载,系统最终都会根据提供的StatusCodePagesOptions对象调用构造函数来创建这个中间件,而且StatusCodePagesOptions必须具有一个作为错误处理器的Func<StatusCodeContext, Task>对象。
- public static class StatusCodePagesExtensions
- {
- public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app)
- => app.UseMiddleware<StatusCodePagesMiddleware>();
- public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, StatusCodePagesOptions options)
- => app.UseMiddleware<StatusCodePagesMiddleware>(Options.Create(options));
- public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, Func<StatusCodeContext, Task> handler)
- => app.UseStatusCodePages(new StatusCodePagesOptions
- {
- HandleAsync = handler
- });
- }
由于StatusCodePagesMiddleware中间件最终的目的还是将定制的错误信息响应给客户端,所以可以在注册该中间件时直接指定响应的内容和媒体类型,这样的注册方式可以通过调用如下所示的UseStatusCodePages方法来完成。从如下所示的代码片段可以看出,通过参数bodyFormat指定的实际上是一个模板,它可以包含一个表示响应状态码的占位符({0})。
- public static class StatusCodePagesExtensions
- {
- public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, string contentType, string bodyFormat)
- {
- return app.UseStatusCodePages(context =>
- {
- var body = string.Format(CultureInfo.InvariantCulture, bodyFormat, context.HttpContext.Response.StatusCode);
- context.HttpContext.Response.ContentType = contentType;
- return context.HttpContext.Response.WriteAsync(body);
- });
- }
- }
四、UseStatusCodePagesWithRedirects
如果调用UseStatusCodePagesWithRedirects扩展方法,就可以使注册的StatusCodePagesMiddleware中间件向指定的路径发送一个客户端重定向。从如下所示的代码片段可以看出,参数locationFormat指定的重定向地址也是一个模板,它可以包含一个表示响应状态码的占位符({0})。我们可以指定一个完整的地址,也可以指定一个相对于PathBase的相对路径,后者需要包含表示基地址的前缀“~/”。
- public static class StatusCodePagesExtensions
- {
- public static IApplicationBuilder UseStatusCodePagesWithRedirects(this IApplicationBuilder app, string locationFormat)
- {
- if (locationFormat.StartsWith("~"))
- {
- locationFormat = locationFormat.Substring(1);
- return app.UseStatusCodePages(context =>
- {
- var location = string.Format(CultureInfo.InvariantCulture, locationFormat, context.HttpContext.Response.StatusCode);
- context.HttpContext.Response.Redirect(context.HttpContext.Request.PathBase + location);
- return Task.CompletedTask;
- });
- }
- else
- {
- return app.UseStatusCodePages(context =>
- {
- var location = string.Format(CultureInfo.InvariantCulture, locationFormat, context.HttpContext.Response.StatusCode);
- context.HttpContext.Response.Redirect(location);
- return Task.CompletedTask;
- });
- }
- }
- }
下面通过一个简单的应用来演示针对客户端重定向的错误页面呈现方式。我们在如下所示的应用中注册了一个路由模板为“error/{statuscode}”的路由,路由参数statuscode代表响应的状态码。在作为路由处理器的HandleAsync方法中,我们会直接响应一个包含状态码的字符串。我们调用UseStatusCodePagesWithRedirects方法注册StatusCodePagesMiddleware中间件时将重定义路径设置为“error/{0}”。
- public class Program
- {
- private static readonly Random _random = new Random();
- public static void Main()
- {
- Host.CreateDefaultBuilder()
- .ConfigureWebHostDefaults(builder => builder
- .ConfigureServices(svcs => svcs.AddRouting())
- .Configure(app => app
- .UseStatusCodePagesWithRedirects("~/error/{0}")
- .UseRouting()
- .UseEndpoints(endpoints => endpoints.MapGet("error/{statuscode}", HandleAsync))
- .Run(ProcessAsync)))
- .Build()
- .Run();
- static async Task HandleAsync(HttpContext context)
- {
- var statusCode = context.GetRouteData().Values["statuscode"];
- await context.Response.WriteAsync($"Error occurred ({statusCode})");
- }
- static Task ProcessAsync(HttpContext context)
- {
- context.Response.StatusCode = _random.Next(400, 599);
- return Task.CompletedTask;
- }
- }
- }
针对该应用的请求总是得到一个状态码为400~599的响应,StatusCodePagesMiddleware中间件在此情况下会向指定的路径(“~/error/{statuscode}”)发送一个客户端重定向。由于重定向请求的路径与注册的路由相匹配,所以作为路由处理器的HandleError方法会响应下图16-11所示的错误页面。
五、UseStatusCodePagesWithReExecute
除了可以采用客户端重定向的方式来呈现错误页面,还可以调用UseStatusCodePagesWithReExecute方法注册StatusCodePagesMiddleware中间件,并让它采用服务端重定向的方式来处理错误请求。如下面的代码片段所示,当我们调用这个方法的时候不仅可以指定重定向的路径,还可以指定查询字符串。这里作为重定向地址的参数pathFormat依旧是一个路径模板,它可以包含一个表示响应状态码的占位符({0})。
- public static class StatusCodePagesExtensions
- {
- public static IApplicationBuilder UseStatusCodePagesWithReExecute( this IApplicationBuilder app, string pathFormat, string queryFormat = null);
- }
现在我们对前面演示的这个实例略做修改来演示采用服务端重定向呈现的错误页面。如下面的代码片段所示,我们将针对UseStatusCodePagesWithRedirects方法的调用替换成针对UseStatusCodePagesWithReExecute方法的调用。
- public class Program
- {
- private static readonly Random _random = new Random();
- public static void Main()
- {
- Host.CreateDefaultBuilder()
- .ConfigureWebHostDefaults(builder => builder
- .ConfigureServices(svcs => svcs.AddRouting())
- .Configure(app => app
- .UseStatusCodePagesWithReExecute("/error/{0}")
- .UseRouting()
- .UseEndpoints(endpoints => endpoints.MapGet("error/{statuscode}", HandleAsync))
- .Run(ProcessAsync)))
- .Build()
- .Run();
- static async Task HandleAsync(HttpContext context)
- {
- var statusCode = context.GetRouteData().Values["statuscode"];
- await context.Response.WriteAsync($"Error occurred ({statusCode})");
- }
- static Task ProcessAsync(HttpContext context)
- {
- context.Response.StatusCode = _random.Next(400, 599);
- return Task.CompletedTask;
- }
- }
- }
对于前面演示的实例,由于错误页面是通过客户端重定向的方式呈现的,所以浏览器地址栏显示的是重定向地址。我们在选择这个实例时采用了服务端重定向,虽然显示的页面内容并没有不同,但是地址栏上的地址是不会发生改变的,如图16-12所示。(S1615)
之所以命名为UseStatusCodePagesWithReExecute,是因为通过这个方法注册的StatusCodePagesMiddleware中间件进行错误处理时,它仅仅将提供的重定向路径和查询字符串应用到当前HttpContext上下文,然后分发给后续管道重新执行。UseStatusCodePagesWithReExecute方法中注册StatusCodePagesMiddleware中间件的实现总体上可以由如下所示的代码片段来体现。
- public static class StatusCodePagesExtensions
- {
- public static IApplicationBuilder UseStatusCodePagesWithReExecute( this IApplicationBuilder app, string pathFormat, string queryFormat = null)
- {
- return app.UseStatusCodePages(async context =>
- {
- var newPath = new PathString(string.Format(CultureInfo.InvariantCulture, pathFormat, context.HttpContext.Response.StatusCode));
- var formatedQueryString = queryFormat == null ? null : string.Format(CultureInfo.InvariantCulture, queryFormat, context.HttpContext.Response.StatusCode);
- context.HttpContext.Request.Path = newPath;
- context.HttpContext.Request.QueryString = newQueryString;
- await context.Next(context.HttpContext);
- });
- }
- }
与ExceptionHandlerMiddleware中间件类似,StatusCodePagesMiddleware中间件在处理请求的过程中会改变当前请求上下文的状态,具体体现在它会将指定的请求路径和查询字符串重新应用到当前请求上下文中。为了不影响前置中间件对请求的正常处理,StatusCodePagesMiddleware中间件在完成自身处理流程之后必须将当前请求上下文恢复到原始状态。StatusCodePagesMiddleware中间件依旧采用一个特性来保存原始路径和查询字符串。这个特性对应的接口是具有如下定义的IStatusCodeReExecuteFeature,但是该接口仅仅包含两个针对路径的属性,并没有用于携带原始请求上下文的属性,但是默认实现类型StatusCodeReExecuteFeature包含了这个属性。
- public interface IStatusCodeReExecuteFeature
- {
- string OriginalPath { get; set; }
- string OriginalPathBase { get; set; }
- }
- public class StatusCodeReExecuteFeature : IStatusCodeReExecuteFeature
- {
- public string OriginalPath { get; set; }
- public string OriginalPathBase { get; set; }
- public string OriginalQueryString { get; set; }
- }
在StatusCodePagesMiddleware中间件处理异常请求的过程中,在将指定的重定向路径和查询字符串应用到当前请求上下文之前,它会根据原始的上下文创建一个StatusCodeReExecuteFeature特性对象,并将其添加到当前HttpContext上下文的特性集合中。当整个请求处理过程结束之后,StatusCodePagesMiddleware中间件还会将这个特性从当前HttpContext上下文中移除,并恢复原始的请求路径和查询字符串。如下所示的代码片段体现了UseStatusCodePagesWithReExecute方法的实现逻辑。
- public static class StatusCodePagesExtensions
- {
- public static IApplicationBuilder UseStatusCodePagesWithReExecute( this IApplicationBuilder app,string pathFormat, string queryFormat = null)
- {
- return app.UseStatusCodePages(async context =>
- {
- var newPath = new PathString( string.Format(CultureInfo.InvariantCulture, pathFormat, context.HttpContext.Response.StatusCode));
- var formatedQueryString = queryFormat == null ? null : string.Format(CultureInfo.InvariantCulture, queryFormat, context.HttpContext.Response.StatusCode);
- var newQueryString = queryFormat == null ? QueryString.Empty : new QueryString(formatedQueryString);
- var originalPath = context.HttpContext.Request.Path;
- var originalQueryString = context.HttpContext.Request.QueryString;
- context.HttpContext.Features.Set<IStatusCodeReExecuteFeature>(new StatusCodeReExecuteFeature()
- {
- OriginalPathBase = context.HttpContext.Request.PathBase.Value,
- OriginalPath = originalPath.Value,
- OriginalQueryString = originalQueryString.HasValue ? originalQueryString.Value : null,
- });
- context.HttpContext.Request.Path = newPath;
- context.HttpContext.Request.QueryString = newQueryString;
- try
- {
- await context.Next(context.HttpContext);
- }
- finally
- {
- context.HttpContext.Request.QueryString = originalQueryString;
- context.HttpContext.Request.Path = originalPath;
- context.HttpContext.Features.Set<IStatusCodeReExecuteFeature>(null);
- }
- });
- }
- }
ASP.NET Core错误处理中间件[1]: 呈现错误信息
ASP.NET Core错误处理中间件[2]: 开发者异常页面
ASP.NET Core错误处理中间件[3]: 异常处理器
ASP.NET Core错误处理中间件[4]: 响应状态码页面
ASP.NET Core错误处理中间件[4]: 响应状态码页面的更多相关文章
- ASP.NET Core错误处理中间件[1]: 呈现错误信息
NuGet包"Microsoft.AspNetCore.Diagnostics"中提供了几个与异常处理相关的中间件.当ASP.NET Core应用在处理请求过程中出现错误时,我们可 ...
- ASP.NET Core错误处理中间件[2]: 开发者异常页面
<呈现错误信息>通过几个简单的实例演示了如何呈现一个错误页面,该过程由3个对应的中间件来完成.下面先介绍用来呈现开发者异常页面的DeveloperExceptionPageMiddlewa ...
- ASP.NET Core错误处理中间件[3]: 异常处理器
DeveloperExceptionPageMiddleware中间件错误页面可以呈现抛出的异常和当前请求上下文的详细信息,以辅助开发人员更好地进行纠错诊断工作.ExceptionHandlerMid ...
- ASP.NET Core URL Rewrite中间件
URL重写是基于一个或多个预置规则修改请求URL的行为.URL重写在资源位置和访问地址之间创建了一种抽象,这样二者之间就减少了紧密的联系.URL重写有多种适用的场景: 临时或永久移动或替换服务器资源, ...
- ASP.NET Core 中的中间件
前言 由于是第一次写博客,如果您看到此文章,希望大家抱着找错误.批判的心态来看. sky! 何为中间件? 在 ASP.NET Framework 中应该都知道请求管道.可参考:浅谈 ASP.NET ...
- ASP.NET Core 3.1 中间件
参考微软官方文档 : https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1 ...
- 翻译 - ASP.NET Core 基本知识 - 中间件(Middleware)
翻译自 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-5.0 中间件是集成 ...
- 理解ASP.NET Core - 错误处理(Handle Errors)
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或[点击此处查看全文目录](https://www.cnblogs.com/xiaoxiaotank/p/151852 ...
- asp.net core 自定义异常处理中间件
asp.net core 自定义异常处理中间件 Intro 在 asp.net core 中全局异常处理,有时候可能不能满足我们的需要,可能就需要自己自定义一个中间件处理了,最近遇到一个问题,有一些异 ...
随机推荐
- QQ音乐PB级ClickHouse实时数据平台架构演进之路
导语 | OLAP(On-Line Analytical Processing),是数据仓库系统的主要应用形式,帮助分析人员多角度分析数据,挖掘数据价值.本文基于QQ音乐海量大数据实时分析场景,通过Q ...
- Hive中自定义序列化器(带编码)
hive SerDe的简介 https://www.jianshu.com/p/afee9acba686 问题 数据文件为文本文件,每一行为固定格式,每一列的长度都是定长或是有限制范围,考虑采用hiv ...
- SpringBoot从入门到精通教程(一)
写在前面的话: 在很早之前,记笔记时候,我就一直在思考一个问题,我记笔记是为了什么,我一直想不明白 ,后面发现技术跟新迭代的速度实在太快了,笔记刚纪完,技术又跟新了,于是我想了想干脆边写博客,边记笔记 ...
- 【收藏】关于元数据(Metadata)和元数据管理,这是我的见过最全的解读!
本文主要从元数据的定义.作用.元数据管理现状.管理标准和元数据管理功能等方面讲述了我对元数据(Metadata)和元数据管理的认知及理解. 元数据管理 一.元数据的定义 按照传统的定义,元数据(Met ...
- Codis集群相关
在大数据高并发场景下,单个 Redis 实例往往会显得捉襟见肘.首先体现在内存上,单个 Redis 的内存不宜过大,内存太大会导致 rdb 文件过大,进一步导致主从同步时全量同步时间过长,在实例重启恢 ...
- burpsuite 2020.12.1最新版蓝色版,下载安装破解
最新版Burpsuite下载破解安装 安装包链接:https://cloud.189.cn/t/qUVzYbuAJzqm(访问码:mw12) 1.首先,我们解压,可以看到解压后的文件夹里有如下文件 2 ...
- tkinter + 爬虫 实现影视在线资源系统
应吾爱朋友现公布代码如下: import tkinter as tk import requests,re,sys,asyncio from tkinter import scrolledtext,E ...
- 【mybatis-plus】主键id生成、字段自动填充
一.主键id的生成 数据库表里通常都会有一个主键id,来作为这条数据的唯一标识. 常见的方式 数据库自动增长 这种很常见了,可以做到全库唯一.因为id是天然排序的,对于涉及到排序的操作会很方便. UU ...
- 在 xunit 测试项目中使用依赖注入
在 xunit 测试项目中使用依赖注入 Intro 之前写过几篇 xunit 依赖注入的文章,今天这篇文章将结合我在 .NET Conf 上的分享,更加系统的分享一下在测试中的应用案例. 之所以想分享 ...
- Redis 设计与实现 6:五大数据类型之字符串
前文 Redis 设计与实现 2:Redis 对象 说到,五大数据类型都会封装成 RedisObject. typedef struct redisObject { unsigned type:4; ...