如何在 asp.net core 3.x 的 startup.cs 文件中获取注入的服务
一、前言
从 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 文件中获取注入的服务的更多相关文章
- asp.net core 发布到 docker 容器时文件体积过大及服务端口的配置疑问
在 asp.net core 发布时,本人先后产生了3个疑问. 1.发布的程序为什么不能在docker容器中运行 当时在window开发环境中发布后,dotnet xxx.dll可以正常运行:但放入d ...
- HTML控件ID和NAME属性的区别,以及如何在asp.net页面的.CS文件中获得.ASPX页面中HTML控件的值
在html中:name指的是用户名称,ID指的是用户注册是系统自动分配给用户的一个序列号. name是用来提交数据的,提供给表单用,可以重复: id则针对文档操作时候用,不能重复.如:document ...
- 如何在ASP.NET Core中实现CORS跨域
注:下载本文的完整代码示例请访问 > How to enable CORS(Cross-origin resource sharing) in ASP.NET Core 如何在ASP.NET C ...
- 如何在ASP.NET Core中实现一个基础的身份认证
注:本文提到的代码示例下载地址> How to achieve a basic authorization in ASP.NET Core 如何在ASP.NET Core中实现一个基础的身份认证 ...
- 如何在ASP.NET Core中应用Entity Framework
注:本文提到的代码示例下载地址> How to using Entity Framework DB first in ASP.NET Core 如何在ASP.NET Core中应用Entity ...
- [转]如何在ASP.NET Core中实现一个基础的身份认证
本文转自:http://www.cnblogs.com/onecodeonescript/p/6015512.html 注:本文提到的代码示例下载地址> How to achieve a bas ...
- 如何在ASP.NET Core中使用Azure Service Bus Queue
原文:USING AZURE SERVICE BUS QUEUES WITH ASP.NET CORE SERVICES 作者:damienbod 译文:如何在ASP.NET Core中使用Azure ...
- 如何在ASP.NET Core中自定义Azure Storage File Provider
文章标题:如何在ASP.NET Core中自定义Azure Storage File Provider 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p ...
- 如何在ASP.NET Core中使用JSON Patch
原文: JSON Patch With ASP.NET Core 作者:.NET Core Tutorials 译文:如何在ASP.NET Core中使用JSON Patch 地址:https://w ...
随机推荐
- 曹工说Spring Boot源码(29)-- Spring 解决循环依赖为什么使用三级缓存,而不是二级缓存
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- api.versioning 版本控制 自动识别最高版本
Microsoft.AspNetCore.Mvc.Versioning //引入程序集 .net core 下面api的版本控制作用不需要多说,可以查阅https://www.cnblogs.com/ ...
- Centos7快速安装RocketMQ
1. 为什么要用MQ 消息队列是一种"先进先出"的数据结构 其应用场景主要包含以下3个方面 应用解耦 系统的耦合性越高,容错性就越低.以电商应用为例,用户创建订单后,如果耦合调用库 ...
- 处理npm安装模块报错01
报错:Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules/nodemon_tmp' 解决:sudo cnpm in ...
- 源码分析 | 手写mybait-spring核心功能(干货好文一次学会工厂bean、类代理、bean注册的使用)
作者:小傅哥 博客:https://bugstack.cn - 汇总系列原创专题文章 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言介绍 一个知识点的学习过程基本分为:运行helloworld ...
- fedora gtk+ 2.0环境安装配置
1.安装gtk yum install gtk2 gtk2-devel gtk2-devel-docs 2.测试是否安装成功 pkg-config --cflags --libs gtk+-2.0 执 ...
- 关于Integer类的值使用==比较
题记:前几天面试Java基础给来了个面试题Integer a=100,b=100;System.out.println(a==b); 当时回答是true,后来面试官又来了一个Integer a=200 ...
- Redis PHP扩展安装步骤
### 下载最新的扩展文件,解压并进入文件夹 wget https://codeload.github.com/phpredis/phpredis/tar.gz/2.2.7 tar -zxvf 2.2 ...
- 腾讯音乐Android工程师一面面试题记录,拿走不谢!
最近参加了一次鹅厂音乐Android工程师面试,这里凭记忆记录了一些一面的面试题,希望能帮到正在面试的你! 1.Java调用函数传入实际参数时,是值传递还是引用传递? 2.单例模式的DCL方式,为什么 ...
- numpy(深)复制一个矩阵的方法
在用Python写代码的时候往往会遇到真复制和假复制的问题,真复制就是创建一个新的实例(instance),而假复制就是把原对象的引用赋给了新的标志符.判断是不是真复制可以使用id()这个函数. 当然 ...