[Abp 源码分析]ASP.NET Core 集成

点击上方蓝字关注我们


0. 简介

整个 Abp 框架最为核心的除了 Abp 库之外,其次就是 Abp.AspNetCore 库了。虽然 Abp 本身是可以用于控制台程序的,不过那样的话 Abp 就基本没什么用,还是需要集合 ASP.NET Core 才能发挥它真正的作用。

在 Abp.AspNetCore 库里面,Abp 通过 WindsorRegistrationHelper.CreateServiceProvider() 接管了 ASP.NET Core 自带的 Ioc 容器。除此之外,还针对 Controller 的生成规则也进行了替换,以便实现 Dynamic API 功能。

总的来说,整个 Abp 框架与 ASP.NET Core 集成的功能都放在这个库里面的,所以说这个库还是相当重要的。这个项目又依赖于 Abp.Web.Common 库,这个库是存放了很多公用方法或者工具类的,后面也会有讲述。

1. 启动流程

首先在 Abp.AspNetCore 库里面,Abp 提供了两个扩展方法。

  • 第一个则是 AddAbp<TStartupModule>() 方法。

    该方法是 IServiceCollection 的扩展方法,用于在 ASP.NET Core 项目里面的 Startup 的 ConfigureService() 进行配置。通过该方法,Abp 会接管默认的 DI 框架,改为使用 Castle Windsor,并且进行一些 MVC 相关的配置。

  • 第二个则是 UseAbp() 方法。

    该方法是 IApplicationBuilder 的扩展方法,用于 Startup 类里面的 Configure() 配置。通过该方法,Abp 会执行一系列初始化操作,在这个时候 Abp 框架才算是真正地启动了起来。

下面则是常规的用法:

public class Startup
{public IServiceProvider ConfigureServices(IServiceCollection services){services.AddMvc();return services.AddAbp<AspNetCoreAppModule>();}public void Configure(IApplicationBuilder app, IHostingEnvironment env){app.UseMvc();app.UseAbp();}
}

基本上可以说,UseAbp() 就是整个 Abp 框架的入口点,负责调用 AbpBootstrapper 来初始化整个 Abp 项目并加载各个模块。

2. 代码分析

在 Abp.AspNetCore 库中,基本上都是针对 ASP.NET Core 的一些相关组件进行替换。大体上有过滤器、控制器、多语言、动态 API、CSRF 防御组件这几大块东西,下面我们先按照 AddAbp() 方法与 UseAbp() 方法内部注入的顺序依次进行讲解。

首先我们讲解一下 AddAbp() 方法与 UseAbp() 方法的内部做了什么操作吧。

2.1 初始化操作

2.1.1 组件替换与注册

我们首先查看 AddAbp() 方法,该方法存在于 AbpServiceCollectionExtensions.cs 文件之中。

public static IServiceProvider AddAbp<TStartupModule>(this IServiceCollection services, [CanBeNull] Action<AbpBootstrapperOptions> optionsAction = null)where TStartupModule : AbpModule
{// 传入启动模块,构建 AddAbpBootstrapper 对象,并将其注入到 Ioc 容器当中var abpBootstrapper = AddAbpBootstrapper<TStartupModule>(services, optionsAction);// 配置 ASP.NET Core 相关的东西ConfigureAspNetCore(services, abpBootstrapper.IocManager);// 返回一个新的 IServiceProvider 用于替换自带的 DI 框架return WindsorRegistrationHelper.CreateServiceProvider(abpBootstrapper.IocManager.IocContainer, services);
}

该方法作为 IServiceCollection 的扩展方法存在,方便用户进行使用,而在 ConfigureAspNetCore() 方法之中,主要针对 ASP.NET Core 进行了相关的配置。

