淺談.Net Core中使用Autofac替換自帶的DI容器

暢飲無緒發表於2021-06-22

為什麼叫 淺談 呢?就是字面上的意思,講得比較淺,又不是不能用(這樣是不對的)!!!

Aufofac大家都不陌生了,說是.Net生態下最優秀的IOC框架那是一點都過分。用的人多了,使用教程也十分豐富,官網教程也比較詳細(如果英文功底還不錯的話)。

那我為什麼還要寫這樣一篇部落格呢,一是用作學習筆記,二就是閒的。

廢話不多說,開始正文


專案建立

雲建立一個.Net Core Api專案,然後再新增一個類庫,大概就是下面這樣的結構:

新建一個類庫專案,分別新增一個介面檔案與類檔案:

就這樣,我們的演示方案就搭建完成了,下面就到了演示階段。

方案演示

原始方案

俗話說的好,沒有物件 new 一個就對了:

1 [HttpGet]
2 public string Original()
3 {
4     IUserService userService = new UserService();
5     return userService.GetName("Original");
6 }

結果當然是沒問題的:

 

 .Net Core自帶DI

微軟給我們提供的 DI 解決方案。如果是小專案,需要注入的服務不多,簡直無敵好用,缺點就是不能批量注入,下面我們來複習一下:

先在 Startup 裡面的 ConfigureServices 方法內注入(預設且只能建構函式注入

services.AddScoped<IUserService, UserService>();

然後在控制器中拿到剛才注入的服務:

 1 public class DefaultController : ControllerBase
 2 {
 3     private readonly IUserService userService;
 4     public DefaultController(IUserService _userService)
 5     {
 6         this.userService = _userService;
 7     }16 
17     [HttpGet]
18     public string CoreDI()
19     {
20         return userService.GetName("CoreDI");
21     }
22 }

很顯然,一點問題都沒有:

 

 

 Autofac

注意事項說在前面:

在 .Net Core2 中一般是把 Startup 的 ConfigureServices 方法返回值型別改為IServiceProvider,然後通過構建Autofac容器並注入服務後返回。

在 .Net Core3.0之後,整合方式做了部分調整

下面演示的版本是.Net Core 3.1,也就是調整後的版本。

1、先引用 Autofac 的包,看看這下載次數

2、在 Program 中改用 Autofac 來實現依賴注入

1 public static IHostBuilder CreateHostBuilder(string[] args) =>
2     Host.CreateDefaultBuilder(args)
3         // 就是這句
4         .UseServiceProviderFactory(new AutofacServiceProviderFactory())
5         .ConfigureWebHostDefaults(webBuilder =>
6         {
7             webBuilder.UseStartup<Startup>();
8         });

3、新增我們自定義的 Autofac 註冊類,並註冊我們需要的服務(預設建構函式注入,支援屬性注入)

1 public class AutofacModuleRegister : Autofac.Module
2 {
3     //重寫Autofac管道Load方法,在這裡註冊注入
4     protected override void Load(ContainerBuilder builder)
5     {
6         builder.RegisterType<UserService>().As<IUserService>();
7    }
8 }

4、在 Startup 類中新增方法:ConfigureContainer,

public void ConfigureContainer(ContainerBuilder builder)
{
    // 直接用Autofac註冊我們自定義的 
    builder.RegisterModule(new AutofacModuleRegister());
}

5、大功告成,控制器內的方法甚至不用去改

 1  public class DefaultController : ControllerBase
 2  {
 3      private readonly IUserService userService;
 4      public DefaultController(IUserService _userService)
 5      {
 6          this.userService = _userService;
 7      }
 8  
 9      [HttpGet]
10      public string Autofac()
11      {
12          return userService.GetName("Autofac");
13      }
14  }

 演示到這裡就結束了,是不是感覺 Autofac 比自帶的 DI 還要麻煩。其實不然,下面我們就來看看 Autofac 對比自帶 DI 的一些特有特性。

不同的特性

批量注入

之前的專案我們有了使用者 UserService,需求更新,加入了商品(ProductService),有了商品那又怎麼能少得了訂單(OrderService),那後面是不是還得有售後、物流、倉庫、營銷......

如果是.Net Core 自帶的注入框架,那就只能不停的:

1 services.AddScoped<IProductService, ProductService>();
2 services.AddScoped<IOrderService, OrderService>();
3 ......

這時候,Autofac 的好處就體現出來了:批量注入。

我們先回到上面的:AutofacModuleRegister 類,加入下面這段程式碼:

1 // 服務專案程式集
2 Assembly service = Assembly.Load("XXX.Service");
3 // 服務介面專案程式集
4 Assembly iservice = Assembly.Load("XXX.IService");
5 builder.RegisterAssemblyTypes(service, iservice)
6     .Where(t => t.FullName.EndsWith("Service") && !t.IsAbstract)
7     .InstancePerLifetimeScope() 
8     .AsImplementedInterfaces();

上面的程式碼就是批量注入 XXX.Service 與 XXX.IService 專案下的服務與介面。

注意:如果需要注入的服務沒有 interfac ,那麼 builder.RegisterAssemblyTypes 就只需要傳一個程式集就OK了。如果服務與介面同在一個專案,那也是要傳兩個程式集的哦。

然後我們在控制器去通過建構函式獲取注入的例項:

1 private readonly IUserService userService;
2 private readonly IProductService productService;
3 
4 public DefaultController(IUserService _userService, IProductService _productService)
5 {
6     this.userService = _userService;
7     this.productService = _productService;
8 }

再對之前的 Autofac 介面添油加醋:

1 [HttpGet]
2 public string Autofac()
3 {
4     var name = userService.GetName("Autofac");
5     return productService.Buy(name, "批量注入");
6 }

結果自然是沒有問題的,如果後續需要加入其它服務都不用再單獨注入了,是不是優點就體現出來了。批量注入還有一些其它的玩法,比如篩選類名,篩選父類等。

 

屬性注入

.Net Core 自帶的 DI 框架與 Autofac 預設都是建構函式注入,官方建議也是建構函式注入。

但是有些同學可能就不喜歡建構函式注入,再加上有些場景確實不適合建構函式注入(比如基類實體),所以 Autofac 也支援屬性注入,下面我們來看看使用方法,在之前批量注入的基礎上,我們簡單改造一下:

1 Assembly service = Assembly.Load("Autofac.Service");
2 Assembly iservice = Assembly.Load("Autofac.Service");
3 builder.RegisterAssemblyTypes(service, iservice)
4     .Where(t => t.FullName.EndsWith("Service") && !t.IsAbstract) 
5     .InstancePerLifetimeScope() 
6     .AsImplementedInterfaces()
7     .PropertiesAutowired(); // 屬性注入

對比建構函式注入,屬性注入就多追加了 PropertiesAutowired() 函式,控制器內修改:

1 public IUserService userService { get; set; }
2 public IProductService productService { get; set; }

注意:屬性注入記得將屬性的訪問修飾符改為註冊類可訪問的修飾符,否則會注入失敗。

下面我們來看看使用效果:

 

咦,怎麼會空引用呢?原因大概就是 Controller 是由 Mvc 模組管理的,不在 IOC 容器內,所以在 Controller 中無法使用 Autofac 注入的例項。

那為什麼建構函式注入的時候又可以呢?大概或許可能他們都是建構函式注入吧...

為什麼是大概呢?因為我暫時也沒有具體去深入研究到底是什麼原因導致的,如果有一天,我想起來去研究了並且有結果了,我會在這裡補上。

我們先解決上面的問題先,在 Startup 的 ConfigureServices 方法底部加入如下程式碼:

// 使用 ServiceBasedControllerActivator 替換 DefaultControllerActivator;
// Controller 預設是由 Mvc 模組管理的,不在 Ioc 容器中。替換之後,將放在 Ioc 容器中。
services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());

