1. 程式人生 > >C# 依賴注入

C# 依賴注入

依賴注入

1. 什麼是依賴注入

   我們建立一個SkiCardController需要應用程式中的一些其他服務才能處理檢視,建立和編輯的請求。具體來說,他用SkiCardContext訪問資料,用UserManager訪問當前使用者的資訊,用IAuthorizationService檢查當前使用者是否有許可權編輯或者檢視所有請求。
   如果不用DI或者其他模式,SkiCardController就負責建立這些服務的新例項。
   沒有DI的SkiCardController

public class SkiCardController:Controller
{
    private readonly SkiCardContext _SkiCardContext;
    private readonly UserManager<ApplicationUser> _UserManager;
    private readonly IAuthorizationService _AuthorizationService;
    public SkiCardController()
    {
        _SkiCardContext = new SkiCardContext(new DbContextOptions<SkiCardContext>());

        _UserManager = new UserManager<ApplicationUser>();

        _AuthorizationService = new DefaultAuthorizationService();
    }
}

   這些看起來還算簡單,但實際上這段程式碼是無法通過編譯的。首先,我們沒有為SkiCardContext指定資料庫或者連線字串,所以他沒有正確建立DbContext。UserManager沒有預設的建構函式,UserManager公開的唯一一個建構函式需要九個引數。
   UserManager類的公開建構函式

public UserManager(IUserStore<TUser> store, IOption<IdentityOptions> optionsAccessor, IPasswordHasher<TUser> passwordHasher, IEnumerable<IUserValidator<TUser>> userValidator, IEnumerable<IPasswordValidator<IUser>> passwordValidator, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider service, ILogger<UserManager<TUser>> logger)
{
    // ...
}

   那麼,我們的SkiCardController現在還需要知道如何去建立這些服務。DefaultAuthorizationService的建構函式也有三個引數。無論是我們的控制器,還是應用程式的其他服務,與之互動的所有服務都需要自己動手建立,這種做法顯然不合適。
   這種做法除了帶來大量重複程式碼之外,還會導致程式碼緊耦合。例如,SkiCardController現在知曉了DefaultAhtorizationService這個類的具體知識,而不是大概瞭解IAuthorizationService介面公開的方法。假如我們想要更改DefaultAuthorizationService的建構函式,我們還需要更改SkiCardController以及其他使用了DefaultAuthorizationService

的類。
   緊耦合還會加大更換實現的難度。雖然我們不太可能自己去實現一個全新的授權服務,但是替換實現的能力依然很重要,他使得mocking變得更加容易。mocking這種技術的重要性則在於它能讓針對應用程式中的服務之間的互動變得更加容易。

2. 使用服務容易解析依賴

   依賴注入是用來解析依賴項的一種常見模式。使用依賴注入之後,建立和管理類的例項的職責就轉交給某個容器。此外,每一個類都需要宣告他所依賴的其他類。然後容器就可以在執行期間解析這些依賴項,並按需傳遞。依賴注入模式是控制反轉(IoC)的一種形式,意思是元件自身無需直接例項化器依賴項的職責。你或許聽過IoC容器,這是DI實現的另一種叫法。
   最常見的依賴注入方法是使用建構函式注入技術。使用建構函式注入時,類會宣告一個建構函式,以引數的形式接受它需要的所有服務。例如,SkiCardController擁有一個接受SkiCardContext、UserManager和IAuthorizationService的建構函式,容器會負責在執行期間將這些類的例項傳遞給它。

public class SkiCardController : Controller
{
    private readonly SkiCardContext _SkiCardContext;
    private readonly UserManager<ApplicationUser> _UserManager;
    private readonly IAthorizationService _AuthorizationService;

    public SkiCardController(SkiCardContext skiCardContext, UserManager<ApplicationUser> userManager, IAthorizationService autherizationService)
    {
        _SkiCardContext = skiCardContext;
        _UserManager = userManager;
        _AuthorizationService = autherizationService
    }
}

   建構函式注入能夠清晰地體現給定的某個類所需要的依賴。甚至連編譯器都會為我們提供幫助,因為不傳遞必需的類無法建立SkiCardController。正如我們之前所說,這種方法的主要好處是能夠讓單元測試更加簡單。
   依賴注入的另一種方法是屬性注入,可以使用一個特性來修飾某個公開的屬性,一次來表明容易應當在執行期間設定該屬性的值。屬性注入不如建構函式注入那麼常見,也不是所有的IoC容器都支援這種方法。
   在應用程式啟動時,可以向容器註冊服務。註冊服務的方法取決於所使用的容器。

注意:
目前,依賴注入是解決依賴問題時最受歡迎的模式,但並不是唯一可用的模式。Service Locator模式在一段時間內曾受到.Net社群的追捧,使用這種模式時,服務會註冊到一箇中央式服務定位器。如果某個服務需要另一種服務的例項,它會向服務定位器請求該服務型別的例項。Service Locator模式的主要缺點是某個服務都顯示地依賴服務定位器。

ASP.NET Core 中的依賴注入

   ASP.NET Core提供了容器的基本實現,原生支援建構函式注入。在應用程式啟動時,可以在Startup類的ConfigureService方法中註冊服務。
   Startup的ConfigureService方法

    public void ConfigureService(IServiceCollection service)
    {
        // add service here.
    }

   哪怕在最簡單的 ASP.NET Core MVC 專案裡,為了讓你的應用程式正常執行,容器也至少要包含一些服務才行。MVC框架自身也依賴容器的一些服務,並通過他們來正確地支援控制器啟用、檢視渲染以及其他核心概念。

使用內建容器

   你要做的首先是新增 ASP.NET Core 框架所提供的服務。如果 ASP.NET Core 提供的每一個服務都需要你手動註冊的話,ConfigureService方法很快就會失控。幸運的是框架所提供的所有功能都有對應的Add*擴充套件方法,可以使用這些擴充套件方法來輕鬆地新增該功能所需要的服務。例如,AddDbContext方法用來註冊Entity FrameworkDbContext。這些方法還提供了選項委託,允許你在註冊服務時進行一些額外設定。例如,在註冊DbContext類時,使用選項委託來將上下文關聯到DefaultConnection連線字串中指定的SQL Server資料庫。
   在AlpineSkiHouse.Web中註冊DbContext

    service.AddDbContext<ApplicationUserContext>(options=>options.UserSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    service.AddDbContext<SkiCardContext>(options=>options.UserSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    service.AddDbContext<PassContext>(options=>options.UserSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    service.AddDbContext<PassTypeContext>(options=>options.UserSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    service.AddDbContext<RestoreContext>(options=>options.UserSqlServer(Configuration.GetConnectionString("DefaultConnection")));

   其他需要新增的框架功能還包括用於認證和授權的Identity、啟用強型別配置的Options以及啟用路由、控制器和其他所有內建功能的MVC。

    service.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStore<ApplicationUserContext>()
        .AddDefaultTokenProviders();
    service.AddOptions();
    service.AddMvc();

   下一步是註冊你編寫的應用程式服務或者第三方類庫中包含的服務。確保任意控制器所需的任意服務都正確地註冊了。在註冊應用服務時,一定要考慮該服務的生命週期。

注意:
容器的職責之一是管理服務的生命週期。服務的生命週期是指服務所存在的時間(從被依賴注入容器建立開始,到容器釋放該服務的所有例項為止)。

生命週期 描述
Transient 每次請求服務時,都會建立一個新例項。這種生命週期適合輕量級服務
Scoped 為每一個HTTP請求建立一個例項
Singleton 在每一次請求服務時,為該服務建立一個例項
Instance 與Singleton類似,但是在應用程式啟動時會將該例項註冊到容器

   使用AddDbContext方法新增DbContext時,會使用Scoped生命週期類註冊該上下文。當一個請求進入管道,如果其後續的路由需要DbContext的一個例項,那麼就會建立一個例項,並將其提供給所有需要用到該資料庫連線的服務。實際上,容器建立的服務會被限制在對應的HTTP請求中,然後用來滿足該請求執行期間的所有依賴項。當請求完成後,容器就會釋放所有被佔用的服務,以便執行時進行清理。
   這裡展示了AlpineSkiHouse.web專案中的一些應用程式服務示例。它們的服務生命週期是通過相應的Add*方法指定的。

    service.AddSingleton<IAuthorizationHandler, EditSkiCardAuthorizationHandler>();
    service.AddTransient<IEmailSender, AuthMessageSender>();
    service.AddTransient<ISmsSender, AuthMessageSender>();
    service.AddScoped<ICsrInformationService, CsrInformationService>();

   隨著應用程式服務逐漸增多,可以通過建立擴充套件方法來簡化ConfigureService方法。舉例來說,如果你的應用程式擁有許多需要註冊的IAuthorizationHandler類,你就可以建立一個AddAuthorizationHandlers擴充套件方法。
  用來新增一組服務的擴充套件方法示例

public static void AddAuthorizationHandlers(this IServiceCollection services)
{
    services.AddSingleton<IAuthorizationHandler, EditSkiCardAuthorizationHandler>();
    // Add other authorization handlers
}

   將服務新增到IServiceCollection之後,框架會在執行期間使用建構函式注入來連線各依賴項。例如,如果一個請求被路由到SkiCardController,框架就會使用SkiCardController的公開建構函式來建立它的例項,同時向它傳遞所需的服務。控制器不再知曉如何建立這些服務以及如何管理他們的生命週期。

注意:
在開發新功能時,可能偶爾會接收到一條類似InvalidOperationException:Unable to resolve service for type 'ServiceType' while attempting to activate 'SomeController'的錯誤訊息。
最可能的原因是忘記在ConfigureServices方法中新增對應的服務型別。在本例中新增CsrInformationService就能解決這個錯誤。

    services.AddScoped<IScrInformationService, CsrInformationService>()

使用第三方容器

   ASP.NET Core 框架內建的容器只提供了用來支援大多數應用程式的必要功能。但.NET平臺還有許多功能更加豐富的成熟的依賴注入框架。幸運的是,ASP.NET Core內建了一種將預設容器替換成第三方容器的方法。
   一些流行於.NET平臺的IoC容器包括NinijectStructureMapAutofac。對於ASP.NET Core支援最好的是Autofac,所以我們會用它當範例。第一步引用NuGetAutofac.Extensions.DependencyInjection。接著,我們需要對Startup中的ConfigureService方法做一些修改。將其修改為返回IServiceProvider,而不是返回原來的void。框架服務依然會被新增到IServiceCollection,我們的應用程式服務則會註冊到Autofac容器。最後返回一個AutofacServiceProvider,它將ASP.NET Core提供用來取代內建容器的Autofac容器。
   使用Autofac的ConfigureServices

    public IServiceProvider ConfigureServices(IServiceCollectioin services)
    {
        // Add framework services
        service.AddDbContext<ApplicationUserContext>(options=>options.UserSqlServer(Configuration.GetConnectionString("DefaultConnection")));
        service.AddDbContext<SkiCardContext>(options=>options.UserSqlServer(Configuration.GetConnectionString("DefaultConnection")));
        service.AddDbContext<PassContext>(options=>options.UserSqlServer(Configuration.GetConnectionString("DefaultConnection")));
        service.AddDbContext<PassTypeContext>(options=>options.UserSqlServer(Configuration.GetConnectionString("DefaultConnection")));
        service.AddDbContext<RestoreContext>(options=>options.UserSqlServer(Configuration.GetConnectionString("DefaultConnection")));

        services.AddIdentity<ApplicationUser,IdentifyRole>()
        .AddEntityFrameworkStores<ApplicationUserContext>()
        .AddDefaultTokenProviders();

        services.AddOptions();
        services.AddMvc();

        // Now register our services with Autofac container
        var builder = new ContainerBuilder();
        builder.RegisterType<CsrInformationService>().As<ICsrInformationService>();
        builder.Populate(services);
        var container = builder.Build();

        // Create the IServiceProvider based on the container.
        return new AutofaceServiceProvider(container);
    }

   這個例項相當簡單,Autofac還提供了一些高階的功能,比如程式集掃描,可以用來查詢符合你選擇的條件的類。舉例來說,我們可以使用程式集掃描來自動註冊專案中所有的IAuthorizationHandler實現。
   使用程式集掃描來自動註冊型別。

    var currentAssembly = Assembly.GetEntryAssembly();
    builder.RegisterAssemblyTypes(currentAssembly)
    .Where(t => t.IsAssignableTo<IAuthorizationHandler>())
    .As<IAuthorizationHandler>();

   Autofac的另一個非常棒的功能是將配置分離到模組中。模組很簡單,就是一個類,它包含了一組相關的服務的配置。在最簡單的情況下,Autofac模組類似於為IServiceCollection建立擴充套件方法。但模組可以用來實現一些更加高階的功能。因為他們是類,在執行期間也可以發現並載入他們,這樣就能實現一種外掛框架了。
   Autofac模組簡單示例

    public class AuthorizationHandlerModule:Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            var currentAssembly = Assembly.GetEntryAssembly();
            builder.RegisterAssemblyTypes(currentAssembly)
            .Where(t=>t.IsAssignableTo<IAuthorizationHandler>())
            .As<IAuthorizationHandler>();
        }
    }

   在Startup.ConfigureServices中載入模組

    builder.RegisterModule(new AuthorizationHandlerModule());