private static void ConfigureAspNetCore(IServiceCollection services, IIocResolver iocResolver)
{// 手动注入 HTTPContext 访问器等services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();// 替换掉默认的控制器构造类,改用 DI 框架负责控制器的创建services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());// 替换掉默认的视图组件构造类,改用 DI 框架负责视图组件的创建services.Replace(ServiceDescriptor.Singleton<IViewComponentActivator, ServiceBasedViewComponentActivator>());// 替换掉默认的 Antiforgery 类 (主要用于非浏览器的客户端进行调用)services.Replace(ServiceDescriptor.Transient<AutoValidateAntiforgeryTokenAuthorizationFilter, AbpAutoValidateAntiforgeryTokenAuthorizationFilter>());services.Replace(ServiceDescriptor.Transient<ValidateAntiforgeryTokenAuthorizationFilter, AbpValidateAntiforgeryTokenAuthorizationFilter>());// 添加 Feature Provider,用于判断某个类型是否为控制器var partManager = services.GetSingletonServiceOrNull<ApplicationPartManager>();partManager?.FeatureProviders.Add(new AbpAppServiceControllerFeatureProvider(iocResolver));// 配置 JSON 序列化services.Configure<MvcJsonOptions>(jsonOptions =>{jsonOptions.SerializerSettings.ContractResolver = new AbpMvcContractResolver(iocResolver){NamingStrategy = new CamelCaseNamingStrategy()};});// 配置 MVC 相关的东西,包括控制器生成和过滤器绑定services.Configure<MvcOptions>(mvcOptions =>{mvcOptions.AddAbp(services);});// 配置 Razor 相关参数services.Insert(0,ServiceDescriptor.Singleton<IConfigureOptions<RazorViewEngineOptions>>(new ConfigureOptions<RazorViewEngineOptions>((options) =>{options.FileProviders.Add(new EmbeddedResourceViewFileProvider(iocResolver));})));
}

之后来到 mvcOptions.AddAbp(services); 所指向的类型,可以看到如下代码:

internal static class AbpMvcOptionsExtensions
{public static void AddAbp(this MvcOptions options, IServiceCollection services){AddConventions(options, services);AddFilters(options);AddModelBinders(options);}// 添加 Abp 定义的 Controller 约定,主要用于配置 Action 方法的 HttpMethod 与路由private static void AddConventions(MvcOptions options, IServiceCollection services){options.Conventions.Add(new AbpAppServiceConvention(services));}// 添加各种过滤器private static void AddFilters(MvcOptions options){options.Filters.AddService(typeof(AbpAuthorizationFilter));options.Filters.AddService(typeof(AbpAuditActionFilter));options.Filters.AddService(typeof(AbpValidationActionFilter));options.Filters.AddService(typeof(AbpUowActionFilter));options.Filters.AddService(typeof(AbpExceptionFilter));options.Filters.AddService(typeof(AbpResultFilter));}// 添加 Abp 定义的模型绑定器,主要是为了处理时间类型private static void AddModelBinders(MvcOptions options){options.ModelBinderProviders.Insert(0, new AbpDateTimeModelBinderProvider());}
}

这里面所做的工作基本上都是进行一些组件的注入与替换操作。

2.1.2 Abp 框架加载与初始化

Abp 框架的初始化与加载则是在 UseAbp() 方法里面进行的,首先看它的两个重载方法。

