背景
在我們編寫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 { }
現在我們只需要注冊UserRepository
和ProductRepository
,
services.Scan(scan => scan
.FromAssemblyOf<Startup>()
.AddClasses(classes => classes.Where(t=>t.Name.EndsWith("repository",StringComparison.OrdinalIgnoreCase)))
.AsImplementedInterfaces()
.WithTransientLifetime()
);
簡單解釋一下,上面的代碼做了什么事:
FromAssemblyOf<Startup>
表示加載Startup
這個類所在的程序集AddClasses
表示要注冊那些類,上面的代碼還做了過濾,只留下了以 repository 結尾的類AsImplementedInterfaces
表示將類型注冊為提供其所有公共接口作為服務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
這個時候,如果我們加了一個 IOrderRepository
和 OrderRepostity
, 就不需要在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提供了三大策略,Append、Skip和Replace。 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