改用 ASP.NET Core 後,DI 已成日常(延伸閱讀:不可不知的 ASP.NET Core 依賴注入),我漸漸習慣將共用程式、元件寫成服務,在 Program.cs 中用 builder.Services.AddSingleton<T>()、AddTransient<T>() 註冊,在其他服務、Controller 宣告成建構式參數透過 DI 取得實體。

最近遇到變化球。一般 Controller 都是單一建構式,如果有一個以上的建構式:

using di_test.Models;
using Microsoft.AspNetCore.Mvc;

namespace di_test.Controllers
{
    public class MultiConstructorController : Controller
    {
        private readonly ILogger<MultiConstructorController> _logger;
        private readonly string dataPath;

        public MultiConstructorController(ILogger<MultiConstructorController> logger, 
                IHostEnvironment environment)
        {
            _logger = logger;
            dataPath = environment.ContentRootPath;
        }

        public MultiConstructorController(string path) {
            dataPath = path;
        }

        public IActionResult Index()
        {
            return Content($"OK-{dataPath}");
        }
    }
}

MVC Action 將出現錯誤:

An unhandled exception occurred while processing the request.
InvalidOperationException: Multiple constructors accepting all given argument types have been found in type 'di_test.Controllers.MultiConstructorController'. There should only be one applicable constructor.
Microsoft.Extensions.DependencyInjection.ActivatorUtilities.TryFindMatchingConstructor(Type instanceType, Type[] argumentTypes, ref ConstructorInfo matchingConstructor, ref Nullable[] parameterMap)

原因是 IServiceProvider 不知道要挑選哪個建構式建立物件,解決方法是在供 DI 使用的建構式加上 [ActivatorUtilitiesConstructor],寫成:

[2023-12-12 更新] .NET 8 有 Breaking Change,[ActivatorUtilitiesConstructor] 已失效,改以參數較多者為準。

[ActivatorUtilitiesConstructor]
public MultiConstructorController(ILogger<MultiConstructorController> logger, 
        IHostEnvironment environment)
{
    _logger = logger;
    dataPath = environment.ContentRootPath;
}

除了 Controller,註冊到 DI 的服務若有多建構式,也可能會出現問題。

用以下服務舉例,MyService 有三個建構式:

public class MyService
{
    private readonly string dataPath;
    public MyService(IHostEnvironment hostEnvironment)
    {
        dataPath = hostEnvironment.ContentRootPath;
    }

    public MyService(string path)
    {
        dataPath = path;
    }

    public MyService(Constants constants)
    {
        dataPath = constants.TempDataPath;
    }

    public string GetDataPath() => dataPath;
}

public class Constants
{
    public string TempDataPath => Path.GetTempPath();
}

有趣的是,若單純 .AddSingleton() 註冊 MyService 沒問題,DI 會挑選 IHostEnvironment 參數建構式建立物件:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddSingleton<MyService>();

var app = builder.Build();

但如果 Constants 也被註冊到 DI,在 builder.Build() 時會出錯:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddSingleton<Constants>();
builder.Services.AddSingleton<MyService>();

var app = builder.Build();

由 Stacktrace 可發現它的出錯點跟 Controller 不同,Controller 是爆在 ActivatorUtilities.TryFindMatchingConstructor(),而這裡是爆在 ServiceLookup.CallSiteFactory.CreateConstructorCallSite()。

Exception has occurred: CLR/System.AggregateException
An unhandled exception of type 'System.AggregateException' occurred in Microsoft.Extensions.DependencyInjection.dll: 'Some services are not able to be constructed'
Inner exceptions found, see $exception in variables window for more details.
Innermost exception System.InvalidOperationException : Unable to activate type 'di_test.Models.MyService'. The following constructors are ambiguous:
Void .ctor(Microsoft.Extensions.Hosting.IHostEnvironment)
Void .ctor(di_test.Models.Constants)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, Type serviceType, Type implementationType, CallSiteChain callSiteChain)

視建構式變數以及適合的建立方式,有一些解法:

  1. AddSignleton() 時指定建構式建立物件:

    builder.Services.AddSingleton<MyService>(sp => new MyService("..."));
    // 或
    builder.Services.AddSingleton<MyService>(sp => new MyService(sp.GetRequiredService<Constants>()));
    
  2. 仿效 Controller 做法,在期望 DI 使用的建構式加上 [ActivatorUtilitiesConstructor],並使用 ActivatorUtilities.CreateInstance<T>() 配合 IServiceProvider 解析類別建立物件:

    builder.Services.AddSingleton<MyService>(sp => ActivatorUtilities.CreateInstance<MyService>(sp));
    

    實測若不指定 [ActivatorUtilitiesConstructor],ActivatorUtilities.CreateInstance 會取第一個建構式。

以上 ASP.NET Core DI 面對多建構式的小技巧,應能滿足一般簡單應用。

Tips of how to handle multiple constructor type in ASP.NET Core DI.


Comments

Be the first to post a comment

Post a comment