一、前言

从 18 年开始接触 .NET Core 开始,在私底下、工作中也开始慢慢从传统的 mvc 前后端一把梭,开始转向 web api + vue,之前自己有个半成品的 asp.net core 2.2 的项目模板,最近几个月的时间,私下除了学习 Angular 也在对这个模板基于 asp.net core 3.1 进行慢慢补齐功能

因为涉及到底层框架大版本升级,由于某些 breaking changes 必定会造成之前的某些写法没办法继续使用,趁着端午节假期,在改造模板时,发现没办法通过构造函数注入的形式在 Startup 文件中注入某些我需要的服务了,因此本篇文章主要介绍如何在 asp.net core 3.x 的 startup 文件中获取注入的服务

二、Step by Step

2.1、问题案例

这个问题的发现源于我需要改造模型验证失败时返回的错误信息,如果你有尝试的话,在 3.x 版本中你会发现在 Startup 类中,我们没办法通过构造函数注入的方式再注入任何其它的服务了,这里仅以我的代码中需要解决的这个问题作为案例

在定义接口时,为了降低后期调整的复杂度,在接收参数时,一般会将参数包装成一个 dto 对象(data transfer object - 数据传输对象),不管是提交数据,还是查询数据,对于这个 dto 中的某些属性,都会存在一定的卡控,例如 xxx 字段不能为空了,xxx 字段的长度不能超过 30

而在 asp.net core 中,因为会自动进行模型验证,当不符合 dto 中的属性要求时,接口会自动返回错误信息,默认的返回信息如下图所示

可以看到,因为这里其实是按照 rfc7231这个 RFC 协议返回的错误信息,这个并不符合我的要求,因此这里我需要改写这个返回的错误信息

自定义 asp.net core 的模型验证错误信息方法有很多种,我的实现方法如下,因为我需要记录请求的标识 Id 和错误日志,所以这里我需要将 ILogger 和 IHttpContextAccessor 注入到 Startup 类中

/// <summary>
/// 修改模型验证错误返回信息
/// </summary>
/// <param name="services">服务容器集合</param>
/// <param name="logger">日志记录实例</param>
/// <param name="httpContextAccessor"></param>
/// <returns></returns>
public static IServiceCollection AddCustomInvalidModelState(this IServiceCollection services,
ILogger<Startup> logger, IHttpContextAccessor httpContextAccessor)
{
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = actionContext =>
{
// 获取验证不通过的字段信息
//
var errors = actionContext.ModelState.Where(e => e.Value.Errors.Count > 0)
.Select(e => new ApiErrorDto
{
Title = "请求参数不符合字段格式要求",
Message = e.Value.Errors.FirstOrDefault()?.ErrorMessage
}).ToList(); var result = new ApiReturnDto<object>
{
TraceId = httpContextAccessor.HttpContext.TraceIdentifier,
Status = false,
Error = errors
}; logger.LogError($"接口请求参数格式错误: {JsonConvert.SerializeObject(result)}"); return new BadRequestObjectResult(result);
};
}); return services;
}

在 asp.net core 2.x 版本中,你完全可以像在别的类中采用构造函数注入的方式一样直接注入使用

public class Startup
{
/// <summary>
/// 日志记录实例
/// </summary>
private readonly ILogger<Startup> _logger; /// <summary>
/// Http 请求实例
/// </summary>
private readonly IHttpContextAccessor _httpContextAccessor; /// <summary>
/// ctor
/// </summary>
/// <param name="configuration"></param>
/// <param name="logger"></param>
/// <param name="httpContextAccessor"></param>
public Startup(IConfiguration configuration, ILogger<Startup> logger, IHttpContextAccessor httpContextAccessor)
{
Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
} /// <summary>
/// 配置实例
/// </summary>
public IConfiguration Configuration { get; } /// <summary>
/// This method gets called by the runtime. Use this method to add services to the container.
/// </summary>
public void ConfigureServices(IServiceCollection services)
{
//注入的其它服务 // 返回自定义的模型验证错误信息
services.AddCustomInvalidModelState(_logger, _httpContextAccessor);
}
}

但是当你直接迁移到 asp.net core 3.x 版本后,你会发现程序会报如下的错误,很常见的一个依赖注入的错误,源头直指我们通过构造函数注入的 ILogger、IHttpContextAccessor 接口

2.2、解决方法

根本原因

通过查阅 stackoverflow 发现了这样的一个问题:How do I write logs from within Startup.cs,在最高赞的回答中提到了在泛型主机(GenericHostBuilder)中,没办法注入除 IConfiguration 之外的任何服务到 Startup类中,而泛型主机则是在 asp.net core 3.0 中添加的功能

查了下升级日志,从中可以看到,在泛型主机中, Startup 类的构造函数注入只支持 IHostEnvironment、 IWebHostEnvironment、IConfiguration ,嗯,不好好看别人文档的锅

为什么使用 WebHostBuilder可以,换成 GenericHostBuilder 就不行了呢

按照正常的逻辑来说,对于一个 asp.net core 应用,原则上来说只有有一个根级(root)的依赖注入容器,但是因为我们在 Startup 类中通过构造函数注入的形式注入服务时,告诉程序了我需要这个服务的实例,从而导致在构建 WebHost 时存在了一个单独的容器,并且这个容器只包含了我们需要使用到的服务信息,之后,因为会创建了一个包含完整服务的依赖注入容器,这里就会存在一个服务哪怕是单例的也可能会存在注册两次的问题,这无疑有些不太合乎规范

在推行泛型主机之后,严格控制了只会存在一个依赖注入容器,而所有的服务都是在 Startup.ConfigureServices 方法执行完成后才会注册到依赖注入容器中,因此没办法像之前一样在根容器注册完成之前通过构造函数注入的形式使用

解决方案

如果你需要在 Startup.Configure 方法中使用自定义的服务,因为这里已经完成了各种服务的注册,和之前一样,我们直接在方法签名中包含需要使用到的服务即可

public void Configure(IApplicationBuilder app, IHostEnvironment env, ILogger<Startup> logger)
{
logger.LogInformation("在 Configure 中使用自定义的服务");
}

如果你需要在 Startup.ConfigureServices 中使用的话,则需要换一种方法

最简单的方法,直接替换泛型主机为原来的 WebHostBuilder,这样就可以直接在 Startup 类中注入各种服务接口了,不过,考虑到这一改动其实是在开倒车,所以这里不推荐采用这种方法

既然没办法正向通过依赖注入容器来自动创建我们需要的服务实例,是不是可以通过服务容器,手动去获取我们需要的服务,也就是被称为服务定位(Service Locator)的方式来获取实例

当然,这似乎与依赖注入的思想相左,对于依赖注入来说,我们将所有需要使用的服务定义好,在应用启动前完成注册,之后在使用时由依赖注入容器提供服务的实例即可,而服务定位则是我们已经知道存在这个服务了,从容器中获取出来然后由自己手动的创建实例

虽然服务定位是一种反模式,但是在某些情况下,我们又不得不采用

这里对于本篇文章开篇中需要解决的问题,我也是采用服务定位的方式,通过构建一个 ServiceProvider 之后,手动的从容器中获取需要使用的服务实例,调整后的代码如下

/// <summary>
/// 添加自定义模型验证失败时返回的错误信息
/// </summary>
/// <param name="services">服务容器集合</param>
/// <returns></returns>
public static IServiceCollection AddCustomInvalidModelState(this IServiceCollection services)
{
// 构建一个服务的提供程序
var provider = services.BuildServiceProvider(); // 获取需要使用的服务实例
//
var logger = provider.GetRequiredService<ILogger<Startup>>();
var httpContextAccessor = provider.GetRequiredService<IHttpContextAccessor>(); services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = actionContext =>
{
// 获取失败信息
//
var errors = actionContext.ModelState.Where(e => e.Value.Errors.Count > 0)
.Select(e => new ApiErrorMessageDto
{
Title = "Request parameters do not meet the field requirements",
Message = e.Value.Errors.FirstOrDefault()?.ErrorMessage
}).ToList(); var result = new ApiResponseDto<object>
{
TraceId = httpContextAccessor.HttpContext.TraceIdentifier,
Status = false,
Error = errors
}; logger.LogError($"接口请求参数格式错误: {JsonSerializer.Serialize(result)}"); return new BadRequestObjectResult(result);
};
}); return services;
}

对于配置一些需要基于某些服务的服务,这里也可以通过委托的形式获取到需要使用的服务实例,示例代码如下

public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IMyService>((container) =>
{
var logger = container.GetRequiredService<ILogger<MyService>>();
return new MyService
{
Logger = logger
};
});
}

三、参考资料