public static class AbpApplicationBuilderExtensions
{public static void UseAbp(this IApplicationBuilder app){app.UseAbp(null);}public static void UseAbp([NotNull] this IApplicationBuilder app, Action<AbpApplicationBuilderOptions> optionsAction){Check.NotNull(app, nameof(app));var options = new AbpApplicationBuilderOptions();// 获取用户传入的配置操作optionsAction?.Invoke(options);// 是否启用 Castle 的日志工厂if (options.UseCastleLoggerFactory){app.UseCastleLoggerFactory();}// Abp 框架开始加载并初始化InitializeAbp(app);// 是否根据请求进行本地化处理if (options.UseAbpRequestLocalization){//TODO: 这个中间件应该放在授权中间件之后app.UseAbpRequestLocalization();}// 是否使用安全头if (options.UseSecurityHeaders){app.UseAbpSecurityHeaders();}}// ... 其他代码
}

在 UseAbp() 当中你需要注意的是 InitializeAbp(app); 方法。该方法在调用的时候,Abp 才会真正开始地进行初始化。在这个时候,Abp 会遍历所有项目并且执行它们的模块的三个生命周期方法。当所有模块都被调用过之后,Abp 框架就已经准备就绪了。

private static void InitializeAbp(IApplicationBuilder app)
{// 使用 IApplicationBuilder 从 IServiceCollection 中获取之前 AddAbp() 所注入的 AbpBootstrapper 对象var abpBootstrapper = app.ApplicationServices.GetRequiredService<AbpBootstrapper>();// 调用 AbpBootstrapper 的初始化方法,加载所有模块abpBootstrapper.Initialize();// 绑定 ASP.NET Core 的生命周期,当网站关闭时,调用 AbpBootstrapper 对象的 Dispose() 方法var applicationLifetime = app.ApplicationServices.GetService<IApplicationLifetime>();applicationLifetime.ApplicationStopping.Register(() => abpBootstrapper.Dispose());
}

2.2 AbpAspNetCoreModule 模块

如果说要了解 Abp 某一个库的话,第一步肯定是阅读该库提供的模块类型。因为不管是哪一个库,都会有一个模块进行库的基本配置与初始化动作,而且肯定是这个库第一个被 Abp 框架所调用到的类型。

首先我们按照模块的生命周期来阅读模块的源代码,下面是模块的预加载 (PreInitialize())方法:

[DependsOn(typeof(AbpWebCommonModule))]
public class AbpAspNetCoreModule : AbpModule
{public override void PreInitialize(){// 添加一个新的注册规约,用于批量注册视图组件IocManager.AddConventionalRegistrar(new AbpAspNetCoreConventionalRegistrar());IocManager.Register<IAbpAspNetCoreConfiguration, AbpAspNetCoreConfiguration>();Configuration.ReplaceService<IPrincipalAccessor, AspNetCorePrincipalAccessor>(DependencyLifeStyle.Transient);Configuration.ReplaceService<IAbpAntiForgeryManager, AbpAspNetCoreAntiForgeryManager>(DependencyLifeStyle.Transient);Configuration.ReplaceService<IClientInfoProvider, HttpContextClientInfoProvider>(DependencyLifeStyle.Transient);Configuration.Modules.AbpAspNetCore().FormBodyBindingIgnoredTypes.Add(typeof(IFormFile));Configuration.MultiTenancy.Resolvers.Add<DomainTenantResolveContributor>();Configuration.MultiTenancy.Resolvers.Add<HttpHeaderTenantResolveContributor>();Configuration.MultiTenancy.Resolvers.Add<HttpCookieTenantResolveContributor>();}// ... 其他代码
}

可以看到在预加载方法内部,该模块通过 ReplaceService 替换了许多接口实现,也有很多注册了许多组件,这其中就包括模块的配置类 IAbpAspNetCoreConfiguration 。

[DependsOn(typeof(AbpWebCommonModule))]
public class AbpAspNetCoreModule : AbpModule
{// ... 其他代码public override void Initialize(){IocManager.RegisterAssemblyByConvention(typeof(AbpAspNetCoreModule).GetAssembly());}// ... 其他代码
}

初始化方法也更加简洁,则是通过 IocManager 提供的程序集扫描注册来批量注册一些组件。这里执行了该方法之后,会调用 BasicConventionalRegistrar 与 AbpAspNetCoreConventionalRegistrar 这两个注册器来批量注册符合规则的组件。

[DependsOn(typeof(AbpWebCommonModule))]
public class AbpAspNetCoreModule : AbpModule
{// ... 其他代码public override void PostInitialize(){AddApplicationParts();ConfigureAntiforgery();}private void AddApplicationParts(){// 获得当前库的配置类var configuration = IocManager.Resolve<AbpAspNetCoreConfiguration>();// 获得 ApplicationPart 管理器,用于发现指定程序集的应用服务,使其作为控制器进行初始化var partManager = IocManager.Resolve<ApplicationPartManager>();// 获得模块管理器,用于插件模块的加载var moduleManager = IocManager.Resolve<IAbpModuleManager>();// 获得控制器所在的程序集集合var controllerAssemblies = configuration.ControllerAssemblySettings.Select(s => s.Assembly).Distinct();foreach (var controllerAssembly in controllerAssemblies){// 用程序集构造 AssemblyPart ,以便后面通过 AbpAppServiceControllerFeatureProvider 判断哪些类型是控制器partManager.ApplicationParts.Add(new AssemblyPart(controllerAssembly));}// 从插件的程序集var plugInAssemblies = moduleManager.Modules.Where(m => m.IsLoadedAsPlugIn).Select(m => m.Assembly).Distinct();foreach (var plugInAssembly in plugInAssemblies){partManager.ApplicationParts.Add(new AssemblyPart(plugInAssembly));}}// 配置安全相关设置private void ConfigureAntiforgery(){IocManager.Using<IOptions<AntiforgeryOptions>>(optionsAccessor =>{optionsAccessor.Value.HeaderName = Configuration.Modules.AbpWebCommon().AntiForgery.TokenHeaderName;});}
}

该模块的第三个生命周期方法主要是为了提供控制器所在的程序集,以便 ASP.NET Core MVC 进行控制器构造,其实这里仅仅是添加程序集的,而程序集有那么多类型,那么 MVC 是如何判断哪些类型是控制器类型的呢?这个问题在下面一节进行解析。

2.3 控制器与动态 API

接着上一节的疑问,那么 MVC 所需要的控制器从哪儿来呢?其实是通过在 AddAbp() 所添加的 AbpAppServiceControllerFeatureProvider 实现的。

private static void ConfigureAspNetCore(IServiceCollection services, IIocResolver iocResolver)
{// ... 其他代码var partManager = services.GetSingletonServiceOrNull<ApplicationPartManager>();partManager?.FeatureProviders.Add(new AbpAppServiceControllerFeatureProvider(iocResolver));// ... 其他代码
}

下面我们分析一下该类型的内部构造是怎样的,首先看一下它的定义与构造器:

public class AbpAppServiceControllerFeatureProvider : ControllerFeatureProvider
{private readonly IIocResolver _iocResolver;public AbpAppServiceControllerFeatureProvider(IIocResolver iocResolver){_iocResolver = iocResolver;}// ... 其他代码
}

类型定义都比较简单,继承自 ControllerFeatureProvider ,然后在构造函数传入了一个解析器。在该类型内部,重写了父类的一个 IsController() 方法,这个方法会传入一个 TypeInfo 对象。其实你看到这里应该就明白了,之前在模块当中添加的程序集,最终会被 MVC 解析出所有类型然后调用这个 Provider 来判断哪些类型是控制器。

如果该类型是控制器的话,则返回 True,不是控制器则返回 False

public class AbpAppServiceControllerFeatureProvider : ControllerFeatureProvider
{// ... 其他代码protected override bool IsController(TypeInfo typeInfo){// 获得 Type 对象var type = typeInfo.AsType();// 判断传入的类型是否继承自 IApplicationService 接口,并且不是泛型类型、不是抽象类型、访问级别为 publicif (!typeof(IApplicationService).IsAssignableFrom(type) ||!typeInfo.IsPublic || typeInfo.IsAbstract || typeInfo.IsGenericType){// 不满足上述条件则说明这个类型不能作为一个控制器return false;}// 获取类型上面是否标注有 RemoteServiceAttribute 特性。var remoteServiceAttr = ReflectionHelper.GetSingleAttributeOrDefault<RemoteServiceAttribute>(typeInfo);// 如果有该特性,并且在特性内部的 IsEnabled 为 False 则该类型不能作为一个控制器if (remoteServiceAttr != null && !remoteServiceAttr.IsEnabledFor(type)){return false;}// 从模块配置当中取得一个 Func 委托,该委托用于指定某些特性类型是否为一个控制器var configuration = _iocResolver.Resolve<AbpAspNetCoreConfiguration>().ControllerAssemblySettings.GetSettingOrNull(type);return configuration != null && configuration.TypePredicate(type);}
}

2.3.1 路由与 HTTP.Method 配置

在 MVC 确定好哪些类型是控制器之后,来到了 AbpAppServiceConvention 内部,在这个方法内部则要进行路由和 Action 的一些具体参数。

这里我们首先看一下这个 AbpAppServiceConvention 类型的基本定义与构造。

public class AbpAppServiceConvention : IApplicationModelConvention
{// 模块的配置类private readonly Lazy<AbpAspNetCoreConfiguration> _configuration;public AbpAppServiceConvention(IServiceCollection services){// 使用 Services 获得模块的配置类,并赋值_configuration = new Lazy<AbpAspNetCoreConfiguration>(() => services.GetSingletonService<AbpBootstrapper>().IocManager.Resolve<AbpAspNetCoreConfiguration>(), true);}// 实现的 IApplicationModelConvention 定义的 Apply 方法public void Apply(ApplicationModel application){// 遍历控制器foreach (var controller in application.Controllers){var type = controller.ControllerType.AsType();var configuration = GetControllerSettingOrNull(type);// 判断控制器类型是否继承自 IApplicationService 接口if (typeof(IApplicationService).GetTypeInfo().IsAssignableFrom(type)){// 重新定义控制器名字,如果控制器名字有以 ApplicationService.CommonPostfixes 定义的后缀结尾,则移除后缀之后,再作为控制器名字controller.ControllerName = controller.ControllerName.RemovePostFix(ApplicationService.CommonPostfixes);// 模型绑定配置,如果有的话,默认为 NULLconfiguration?.ControllerModelConfigurer(controller);// 配置控制器 Area 路由ConfigureArea(controller, configuration);// 配置控制器路由与 Action 等...ConfigureRemoteService(controller, configuration);}else{var remoteServiceAtt = ReflectionHelper.GetSingleAttributeOrDefault<RemoteServiceAttribute>(type.GetTypeInfo());if (remoteServiceAtt != null && remoteServiceAtt.IsEnabledFor(type)){ConfigureRemoteService(controller, configuration);}}}}// ... 其他代码
}

这里我们再跳转到 ConfigureRemoteService() 方法内部可以看到其定义如下:

private void ConfigureRemoteService(ControllerModel controller, [CanBeNull] AbpControllerAssemblySetting configuration)
{// 配置控制器与其 Action 的可见性ConfigureApiExplorer(controller);// 配置 Action 的路由ConfigureSelector(controller, configuration);// 配置 Action 传参形式ConfigureParameters(controller);
}

【注意】

AbpAppServiceControllerFeatureProvider 与 AbpAppServiceConvention 的调用都是在第一次请求接口的时候才会进行初始化,所以这就会造成第一次接口请求缓慢的问题,因为要做太多的初始化工作了。

2.4 过滤器

过滤器是在 AddAbp() 的时候被注入到 MVC 里面的,这些过滤器其实大部分在之前的 Abp 源码分析都有见过。

2.4.1 工作单元过滤器

工作单元过滤器是针对于启用了 UnitOfWorkAttribute 特性标签的应用服务/控制器进行处理。其核心思想就是在调用接口时,在最外层就使用 IUnitOfWorkManager 构建一个新的工作单元,然后将应用服务/控制器的调用就包在内部了。

首先来看一下这个过滤器内部定义与构造器:

public class AbpUowActionFilter : IAsyncActionFilter, ITransientDependency
{// 工作单元管理器private readonly IUnitOfWorkManager _unitOfWorkManager;// ASP.NET Core 配置类private readonly IAbpAspNetCoreConfiguration _aspnetCoreConfiguration;// 工作单元配置类private readonly IUnitOfWorkDefaultOptions _unitOfWorkDefaultOptions;public AbpUowActionFilter(IUnitOfWorkManager unitOfWorkManager,IAbpAspNetCoreConfiguration aspnetCoreConfiguration,IUnitOfWorkDefaultOptions unitOfWorkDefaultOptions){_unitOfWorkManager = unitOfWorkManager;_aspnetCoreConfiguration = aspnetCoreConfiguration;_unitOfWorkDefaultOptions = unitOfWorkDefaultOptions;}// ... 其他代码
}

可以看到在这个工作单元过滤器,他通过实现 ITransientDependency 来完成自动注入,之后使用构造注入了两个配置类和一个工作单元管理器。

在其 OnActionExecutionAsync() 方法内部的代码如下:

public class AbpUowActionFilter : IAsyncActionFilter, ITransientDependency
{// ... 其他代码public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next){// 判断当前调用是否是控制器方法if (!context.ActionDescriptor.IsControllerAction()){// 如果不是,则不执行任何操作await next();return;}// 获得控制器/应用服务所标记的工作单元特性var unitOfWorkAttr = _unitOfWorkDefaultOptions.GetUnitOfWorkAttributeOrNull(context.ActionDescriptor.GetMethodInfo()) ??_aspnetCoreConfiguration.DefaultUnitOfWorkAttribute;// 如果特性的 IsDisabled 为 True 的话,不执行任何操作if (unitOfWorkAttr.IsDisabled){await next();return;}// 使用工作单元管理器开启一个新的工作单元using (var uow = _unitOfWorkManager.Begin(unitOfWorkAttr.CreateOptions())){var result = await next();if (result.Exception == null || result.ExceptionHandled){await uow.CompleteAsync();}}}
}

逻辑也很简单,这里就不再赘述了。

2.4.2 授权过滤器

授权过滤器的基本原理在文章 《[Abp 源码分析]十一、权限验证》 有讲到过,这里就不在赘述。

2.4.3 参数校验过滤器

参数校验过滤器在文章 《[Abp 源码分析]十四、DTO 自动验证》 有讲到过,这里不再赘述。

2.4.4 审计日志过滤器

其实这个过滤器,在文章 《十五、自动审计记录》 有讲到过,作用比较简单。就是构造一个 AuditInfo 对象,然后再调用 IAuditingStore 提供的持久化功能将审计信息储存起来。

2.4.5 异常过滤器

异常过滤器在文章 《[Abp 源码分析]十、异常处理》 有讲解,这里不再赘述。

2.4.6 返回值过滤器

这个东西其实就是用于包装返回值的,因为只要使用的 Abp 框架,其默认的返回值都会进行包装,那我们可以通过 DontWarpAttribute 来取消掉这层包装。

那么包装是在什么地方进行的呢?其实就在 AbpResultFilter 的内部进行的。

public class AbpResultFilter : IResultFilter, ITransientDependency
{private readonly IAbpAspNetCoreConfiguration _configuration;private readonly IAbpActionResultWrapperFactory _actionResultWrapperFactory;public AbpResultFilter(IAbpAspNetCoreConfiguration configuration, IAbpActionResultWrapperFactory actionResultWrapper){_configuration = configuration;_actionResultWrapperFactory = actionResultWrapper;}public virtual void OnResultExecuting(ResultExecutingContext context){if (!context.ActionDescriptor.IsControllerAction()){return;}var methodInfo = context.ActionDescriptor.GetMethodInfo();var wrapResultAttribute =ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(methodInfo,_configuration.DefaultWrapResultAttribute);if (!wrapResultAttribute.WrapOnSuccess){return;}// 包装对象_actionResultWrapperFactory.CreateFor(context).Wrap(context);}public virtual void OnResultExecuted(ResultExecutedContext context){//no action}
}

这里传入了 context ,然后基于这个返回值来进行不同的操作:

public class AbpActionResultWrapperFactory : IAbpActionResultWrapperFactory
{public IAbpActionResultWrapper CreateFor(ResultExecutingContext actionResult){Check.NotNull(actionResult, nameof(actionResult));if (actionResult.Result is ObjectResult){return new AbpObjectActionResultWrapper(actionResult.HttpContext.RequestServices);}if (actionResult.Result is JsonResult){return new AbpJsonActionResultWrapper();}if (actionResult.Result is EmptyResult){return new AbpEmptyActionResultWrapper();}return new NullAbpActionResultWrapper();}
}

2.3 CSRF 防御组件

就继承自 MVC 的两个类型,然后重新做了一些判断逻辑进行处理,这里直接参考 AbpAutoValidateAntiforgeryTokenAuthorizationFilter 与 AbpValidateAntiforgeryTokenAuthorizationFilter 源码。

如果不太懂 AntiforgeryToken 相关的知识,可以参考 这一篇 博文进行了解。

2.4 多语言处理

针对于多语言的处理规则,其实在文章 《[Abp 源码分析]十三、多语言(本地化)处理》 就有讲解,这里只说明一下,在这个库里面通过 IApplicationBuilder 的一个扩展方法 UseAbpRequestLocalization() 注入的一堆多语言相关的组件。

public static void UseAbpRequestLocalization(this IApplicationBuilder app, Action<RequestLocalizationOptions> optionsAction = null)
{var iocResolver = app.ApplicationServices.GetRequiredService<IIocResolver>();using (var languageManager = iocResolver.ResolveAsDisposable<ILanguageManager>()){// 获得当前服务器支持的区域文化列表var supportedCultures = languageManager.Object.GetLanguages().Select(l => CultureInfo.GetCultureInfo(l.Name)).ToArray();var options = new RequestLocalizationOptions{SupportedCultures = supportedCultures,SupportedUICultures = supportedCultures};var userProvider = new AbpUserRequestCultureProvider();//0: QueryStringRequestCultureProvideroptions.RequestCultureProviders.Insert(1, userProvider);options.RequestCultureProviders.Insert(2, new AbpLocalizationHeaderRequestCultureProvider());//3: CookieRequestCultureProvideroptions.RequestCultureProviders.Insert(4, new AbpDefaultRequestCultureProvider());//5: AcceptLanguageHeaderRequestCultureProvideroptionsAction?.Invoke(options);userProvider.CookieProvider = options.RequestCultureProviders.OfType<CookieRequestCultureProvider>().FirstOrDefault();userProvider.HeaderProvider = options.RequestCultureProviders.OfType<AbpLocalizationHeaderRequestCultureProvider>().FirstOrDefault();app.UseRequestLocalization(options);}
}

这些组件都存放在 Abp.AspNetCore 库下面的 Localization 文件夹里面。

作者:myzony

出处:https://www.cnblogs.com/myzony/p/9993386.html

公众号“码侠江湖”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!

扫描二维码

获取更多精彩

码侠江湖


喜欢就点个在看再走吧

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/304946.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从头到尾彻底理解傅里叶变换算法(下)

从头到尾彻底理解傅里叶变换算法&#xff08;上&#xff09;&#xff0c;请看今天第一条。 以下继续&#xff1a; 第三章、复数 复数扩展了我们一般所能理解的数的概念&#xff0c;复数包含了实数和虚数两部分&#xff0c;利用复数的形式可以把由两个变量表示的表达式变成由一个…

树莓派安装python3.5_梦见树_周公解梦梦到树是什么意思_做梦梦见树好不好_周公解梦官网...

梦见树是什么意思&#xff1f;做梦梦见树好不好&#xff1f;梦见树有现实的影响和反应&#xff0c;也有梦者的主观想象&#xff0c;请看下面由(周公解梦官网www.zgjm.org)小编帮你整理的梦见树的详细解说吧。树主健康&#xff0c;树笔直挺拔&#xff0c;象征着人的健康。 在梦中…

[Abp 源码分析]后台作业与后台工作者

点击上方蓝字关注我们0. 简介在某些时候我们可能会需要执行后台任务&#xff0c;或者是执行一些周期性的任务。比如说可能每隔 1 个小时要清除某个临时文件夹内的数据&#xff0c;可能用户会要针对某一个用户群来群发一组短信。前面这些就是典型的应用场景&#xff0c;在 Abp 框…

【转】x.509证书在WCF中的应用(CS篇)

【转自】x.509证书在WCF中的应用(CS篇) 为什么要用x.509证书? WCF的服务端和客户端之间&#xff0c;如 果不作任何安全处理(即服务端的<security mode"None">)&#xff0c;则所有传输的消息将以明文方式满天飞&#xff0c;在internet/intranet环境下无疑是很不…

从概念到案例:初学者须知的十大机器学习算法

本文先为初学者介绍了必知的十大机器学习&#xff08;ML&#xff09;算法&#xff0c;并且我们通过一些图解和实例生动地解释这些基本机器学习的概念。我们希望本文能为理解机器学习基本算法提供简单易读的入门概念。 机器学习模型 在《哈佛商业评论》发表「数据科学家是 21 世…

测试Live Write的插件

1、文字竖排&#xff1a; 删除&#xff0c;只因首页显示时太占空间。2、酷表情&#xff1a;3、Rhapsody SongI am listening to Sad Songs And Waltzes by Cake . Rhapsody.

手把手教你用7行代码实现微信聊天机器人 -- Python wxpy

环境要求&#xff1a; Windows / Linux / Mac OS Python 3.4-3.6&#xff0c;以及 2.7 版本 wxpy安装 ## 使用国内源安装速度快 pip install -U wxpy -i "https://pypi.doubanio.com/simple/" 实例 让机器人与所有好友聊天 from wxpy import * # 实例化&#xff0c;并…

Dapr 已在塔架就位 将发射新一代微服务

微服务是云原生架构的核心&#xff0c;通常使用Kubernetes 来按需管理服务扩展。微软一直走在 Cloud Native Computing Foundation的 最前沿&#xff0c;并通过使用Kubernetes来支持其超大规模Azure和其混合云Azure Stack&#xff0c;微软对云原生的投资一部分来自其工具&#…

python 复制文件_10 行 Python 代码写 1 个 USB 病毒

(给Python开发者加星标&#xff0c;提升Python技能)转自&#xff1a; 知乎-DeepWeaver昨天在上厕所的时候突发奇想&#xff0c;当你把usb插进去的时候&#xff0c;能不能自动执行usb上的程序。查了一下&#xff0c;发现只有windows上可以&#xff0c;具体的大家也可以搜索(搜索…

html5中外描边怎么写,CSS3实现文字描边的2种方法(小结)

问题最近遇到一个需求&#xff0c;需要实现文字的描边效果&#xff0c;如下图解决方法一首先想到去看CSS3有没有什么属性可以实现&#xff0c;后来被我找到了text-stroke该属性是一个复合属性&#xff0c;可以设置文字宽度和文字描边颜色该属性使用很简单&#xff1a;text-stro…

混凝土墙开洞_满城混凝土柱子切割资质齐全

满城混凝土柱子切割资质齐全专业楼板切割开洞&#xff0c;钢筋混凝土墙开门&#xff0c;开窗&#xff0c;开方洞。混泥土承重墙新开门洞、开窗、通风管道开洞、专业开楼梯口&#xff0c;楼梯口加固&#xff0c;地下室开门洞&#xff0c;水泥墙开门加固、楼板加固、砖墙开门开窗…

马云害怕的事还是发生了

当前&#xff0c;余额宝的收益维持在4%左右不能突破&#xff0c;只能用作“钱包”放点零钱了。 放银行或者余额宝收益偏低&#xff0c;股票市场又处于震荡周期&#xff0c;期货市场等不是普通人进得去的&#xff0c;还不如直接买较高收益的互联网理财产品。 比如屡受政策利好…

cmosfixr插件怎么用_3dmax插件神器|怎么用3dmax插件神器去完成背景墙的效果图设计?...

又到3dmax插件神器的小课堂时间了&#xff01;小伙伴们还记得之前几张的知识点吗&#xff1f;如果不记得自己去温习&#xff0c;温故而知新哦&#xff01;如果学会了&#xff0c;下面学习3dmax插件神器小技巧的第四章建模篇的第4.16小节&#xff1a;怎么用3dmax插件神器去完成背…

不懂这25个名词,好意思说你懂大数据?

如果你刚接触大数据&#xff0c;你可能会觉得这个领域很难以理解&#xff0c;无从下手。近日&#xff0c;Ramesh Dontha在DataConomy上连发两篇文章&#xff0c;扼要而全面地介绍了关于大数据的75个核心术语&#xff0c;这不仅是大数据初学者很好的入门资料&#xff0c;对于高阶…

ab压力测试_Apache ab压力测试的知识点

Apache-ab是著名的Web服务器软件Apache附带的一个小工具&#xff0c;它可以模拟多个并发请求&#xff0c;测试服务器的最大承载压力。ab 是apachebench的缩写,ab命令会创建多个并发访问线程&#xff0c;模拟多个访问者同时对某一URL地址进行访问。它的测试目标是基于URL的&…

现代云原生设计理念

前文传送门什么是云原生&#xff1f;现代设计理念你会如何设计云原生应用程序&#xff1f;需要遵循哪些原则、模式和最佳实践&#xff1f;需要特别关注哪些底层/操作&#xff1f;十二要素应用程序目前被普遍认可的基于云的方法论是"十二要素应用程序"&#xff0c;它给…

NFS服务器架设篇

大家好&#xff0c;本周我们的课程是NFS服务器的架设。下面我们分几个部分来介绍NFS服务器。一、NFS简介NFS是分布式计算机系统的一部分&#xff0c;一般在用unix和类unix的系统上实现文件的传输。而且可以把NFS服务器共享的目录挂载到本地&#xff0c;使用cp&#xff0c;cd&am…

用画小狗的方法来解释Java中的值传递

在开始看我画小狗之前&#xff0c;咱们先来看道很简单的题目&#xff1a; 下面程序的输出是什么&#xff1f; 如果你的回答是“小强”&#xff0c;好&#xff0c;恭喜你答对了。下面我们改一下代码&#xff1a; 是的&#xff0c;我只是在changeName方法里面加了一句代码 这一次…

gif分解工具_Python之GIF图倒放,沙雕快乐源泉

GIF图现在已经融入了我们的日常网络生活&#xff0c;微信群、QQ群、朋友圈......一言不合就斗图&#xff0c;你怕了吗&#xff1f;不用担心&#xff0c;只要学会了Python之GIF倒放技能&#xff0c;你就是“斗图王”。咱们直接开始本文的内容&#xff01;使用的工具1PIL(Python …

微软亚洲研究院全球院友线上欢聚,共话新春

金鼠辞旧岁&#xff0c;金牛报春时&#xff1b;万象正更新&#xff0c;乾坤喜气多。西雅图时间 2 月 6 日&#xff0c;北京时间 2 月 7 日&#xff0c;由微软亚洲研究院院友会西雅图分会主办的“牛转新运”院友新春线上茶话会圆满落幕。重量级嘉宾沈向洋、洪小文、张亚勤、张宏…