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


背景

在我們編寫ASP.NET Core代碼的時候,總是離不開依賴注入這東西。而且對於這一塊,我們有非常多的選擇,比如:M$ 的DI,Autofac,Ninject,Windsor 等。

由於M$自帶了一個DI框架,所以一般情況下都會優先使用。雖說功能不是特別全,但也基本滿足使用了。

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

好比下面的示例:

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

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

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

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)

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

為了避免這種情況,我們往往會根據規律在注冊的時候,用反射進行批量注冊,后面按照對應的規律去寫業務代碼,就可以避免上面這種問題了。

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

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


免責聲明!

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



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