- 僅針對Controller的屬性注入;
- 使用預設容器,不依賴第三方庫;
故事背景
閒來無事給專案做優化,發現大多數Controller裡面都會用到Logger和AutoMapper,每個Controller都建構函式注入,感覺重複勞動太多了,ASP.NET Core預設容器也並不支援屬性注入。寫到基類裡面通過構造注入ServiceProvider
進行獲取?這樣每次都要傳遞ServiceProvider,也並不方便。嗯。。。有沒有辦法使用預設容器實現Controller的屬性注入呢。本著有問題先搜一搜的態度,找到了一篇相關文章。
文中使用替換IControllerActivator
,並將Controller手動新增到DI容器(可以直接使用AddControllersAsServices
擴充方法)的方式實現了屬性注入。也就是說必須使用從DI獲取Controller的方式,才能使用這種屬性注入的方式。不過之前看過一篇文章(找不到了。。。)裡面的回覆,大意為ASP.NET Core預設不使用DI獲取Controller,是因為DI容器構建完成後就不能變更了,但是Controller是可能有動態載入的需求的
。嗯。。。本著最大相容性的考慮,看看有沒有不干涉Controller建立方式
也能實現這個功能的辦法。
想了個邏輯
首先確認目的:在不改變ControllerActivator功能的前提下,為其生成的Controller設定屬性
。具體的邏輯就有點繞了:我們需要實現一個設定Controller屬性的IControllerActivator
(一個靜態代理類),並替換掉DI容器中的IControllerActivator
宣告,但仍然需要DI容器構建之前已宣告的ControllerActivator
(因為我們不知道具體如何構建),並且在我們實現的IControllerActivator
中訪問(作為被代理的物件)。
針對每個邏輯:
- 實現一個設定Controller屬性的
IControllerActivator
靜態代理:這個很好寫; - 用靜態代理類替換掉DI容器中的
IControllerActivator
宣告:這個也很簡單; - DI容器保留之前已宣告的
ControllerActivator
:這個可以直接將IControllerActivator
宣告中的實現型別作為服務新增到ServiceCollection
中; - 在靜態代理類中訪問上一步新增到
ServiceCollection
的IControllerActivator
具體實現:這個問題有點小麻煩。因為靜態代理類和原始的IControllerActivator
實現類都需要使用DI容器構建,那麼靜態代理類只能通過建構函式注入IControllerActivator
實現類,但是我們沒辦法在寫程式碼的時候就確定要注入的型別,也不能直接依賴IControllerActivator
(自己依賴自己了)。想來想去。。。嗯。。。我們可以使用泛型實現代理類的邏輯,並在新增服務描述時動態生成一個泛型型別。
寫程式碼
首先需要一個ASP.NET Core
專案。
關鍵程式碼
嗯。。屬性注入的關鍵點,只要有ServiceProvider
,其它的就好說了。
- 那就先寫一個
IServiceProviderSetter
放在這裡
using System;
public interface IServiceProviderSetter
{
void SetServiceProvider(IServiceProvider serviceProvider);
}
- 實現泛型靜態代理
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
public class ControllerActivatorProxy<TControllerActivator> : IControllerActivator where TControllerActivator : IControllerActivator
{
private readonly TControllerActivator _originalControllerActivator;
public ControllerActivatorProxy(TControllerActivator originalControllerActivator)
{
//引用原始ControllerActivator
_originalControllerActivator = originalControllerActivator;
}
public object Create(ControllerContext context)
{
//使用原始ControllerActivator建立controller
var controller = _originalControllerActivator.Create(context);
if (controller is IServiceProviderSetter serviceProviderSetter)
{
//具體要幹什麼由controller內部決定
serviceProviderSetter.SetServiceProvider(context.HttpContext.RequestServices);
}
return controller;
}
public void Release(ControllerContext context, object controller)
{
_originalControllerActivator.Release(context, controller);
}
}
- 替換服務描述,保留之前已宣告的
ControllerActivator
。為了方便,寫個擴充方法吧。
using System.Linq;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
public static class AutowiredControllerServiceProviderExtensions
{
public static void AutowiredControllerServiceProvider(this IMvcBuilder mvcBuilder)
{
var services = mvcBuilder.Services;
//查詢原始服務描述
var serviceDescriptor = services.Where(m => m.ServiceType == typeof(IControllerActivator)).First();
var existedServiceType = serviceDescriptor.ImplementationType;
//動態建立代理型別
var controllerActivatorType = typeof(ControllerActivatorProxy<>).MakeGenericType(existedServiceType);
//將原始實現直接新增到services中
services.Add(ServiceDescriptor.Describe(existedServiceType, existedServiceType, serviceDescriptor.Lifetime));
//替換IControllerActivator服務為使用動態構建的代理型別
services.Replace(ServiceDescriptor.Describe(typeof(IControllerActivator), controllerActivatorType, serviceDescriptor.Lifetime));
}
}
嗯。。這好像有點有趣的地方。。
關鍵程式碼寫完了,接下來是使用
- 建立一個
Controller
基類並應用屬性
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
[ApiController]
[Route("api/[controller]")]
public class TestBaseController : ControllerBase, IServiceProviderSetter
{
//logger只是個示例,僅在使用時才建立,實際還可以直接獲取,或者使用Lazy<T>等各種操作。其它屬性同理。
private ILogger _logger;
private IServiceProvider _serviceProvider;
protected ILogger Logger
{
get
{
if (_logger != null)
{
return _logger;
}
_logger = _serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(GetType());
return _logger;
}
}
[NonAction]
public void SetServiceProvider(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
}
- 在
Startup
中配置使用
public void ConfigureServices(IServiceCollection services)
{
//使用預設Controller生成方式
services.AddControllers().AutowiredControllerServiceProvider();
//使用DI生成Controller方式
//services.AddControllers().AddControllersAsServices().AutowiredControllerServiceProvider();
}
- 示例
Controller
,這裡就直接使用預設生成的Controller
示範了
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
public class WeatherForecastController : TestBaseController
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
Logger.LogInformation("Start");
var rng = new Random();
var result = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
Logger.LogInformation("End");
return result;
}
}
好了,可以看效果了
僅僅是個思路,僅供參考。程式碼