關於.NET中的控制反轉及AutoFac的簡單說明

  • 2020 年 3 月 16 日
  • 筆記
shanzm-2020年3月16日 02:17:35

1.控制反轉

1.1 什麼是依賴?

依賴是面向對象中用來描述類與類之間一種關係的概念。兩個相對獨立的對象,當一個對象負責構造另一個對象的實例,或者依賴另一個對象的服務,這樣的兩個對象之間主要體現為依賴關係

1.2 什麼是控制反轉?

說反轉則要先說「正轉」,傳統中,在程式中使用new關鍵字配合構造函數去創建一個對象,這就是程式主動的創建其所依賴對象,這就是「正轉」。

調用者不自己創建被調用者對象,而交由第三方(容器)進行創建被調用者對象,這個過程稱為控制反轉(inversion of control,IOC)。

為什麼要控制反轉?控制反轉是面向對象編程中的一種設計原則,可以用來減低電腦程式碼之間的耦合度,便於擴展和後期維護。

1.3 什麼是依賴注入?

實現控制反轉的主要方式是依賴注入。(當然不止依賴注入這一種方法,還有依賴查找(Dependency Lookup,DL)。二者區別可參考:維基:控制反轉)

依賴注入具體是指:調用類 不主動創建依賴對象,而是使用容器來幫忙創建及注入依賴對象,這個過程就稱為依賴注入(Dependency Injection,DI

具體的說:Class A(調用類)中用到 Class B 類型的對象(依賴對象),通常情況下,我們在 Class A 中使用new關鍵字配合構造函數創建一個 Class B 的對象

但是,採用依賴注入技術之後, Class A只需要定義一個Class B類型的屬性,不需要直接new來獲得這個對象,而是通過IOC容器 將Class B類型的對象在外部new出來並注入到Class A里的引用中,從而實現Class A和Class B解耦

1.4 簡單總結

明白了上述幾個概念,那麼就可以理解這樣一句話「模組間的依賴關係從程式內部提到外部來實例化管理稱之為控制反轉,這個實例化的過程就叫做依賴注入。」



2.控制反轉容器

2.1 IOC容器說明

在說到控制反轉時提到「使用IOC容器在 調用類 外部創建 依賴對象 並注入到 調用類」,其中IOC容器是什麼?

IOC容器就是具有依賴注入功能的容器,IOC容器負責實例化、定位、配置應用程式中的對象及建立這些對象間的依賴。從而,應用程式無需直接在程式碼中new相關的對象,應用程式由IOC容器進行組裝。

簡而言之,IOC容器主要就兩個作用:1、綁定服務與實例之間的關係。2、對實例進行創建和銷毀

在.NET程式中IOC容器有許多,如Unity、AutoFac、Spring.net等等。

據說AutoFac是關於.NET的最流行的IOC容器,本文簡單的介紹一下AutoFac的使用方式。

2.2 使用AutoFac的簡介示例

使用AutoFac容器一般是面向介面編程。所以這裡使用一個分層項目來演示AutoFac的使用,即使用AutoFac創建介面實現類的對象

完整Demo下載

①新建一個類庫項目TestIBLL

用於定義一些介面

public interface IUserBll  {      //檢查登錄資訊      bool Login(string userName, string pwd);      //添加新用戶      void AddNew(string userName, string pwd);  }

②新建一個類庫項目TestBLLImpl

添加對TestIBLL項目的引用,用於定義介面的實現類

 public class UserBll : IUserBll  {      //實現介面      public void AddNew(string userName, string pwd)      {          Console.WriteLine($"新增了一個用戶:{userName}");//為了演示,簡單模擬      }      public bool Login(string userName, string pwd)      {          Console.WriteLine($"登錄用戶是:{userName}");//為了演示,簡單模擬          return true;      }  }

【說明】

這裡定義了UserBll類,實現了IUserBll介面,

按照AutoFac中的術語,有如下稱呼:

  • UserBll類稱為組件(Component)

  • IUserBll介面稱為服務(Service)

③新建一個控制台項目TestUI

用於模擬UI層

添加對TestIBLL和TestBLLImpl項目的引用

安裝AutoFac:PM>Install-Package Autofac

static void Main(string[] args)  {      //創建容器構造者      ContainerBuilder builder = new ContainerBuilder();      //註冊組件UserBll類,並把服務IUserBll介面暴露給該組件      //把服務(IUserBll)暴露給組件(UserBll)      builder.RegisterType<UserBll>().As<IUserBll>();      //創建容器      IContainer container = builder.Build();      //使用容器解析服務,創建實例(不推薦,見下面說明):IUserBll userBll = container.Resolve<IUserBll>();      //使用生命周期解析服務,創建實例      using (ILifetimeScope scope = container.BeginLifetimeScope())      {          IUserBll userBll = scope.Resolve<IUserBll>();          userBll.Login("shanzm", "123456");      }    }    

【說明】

  • 其中關於AutoFac的術語:

    • 容器(Container) :用於管理程式中所有的組件的結構(簡單的說就是管理所有介面的實現類)
    • 生命周期(Lifetime): 實例 的從 創建 到 釋放 的持續時間
    • 註冊(Registration): 添加和配置 組件 到 容器 的行為
    • 作用域(Scope): 一個特定的 上下文 , 在其中 組件 的 實例 將會被其他 組件 依據它們的 服務 所共享
    • 解析服務: 相當於給服務實例化對象
    • 註冊:容器中添加一個實現了服務(介面)的組件(實現類)的操作
  • 通過創建 ContainerBuilder 來註冊組件

    ContainerBuilder有一系列的註冊方法,這裡使用的是通過類型註冊:RegisterType()

    任何通過 RegisterType() 註冊的組件必須是個具體的類型, 後面解析服務的時候,Autofac就會創建了一個你註冊對象的實例

  • 每個組件暴露一個或多個服務(簡單地說就是一個類(組件)可能實現一個或多個介面(服務)) ,他們使用 ContainerBuilder 上的 As() 方法連接起來.

  • 解析服務,即創建一個服務的提供對象(簡單的說就是為介面創建一個註冊的實現類)

    不推薦使用使用容器直接解析服務:IUserBll userBll = container.Resolve<IUserBll>();

    我看到一些文章和影片中使用容器去解析服務,但是我看了官方文檔,其中是不推薦這麼使用的,因為可能造成記憶體的泄露。

    推薦你總是從生命周期中解析服務(即我的示例中的方式), 以確保服務實例被妥善地釋放和垃圾回收

    AutoFac文檔中:「永遠從一個生命周期作用域而不是從根容器中解析服務!

    後續為了示例程式碼的簡潔,我還是直接使用容器解析服務,周知!



3 使用AutoFac的一些細節

下面演示一下AutoFac最基本的一些API,具體細節和其他的功能,可以參考AutoFac文檔,其文檔非常詳細且有中文版本(AutoFac文檔

3.1 準備工作

接著上面的示例,在類庫項目TestIBLL中添加以下介面:

創建IAnimalBll.cs

//IAnimalBll介面  public interface IAnimalBll  {      void Cry();//動物都有叫的動作  }

創建IMasterBll.cs

//IMasterBll介面  public interface IMasterBll  {      void Walk();  }

在類庫項目TestBLLImpl中分別實現上述介面

創建DogBll.cs

//DogBll類實現IAnimalBll介面  public class DogBll : IAnimalBll  {      public void Cry()      {          Console.WriteLine("汪汪汪!");      }  }

創建CatBll.cs

//CatBll類實現IAnimalBll介面  public class CatBll : IAnimalBll  {      public void Cry()      {          Console.WriteLine("喵喵喵!");      }  }

創建MasterBll.cs

//MasterBll類,實現了IMasterBll介面和IUserBll介面  public class MasterBll : IMasterBll,IUserBll  {      //注意這裡,MasterBll是介面的實現類,這個類還有一個介面類型的屬性      public IAnimalBll dogBll { get; set; }        public void AddNew(string userName, string pwd)      {          Console.WriteLine($"新增了一個Master用戶:{userName}");      }      public bool Login(string userName, string pwd)      {          Console.WriteLine($"登錄用戶是Master:{userName}");          return true;      }      public void Walk()      {          Console.WriteLine("帶著狗散步!");          dogBll.Cry();//在調用中,使用.PropertiesAutowired()方法給dogBll註冊其實現類      }  }

3.2 註冊整個程式集中的所有實現類

項目中,其實我們可以使用.RegisterAssemblyTypes(),一次性把程式集(類庫項目)中的的所有介面實現類都註冊給相應的介面

static void Main(string[] args)  {      ContainerBuilder builder = new ContainerBuilder();//創建容器構造者        Assembly asm = Assembly.Load(" TestBLLImpl");//獲取指定的程式集        builder.RegisterAssemblyTypes(asm).AsImplementedInterfaces();//註冊指定程式集中的所有介面實現類        IContainer container = builder.Build();//創建容器        IUserBll userBll = container.Resolve<IUserBll>();//解析服務,創建實例        userBll.Login("shanzm", "123456");//使用服務提供者  }  

【說明】

  • 關於使用.RegisterAssemblyTypes()對指定的程式集掃描註冊,可以使用Where()Except()對類型進行過濾
    具體的使用方式可以,查看文檔:程式集掃描

  • .AsImplementedInterfaces():將程式集中的實現類註冊給它所實現的所有介面。

3.3 注入介面實現類中的介面類型的屬性

對實現類中的屬性也是可以使用AutoFac注入的,

對於介面的實現類中若是有某個介面類型的屬性,我們可以使用.PropertiesAutowired()在註冊該實現類的同時,把該屬性同時註冊,即實現屬性的自動裝配,即屬性注入

static void Mian(string[] args)  {        ContainerBuilder builder = new ContainerBuilder();        Assembly asm = Assembly.Load("TestBLLImpl");        //在這裡通過.PropertiesAutowired(),給介面實現類中的介面屬性也註冊一個該類型的介面的實現類,即實現屬性自裝配      builder.RegisterAssemblyTypes(asm).AsImplementedInterfaces().PropertiesAutowired();      builder.RegisterType<DogBll>().As<IAnimalBll>();        IContainer container = builder.Build();        IMasterBll masterBll = container.Resolve<IMasterBll>();      masterBll.Walk();//列印:帶著狗散步!汪汪汪!  }
  • 注意這裡的一個細節,在MasterBll類中有一個IAnimalBll類型的屬性dogBll,我們使用PropertiesAutowired()方法實現屬性的自動裝配,

    但是呀,IAnimalBll介面在TestBLLImpl程式集中有兩個實現類,而自動裝配按順序給dogBll屬性註冊的是CatBll類型的對象

    而我的期望是註冊DogBll類型的對象給IAnimalBll類型的屬性

    所以這裡還要顯示的把DogBll類註冊給IAnimalBll介面

  • 如果你預先知道屬性的名字和值,你可以使用WithProperty("PropertyName", propertyValue)

    所以示例中可以這樣寫:

    builder.RegisterType<MasterBll>().As<IMasterBll>().WithProperty("dogBll",new DogBll());

3.4 關於一個介面有多個不同的實現類

為已給介面註冊實現類的時候,可能該介面有多個實現類,則我們可以為每一個註冊提供已給命名
builder.RegisterType<Object>().Named<IObject>(string name)
在解析服務的時候,可以通過名稱指定創建哪個服務提供者(實現類)
IContainer.ResolveNamed<IObject>(string name)

static void Main(string[] args)  {      ContainerBuilder builder = new ContainerBuilder();        //builder.RegisterType<DogBll>().As<IAnimalBll>();//這樣寫,下面註冊服務的時候,你只能給IAnimalBll對象創建一個DogBll類型的實例      builder.RegisterType<DogBll>().Named<IAnimalBll>("Dog");      builder.RegisterType<CatBll>().Named<IAnimalBll>("Cat");        IContainer container = builder.Build();        using (ILifetimeScope scope = container.BeginLifetimeScope())      {          IAnimalBll dogBll = scope.ResolveNamed<IAnimalBll>("Dog");          IAnimalBll catBll = scope.ResolveNamed<IAnimalBll>("Cat");          dogBll.Cry();          catBll.Cry();      }  }

但是我們在註冊整個程式集中的實現類的時候,該怎麼註冊已給介面的不同的實現類呢?

使用IEnumerable<IObject> objects =container.Resolve<IEnumerable<IObject>>()

static void Main(string[] args)  {      ContainerBuilder builder = new ContainerBuilder();        Assembly asm = Assembly.Load(" TestBLLImpl");      builder.RegisterAssemblyTypes(asm).AsImplementedInterfaces();      IContainer container = builder.Build();        //解析服務,將所有實現了IAnimalBll介面的類都註冊,結果存放在集合中      IEnumerable<IAnimalBll> animalBlls = container.Resolve<IEnumerable<IAnimalBll>>();        foreach (var bll in animalBlls)      {          Console.WriteLine(bll.GetType());          bll.Cry();      }      //選取指定的實現類      IAnimalBll dogBll = animalBlls.Where(t => t.GetType() == typeof(DogBll)).First();      dogBll.Cry();  }

3.5 關於一個實現類實現了多個介面

之前我們說了使用在註冊組件時,一個組件暴露多個服務的時候,可以連續使用 .As()方法

使用.AsImplementedInterfaces()可以達到同樣的效果

MasterBll類實現了多個介面,我們可以把該類註冊給他所有實現的介面
換言之,只要是MasterBll實現的介面,我們都註冊給他一個MasterBll類型的對象
但是注意,這個MasterBll對象只包含當前介面中的方法

static void Mian(string[] args)  {       ContainerBuilder builder = new ContainerBuilder();        //builder.RegisterType<MasterBll>().As<IUserBll>().As<IMasterBll>();      //即一個組件暴露了多個服務,這裡就等價於:      builder.RegisterType<MasterBll>().AsImplementedInterfaces();//把MasterBll類註冊給所有他實現的介面          IContainer container = builder.Build();        //解析IUserBll服務      //其實這裡的userBll是MasterBll類型的對象,但是這個MasterBll類型的對象只具有IUserBll介面中的方法,不具有IMasterBll介面中的方法      IUserBll userBll = container.Resolve<IUserBll>();        userBll.Login("shanzm", "11111");//列印:登錄用戶是Master:shanzm      Console.WriteLine(userBll.GetType());//列印:TestBLLImpl.MasterBll      //userBll.Walk();//注意雖然是MasterBll類型對象,但是只具有當前解析的IUserBll介面中的方法  }

【說明】:

  • 在註冊組件(實現類)的時候連續使用As()方法,可以暴露所有其實現的介面(服務),比較麻煩,所以使用.AsImplementedInterfaces()方法,將實現類的註冊給其所實現的所有介面

  • 該實現類實現了多個介面,但是在解析服務的時候,只具有當前解析的介面中的方法。

3.6 關於實例作用域

在使用AutoFac的時候,最後解析服務,創建提供服務的實例對象

這個對象的在程式中存在時長,也就是從實例化到最後釋放的時間,稱之為服務的生命周期

這個對象在應用中能共享給其他組件並被消費的作用域,稱之為服務的作用域

在理解了以上的概念後,我們才能解釋什麼是實例作用域

1.一個依賴一個實例(Instance Per Dependency)

當我們調用 Resolve() 解析服務的時候返回一個實例,每次請求都返回一個唯一的實例,如無說明,默認就是這種作用域!

static void Mian(string[] args)  {      ContainerBuilder builder = new ContainerBuilder();      //註冊實例      //builder.RegisterType<UserBll>().As<IUserBll>().InstancePerDependency(); //默認就是這種      builder.RegisterType<UserBll>().As<IUserBll>();      //創建容器      IContainer container = builder.Build();       //解析服務      using (var scope = container.BeginLifetimeScope())      {          IUserBll userBll1 = scope.Resolve<IUserBll>();          userBll1.Login("shanzm", "1111");//列印:登錄用戶是普通用戶:shanzm          IUserBll userBll2 = scope.Resolve<IUserBll>();          userBll2.Login("shanzm", "2222");//列印:登錄用戶是普通用戶:shanzm            Console.WriteLine(ReferenceEquals(userBll1, userBll2));//列印結果:false      }  }  //說明:根據調試,結果就可以看出,每次在解析服務,創建的服務提供者都是新的。  //你要注意,我們上面的示例程式碼在同一個生命周期中註冊的兩個IUserBll介面的實例,但是它們依舊是兩個不同的實例

2.單一實例(Single Instance)

它也被稱為 ‘單例.’ 使用單一實例作用域, 在根容器和所有嵌套作用域內所有的請求都將會返回同一個實例.

建議在面向介面的編程中,實例作用域採用:單一實例。防止出現並發操作,造成臟數據!

static void Mian(string[] args)  {      ContainerBuilder builder = new ContainerBuilder();      //註冊實例      builder.RegisterType<UserBll>().As<IUserBll>().SingleInstance();//設置為單一實例      //創建容器      IContainer container = builder.Build();        //解析服務      using (var scope1 = container.BeginLifetimeScope())      {          IUserBll userBll1 = scope1.Resolve<IUserBll>();          userBll1.Login("shanzm", "1111");          using (var scope2 = container.BeginLifetimeScope())          {              IUserBll userBll2 = scope2.Resolve<IUserBll>();              userBll2.Login("shanzm", "2222");              Console.WriteLine(ReferenceEquals(userBll1, userBll2));              //因為是單一實例,所以就是在不同的生命周期中,也是同一個實例,列印結果:true          }      }  }  //說明:最終的列印結果:true 。即使在不同的生命周期中每次在解析服務,創建的服務提供者都是同一個!

其他的實例作用域,詳細可以參考文檔:實例作用域

  1. 每個生命周期作用域一個實例(Instance Per Lifetime Scope)
  2. 每個匹配的生命周期作用域一個實例(Instance Per Matching Lifetime Scope)
  3. 每個請求一個實例(Instance Per Request)
  4. 每次被擁有一個實例(Instance Per Owned)
  5. 執行緒作用域(Thread Scope)



4.在MVC中使用AutoFac

AutoFac在 ASP .NET MVC中使用更加的方便,主要需要注意的地方就是在Global.asax.cs文件中對AutoFac配置

做一個簡單示例:(點擊下載完整的Demo源碼)

①創建名為TestIService的類庫項目,定義所有介面

創建IUserService.cs 文件

public interface IUserService  {      bool CheckLogin(string userName, string pwd);      bool CheckUserNameExists(string userName);  }

創建INewsService.cs 文件

public interface INewsService  {      string AddNews(string title, string body);  }

②創建名為TestServiceImpl的類庫項目,定義介面的實現類

首先,添加對TestIService項目的引用

創建UserService.cs 文件

public class UserService : IUserService  {      //注意介面的實現類是可以有介面類型的屬性,該屬性也會被註冊一個實現對應類型介面的類的對象      public INewsService newsService { get; set; }      public bool CheckLogin(string userName, string pwd)      {          return true;      }      public string UserAction(string userName)      {          string result = newsService.AddNews("2020年3月16日-新冠病毒", "中國境內的新冠病毒被有效遏制");          return userName+"  添加新聞  :"+result;      }  }

創建NewsService.cs 文件

public class NewsService : INewsService  {      public string AddNews(string title, string body)      {          return ($"Title:{title},Content:{body}");      }  }

③創建名為TestMVC 的Web MVC項目

首先,添加對TestIService項目和TestServiceImpl項目的引用

接著安裝AutoFac在MVC中插件:PM> Install-Package AutoFac.Mvc5

在Global.asax.cs中添加對AutoFac的配置:

using Autofac.Integration.Mvc;    protected void Application_Start()  {      AreaRegistration.RegisterAllAreas();      RouteConfig.RegisterRoutes(RouteTable.Routes);        /*------------------------------AutoFac配置--開始--------------*/        ContainerBuilder builder = new ContainerBuilder();      //此處需要:using Autofac.Integration.Mvc;      //把當前程式集中的所有Controllerr類中的介面類型的屬性註冊      builder.RegisterControllers(typeof(MvcApplication).Assembly).PropertiesAutowired();        Assembly asmSevice = Assembly.Load("TestServiceImpl");      builder.RegisterAssemblyTypes(asmSevice)          .Where(type => !type.IsAbstract)//除去抽象類,抽象類不可以實例化(其實這一句也可以不寫)          .AsImplementedInterfaces()//將實現類註冊給其實現的所有介面          .PropertiesAutowired();//介面實現類中介面類型的屬性也註冊      IContainer container = builder.Build();      //MVC中的所有Controller類都是由AutoFac幫我們創建對象      DependencyResolver.SetResolver(new AutofacDependencyResolver(container));        /*------------------------------AutoFac配置--結束--------------*/  }

創建HomeController.cs 控制器

public class HomeController : Controller  {      public IUserService userService { get; set; }//通過AutoFac自動為我們賦值一個IUserService介面實現對象          public ActionResult CheckLogin()      {          bool b = userService.CheckLogin("shanzm", "123456");          return Content(b.ToString());//結果:頁面顯示true      }        public ActionResult UserAddNews()      {          string result = userService.UserAction("shanzm");          return Content(result);//結果:頁面顯示:shanzm 添加新聞 :Title:2020年3月16日-新冠病毒,Content:中境內的新冠病毒被有效遏制      }  }

在瀏覽器中分別請求HomeController中的兩個Action,即可以看到我們使用AutoFac給userService屬性注入相應的實例成功了!



5.參考及示例源碼下載

全文示例的源程式碼下載

文檔:AutoFac官方中文文檔

部落格園:全面理解 ASP.NET Core 依賴注入

部落格園:ASP.NET MVC IOC 之AutoFac攻略

部落格園:Autofac 組件、服務、自動裝配 《第二篇》

簡書:AutoFac的使用