Try   HackMD

APS.NET 使用 Autofac 實作 DI

hackmd-github-sync-badge

Autofac

AutofacASP.NET 比較有名的 DI 套件,早期還有其他的套件,但後續因與 ASP.NET Core 相容問題,很多都淘汰掉了(雖然 ASP.NET Core 有内建 DI 工具,但因為功能相對較為陽春,所以很多人還是會裝其他套件來擴充使用)。 最初選擇 Autofac 的原因是因為它的型別註冊功能很強大,且官方文件也滿詳細的,又有提供多個框架的支援,結果剛好這套套件也順利活到 ASP.NET Core 時代。

Autofac 的使用方式如下面範例,其中如果是在 Web 使用時,Autofac 會幫忙在每個 Request 建立一個 Lifetime Scope。

var builder = new ContainerBuilder();

// 註冊型別
builder.RegisterType<Service>();

// 建立一個 Autofac Container
var container = builder.Build();

// 建立一個 Lifetime Scope
using(var scope = container.BeginLifetimeScope())
{
   Service service = scope.Resolve<Service>();
}

Register Type

Autofac 註冊型別使用的 Method 為 RegisterType<{Instance Type}>().As({Declare Type}),也就是說當 Autofac 遇到一個取得 Service Type 的請求時,會建立一個 Instance Type 的物件回傳。

使用 Reflection 大量註冊型別

由於每個型別都要個別進行註冊過於繁雜,Autofac 有提供使用 Reflection 去搜尋 Assembly 底下的特定型別進行註冊,官網本身也提供一些範例可以參考。

builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
    .Where(x => typeof(IAppService).IsAssignableFrom(x));

自行設定 Instance 的建立方法

當 Class 裡有多個 Constructor 時,會找出全部能透過 Autofac 建立的 Constructor(意思是全部參數都能用 DI 設值),選擇參數最多的來使用,詳情參閱黑暗執行緒 - Autofac筆記4-建構參數與建構式選擇,但如果我們希望能自行決定物件建立方法,可以用以下程式碼設定。

builder.Register(c => new TypeA(c.Resolve<TypeB>()));

指定註冊型別給哪些型別使用

Method 描述
As() 註冊型別給指定型別使用
AsImplementedInterfaces() 註冊型別給自身所實作的 Interface(不包含IDisposable)使用。
AsClosedTypesOf(open) 註冊給可分配給開放泛型型別的封閉實例的型別使用。
AsSelf() 將型別註冊給自身使用。

如果未設定使用型別,預設使用 AsSelf(),但如果有指定時,就不會自動增加 AsSelf(),可多項指定一起使用。

// 自動設定 AsSelf()
builder.RegisterType<Service>();

// 有指定 AsImplementedInterfaces(),所以不會自動設定 AsSelf()
builder.RegisterType<Service>()
    .AsImplementedInterfaces();

// 要在使用其他指定的情況下使用 AsSelf(),必須手動增加
builder.RegisterType<Service>()
    .AsImplementedInterfaces()
    .AsSelf();

Instance Scope

Autofac 提供以下 Instance Scope:

Instance Scope 描述 .NET Core 的 對應
Instance Per Dependency 每一次呼叫都是一個新的 Instance,為預設值。 Transient
Instance Per Lifetime Scope 每個 Scope 都只會產生一個 Instance。 Scoped
Single Instance 整個 Autofac Container 都是同一個 Instance。 Singleton
var builder = new ContainerBuilder();

// 註冊型別
builder.RegisterType<Worker>()
    // 宣告 Instance Scope
    .InstancePerDependency();
    //.InstancePerLifetimeScope()

// 建立一個 Autofac Container
var container = builder.Build();

// 建立一個 Lifetime Scope
using(var scope1 = container.BeginLifetimeScope())
{
   for(var i = 0; i < 100; i++)
   {
     // 每一次呼叫
     var w1 = scope1.Resolve<Worker>();
   }
}

// 建立一個 Web Request Scope
using(var scope2 = container.BeginLifetimeScope("AutofacWebRequest"))
{
   for(var i = 0; i < 100; i++)
   {
     // 每一次呼叫
     var w1 = scope2.Resolve<Worker>();
   }
}

+----------------------------------------------------+
|                 Autofac Container                  |
|                                                    |
| +------------------------------------------------+ |
| |                 Lifetime Scope                 | |
| |                                                | |
| | +--------------------+  +--------------------+ | |
| | |    Get Instance    |  |    Get Instance    | | |
| | +--------------------+  +--------------------+ | |
| +------------------------------------------------+ |
|                                                    |      
| +------------------------------------------------+ |
| |                 Lifetime Scope                 | |
| |                                                | |
| | +--------------------+  +--------------------+ | |
| | |    Get Instance    |  |    Get Instance    | | |
| | +--------------------+  +--------------------+ | |
+----------------------------------------------------+

InstancePerMatchingLifetimeScope({Tag})InstancePerRequest() 皆為 InstancePerLifetimeScope() 的變種。 當在呼叫 container.BeginLifetimeScope({Tag}) 建立 Scope 時,可以設定 Tag,而宣告 InstancePerMatchingLifetimeScope({Tag}) 的型別,只能建立在標記該 Tag 的 Scope 底下。 Autofac在 Web 的每個 Request 裡,都會建立一個 Tag 為 「AutofacWebRequest」 的 Scope,而 InstancePerRequest() 大致等同於 InstancePerMatchingLifetimeScope("AutofacWebRequest")

官網文件

設定允許使用 Property Injection

  • 一般會建議盡量使用 Constructor Injection,但有些情況下不得不使用 Property Injection,如框架本身沒支援 Constructor Injection,或是有循環依賴的情況發生。
  • 循環依賴指的是 Main Class 裡包含 Sub Class,Sub Class 裡也包含 Main Class,此時設計如下:
class Main
{
    private readonly Sub sub;
    
    public Main(Sub sub)
    {
        this.sub = sub;
    }
}

class Sub
{
    public Main Main { get; set; }
}

//......

builder.RegisterType<Main>()
    .InstancePerLifetimeScope();
builder.RegisterType<Sub>()
    .InstancePerLifetimeScope()
    .PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies);
  • 兩個型別註冊都不可使用 InstancePerDependency()。
  • 如果沒有要使用循環依賴,則不需要傳入 PropertyWiringOptions.AllowCircularDependencies

官網文件

在 MVC 上使用 Autofac

NuGet 套件

  • Autofac
  • Autofac.Mvc5

程式碼範例

Golbal.asax.cs

以下程式碼來自官網範例,RegisterControllers() 為必要,要設定後,才可以將 Instance Injection 到 Controller。 註解被標註「OPTIONAL」視情況是否添加,例如要使用 HttpContextBase 等型別注入,則需要 Register AutofacWebTypesModule

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        //...實作 MVC 設定...
        // e.g. RouteConfig.RegisterRoutes(RouteTable.Routes);

        // 以下為 Autofac 相關程式碼
        var builder = new ContainerBuilder();

        // Register your MVC controllers. (MvcApplication is the name of
        // the class in Global.asax.)
        builder.RegisterControllers(typeof(MvcApplication).Assembly);

        // OPTIONAL: Register model binders that require DI.
        builder.RegisterModelBinders(typeof(MvcApplication).Assembly);
        builder.RegisterModelBinderProvider();

        // OPTIONAL: Register web abstractions like HttpContextBase.
        builder.RegisterModule<AutofacWebTypesModule>();

        // OPTIONAL: Enable property injection in view pages.
        builder.RegisterSource(new ViewRegistrationSource());

        // OPTIONAL: Enable property injection into action filters.
        builder.RegisterFilterProvider();
            
        builder.RegisterType<AppService>()
            .As<IAppService>()
            .InstancePerLifetimeScope();

        // Set the dependency resolver to be Autofac.
        var container = builder.Build();
        DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
    }
}

HomeController

 public class HomeController : Controller
{
    private readonly IAppService appService;

