當有一個介面(抽象),有多個實作(細節),在 Autofac 和 Unity 預設都有相關的解決方案,可以注入到屬性、方法、建構函數,這都需要使用特殊的 Attribute 來描述參數,比如 Autofac 的 [KeyFilter]、Unity 的 [Dependency],這將會讓你的物件變得不單純,必須要特殊的用法才會工作,接下來我會分享實作的方式,要怎麼選就看你了
微軟的 DI Container (Microsoft.Extensions.DependencyInjection),目前只有支援建構函數注入
開發環境
- Rider 2021.1.2
- NET 5
- Autofac.Extensions.DependencyInjection 7.1.0
- Unity.Microsoft.DependencyInjection 5.11.5
問題
我有一個 IFileProvider,分別有 FileProvider、ZipFileProvider
public interface IFileProvider
{
string Print();
}
public class FileProvider : IFileProvider
{
public string Print()
{
var msg = "FileProvider";
Console.WriteLine(msg);
return msg;
}
}
public class ZipFileProvider : IFileProvider
{
public string Print()
{
var msg = "ZipFileProvider";
Console.WriteLine(msg);
return msg;
}
}
物件依賴抽象
public class FileAdapter
{
private readonly IFileProvider _fileProvider;
public FileAdapter(IFileProvider fileProvider)
{
this._fileProvider = fileProvider;
}
public string Get()
{
return this._fileProvider.Print();
}
}
你可能會這樣註冊
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSingleton<IFileProvider, ZipFileProvider>();
services.AddSingleton<IFileProvider, FileProvider>();
}
有經驗的一看就知道,在容器註冊相同的 Key,也就是 typeof(IFileProvider),假如有人依賴 IFileProvider 時,容器選用注入的執行個體會是你最後註冊的那一個。
依照上面的範例 ServiceProvider.GetService<IFileProvider> 取出來的實例永遠都是 FileProvider,該怎麼取得 ZipFileProvider 實例呢?小孩子才做選擇,可以通通都要嗎?
解決方案
先問問你自己,需不需要由外部決定要使用哪一個實作?
- 不需要,手動建立,直接註冊正確的實作,調用端直接取得。
- 需要,由調用端點透過 key 來取得實作。
開始之前,我先新增一個 .NET 5 測試專案,名為 NET5.TestProject,安裝以下套件
dotnet add package Autofac.Extensions.DependencyInjection --version 7.1.0
dotnet add package Unity.Microsoft.DependencyInjection --version 5.11.5
dotnet add package Microsoft.AspNetCore.TestHost --version 5.0.6
接下來我會使用使用 ASP.NET Core 來當範例,Controller 的生命由 ASP.NET Core 框架來決定,我們不可以隨便自己 new Controller,必須要透過 DI Container;為了方便每次執行時使用不同的組態,我選擇用測試專案 + TestServer。
請參考 [ASP.NET Core 3] 利用 TestServer 進行 Web API 測試
新增 Startup.cs,這個是 ASP.NET Core 5 預設的檔案
public class Startup
{
public Startup(IConfiguration configuration)
{
this.Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}
手動建立註冊資訊
DI Container 沒有厲害到可以知道你現在想要使用哪一個實作(執行個體),我們得好好的跟他溝通,
AddSingleton 裡面放的匿名委派有延遲執行的效果,雖然裡面寫的 new DefaultController 並不會馬上執行,要等到 Request 建立起來的時候才會執行
s.AddSingleton(p =>
{
var fileProvider = p.GetService<ZipFileProvider>();
var logger = p.GetService<ILogger<DefaultController>>();
return new DefaultController(logger, fileProvider);
});
還要跟 DI Conttainer 講,要用自訂的 Controller
services.AddControllers().AddControllersAsServices();
完整代碼如下:
[TestMethod]
public void 手動註冊()
{
var hostBuilder =
WebHost.CreateDefaultBuilder()
.UseStartup<Startup>()
.ConfigureServices(s =>
{
s.AddSingleton<ZipFileProvider>();
s.AddSingleton<FileProvider>();
s.AddSingleton(p =>
{
var fileProvider = p.GetService<ZipFileProvider>();
var logger = p.GetService<ILogger<DefaultController>>();
return new DefaultController(logger, fileProvider);
});
s.AddControllers().AddControllersAsServices(); //<-- add line
})
;
using var server = new TestServer(hostBuilder)
{
BaseAddress = new Uri("http://localhost:9527")
};
var client = server.CreateClient();
var url = "default";
var response = client.GetAsync(url).Result;
response.EnsureSuccessStatusCode();
var result = response.Content.ReadAsStringAsync().Result;
Assert.AreEqual("ZipFileProvider", result);
}
DefaultController 代碼如下
[[ApiController]
[Route("[controller]")]
public class DefaultController : ControllerBase
{
private readonly ILogger<DefaultController> _logger;
private readonly IFileProvider _fileProvider;
public DefaultController(ILogger<DefaultController> logger,
IFileProvider fileProvider)
{
this._logger = logger;
this._fileProvider = fileProvider;
}
[HttpGet]
public IActionResult Get()
{
// var fileProvider = this.HttpContext.RequestServices.GetService<IFileProvider>();
var fileProvider = this._fileProvider;
var result = fileProvider.Print();
return this.Ok(result);
}
}
以下是我知道透過 key 來取得執行個體的用法
先依賴多筆再過濾
註冊時,註冊相同的介面
[TestMethod]
public void 注入相同的介面()
{
var hostBuilder = WebHost.CreateDefaultBuilder()
.UseStartup<Startup>() //<-- add line
.ConfigureServices(s =>
{
s.AddSingleton<IFileProvider, ZipFileProvider>();
s.AddSingleton<IFileProvider, FileProvider>();
})
;
using var server = new TestServer(hostBuilder)
{
BaseAddress = new Uri("http://localhost:9527")
};
var client = server.CreateClient();
var url = "Multi";
var response = client.GetAsync(url).Result;
response.EnsureSuccessStatusCode();
var result = response.Content.ReadAsStringAsync().Result;
Assert.AreEqual("ZipFileProvider", result);
}
這裡我演練如何從 DI Container 取出執行個體,有兩種方法
- Controller 建構函數開一個洞,依賴 IEnumerable<IFileProvider>,ASP.NET Core 會從 DI Container 取得 IEnumerable<IFileProvider> 執行個體
- 從 Request IEnumerable<IFileProvider> 執行個體
- 根據 Key 取出正確執行個體
[ApiController]
[Route("[controller]")]
public class MultiController : ControllerBase
{
private readonly IFileProvider _fileProvider;
private readonly ILogger<AutofacController> _logger;
public MultiController(ILogger<AutofacController> logger,
IEnumerable<IFileProvider> pool)
{
this._logger = logger;
this._fileProvider = pool.FirstOrDefault(p => p.GetType().Name == "ZipFileProvider");
var msg = $"{this._fileProvider.Print()} in {this.GetType().Name} constructor";
Console.WriteLine(msg);
}
[HttpGet]
public IActionResult Get()
{
var serviceProvider = this.HttpContext.RequestServices;
var pool = serviceProvider.GetServices<IFileProvider>();
var fileProvider = pool.FirstOrDefault(p => p.GetType().Name == "ZipFileProvider");
return this.Ok(fileProvider.Print());
}
}
接下來把它轉成字典
[TestMethod]
public void 注入相同的介面()
{
var hostBuilder = WebHost.CreateDefaultBuilder()
.UseStartup<Startup>() //<-- add line
.ConfigureServices(s =>
{
s.AddSingleton<ZipFileProvider>();
s.AddSingleton<FileProvider>();
s.AddSingleton(p =>
{
var pool =
new Dictionary<string, IFileProvider>
{
{"zip", p.GetService<ZipFileProvider>()},
{"file", p.GetService<FileProvider>()}};
return pool;
});
})
;
using var server = new TestServer(hostBuilder)
{
BaseAddress = new Uri("http://localhost:9527")
};
var client = server.CreateClient();
var url = "multi/zip";
var response = client.GetAsync(url).Result;
response.EnsureSuccessStatusCode();
var result = response.Content.ReadAsStringAsync().Result;
Assert.AreEqual("ZipFileProvider", result);
}
改依賴 Dictionary<string, IFileProvider>
[ApiController]
[Route("[controller]")]
public class MultiController : ControllerBase
{
private readonly IFileProvider _fileProvider;
private readonly ILogger<AutofacController> _logger;
public MultiController(ILogger<AutofacController> logger,
Dictionary<string, IFileProvider> pool)
{
this._logger = logger;
this._fileProvider = pool["zip"];
var msg = $"{this._fileProvider.Print()} in {this.GetType().Name} constructor";
Console.WriteLine(msg);
}
[HttpGet]
[Route("{key}")]
public IActionResult Get(string key)
{
var serviceProvider = this.HttpContext.RequestServices;
var pool = serviceProvider.GetService<Dictionary<string, IFileProvider>>();
var fileProvider = pool[key];
return this.Ok(fileProvider.Print());
}
}
使用委派
註冊 Func<string, IFileProvider>, 就可以用字串 Key,從 DI Container 有條件的取得 IFileProvider 的執行個體
完整代碼如下:
[TestMethod]
public void 注入FuncName()
{
var builder = WebHost.CreateDefaultBuilder()
.UseStartup<Startup>()
.ConfigureServices(s =>
{
s.AddSingleton<ZipFileProvider>();
s.AddSingleton<FileProvider>();
s.AddSingleton<Func<string, IFileProvider>>(p =>
key =>
{
switch (key)
{
case "zip":
return p.GetService<ZipFileProvider>();
case "file":
return p.GetService<FileProvider>();
default:
throw new NotSupportedException();
}
});
})
;
using var server = new TestServer(builder)
{
BaseAddress = new Uri("http://localhost:9527")
};
var client = server.CreateClient();
var url = "func/zip";
var response = client.GetAsync(url).Result;
response.EnsureSuccessStatusCode();
var result = response.Content.ReadAsStringAsync().Result;
Assert.AreEqual("ZipFileProvider", result);
}
為了從 DI Container 方便取出執行個體,寫了一個擴充方法
public static class ServiceProviderExtension
{
public static T GetService<T>(this IServiceProvider provider, string name)
{
var pool = (Func<string, IFileProvider>) provider.GetService(typeof(Func<string, IFileProvider>));
return (T) pool(name);
}
}
這裡我演練如何從 DI Container 取出執行個體,有兩種方法
- Controller 建構函數開一個洞,依賴 Func<string, IFileProvider>,ASP.NET Core 會從 DI Container 取得 Func<string, IFileProvider> 執行個體
- 從 Request 取出 Func<string, IFileProvider> 執行個體
完整代碼如下:
[ApiController]
[Route("[controller]")]
public class FuncController : ControllerBase
{
private readonly IFileProvider _fileProvider;
private readonly ILogger<FuncController> _logger;
public FuncController(ILogger<FuncController> logger,
Func<string, IFileProvider> pool)
{
this._fileProvider = pool("zip");
this._logger = logger;
var msg = $"{this._fileProvider.Print()} in {this.GetType().Name} constructor";
Console.WriteLine(msg);
}
[HttpGet]
[Route("{type}")]
public IActionResult Get(string type)
{
var fileProvider = this.HttpContext.RequestServices.GetService<IFileProvider>(type);
var result = fileProvider.Print();
return this.Ok(result);
}
}
使用 Unity
Unity 相當的簡單,在 UnityContainer 給予對應的資料
unityContainer.RegisterType<IFileProvider, ZipFileProvider>("zip");
還要跟 DI Container 講,Controller 要用自訂的 ServiceProvider,也就是 Unity
s.AddControllers().AddControllersAsServices(); //<-- add line
完整代碼如下:
[TestMethod]
public void Unity注入ServiceName()
{
var unityContainer = new UnityContainer();
unityContainer.RegisterType<IFileProvider, ZipFileProvider>("zip");
unityContainer.RegisterType<IFileProvider, FileProvider>("file"); //<-- add line
var builder = WebHost.CreateDefaultBuilder()
.UseStartup<Startup>()
.UseUnityServiceProvider(unityContainer) //<-- add line
.ConfigureServices(s =>
{
s.AddControllers()
.AddControllersAsServices(); //<-- add line
})
;
using var server = new TestServer(builder)
{
BaseAddress = new Uri("http://localhost:9527")
};
var client = server.CreateClient();
var url = "unity/zip";
var response = client.GetAsync(url).Result;
response.EnsureSuccessStatusCode();
var result = response.Content.ReadAsStringAsync().Result;
Assert.AreEqual("ZipFileProvider", result);
}
一樣,這裡我演練如何從 DI Container 取出執行個體,有兩種方法
- Controller 的建構函數依賴 [Dependency("zip")] IFileProvider,ASP.NET Core 會從 DI Container 取得 IFileProvider 執行個體
- 從 Request 取出 IFileProvider 執行個體,因為已經從微軟的 ServicePovider 已經變成 Unity 的 ServicePovider ,所以需要轉型
完整代碼如下:
[ApiController]
[Route("[controller]")]
public class UnityController : ControllerBase
{
private readonly IFileProvider _fileProvider;
private readonly ILogger<UnityController> _logger;
public UnityController(ILogger<UnityController> logger,
[Dependency("zip")] IFileProvider fileProvider)
{
this._logger = logger;
this._fileProvider = fileProvider;
var msg = $"{this._fileProvider.Print()} in {this.GetType().Name} constructor";
Console.WriteLine(msg);
}
[HttpGet]
[Route("{key}")]
public IActionResult Get(string key)
{
var serviceProvider = this.HttpContext.RequestServices;
var unityServiceProvider = (ServiceProvider) serviceProvider;
var unityContainer = (UnityContainer) unityServiceProvider;
var fileProvider = unityContainer.Resolve<IFileProvider>(key);
var result = fileProvider.Print();
return this.Ok(result);
}
}
使用 Autofac
Autofac 在不同 .NET Core 有不一樣的用法,這要稍微注意一下
https://autofac.readthedocs.io/en/latest/integration/aspnetcore.html
新增一個 AutofacStartup 類別,在 ConfigureContainer 方法裡面描述對應關係
builder.RegisterType<FileProvider>().Keyed<IFileProvider>("file");
部分代碼如下:
public class AutofacStartup
{
...
public void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterType<FileProvider>().Keyed<IFileProvider>("file");
builder.RegisterType<ZipFileProvider>().Keyed<IFileProvider>("zip");
builder.RegisterType<AutofacController>().WithAttributeFiltering();//<-- add line
}
...
}
還要跟 DI Container 講,Controller 要用自訂的 ServiceProvider,也就是 Autofac
s.AddControllers().AddControllersAsServices(); //<-- add line
完整代碼如下:
[TestMethod]
public void Autofac注入ServiceName()
{
var hostBuilder = WebHost.CreateDefaultBuilder()
// .UseServiceProviderFactory(new AutofacServiceProviderFactory()) // .net core 3 after
.UseStartup<AutofacStartup>() //<-- add line
.ConfigureServices(services =>
{
services.AddAutofac();
services.AddControllers().AddControllersAsServices(); //<-- add line
})
;
using var server = new TestServer(hostBuilder)
{
BaseAddress = new Uri("http://localhost:9527")
};
var client = server.CreateClient();
var url = "autofac/zip";
var response = client.GetAsync(url).Result;
response.EnsureSuccessStatusCode();
var result = response.Content.ReadAsStringAsync().Result;
Assert.AreEqual("ZipFileProvider", result);
}
一樣,這裡我演練如何從 DI Container 取出執行個體,有兩種方法
- Controller 的建構函數依賴 [KeyFilter("zip")] IFileProvider,ASP.NET Core 會從 DI Container 取得 IFileProvider 執行個體
- 從 Request 取出 IFileProvider 執行個體,因為已經從微軟的 ServicePovider 已經變成 Autofac 的 ServicePovider ,所以需要轉型
完整代碼如下:
[ApiController]
[Route("[controller]")]
public class AutofacController : ControllerBase
{
private readonly IFileProvider _fileProvider;
private readonly ILogger<AutofacController> _logger;
public AutofacController(ILogger<AutofacController> logger,
[KeyFilter("zip")] IFileProvider fileProvider)
{
this._logger = logger;
this._fileProvider = fileProvider;
var msg = $"{this._fileProvider.Print()} in {this.GetType().Name} constructor";
Console.WriteLine(msg);
}
[HttpGet]
[Route("{key}")]
public IActionResult Get(string key)
{
var serviceProvider = this.HttpContext.RequestServices;
var autofacServiceProvider = (AutofacServiceProvider) serviceProvider;
var fileProvider = autofacServiceProvider.LifetimeScope.ResolveKeyed<IFileProvider>(key);
return this.Ok(fileProvider.Print());
}
}
參考資料
https://www.tutorialsteacher.com/ioc/constructor-injection-using-unity-container
https://autofac.readthedocs.io/en/latest/advanced/keyed-services.html
範例位置
https://github.com/yaochangyu/sample.dotblog/tree/master/DI/Lab.MultipleImpl/NET5.TestProject
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET