ASP.NET Core DI 之多建構式問題
0 | 6,195 |
改用 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)
視建構式變數以及適合的建立方式,有一些解法:
AddSignleton() 時指定建構式建立物件:
builder.Services.AddSingleton<MyService>(sp => new MyService("...")); // 或 builder.Services.AddSingleton<MyService>(sp => new MyService(sp.GetRequiredService<Constants>()));
仿效 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