一、前言
從 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.0 的新增功能
-
Generic Host restricts Startup constructor injection
-
依賴注入模式
-
Avoiding Startup service injection in ASP.NET Core 3
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/6438.html
標籤:.NET Core
上一篇:基于.NetCore3.1系列 —— 認證授權方案之授權初識
下一篇:百萬級資料遷移方案測評小記
