用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

相关文章
相关标签/搜索