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());


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM