ASP.NET Core預設容器實現Controller的屬性注入

thpmcodjkbm發表於2021-02-04
  • 僅針對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中;
  • 在靜態代理類中訪問上一步新增到ServiceCollectionIControllerActivator具體實現:這個問題有點小麻煩。因為靜態代理類和原始的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;
    }
}

好了,可以看效果了

僅僅是個思路,僅供參考。程式碼

相關文章