一、前言

从 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 和错误日志,所以这里我需要将 ILoggerIHttpContextAccessor 注入到 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 版本后,你会发现程序会报如下的错误,很常见的一个依赖注入的错误,源头直指我们通过构造函数注入的 ILoggerIHttpContextAccessor 接口

2.2、解决方法

根本原因

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

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

为什么使用 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. Vue中导出Excel表格方法

    本文记录一下在Vue中实现导出Excel表格的做法.参考度娘上各篇博客,最后实现功能 Excel表格,我的后端返回的是数据流,然后文件名是放进了content-disposition中,前端进行获取. ...

  2. Java实现 LeetCode 236 二叉树的最近公共祖先

    236. 二叉树的最近公共祖先 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先. 百度百科中最近公共祖先的定义为:"对于有根树 T 的两个结点 p.q,最近公共祖先表示为一个结点 x ...

  3. 第八届蓝桥杯JavaA组省赛真题

    解题代码部分来自网友,如果有不对的地方,欢迎各位大佬评论 题目1.迷宫 题目描述 X星球的一处迷宫游乐场建在某个小山坡上. 它是由10x10相互连通的小房间组成的. 房间的地板上写着一个很大的字母. ...

  4. android日常开发总结的技术经验60条

    全部Activity可继承自BaseActivity,便于统一风格与处理公共事件,构建对话框统一构建器的建立,万一需要整体变动,一处修改到处有效. 数据库表段字段常量和SQL逻辑分离,更清晰,建议使用 ...

  5. js排他性算法

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  6. Jupyter的搭建

    在家实在无聊,伏案沉思良久,忽然灵机一动,何不写写Python?然而电脑上的软件早已人是物非,Pycharm已然不复存在.但是又不想装软件找激活码,于是,只好建个Jupyter先凑合一下. 1. 安装 ...

  7. 当小程序的flex布局遇到button时,justify-content不起作用的原因及解决方案

    当小程序的flex布局遇到button时 发现justify-content不起作用,无论怎么设置都是space-around的效果. 经过排查,发现原因是小程序button中的默认样式中的margi ...

  8. isinstance用法

    ''' 作用:来判断一个对象是否是一个已知的类型. 其第一个参数(object)为对象,第二个参数(type)为类型名(int...)或类型名的一个列表((int,list,float)是一个列表). ...

  9. numpy矩阵相加时需注意的一个点

    今天在进行numpy矩阵相加的时候出现了一个小的奇怪的地方,下面我们来看看: >>>P = np.array([1,2,3,4]) >>>F = np.array( ...

  10. numpy中的max和maximum

    numpy科学计算包中有两个函数np.max()和np.maximum(),他们的功能截然不同.简单而言即前者作用于ndarray对象,求的是它自身的最大.而后者是一个数学上的取$\max$的效果,它 ...