如何在 asp.net core 3.x 的 startup.cs 文件中获取注入的服务的更多相关文章

  1. asp.net core 发布到 docker 容器时文件体积过大及服务端口的配置疑问

    在 asp.net core 发布时,本人先后产生了3个疑问. 1.发布的程序为什么不能在docker容器中运行 当时在window开发环境中发布后,dotnet xxx.dll可以正常运行:但放入d ...

  2. HTML控件ID和NAME属性的区别,以及如何在asp.net页面的.CS文件中获得.ASPX页面中HTML控件的值

    在html中:name指的是用户名称,ID指的是用户注册是系统自动分配给用户的一个序列号. name是用来提交数据的,提供给表单用,可以重复: id则针对文档操作时候用,不能重复.如:document ...

  3. 如何在ASP.NET Core中实现CORS跨域

    注:下载本文的完整代码示例请访问 > How to enable CORS(Cross-origin resource sharing) in ASP.NET Core 如何在ASP.NET C ...

  4. 如何在ASP.NET Core中实现一个基础的身份认证

    注:本文提到的代码示例下载地址> How to achieve a basic authorization in ASP.NET Core 如何在ASP.NET Core中实现一个基础的身份认证 ...

  5. 如何在ASP.NET Core中应用Entity Framework

    注:本文提到的代码示例下载地址> How to using Entity Framework DB first in ASP.NET Core 如何在ASP.NET Core中应用Entity ...

  6. [转]如何在ASP.NET Core中实现一个基础的身份认证

    本文转自:http://www.cnblogs.com/onecodeonescript/p/6015512.html 注:本文提到的代码示例下载地址> How to achieve a bas ...

  7. 如何在ASP.NET Core中使用Azure Service Bus Queue

    原文:USING AZURE SERVICE BUS QUEUES WITH ASP.NET CORE SERVICES 作者:damienbod 译文:如何在ASP.NET Core中使用Azure ...

  8. 如何在ASP.NET Core中自定义Azure Storage File Provider

    文章标题:如何在ASP.NET Core中自定义Azure Storage File Provider 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p ...

  9. 如何在ASP.NET Core中使用JSON Patch

    原文: JSON Patch With ASP.NET Core 作者:.NET Core Tutorials 译文:如何在ASP.NET Core中使用JSON Patch 地址:https://w ...

随机推荐

  1. 【JVM】关于OOM的二三事

    组织架构 严格来说,StackOverflowError和OutOfMemoryError都属于错误,而不是异常. java.lang.StackOverflowError public class ...

  2. Java实现蓝桥杯模拟组织晚会

    问题描述 小明要组织一台晚会,总共准备了 n 个节目.然后晚会的时间有限,他只能最终选择其中的 m 个节目. 这 n 个节目是按照小明设想的顺序给定的,顺序不能改变. 小明发现,观众对于晚上的喜欢程度 ...

  3. Java实现 LeetCode 338 比特位计数

    338. 比特位计数 给定一个非负整数 num.对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回. 示例 1: 输入: 2 输出: [0,1,1 ...

  4. Java实现 LeetCode 260 只出现一次的数字 III(三)

    260. 只出现一次的数字 III 给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次. 找出只出现一次的那两个元素. 示例 : 输入: [1,2,1,3,2,5] 输出 ...

  5. Java实现 蓝桥杯 算法提高 矩形靶

    试题 算法提高 矩形靶 资源限制 时间限制:1.0s 内存限制:256.0MB 问题描述 在矩形的世界里任何事物都是矩形的,矩形的枪靶,甚至矩形的子弹.现在给你一张NM的枪靶,同时告诉你子弹的大小为( ...

  6. 总结:Jmeter常用参数化方式

    一.从CSV文件中读取 二.通过函数生成 三.用户自定义变量 四.用户参数 五.使用正则表达式提取 六.从数据库中读取

  7. (四)SQLMap之Tamper篡改脚本的类型、作用、适用场景

    目录结构 一.SQLMap中tamper的简介 1.tamper的作用 2.tamper用法 二.适配不同数据库类型的测试tamper 三.SQLMap中tamper篡改脚本的功能解释 一.SQLMa ...

  8. Python 报错:NameError: name 'ctypes' is not defined

    使用python中的ctypes模块可以很方便的调用windows的dll(也包括linux下的so等文件) 引入ctypes库 有两种方法 from ctypes import * import c ...

  9. 【php】 jsonp转数组函数jsonp_decode

    分享一个可以跟json一样用的函数jsonp_decode,能把jsonp格式数据转为php数组或对象. /**  * 把jsonp转为php数组  * @param string $jsonp js ...

  10. 【代理】内网穿透工具 frp&frps

    frp 是一个高性能的反向代理应用,可以帮助您轻松地进行内网穿透,对外网提供服务,支持 tcp, http, https 等协议类型,并且 web 服务支持根据域名进行路由转发. ### frp 的作 ...