1. 程式人生 > 實用技巧 >如何在 asp.net core 3.x 的 startup.cs 檔案中獲取注入的服務

如何在 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 和錯誤日誌,所以這裡我需要將 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
};
});
}

三、參考資料