    public HomeController(IAppService appService)
    {
        this.appService = appService ?? throw new ArgumentNullException(nameof(appService));
    }
    
    //...實作 HomeController Action...
}

在 View 使用 DI

WebViewPageBase

// 沒有使用 Model 的 View
public abstract class WebViewPageBase : WebViewPage
{
    // 靠 builder.RegisterSource(new ViewRegistrationSource()) 的設定 Injection
    public IAppService AppService { get; set; }
    
    public IAppService AppService2 => GetDependencyService<IAppService>

    public TService GetDependencyService<TService>()
    {
        return DependencyResolver.Current.GetService<TService>();
    }
}

// 有使用 Model 的 View
public abstract class WebViewPageBase<T> : WebViewPage<T>
{
    // 靠 builder.RegisterSource(new ViewRegistrationSource()) 的設定 Injection
    public IAppService AppService { get; set; }
    
    public IAppService AppService2 => GetDependencyService<IAppService>

    public TService GetDependencyService<TService>()
    {
        return DependencyResolver.Current.GetService<TService>();
    }
}

Index.cshtml

設定 Index.cshtml 繼承 WebViewPageBase,此時有三種方法可以使用 IAppService:

  1. 使用屬性 AppService。
  2. 使用屬性 AppService2。
  3. 使用 GetDependencyService<IAppService>(),其實等同於直接在 View 使用DependencyResolver.Current.GetService<TService>(),只是在父類別簡化呼叫。

就個人偏好,每個 View 都有機會用到的使用方法 2,個別 View 用到的使用方法 3。 如果沒使用方法 1,Global 就不需要設定 builder.RegisterSource(new ViewRegistrationSource())

無使用 Model 寫法如下:

@inherits DISample.MVC.WebViewPageBase
@{
    IAppService appService = GetDependencyService<IAppService>();
}

有使用 Model 寫法如下:

@inherits DISample.MVC.WebViewPageBase<ViewModel>
@{
    IAppService appService = GetDependencyService<IAppService>();
}

如果希望可以和原來一樣不使用 Model 就不用特別宣告,使用 Model 則使用 @model ViewModel 宣告的作法,請參考此篇文章修改 \View\Web.config 內容。

<configuration>
  <system.web.webPages.razor>
    <pages pageBaseType="MyNamespace.WebViewPageBase">
    </pages>
  </system.web.webPages.razor>
</configuration>
  • 需注意修改的「Web.config」是資料夾「View」底下的,而非專案根目錄底下的。
  • MyNamespace 請替換成實際專案的 Namespace。

模擬 ASP.NET Core 的 FromServicesAttribute Injection 至 Action Parameter

ServicesModelBinder

public class ServicesModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        return bindingContext is null
            ? throw new ArgumentNullException(nameof(bindingContext))
            : DependencyResolver.Current.GetService(bindingContext.ModelType);
    }
}

FromServicesAttribute

[AttributeUsage(AttributeTargets.Parameter)]
public sealed class FromServicesAttribute : CustomModelBinderAttribute
{
    public override IModelBinder GetBinder() => new ServicesModelBinder();
}

Controller Action

public ActionResult Index([FromServices] IAppService appService)
{
    //...Action...
    return View();
}

封裝 AppSettings 來作 Injection

一般取得 AppSettings 都是使用 WebConfigurationManager.AppSettings 來取得設定值,但直接使用它有兩個缺點:

  1. AppSettings 值只有 string,如果有 bool 或數值型別需求時,每次使用都要進行型別轉換,所以最好是可以在進一步進行封裝處理。
  2. WebConfigurationManager 是 Static Class,但有些情況下(e.g. 單元測試),會希望值能用參數的方式傳入,所以有些人會用 Singleton 進行封裝。

以下程式碼是基於一個原則進行設定,如果有其他需求,可自行調整,AppSetting Key 必需為 {Options Class Name(不包含 Options)}:{Constructor Parameter Name},大小寫隨意,例如有一個 Options 名為 TestOptions,Constructor 參數名為 isTest,那 AppSetting Key 為 Test:IsTest

