用Scrutor來簡化ASP.NET Core的DI註冊

背景

在咱們編寫ASP.NET Core代碼的時候,老是離不開依賴注入這東西。並且對於這一塊,咱們有很是多的選擇,好比:M$ 的DI,Autofac,Ninject,Windsor 等。github

因爲M$自帶了一個DI框架,因此通常狀況下都會優先使用。雖然說功能不是特別全,但也基本知足使用了。框架

正常狀況下(包括好多示例代碼),在要註冊的服務數量比較少時,咱們會選擇一個一個的去註冊。ide

比如下面的示例:post

services.AddTransient<IUserRepository, UserRepository>();
services.AddTransient<IUserService, UserService>();

在數量小於5個的時候,這樣的作法還能夠接受,可是,數量一多,還這樣子秀操做,可就有點接受不了了。ui

可能會常常出現這樣的問題,新加了一個東西,忘記在Startup上面註冊,下一秒獲得的就是相似下面的錯誤:spa

System.InvalidOperationException: Unable to resolve service for type 'ScrutorTest.IProductRepository' while attempting to activate 'ScrutorTest.Controllers.ValuesController'.
   at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)
   at lambda_method(Closure , IServiceProvider , Object[] )
   at Microsoft.AspNetCore.Mvc.Controllers.ControllerActivatorProvider.<>c__DisplayClass4_0.<CreateActivator>b__0(ControllerContext controllerContext)
   at Microsoft.AspNetCore.Mvc.Controllers.ControllerFactoryProvider.<>c__DisplayClass5_0.<CreateControllerFactory>g__CreateController|0(ControllerContext controllerContext)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()
   at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

這樣一來一回,其實也是挺浪費時間的。.net

爲了不這種狀況,咱們每每會根據規律在註冊的時候,用反射進行批量註冊,後面按照對應的規律去寫業務代碼,就能夠避免上面這種問題了。code

對於這個問題,本文將介紹一個擴展庫,來幫咱們簡化這些操做。接口

Scrutor簡介

Scrutor是 Kristian Hellang 大神寫的一個基於Microsoft.Extensions.DependencyInjection的一個擴展庫,主要是爲了簡化咱們對DI的操做。

Scrutor主要提供了兩個擴展方法給咱們使用,一個是Scan,一個是Decorate

本文主要講的是Scan這個方法。

Scrutor的簡單使用

註冊接口的實現類

這種情形應該是咱們用的最多的一種,因此優先來講這種狀況。

假設咱們有下面幾個接口和實現類,

public interface IUserService { }
public class UserService : IUserService { }

public interface IUserRepository { }
public class UserRepository : IUserRepository { }

public interface IProductRepository { }
public class ProductRepository : IProductRepository { }

如今咱們只須要註冊UserRepositoryProductRepository

services.Scan(scan => scan
    .FromAssemblyOf<Startup>()
        .AddClasses(classes => classes.Where(t=>t.Name.EndsWith("repository",StringComparison.OrdinalIgnoreCase)))
        .AsImplementedInterfaces()
        .WithTransientLifetime()
    );

簡單解釋一下,上面的代碼作了什麼事:

  1. FromAssemblyOf<Startup> 表示加載Startup這個類所在的程序集
  2. AddClasses 表示要註冊那些類,上面的代碼還作了過濾,只留下了以 repository 結尾的類
  3. AsImplementedInterfaces 表示將類型註冊爲提供其全部公共接口做爲服務
  4. WithTransientLifetime 表示註冊的生命週期爲 Transient

若是瞭解過Autofac的朋友,看到這樣的寫法應該很熟悉。

對於上面的例子,它等價於下面的代碼

services.AddTransient<IUserRepository, UserRepository>();
services.AddTransient<IProductRepository, ProductRepository>();

若是咱們在註冊完成後,想看一下咱們本身註冊的信息,能夠加上下面的代碼:

var list = services.Where(x => x.ServiceType.Namespace.Equals("ScrutorTest", StringComparison.OrdinalIgnoreCase)).ToList();

foreach (var item in list)
{
    Console.WriteLine($"{item.Lifetime},{item.ImplementationType},{item.ServiceType}");
}

運行dotnet run以後,能夠看到下面的輸出

Singleton,ScrutorTest.UserRepository,ScrutorTest.IUserRepository
Singleton,ScrutorTest.ProductRepository,ScrutorTest.IProductRepository

這個時候,若是咱們加了一個 IOrderRepositoryOrderRepostity , 就不須要在Startup上面多寫一行註冊代碼了,Scrutor已經幫咱們自動處理了。

接下來,咱們須要把UserService也註冊進去,咱們徹底能夠照葫蘆畫瓢了。

services.Scan(scan => scan
    .FromAssemblyOf<Startup>()
        .AddClasses(classes => classes.Where(t=>t.Name.EndsWith("repository",StringComparison.OrdinalIgnoreCase)))
        .AsImplementedInterfaces()
        .WithTransientLifetime()
    );

