# 在 APS.NET 使用 Autofac 實作 DI [![hackmd-github-sync-badge](https://hackmd.io/ijTUhSmARH-LYdTYXxoSZA/badge)](https://hackmd.io/ijTUhSmARH-LYdTYXxoSZA) ## Autofac [Autofac](https://autofac.readthedocs.io/en/latest/index.html) 是 ASP.NET 比較有名的 DI 套件,早期還有其他的套件,但後續因與 ASP.NET Core 相容問題,很多都淘汰掉了(雖然 ASP.NET Core 有内建 DI 工具,但因為功能相對較為陽春,所以很多人還是會裝其他套件來擴充使用)。 最初選擇 Autofac 的原因是因為它的型別註冊功能很強大,且官方文件也滿詳細的,又有提供多個框架的支援,結果剛好這套套件也順利活到 ASP.NET Core 時代。 Autofac 的使用方式如下面範例,其中如果是在 Web 使用時,Autofac 會幫忙在每個 Request 建立一個 Lifetime Scope。 ```csharp 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 底下的特定型別進行註冊,官網本身也提供一些[範例](https://autofac.readthedocs.io/en/latest/register/scanning.html)可以參考。 ```csharp builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) .Where(x => typeof(IAppService).IsAssignableFrom(x)); ``` #### 自行設定 Instance 的建立方法 當 Class 裡有多個 Constructor 時,會找出全部能透過 Autofac 建立的 Constructor(意思是全部參數都能用 DI 設值),選擇參數最多的來使用,詳情參閱[黑暗執行緒 - Autofac筆記4-建構參數與建構式選擇](https://blog.darkthread.net/blog/autofac-notes-4-constructor/),但如果我們希望能自行決定物件建立方法,可以用以下程式碼設定。 ```csharp builder.Register(c => new TypeA(c.Resolve<TypeB>())); ``` ### 指定註冊型別給哪些型別使用 | Method | 描述 | | ------------------------- | ----------------------------------------------------------- | | As() | 註冊型別給指定型別使用 | | AsImplementedInterfaces() | 註冊型別給自身所實作的 Interface(不包含IDisposable)使用。 | | AsClosedTypesOf(open) | 註冊給可分配給開放泛型型別的封閉實例的型別使用。 | | AsSelf() | 將型別註冊給自身使用。 | :::info 如果未設定使用型別,預設使用 `AsSelf()`,但如果有指定時,就不會自動增加 `AsSelf()`,可多項指定一起使用。 ```csharp // 自動設定 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 | ```csharp 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 | | | | | +--------------------+ +--------------------+ | | +----------------------------------------------------+ ``` :::info `InstancePerMatchingLifetimeScope({Tag})` 和 `InstancePerRequest()` 皆為 `InstancePerLifetimeScope()` 的變種。 當在呼叫 `container.BeginLifetimeScope({Tag})` 建立 Scope 時,可以設定 Tag,而宣告 `InstancePerMatchingLifetimeScope({Tag})` 的型別,只能建立在標記該 Tag 的 Scope 底下。 Autofac在 Web 的每個 Request 裡,都會建立一個 Tag 為 「AutofacWebRequest」 的 Scope,而 `InstancePerRequest()` 大致等同於 `InstancePerMatchingLifetimeScope("AutofacWebRequest")`。 ::: #### 官網文件 * [Instance Scope](https://autofac.readthedocs.io/en/latest/lifetime/instance-scope.html) * [How do I work with per-request lifetime scope?](https://autofac.readthedocs.io/en/latest/faq/per-request-scope.html) * ### 設定允許使用 Property Injection * 一般會建議盡量使用 Constructor Injection,但有些情況下不得不使用 Property Injection,如框架本身沒支援 Constructor Injection,或是有循環依賴的情況發生。 * 循環依賴指的是 Main Class 裡包含 Sub Class,Sub Class 裡也包含 Main Class,此時設計如下: ```csharp 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); ``` :::warning * 兩個型別註冊都不可使用 InstancePerDependency()。 * 如果沒有要使用循環依賴,則不需要傳入 `PropertyWiringOptions.AllowCircularDependencies`。 ::: #### 官網文件 * [Circular Dependencies](https://autofac.readthedocs.io/en/latest/advanced/circular-dependencies.html) ## 在 MVC 上使用 Autofac ### NuGet 套件 * Autofac * Autofac.Mvc5 ### 程式碼範例 #### Golbal.asax.cs 以下程式碼來自官網範例,`RegisterControllers()` 為必要,要設定後,才可以將 Instance Injection 到 Controller。 註解被標註「OPTIONAL」視情況是否添加,例如要使用 `HttpContextBase` 等型別注入,則需要 Register `AutofacWebTypesModule`。 ```csharp 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 ```csharp 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 ```csharp // 沒有使用 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 寫法如下: ```htmlmixed @inherits DISample.MVC.WebViewPageBase @{ IAppService appService = GetDependencyService<IAppService>(); } ``` 有使用 Model 寫法如下: ```htmlmixed @inherits DISample.MVC.WebViewPageBase<ViewModel> @{ IAppService appService = GetDependencyService<IAppService>(); } ``` 如果希望可以和原來一樣不使用 Model 就不用特別宣告,使用 Model 則使用 `@model ViewModel` 宣告的作法,請參考此篇[文章](https://github.com/autofac/Autofac/issues/349)修改 `\View\Web.config` 內容。 ```xml <configuration> <system.web.webPages.razor> <pages pageBaseType="MyNamespace.WebViewPageBase"> </pages> </system.web.webPages.razor> </configuration> ``` :::info * 需注意修改的「Web.config」是資料夾「View」底下的,而非專案根目錄底下的。 * MyNamespace 請替換成實際專案的 Namespace。 ::: ### 模擬 ASP.NET Core 的 `FromServicesAttribute` Injection 至 Action Parameter #### ServicesModelBinder ```csharp 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 ```csharp [AttributeUsage(AttributeTargets.Parameter)] public sealed class FromServicesAttribute : CustomModelBinderAttribute { public override IModelBinder GetBinder() => new ServicesModelBinder(); } ``` #### Controller Action ```csharp 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 ```xml <appSettings> <add key="Path:Upload" value="C:\Upload\" /> <add key="Test:IsTest" value="true" /> <add key="Test:TestName" value="Test" /> </appSettings> ``` #### Golbal.asax.cs ```csharp 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 的地方 ```csharp 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 ```csharp 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 ```csharp 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](https://autofac.readthedocs.io/en/latest/integration/mvc.html) ## 在 Web API 上使用 Autofac ### NuGet 套件 * Autofac * Autofac.WebApi2 ### 程式碼範例 #### Golbal.asax.cs 以下程式碼來自官網範例,`RegisterApiControllers()` 為必要,要設定後,才可以將 Instance Injection 到 ApiController。 註解被標註「OPTIONAL」視情況是否添加。 ```csharp 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 ```csharp public class ValuesController : ApiController { private readonly IAppService appService; public ValuesController(IAppService appService) { this.appService = appService ?? throw new ArgumentNullException(nameof(appService)); } //...實作 ValuesController Action... } ``` ### 官網文件 [Web API](https://autofac.readthedocs.io/en/latest/integration/webapi.html) ## 在 Web Form 上使用 Autofac 由於 Web Form 不支援 Constructor Injection,所以需要用 Property Injection。 ### NuGet 套件 * Autofac * Autofac.Web ### 程式碼範例 Web.config ```xml <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> ``` :::warning 官網建議兩種寫法都寫,來相容不同的 IIS 版本,但實際上兩個都寫有可能會有 Error。 ::: #### Golbal.asax.cs ```csharp 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 ```csharp public partial class _Default : Page { public IAppService AppService { get; set; } protected void Page_Load(object sender, EventArgs e) { //...實作... } } ``` ### 官網文件 [Web Forms](https://autofac.readthedocs.io/en/latest/integration/webforms.html) ## 在 Web Service 上使用 Autofac * Web Service 不支援 Constructor Injection,所以需要用 Property Injection。 * Web.config 裡的 Xml 設定是提供給 Web Form 使用的,Web Service 無法用它來設定 Property Injection,所以需要藉由 `WebServiceBase` 來實作這部分。 ### NuGet 套件 * Autofac * Autofac.Web ### 程式碼範例 #### Golbal.asax.cs 同 Web Form。 #### WebServiceBase ```csharp public abstract class WebServiceBase : System.Web.Services.WebService { public WebServiceBase() { IContainerProviderAccessor cpa = (IContainerProviderAccessor)HttpContext.Current.ApplicationInstance; // 為自身進行 Property Injection cpa.ContainerProvider.RequestLifetime.InjectProperties(this); } } ``` #### WebService ```csharp [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`