通過 Microsoft.Extensions.DependencyInjection,多個實作如何註冊相同的介面

當有一個介面(抽象),有多個實作(細節),在 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),後面的註冊定義會蓋掉前面,比如 FileAdapter 只會得到 FileProvider 的執行個體

解決方案

先問問你自己,需不需要由外部決定要使用哪一個實作?

  1. 不需要,手動建立,直接註冊正確的實作,調用端直接取得。
  2. 需要,由調用端點透過 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 取出執行個體,有兩種方法

  1. Controller 建構函數開一個洞,依賴 IEnumerable<IFileProvider>,ASP.NET Core 會從 DI Container 取得 IEnumerable<IFileProvider> 執行個體
  2. 從 Request IEnumerable<IFileProvider> 執行個體
  3. 根據 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 取出執行個體,有兩種方法

  1. Controller 建構函數開一個洞,依賴 Func<string, IFileProvider>,ASP.NET Core 會從 DI Container 取得 Func<string, IFileProvider> 執行個體
  2. 從 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 取出執行個體,有兩種方法

  1. Controller 的建構函數依賴 [Dependency("zip")] IFileProvider,ASP.NET Core 會從 DI Container 取得 IFileProvider 執行個體
  2. 從 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 取出執行個體,有兩種方法

  1. Controller 的建構函數依賴 [KeyFilter("zip")] IFileProvider,ASP.NET Core 會從 DI Container 取得 IFileProvider 執行個體
  2. 從 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://stackoverflow.com/questions/39174989/how-to-register-multiple-implementations-of-the-same-interface-in-asp-net-core

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~2019 .NET

Image result for microsoft+mvp+logo