前言
在日常使用ASP.NET Core开发的过程中我们多多少少会设计到使用中间件的场景,ASP.NET Core默认也为我们内置了许多的中间件,甚至有时候我们需要自定义中间件来帮我们处理一些请求管道过程中的处理。接下来,我们将围绕着以下几个问题来简单探究一下,关于ASP.NET Core中间件是如何初始化的
- 首先,使用UseMiddleware注册自定义中间件和直接Use的方式有何不同
- 其次,使用基于约定的方式定义中间件和使用实现IMiddleware接口的方式定义中间件有何不同
- 再次,使用基于约定的方式自定义中间件的究竟是如何约束我们编写的类和方法格式的
- 最后,使用约定的方式定义中间件,通过构造注入和通过Invoke方法注入的方式有何不同
接下来我们将围绕这几个核心点来逐步探究关于ASP.NET Core关于中间件初始化的神秘面纱,来指导我们以后使用它的时候需要有注意点,来减少踩坑的次数。
自定义的方式
使用自定义中间件的方式有好几种,咱们简单来演示一下三种比较常用方式。
Use方式
首先,也是最直接最简单的使用Use的方式,比如
1
2
3
4
5
6
7
8
9
10
11
12
13
|
app.Use(async (context, next) =>
{
var endpoint = context.Features.Get<IEndpointFeature>()?.Endpoint;
if (endpoint != null )
{
ResponseCacheAttribute responseCache = endpoint.Metadata.GetMetadata<ResponseCacheAttribute>();
if (responseCache != null )
{
//做一些事情
}
}
await next();
});
|
基于约定的方式
然后使用UseMiddleware也是我们比较常用的一种方式,这种方式使用起来相对于第一种来说,虽然使用起来可能会稍微繁琐一点,毕竟需要定义一个类,但是更好的符合符合面向对象的封装思想,它的使用方式大致如下,首先定义一个Middleware的类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public class RequestCultureMiddleware
{
private readonly RequestDelegate _next;
public RequestCultureMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var cultureQuery = context.Request.Query[ "culture" ];
if (! string .IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
await _next(context);
}
}
|
编写完成之后,需要手动的将类注册到管道中才能生效,注册方式如下所示
1
|
app.UseMiddleware<RequestCultureMiddleware>();
|
实现IMiddleware的方式
还有一种方式是实现IMiddleware接口的方式,这种方式比如前两种方式常用,但是也确确实实的存在于ASP.NET Core中,既然存在也就有它存在的理由,我们也可以探究一下,它的使用方式也是需要自定义一个类去实现IMiddleware接口,如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class RequestCultureOtherMiddleware:IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var cultureQuery = context.Request.Query[ "culture" ];
if (! string .IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
await next(context);
}
}
|
这种方式和第二种方式略有不同,需要手动将中间件注册到容器中,至于声明周期也没做特殊要求,可以直接注册为单例模式
1
|
services.AddSingleton<IMiddleware,RequestCultureOtherMiddleware>();
|
完成上步操作之后,同样也需要将其注册到管道中去
1
|
app.UseMiddleware<RequestCultureOtherMiddleware>();
|
这种方式相对于第二种方式的主要区别在于灵活性方面的差异,它实现了IMiddleware接口,那就要受到IMiddleware接口的约束,也就是我们常说的里氏代换原则,首先我们可以先来看下IMiddleware接口的定义[点击查看源码]
1
2
3
4
5
6
7
8
9
|
public interface IMiddleware
{
/// <summary>
/// 请求处理方法
/// </summary>
/// <param name="context">当前请求上下文</param>
/// <param name="next">请求管道中下一个中间件的委托</param>
Task InvokeAsync (HttpContext context, RequestDelegate next);
}
|
通过这个接口也就看出来InvokeAsync只能接受HttpContext和RequestDelegate参数,无法定义其他形式的参数,也没办法通过注入的方式编写InvokeAsync方法参数,说白了就是没有第二种方式灵活,受限较大。
关于常用的自定义中间件的方式,我们就先说到这里,我们也知道了如何定义使用中间件。接下来我们就来探讨一下,这么多种方式之间到底存在怎样的联系。
源码探究
上面我们已经演示了关于使用中间件的几种方式,那么这么几种使用方式之间有啥联系或区别,我们只看到了表面的,接下来我们来看一下关于中间件初始化的源码来一探究竟。
首先,无论那种形式都是基于IApplicationBuilder这个接口扩展而来的,所以我们先从这里下手,找到源码IApplicationBuilder位置[点击查看源码]可以看到以下代码
1
2
3
4
5
6
|
/// <summary>
/// 将中间件委托添加到应用程序的请求管道。
/// </summary>
/// <param name="middleware">中间件委托</param>
/// <returns>The <see cref="IApplicationBuilder"/>.</returns>
IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
|
IApplicationBuilder接口里只有Use的方式可以添加中间件,由此我们可以大致猜到两点信息
- 其它添加中间件的方式,都是在扩展自IApplicationBuilder,并不是IApplicationBuilder本身的方法。
- 其它添加中间件的形式,最终都会转换为Use的方式。
Use扩展方法
上面我们看到了IApplicationBuilder只包含了一个Use方法,但是我们日常编程中最常使用到的却并不是这一个,而是来自UseExtensions扩展类的Use扩展方法,实现如下所示[点击查看源码]
1
2
3
4
5
6
7
8
9
10
11
12
|
public static IApplicationBuilder Use( this IApplicationBuilder app, Func<HttpContext, Func<Task>, Task> middleware)
{
//将middleware转换为Use(Func<RequestDelegate, RequestDelegate> middleware)的形式
return app.Use(next =>
{
return context =>
{
Func<Task> simpleNext = () => next(context);
return middleware(context, simpleNext);
};
});
}
|
如预料的那样,Use的扩展方法最终都会转换为Use(Func<RequestDelegate, RequestDelegate> middleware)的形式去执行。Use扩展方法的形式还是比较清晰的,毕竟也是基于委托的形式,而且参数是固定的。
UseMiddleware
上面我们看到了Use的扩展方法,它最终还是转换为Use(Func<RequestDelegate, RequestDelegate> middleware)的形式去执行。接下来我们来看下通过编写类的形式定义中间件会是怎样的转换操作。找到UseMiddleware扩展方法所在的地方,也就是UseMiddlewareExtensions扩展类里[点击查看源码],我们最常用的是UseMiddleware这个方法,而且这个方法是UseMiddlewareExtensions扩展类的入口方法[点击查看源码],说白了就是它是完全调用别的方法没有自己的实现逻辑
1
2
3
4
5
6
7
8
9
10
|
/// <summary>
/// 将中间件类型添加到应用程序的请求管道.
/// </summary>
/// <typeparam name="TMiddleware">中间件类型</typeparam>
/// <param name="args">传递给中间件类型实例的构造函数的参数.</param>
/// <returns>The <see cref="IApplicationBuilder"/> instance.</returns>
public static IApplicationBuilder UseMiddleware<[DynamicallyAccessedMembers(MiddlewareAccessibility)]TMiddleware>( this IApplicationBuilder app, params object [] args)
{
return app.UseMiddleware( typeof (TMiddleware), args);
}
|
继续向下看找到它调用的扩展方法,在展示该方法之前我们先罗列一下该类的常量属性,因为类中的方法有用到,如下所示
1
2
|
internal const string InvokeMethodName = "Invoke" ;
internal const string InvokeAsyncMethodName = "InvokeAsync" ;
|
从这里我们可以得到一个信息,基于约定的形式自定义的中间件触发方法名可以是Invoke或InvokeAsync
继续看执行方法的实现代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
public static IApplicationBuilder UseMiddleware( this IApplicationBuilder app, [DynamicallyAccessedMembers(MiddlewareAccessibility)] Type middleware, params object [] args)
{
//判断自定义的中间件是否是实现了IMiddleware接口
if ( typeof (IMiddleware).GetTypeInfo().IsAssignableFrom(middleware.GetTypeInfo()))
{
//Middleware不支持直接传递参数
//因为它是注册到容器中的,所以不能通过构造函数传递自定义的参数,否则抛出异常
if (args.Length > 0)
{
throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported( typeof (IMiddleware)));
}
//实现IMiddleware接口的中间件走的是这个逻辑,咱们待会看
return UseMiddlewareInterface(app, middleware);
}
var applicationServices = app.ApplicationServices;
return app.Use(next =>
{
//获取自定义中间件类的非静态public方法
var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public);
//查找方法名为Invoke或InvokeAsync的方法
var invokeMethods = methods.Where(m =>
string .Equals(m.Name, InvokeMethodName, StringComparison.Ordinal)
|| string .Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal)
).ToArray();
//方法名为Invoke或InvokeAsync的方法只能有有一个,存在多个话会抛出异常
if (invokeMethods.Length > 1)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName));
}
//自定义的中间件类中必须包含名为Invoke或InvokeAsync的方法,否则也会抛出异常
if (invokeMethods.Length == 0)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware));
}
//名为Invoke或InvokeAsync的方法的返回值类型必须是Task类型,否则会抛出异常
var methodInfo = invokeMethods[0];
if (! typeof (Task).IsAssignableFrom(methodInfo.ReturnType))
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));
}
//获取Invoke或InvokeAsync方法的参数
var parameters = methodInfo.GetParameters();
//如果该方法不存在参数或方法的第一个参数不是HttpContext类型的实例,会抛出异常
if (parameters.Length == 0 || parameters[0].ParameterType != typeof (HttpContext))
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));
}
//定义新的数组比传递的参数长度多一个,为啥呢?往下看。
var ctorArgs = new object [args.Length + 1];
//因为方法数组的首元素是RequestDelegate类型的next
//也就是基于约定定义的中间件构造函数的第一个参数是RequestDelegate类型的实例
ctorArgs[0] = next;
Array.Copy(args, 0, ctorArgs, 1, args.Length);
//创建基于约定的中间件实例
//又看到ActivatorUtilities这个类了,关于这个类有兴趣的可以研究一下,可以根据容器创建类型实例,非常好用
var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);
//如果Invoke或InvokeAsync方法只有一个参数,则直接创建RequestDelegate委托返回
if (parameters.Length == 1)
{
//RequestDelegate其实就是public delegate Task RequestDelegate(HttpContext context);
return (RequestDelegate)methodInfo.CreateDelegate( typeof (RequestDelegate), instance);
}
//编译Invoke或InvokeAsync方法,关于Compile的实现等会咱们再看
var factory = Compile< object >(methodInfo, parameters);
//返回这个委托
//看着这个委托的格式有点眼熟,其实就是RequestDelegate即public delegate Task RequestDelegate(HttpContext context);
return context =>
{
var serviceProvider = context.RequestServices ?? applicationServices;
//serviceProvider不能为空,否则没法玩了
if (serviceProvider == null )
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
}
//返回委托执行结果
return factory(instance, context, serviceProvider);
};
});
}
|
这个方法其实是工作的核心方法,通过这里可以看出来,自定义中间件的大致执行过程。代码中的注释我写的比较详细,有兴趣的可以仔细了解一下,如果懒得看我们就大致总结一下大致的核心点
- 首先UseMiddleware的本质确实还是执行的Use方法
- 实现IMiddleware接口的中间件走的是独立的处理逻辑,而且构造函数传递自定义的参数,因为它的数据来自于容器的注入。
-
基于约定定义中间件的情况,即不实现IMiddleware的情况下。
- ①基于约定定义的中间件,构造函数的第一个参数需要是RequestDelegate类型
- ②查找方法名可以为Invoke或InvokeAsync,且存在而且只能存在一个
- ③Invoke或InvokeAsync方法返回值需为Task,且方法的第一个参数必须为HttpContext类型
- ④Invoke或InvokeAsync方法如果只包含HttpContext类型参数,则该方法直接转换为RequestDelegate
- ⑤我们之所以可以通过构造注入在中间件中获取服务是因为基于约定的方式是通过ActivatorUtilities类创建的实例
通过上面的源码我们了解到了实现IMiddleware接口的方式自定义中间件的方式是单独处理的即在UseMiddlewareInterface方法中[点击查看源码],接下来我们查看一下该方法的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
private static IApplicationBuilder UseMiddlewareInterface(IApplicationBuilder app, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type middlewareType)
{
return app.Use(next =>
{
return async context =>
{
var middlewareFactory = (IMiddlewareFactory?)context.RequestServices.GetService( typeof (IMiddlewareFactory));
if (middlewareFactory == null )
{
// 没有middlewarefactory直接抛出异常
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoMiddlewareFactory( typeof (IMiddlewareFactory)));
}
//创建middleware实例
var middleware = middlewareFactory.Create(middlewareType);
if (middleware == null )
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareUnableToCreateMiddleware(middlewareFactory.GetType(), middlewareType));
}
try
{
//执行middleware的InvokeAsync方法
await middleware.InvokeAsync(context, next);
}
finally
{
//释放middleware
middlewareFactory.Release(middleware);
}
};
});
}
|
通过上面的代码我们可以看到,IMiddleware实例是通过IMiddlewareFactory实例创建而来,ASP.NET Core中IMiddlewareFactory默认注册的实现类是MiddlewareFactory,接下来我们看下这个类的实现[点击查看源码]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public class MiddlewareFactory : IMiddlewareFactory
{
private readonly IServiceProvider _serviceProvider;
public MiddlewareFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IMiddleware? Create(Type middlewareType)
{
//根据类型从容器中获取IMiddleware实例
return _serviceProvider.GetRequiredService(middlewareType) as IMiddleware;
}
public void Release(IMiddleware middleware)
{
//因为容器控制了对象的生命周期,所以这里啥也没有
}
}
|
好吧,其实就是在容器中获取的IMiddleware实例,通过这个我们就可以总结出来实现IMiddleware接口的形式创建中间件的操作
- 需要实现IMiddleware接口,来约束中间件的行为,方法名只能为InvokeAsync
- 需要手动注册IMiddleware和实现类到容器中,生命周期可自行约束,如果生命周期为Scope或瞬时,那么每次请求都会创建新的中间件实例
- 没办法通过InvokeAsync方法注入服务,因为受到了IMiddleware接口的约束
上面我们看到了实现IMiddleware接口的方式中间件是如何被初始化的,接下来我们继续来看,基于约定的方式定义的中间件是如何被初始化的。通过上面我们展示的源码可知,实现逻辑在Compile方法中,该方法整体实现方式就是基于Expression,主要原因个人猜测有两点,一个是形式比较灵活能应对的场景较多,二是性能稍微比反射好一点。在此之前,我们先展示一下Compile方法依赖的操作,首先反射是获取UseMiddlewareExtensions类的GetService方法操作
1
|
private static readonly MethodInfo GetServiceInfo = typeof (UseMiddlewareExtensions).GetMethod(nameof(GetService), BindingFlags.NonPublic | BindingFlags.Static)!;
|
其中GetService方法的实现如下所示,其实就是在容器ServiceProvider中获取指定类型实例
1
2
3
4
5
6
7
8
9
|
private static object GetService(IServiceProvider sp, Type type, Type middleware)
{
var service = sp.GetService(type);
if (service == null )
{
throw new InvalidOperationException(Resources.FormatException_InvokeMiddlewareNoService(type, middleware));
}
return service;
}
|
好了上面已将Compile外部依赖已经展示出来了,接下来我们就可以继续探究Compile方法了[点击查看源码]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
private static Func<T, HttpContext, IServiceProvider, Task> Compile<T>(MethodInfo methodInfo, ParameterInfo[] parameters)
{
var middleware = typeof (T);
//构建三个Parameter名为httpContext、serviceProvider、middleware
var httpContextArg = Expression.Parameter( typeof (HttpContext), "httpContext" );
var providerArg = Expression.Parameter( typeof (IServiceProvider), "serviceProvider" );
var instanceArg = Expression.Parameter(middleware, "middleware" );
//穿件Expression数组,且数组第一个参数为httpContextArg
var methodArguments = new Expression[parameters.Length];
methodArguments[0] = httpContextArg;
//因为Invoke或InvokeAsync方法第一个参数为HttpContext,且methodArguments第一个参数占位,所以跳过第一个参数
for ( int i = 1; i < parameters.Length; i++)
{
//获取方法参数
var parameterType = parameters[i].ParameterType;
//不支持ref类型操作
if (parameterType.IsByRef)
{
throw new NotSupportedException(Resources.FormatException_InvokeDoesNotSupportRefOrOutParams(InvokeMethodName));
}
//构建参数类型表达式,即用户构建方法参数的操作
var parameterTypeExpression = new Expression[]
{
providerArg,
Expression.Constant(parameterType, typeof (Type)),
Expression.Constant(methodInfo.DeclaringType, typeof (Type))
};
//声明调用GetServiceInfo的表达式
var getServiceCall = Expression.Call(GetServiceInfo, parameterTypeExpression);
//将getServiceCall操作转换为parameterType
methodArguments[i] = Expression.Convert(getServiceCall, parameterType);
}
//获取中间件类型表达式
Expression middlewareInstanceArg = instanceArg;
if (methodInfo.DeclaringType != null && methodInfo.DeclaringType != typeof (T))
{
//转换中间件类型表达式类型与声明类型一致
middlewareInstanceArg = Expression.Convert(middlewareInstanceArg, methodInfo.DeclaringType);
}
//调用middlewareInstanceArg(即当前中间件)的methodInfo(即获取Invoke或InvokeAsync)方法参数(methodArguments)
var body = Expression.Call(middlewareInstanceArg, methodInfo, methodArguments);
//转换为lambda
var lambda = Expression.Lambda<Func<T, HttpContext, IServiceProvider, Task>>(body, instanceArg, httpContextArg, providerArg);
return lambda.Compile();
}
|
上面的代码比较抽象,其实主要是因为它是基于表达式树进行各种操作的,如果对表达式树比较熟悉的话,可能对上面的代码理解起来还好一点,如果不熟悉表达式树的话,可能理解起来比较困难,不过还是建议简单学习一下Expression相关的操作,慢慢的发现还是挺有意思的,它的性能整体来说比传统的反射性能也会更好一点。其实Compile主要实现的操作转化为我们比较容易理解的代码的话就是下面所示的操作,如果我们编写了一个如下的中间件代码
1
2
3
4
5
6
|
public class Middleware
{
public Task Invoke(HttpContext context, ILoggerFactory loggerFactory)
{
}
}
|
那么通过Compile方法将转换为类似以下形式的操作,这样说的话可能会好理解一点
1
2
3
4
|
Task Invoke(Middleware instance, HttpContext httpContext, IServiceProvider provider)
{
return instance.Invoke(httpContext, (ILoggerFactory)UseMiddlewareExtensions.GetService(provider, typeof (ILoggerFactory));
}
|
通过上面的源码分析我们了解到,基于约定的方式定义的中间件实例是通过ActivatorUtilities类创建的,而且创建实例是在返回RequestDelegate委托之前,IApplicationBuilder的Use方法只会在首次运行的时候执行,后续管道串联执行的其实正是它返回的结果RequestDelegate这个委托。但是执行转换Invoke或InvokeAsync方法为执行委托的操作却是在返回的RequestDelegate委托当中,也就是我们每次请求管道会处理的逻辑中。这个逻辑可以在IApplicationBuilder默认的实现类ApplicationBuilder类的Build方法中可以得知[点击查看源码],它的实现逻辑如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public RequestDelegate Build()
{
//最后的管道处理,即请求未能匹配到任何终结点的情况
RequestDelegate app = context =>
{
var endpoint = context.GetEndpoint();
var endpointRequestDelegate = endpoint?.RequestDelegate;
if (endpointRequestDelegate != null )
{
var message =
$ "The request reached the end of the pipeline without executing the endpoint: '{endpoint!.DisplayName}'. " +
$ "Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " +
$ "routing." ;
throw new InvalidOperationException(message);
}
//执行管道的重点是404,只有未命中任何终结点的情况下才会走到这里
context.Response.StatusCode = StatusCodes.Status404NotFound;
return Task.CompletedTask;
};
//_components即我们通过Use添加的中间件
foreach (var component in _components.Reverse())
{
//得到执行结果即RequestDelegate
app = component(app);
}
//返回第一个管道中间件
return app;
}
|
通过上面的代码我们可以清楚的看到,管道最终执行的就是执行Func<RequestDelegate, RequestDelegate>这个委托的返回结果RequestDelegate。
由此得到结论,基于约定的中间件形式,通构造函数注入的服务实例,是和应用程序的生命周期一致的。通过Invoke或InvokeAsync方法注入的服务实例每次请求都会被执行到,即生命周期是Scope的。
总结
通过本次对源码的研究,我们认识到了自定义的ASP.NET Core中间件是如何被初始化的。虽然自定义的中间件的形式有许多种方式,但是最终还都是转换为IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)这种方式。将中间件抽离为独立的类有两种方式,即基于约定的方式和实现IMiddleware接口的形式,通过分析源码我们也更深刻的了解两种方式的不同之处。基于约定的方式更灵活,它的声明周期是单例的,但是通过它的Invoke或InvokeAsync方法注入的服务实例生命周期是Scope的。实现IMiddleware接口的方式生命周期取决于自己注册服务实例时候声明的周期,而且这种方式没办法通过方法注入服务,因为有IMiddleware接口InvokeAsync方法的约束。
当然不仅仅是我们在总结中说的的这些,还存在更多的细节,这些我们在分析源码的时候都有涉及,相信阅读文章比较仔细的同学肯定会注意到这些。阅读源码收获正是这些,解决心中的疑问,了解更多的细节,有助于在实际使用中避免一些不必要的麻烦。本次讲解就到这里,愿各位能有所收获。
到此这篇关于ASP.NET Core中间件初始化的实现的文章就介绍到这了,更多相关ASP.NET Core中间件初始化内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!
原文链接:https://www.cnblogs.com/wucy/p/14496923.html