[Autofac套件介紹-3] 深入討論Autofac Resolving Services

Autofac Resolving Service我認為會遇到的第一個困難就是參數的傳遞,本文主要是介紹多種參數傳遞的方式,以及如何透過工廠來生成物件,並附帶上簡單的實作範例幫助理解。

前言

我們前面分享了如何註冊Component到Autofac的Container中,接下來本篇的重點是如何透過Rsolve方法取得實際的物件。

在實際應用上,大部分物件都會提供需要參數的建構子來做初始化的動作。所以,如果要使用Autofac來生成物件,便需要告訴Autofac該如何傳遞參數。而傳遞的參數可以在程式碼執行階段時動態賦予,但在某些情況下,假設已經知道該參數的內容,而非動態產生的,舉例來說,像是寫在Config裡的連線字串,那就可在註冊Component時就定義好要傳遞的參數,這樣一來,可讓Resolve方法的動作更為單純。

 

傳遞參數篇

Autofac支援的參數傳遞的方式,大致上分為三種

  • NamedParameter => 利用名稱來對應 
  • TypedParameter  => 利用型別來對應 
  • ResolvedParameter => 透過Container相關的資訊傳遞參數 (較難理解,後面再以範例說明)

NamedParameter&TypedParameter 範例

前面兩種參數傳遞的方式,看敘述就能理解,但還是寫個小範例踏實點。第一個範例超簡單,直接看程式碼就能懂了,沿用前面的例子,簡單新增一個Truck類別,特別的地方只有在建構子處要求傳遞一個字串參數。

//建構子需要吃一個字串參數
public Truck(string name)
{
    this.Name = name;
}

private string Name;

public void Drive()
{
    Console.Write(string.Format("{0} Drive Truck",this.Name));
}

接著把它註冊到Container裡面,如對此段不熟悉可參考淺談Autofac一文。

var builder = new ContainerBuilder();

builder.RegisterType<Truck>().As<ICar>();

var container = builder.Build();

最後進入正題,透過Resolve方法來取的Truck的實體,利用NamedParameter方式來處理,只需先實體化一個NamedParameter物件,第一個參數對應到Truck建構子參數的名稱"name",第二個參數是希望傳遞的值,這個例子是"Hank"。

using (var scope = container.BeginLifetimeScope())
{
   //new 一個NamedParameter物件 
    var truck = scope.Resolve<ICar>(new NamedParameter("name", "Hank"));
    truck.Drive();
    Console.ReadLine();
}

執行畫面如下。

再來把參數傳遞的方式從Named對應改寫成Typed對應,只需將上述例子的NamedParameter改為TypedParameter即可,第一個參數是Truck建構子參數對應的類別,第二個參數一樣是希望傳遞的值。修改完成後,再執行一次會發現結果相同。

var truck = scope.Resolve<ICar>(new TypedParameter(typeof(string), "Hank"));

使用Type的方式註冊可避免建構子的參數名稱改變而受到影響,但是假如參數有多個是相同型別,就會比較麻煩。反觀用Name的方式就蠻直覺,即使建構子內有多個相同型別的參數也沒問題,因為參數名稱是絕對不會重複的。

 

ResolvedParameter 範例

ResolvedParameter是很有彈性的一個給予參數的方式,但也稍難理解。下圖簡單描述一下Autofac的運作流程。起手式產生一個Builder,透過Builder來註冊Component,然後呼叫Builder的Build方法可得到一個Container,最後,就可以透過Container的Resolve方法生成物件。

一般不會直接透過Container 呼叫 Resolve方法,而會由Container的BeginLifetimeScope()方法先得到一個scope,定義物件的生命週期,然後透過scope.Resolve方法來生成物件,這樣一來只要釋放掉scope,所有由scope生成的物件都會自動釋放。

有了上面的概念後,再來看ResolvedParameter的方法,它需要兩個參數,兩個參數的型別都很長一串,難以口述解釋。直接由下圖來理解,先來看第一個參數,這是一個委派,此委派需要兩個參數,ParameterInfo封裝了目標物件建構子參數的資訊,IComponentContext提供可透過介面操作Container的功能,最後回傳一個布林值,表示是否Match;第二個參數也是委派,同樣需要兩個參數,因跟前面相同就不贅述,差別在它回傳的是一個object,也就是要注入目標物件建構子參數的值。

