一、前言

从 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. 手写 new

    /* 基于内置的 new 关键词,我们可以创建 Dog 的一个实例 zhangsan ,实例可以调用原型上的属性和方法 需求:自己实现一个 _new 方法,也可以模拟出内置 new 后的结果 */ / ...

  2. 【Hadoop】Hadoop的安装,本地模式、伪分布模式的配置

    Download hadoop-2.7.7.tar.gz 下载稳定版本的hadoop-2.7.7.tar.gz(我用的2.6.0,但是官网只能下载2.7.7的了) Required Software ...

  3. Redis 的原理与应用场景及数据库关系

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 一. Redis 是什么? Redis是一个开源的使用ANSIC语言编写.支持网络.单进程单线程.可基于 ...

  4. Java实现 LeetCode 503 下一个更大元素 II

    503. 下一个更大元素 II 给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素.数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大 ...

  5. Java实现 LeetCode 467 环绕字符串中唯一的子字符串

    467. 环绕字符串中唯一的子字符串 把字符串 s 看作是"abcdefghijklmnopqrstuvwxyz"的无限环绕字符串,所以 s 看起来是这样的:"-zabc ...

  6. Java实现蓝桥杯VIP 算法训练 P0504

    试题 算法训练 P0504 资源限制 时间限制:1.0s 内存限制:256.0MB Anagrams指的是具有如下特性的两个单词:在这两个单词当中,每一个英文字母(不区分大小写)所出现的次数都是相同的 ...

  7. java中Condition类的详细介绍(详解)

    已找不到原文了,还望原文博主看到能告诉小白一下,一定标注原文地址 一 condition 介绍及demo Condition是在java 1.5中才出现的,它用来替代传统的Object的wait(). ...

  8. Java实现第九届蓝桥杯螺旋折线

    螺旋折线 题目描述 如图p1.pgn所示的螺旋折线经过平面上所有整点恰好一次. 对于整点(X, Y),我们定义它到原点的距离dis(X, Y)是从原点到(X, Y)的螺旋折线段的长度. 例如dis(0 ...

  9. 本地存储 localStorage

    本地存储localStorage 概念:window对象下面的属性,html5新增的,将5M大小的数据存储本地的浏览器上面. 浏览器支持存储5M大小 本地存储localStorage特点 本地存储属于 ...

  10. 利用tcpdump命令统计http的GET和POST请求

    1.搭建的知识库服务器, 需要统计来访者都是哪些人,因为系统不是自己开发的,看不到访问日志.所以考虑从系统层面抓取访问流量来实现. 2.通过tcpdump抓取的数据包,在wireshark中打开发现, ...