然後回到我們的 AutofacModuleRegister 注入 Controller:

builder.RegisterTypes(GetAssemblyTypes<Startup>(type => typeof(ControllerBase).IsAssignableFrom(type)))
                .PropertiesAutowired();

這樣處理完後,屬性注入就Ok了。

儲存並提取容器例項 

我們在之前專案的基礎上新增兩個專案 Common 與 Entities,存放公共類與實體類。

我們需要在實體類裡面使用到 Common 專案中的某個類,結構如下:

    
   // 基類實體
   public class BaseEntity { public Class1 common_Class1 { get; set; } public string CreateId { get; set; } public void Create() { this.CreateId = common_Class1.getCurrentUserId(); } } // 公共類 public class Class1 { public string getCurrentUserId() { return Guid.NewGuid().ToString(); } }

從上面的介面中我們可以看到,我需要將 Class1 通過屬性注入到容器中:

builder.RegisterType<Class1>().PropertiesAutowired().InstancePerLifetimeScope();

我們先在 Controller 中看看效果:

public Class1 class1 { get; set; }

[HttpGet]
public string Autofac()
{
    return class1.getCurrentUserId();
}

很顯然結果是沒問題的:

 那我們再到 BaseEntity 中去試試看:

 

 

 咦,又出現空引用,注入失敗了。其實這個問題很明顯,我們使用的是 new 來例項化的 BaseEntity物件,沒有遵循容器例項使用規則,自然就無法使用容器中的例項了。

大家可以自己試一下,將 new 改為屬性注入就沒問題了,但是這種方案並不友好,下面要說的是另一種方案。

我們再新新增一個公共類:ContainerHelper,並宣告一個屬性用來儲存容器的例項:

public static class ContainerHelper
{
    public static ILifetimeScope ContainerBuilder { get; set; }
}

然後回到 Startup 中,在 Configure 方法的底部加入如下程式碼:

ContainerHelper.ContainerBuilder = app.ApplicationServices.CreateScope().ServiceProvider.GetAutofacRoot();

再回到實體類中去使用:

public void Create()
{
    if (common_Class1 == null)
    {
        using (var scope = ContainerHelper.ContainerBuilder.BeginLifetimeScope())
        {
            common_Class1 = scope.Resolve<Class1>();
        }
    }
    this.CreateId = common_Class1.getCurrentUserId();
}

 

 

 

 


Autofac 的替換方案暫時就寫到這裡了,後續如果有新的理解或心得會再做修改,淺談嘛就真的是淺談,有錯誤或補充的地方請大家不吝賜教。

原始碼這裡就不提供了,大家有耐心的可以跟著手敲一遍,雖然對理解沒啥作用,但能使記憶更深刻一點。

 

相關文章