Web.config
<appSettings>
  <add key="Path:Upload" value="C:\Upload\" />
  <add key="Test:IsTest" value="true" />
  <add key="Test:TestName" value="Test" />
</appSettings>

Golbal.asax.cs

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        var builder = new ContainerBuilder();
        
        builder.RegisterModule<AutofacWebTypesModule>();
        
        // 設定 Options DI
        builder.RegisterModule<OptionsModule>();

        var container = builder.Build();
        DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
    }
}

OptionsModule

實際設定 Option DI 的地方

public class OptionsModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        // 如果會需要執行中的才能決定的參數使用這個 Register
        RegisterOptions<PathOptions>(builder);
        // 如果會純 AppSettings 的設定就使用這個  Register
        RegisterOptionsInstance<TestOptions>(builder);
    }

    private void RegisterOptionsInstance<T>(ContainerBuilder builder)
        where T : class
    {
        builder.RegisterInstance(OptionUtils.CreateInstance<T>())
            .AsImplementedInterfaces()
            .AsSelf()
            .SingleInstance();
    }

    private void RegisterOptions<T>(ContainerBuilder builder)
        where T : class
    {
        string optionsName = typeof(T).Name.Replace("Options", "");
        var registrationBuilder = builder.RegisterType<T>()
            .AsSelf()
            .InstancePerLifetimeScope();

        foreach (string key in WebConfigurationManager.AppSettings.AllKeys.Where(x => x.StartsWith(optionsName)))
        {
            registrationBuilder.WithParameter(new ResolvedParameter(
                (pi, ctx) => pi.Name.Equals(
                                Regex.Replace(key, $@"^{optionsName}:", "", RegexOptions.IgnoreCase),
                                StringComparison.OrdinalIgnoreCase
                            ),
                (pi, ctx) => FixValue(pi.ParameterType, WebConfigurationManager.AppSettings[key])));
        }

        object FixValue(Type type, string value)
        {
            return Convert.ChangeType(value, type);
        }
    }
}

PathOptions

public class PathOptions
{
    private readonly HttpServerUtilityBase httpServer;

    // HttpServerUtilityBase 是在 AutofacWebTypesModule 裡作 DI 設定;
    public PathOptions(HttpServerUtilityBase httpServer, string upload)
    {
        this.httpServer = httpServer ?? throw new ArgumentNullException(nameof(httpServer));
        Upload = upload ?? throw new ArgumentNullException(nameof(upload));
    }

    public string Upload { get; }

    private string GetRealPath(string path)
    {
        return IsVirtualDirectory(path)
            ? httpServer.MapPath(path)
            : path;
    }

    private static bool IsVirtualDirectory(string path)
    {
        return path.Length < 2 || path[1] != Path.VolumeSeparatorChar;
    }
}

TestOptions

public class TestOptions
{
    public TestOptions(bool isTest, string testName)
    {
        IsTest = isTest;
        TestName = testName ?? throw new ArgumentNullException(nameof(testName));
    }

    public bool IsTest { get; }

    public string TestName { get; }
}

官網文件

MVC

在 Web API 上使用 Autofac

NuGet 套件

  • Autofac
  • Autofac.WebApi2

程式碼範例

Golbal.asax.cs

以下程式碼來自官網範例,RegisterApiControllers() 為必要,要設定後,才可以將 Instance Injection 到 ApiController。 註解被標註「OPTIONAL」視情況是否添加。

public class WebApiApplication : HttpApplication
{
    protected void Application_Start()
    {
        //...實作 Web API 設定...
        //e.g. GlobalConfiguration.Configure(WebApiConfig.Register);
    
        // 以下為 Autofac 相關程式碼
ContainerBuilder builder = new ContainerBuilder();
        HttpConfiguration config = GlobalConfiguration.Configuration;

        // Register your Web API controllers.
        builder.RegisterApiControllers(Assembly.GetExecutingAssembly());

        // OPTIONAL: Register the Autofac filter provider.
        builder.RegisterWebApiFilterProvider(config);

        // OPTIONAL: Register the Autofac model binder provider.
        builder.RegisterWebApiModelBinderProvider();

        builder.RegisterType<AppService>()
            .As<IAppService>()
            .InstancePerLifetimeScope();

        // Set the dependency resolver to be Autofac.
        IContainer container = builder.Build();
        config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
    }
}