再來看一個範例幫助理解,小小修改原本的Truck的例子,改為接受一個IDriver的參數。

public Truck(IDriver driver)
{
    this.Driver = driver;
}


public void Drive()
{
    this.Driver.SayName();//實作內容是Console.Write("My name is Hank");
    Console.Write("Drive Truck ");
}

在註冊的部分,註冊HankDriver和Truck。

var builder = new ContainerBuilder();

builder.RegisterType<HankDriver>().As<IDriver>();

builder.RegisterType<Truck>().As<ICar>();

var container = builder.Build();

接著Resolve<ICar>時,採用ResolvedParameter的方式給予參數,ResolvedParameter的第一個參數是先下條件限制來找到要注入的參數,ResolvedParameter的第二個參數則是產生要注入參數的值,此範例是透過contrainer的Resolve方法產生要注入的值。

using (var scope = container.BeginLifetimeScope())
{
    var truck = scope.Resolve<ICar>(new ResolvedParameter(
        //pi 就是truck的建構子參數資訊,在這裡判斷建構子的參數名稱必須是driver 且型別為IDriver 才注入
        (pi, ctx) => pi.ParameterType == typeof(IDriver) && pi.Name == "driver",

        //前面參數條件符合,就注入該參數的值,這裡可透過ctx取得Container物件然後由Resolve方法生成物件
        (pi, ctx) => ctx.Resolve<IDriver>()
        ));

    truck.Drive();
    Console.ReadLine();
}

最後執行一下,可看到結果如下。以上就是透過參數傳遞的方式來請Resolve生成物件。

同場加映(工廠方法)

除此之外,我們也可透過工廠的方式來生成物件,可以讓注入物件的方式更加彈性。我們就根據前面的例子來改寫,改由工廠的方式來生成物件,首先,在Truck類別內宣告一個delegate,回傳的類別是Truck,並且簽名對應到Truck的建構子。

//新增此行
public delegate Truck CarFactory(IDriver driver);

private IDriver Driver;

public Truck(IDriver driver)
{
    this.Driver = driver;
}

接著把Truck類別註冊到Container內,因為建構子有用到IDriver,所以順手也註冊一下HankDriver。

var builder = new ContainerBuilder();

builder.RegisterType<Truck>();

builder.RegisterType<HankDriver>().As<IDriver>();

 var container = builder.Build();

最後,透過resolve方法可取得carFactory,然後透過carFactory.Invoke方法判斷簽名,呼叫對應的建構子生成物件。

//取得Factory 
var carFacotory = scope.Resolve<Truck.CarFactory>();

//透過Invoke方法呼叫建構子生成物件
var driver = scope.Resolve<IDriver>();
//也可寫成var truck  = carFacotory(driver);
var truck  = carFacotory.Invoke(driver);

truck.Drive();
Console.ReadLine();

這裡,也許大家會發現,用工廠的方式,好像程式碼變多了,比如說我要先透過var driver = R<IDriver>() 取得要注入的driver,然後再將driver當參數餵給工廠,就顯得很麻煩。其實,Autofac是很聰明的,它會自動幫你注入你有註冊的物件,所以來看一下如何修改這段,首先,把delegate的參數拿掉。

//public delegate Truck CarFactory(IDriver driver);
public delegate Truck CarFactory();

private IDriver Driver;

public Truck(IDriver driver)
{
    this.Driver = driver;
}

接下來生成物件的方式就顯得簡單多了,只有一個參數也許感覺差別不大,但假如參數有10個以上時,就會發現Autofac真的蠻聰明的。

//取得Factory 
var carFacotory = scope.Resolve<Truck.CarFactory>();

//透過Invoke方法呼叫建構子生成物件
//var driver = scope.Resolve<IDriver>();
//var truck  = carFacotory.Invoke(driver);
var truck  = carFacotory.Invoke();

truck.Drive();
Console.ReadLine();

輸出結果如下圖所示。

小結

本文介紹了一些常見的參數傳遞方式來透過Resolve方法生成物件,同時也介紹了工廠模式,接下來的主題會探討透過Autofac生成物件後,如何做生命週期管理的相關內容。