services.Scan(scan => scan
    .FromAssemblyOf<Startup>()
        .AddClasses(classes => classes.Where(t => t.Name.EndsWith("service", StringComparison.OrdinalIgnoreCase)))
        .AsImplementedInterfaces()
        .WithTransientLifetime()
    );

也能夠略微簡單一點點,一個scan裏面搞定全部

services.Scan(scan => scan
    .FromAssemblyOf<Startup>()
        .AddClasses(classes => classes.Where(t=>t.Name.EndsWith("repository",StringComparison.OrdinalIgnoreCase)))
            .AsImplementedInterfaces()
            .WithTransientLifetime()
            
        .AddClasses(classes => classes.Where(t => t.Name.EndsWith("service", StringComparison.OrdinalIgnoreCase)))
            .AsImplementedInterfaces()
            .WithScopedLifetime()//換一下生命週期
    );

這個時候結果以下:

Transient,ScrutorTest.UserRepository,ScrutorTest.IUserRepository
Transient,ScrutorTest.ProductRepository,ScrutorTest.IProductRepository
Scoped,ScrutorTest.UserService,ScrutorTest.IUserService

雖然效果同樣,可是總想着有沒有一些更簡單的方法。

不少時候,咱們寫一些接口和實現類的時候,都會根據這樣的習慣來命名,定義一個接口IClass,它的實現類就是Class

針對這種情形,Scrutor提供了一個簡便的方法來幫助咱們處理。

使用 AsMatchingInterface 方法就能夠很輕鬆的幫咱們處理註冊好對應的信息。

services.Scan(scan => scan
    .FromAssemblyOf<Startup>()
        .AddClasses()
            .AsMatchingInterface()
            .WithTransientLifetime()
    );

這個時候會輸出下面的結果:

Transient,ScrutorTest.UserService,ScrutorTest.IUserService
Transient,ScrutorTest.UserRepository,ScrutorTest.IUserRepository
Transient,ScrutorTest.ProductRepository,ScrutorTest.IProductRepository

固然這種方法也有對應的缺點,那就是對生命週期的控制。舉個例子,有兩大類,一大類要Transient,一大類要Scoped,這個時候,咱們也只能過濾掉部份內容才能註冊 。

須要根據自身的狀況來選擇是否要使用這個方法,或者何時使用這個方法。

註冊類自身

有時候,咱們建的一些類是沒有實現接口的,就純粹是在「裸奔」的那種,而後直接用單例的方式來調用。

Scrutor也提供了方法AsSelf來處理這種情形。

來看下面這段代碼。

services.Scan(scan => scan
    .AddTypes(typeof(MyClass))
        .AsSelf()
        .WithSingletonLifetime()
    );

這裏和前面的註冊代碼有一點點差別。

AddTypes是直接加載具體的某個類或一批類,這個的做用能夠認爲和FromXxx是同樣的。

它等價於下面的代碼

services.AddSingleton<MyClass>();

相對來講批量操做的時候仍是有點繁鎖,由於須要把每一個類型都扔進去,咱們不可能事先知道全部的類。

下面的方法能夠把MyClass所在的程序集的類都註冊了。

services.Scan(scan => scan
    .FromAssemblyOf<MyClass>()
        .AddClasses()
        .AsSelf()
        .WithSingletonLifetime()
    );

這樣的作法也有一個缺點,會形成部分咱們不想讓他註冊的,也註冊進去了。

過濾一下或者規範一下本身的結構,就能夠處理這個問題了。

重複註冊處理策略

還有一個比較常見的情形是,重複註冊,即同一個接口,有多個不一樣的實現。

Scrutor提供了三大策略,AppendSkipReplace。 Append是默認行爲,就是疊加。

下面來看這個例子

public interface IDuplicate { }
public class FirstDuplicate : IDuplicate { }
public class SecondDuplicate : IDuplicate { }
services.Scan(scan => scan
    .FromAssemblyOf<Startup>()                    
        .AddClasses(classes=>classes.AssignableTo<IDuplicate>())
            .AsImplementedInterfaces()
            .WithTransientLifetime()                    
);

這個時候的輸出以下

Transient,ScrutorTest.FirstDuplicate,ScrutorTest.IDuplicate
Transient,ScrutorTest.SecondDuplicate,ScrutorTest.IDuplicate

下面咱們用Skip策略來替換默認的策略

services.Scan(scan => scan
    .FromAssemblyOf<Startup>()                    
        .AddClasses(classes=>classes.AssignableTo<IDuplicate>())
            //手動高亮
            .UsingRegistrationStrategy(RegistrationStrategy.Skip)
            .AsImplementedInterfaces()
            .WithTransientLifetime()                    
);

這個時候的輸出以下

Transient,ScrutorTest.FirstDuplicate,ScrutorTest.IDuplicate

可見獲得的結果確實沒有了第二個註冊。

總結

Scrutor的Scan方法確實很方便,可讓咱們很容易的擴展M$ 的DI。

固然Scrutor還有其餘的用法,詳細的能夠參考它的Github頁面。

相關文章

Introducing Scrutor - Convention based registration for Microsoft.Extensions.DependencyInjection

Using Scrutor to automatically register your services with the ASP.NET Core DI container

相關文章
相關標籤/搜索