ValuesController

public class ValuesController : ApiController
{
    private readonly IAppService appService;

    public ValuesController(IAppService appService)
    {
        this.appService = appService ?? throw new ArgumentNullException(nameof(appService));
    }
    
    //...實作 ValuesController Action...
}

官網文件

Web API

在 Web Form 上使用 Autofac

由於 Web Form 不支援 Constructor Injection,所以需要用 Property Injection。

NuGet 套件

程式碼範例

Web.config

<configuration>
  <system.web>
    <httpModules>
      <!-- This section is used for IIS6 -->
      <add
        name="ContainerDisposal"
        type="Autofac.Integration.Web.ContainerDisposalModule, Autofac.Integration.Web"/>
      <add
        name="PropertyInjection"
        type="Autofac.Integration.Web.Forms.PropertyInjectionModule, Autofac.Integration.Web"/>
    </httpModules>
  </system.web>
  <system.webServer>
    <!-- This section is used for IIS7 -->
    <modules>
      <add name="ContainerDisposal" type="Autofac.Integration.Web.ContainerDisposalModule, Autofac.Integration.Web" preCondition="managedHandler" />
      <add name="PropertyInjection" type="Autofac.Integration.Web.Forms.PropertyInjectionModule, Autofac.Integration.Web" preCondition="managedHandler" />
    </modules>
  </system.webServer>
</configuration>

官網建議兩種寫法都寫,來相容不同的 IIS 版本,但實際上兩個都寫有可能會有 Error。

Golbal.asax.cs

public class Global : HttpApplication, IContainerProviderAccessor
{
    private static IContainerProvider containerProvider;

    public IContainerProvider ContainerProvider
    {
        get
        {
            return containerProvider;
        }
    }

    protected void Application_Start(object sender, EventArgs e)
    {
        containerProvider = new ContainerProvider(CreateContainer());
    }

    public static IContainer CreateContainer()
    {
        ContainerBuilder builder = new ContainerBuilder();

        builder.RegisterType<AppService>()
            .As<IAppService>()
            .InstancePerLifetimeScope()
            .PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies);

        IContainer container = builder.Build();
        
        return container;
    }
}

Default.aspx.cs

public partial class _Default : Page
{
    public IAppService AppService { get; set; }

    protected void Page_Load(object sender, EventArgs e)
    {
        //...實作...
    }
}

官網文件

Web Forms

在 Web Service 上使用 Autofac

  • Web Service 不支援 Constructor Injection,所以需要用 Property Injection。
  • Web.config 裡的 Xml 設定是提供給 Web Form 使用的,Web Service 無法用它來設定 Property Injection,所以需要藉由 WebServiceBase 來實作這部分。

NuGet 套件

程式碼範例

Golbal.asax.cs

同 Web Form。

WebServiceBase

public abstract class WebServiceBase : System.Web.Services.WebService
{
    public WebServiceBase()
    {
        IContainerProviderAccessor cpa = (IContainerProviderAccessor)HttpContext.Current.ApplicationInstance;
        // 為自身進行 Property Injection
        cpa.ContainerProvider.RequestLifetime.InjectProperties(this);
    }
}

WebService

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
// 若要允許使用 ASP.NET AJAX 從指令碼呼叫此 Web 服務,請取消註解下列一行。
// [System.Web.Script.Services.ScriptService]
public class WebService : WebServiceBase
{
    public IAppService AppService { get; set; } // 注入成功
    
    //...WebService 實作...
    // e.g. 如下
    [WebMethod]
    public DateTime GetNow()
    {
        return AppService.GetNow();
    }
}
tags: .NET .NET Framework ASP.NET Dependency Injection Autofac