Skip to content
On this page

Asp.Net 依赖注入使用

1. 依赖注入在管道构建过程中的使用

在ASP.Net管道的构架过程中主要涉及三个对象/类型,作为宿主的WebHost和它的创建者WebHostBuilder,以及注册到WebHostBuilderStartup类型。 如下的代码片段体现了启动ASP.Net应用采用的典型编程模式:我们首先创建一个IWebHostBuilder对象,并将Startup类型注册到它之上。在调用Build方法创建WebHost之前,我们还可以调用相应的方式做其它所需的注册工作。当我们调用WebHostRun方法之后,后者会利用注册的Startup类型来构建完整的管道。那么在管道的构建过程中,DI是如何被应用的呢?

csharp
WebHost.CreateDefaultBuilder(args)
    .UseStartup<Startup>()
    .Xxx
    .Build()
    .Run();

DI在ASP.Net管道构建过程中的应用基本体现在下面这个序列图中。当我们调用WebHostBuilderBuild方法创建对应的WebHost的时候,前者会创建一个ServiceCollection对象,并将一系列预定义的服务注册在它之上。接下来WebHostBuilder会利用这个ServiceCollection对象创建出对应的ServiceProvider,这个ServiceProviderServiceCollection对象会一并传递给最终创建WebHost对象。当我们调用WebHostRun方法启动它的时候,如果注册的Startup是一个实例类型,则会以构造器注入的方式创建对应的Startup对象。我们注册的Startup类型的构造函数是允许定义参数的,但是参数类型必须是预先注册到ServiceCollection中的服务类型。

DI在ASP.Net管道构建过程中的应用

注册的Startup方法可以包含一个可选的ConfigureServices方法,这个方法具有一个类型为IServiceCollection接口的参数。WebHost会将WebHostBuilder传递给它的ServiceCollection作为参数调用这个ConfigureServices方法,而我们则利用这个方法将注册的中间件和应用所需的服务注册到这个ServiceCollection对象上。在这之后,所有需要的服务(包括框架和应用注册的服务)都注册到这个ServiceCollection上面,WebHost会利用它创建一个新的ServiceProviderWebHost会利用这个ServiceProvider对象以方法注入的方式调用Startup对象/类型的Configure方法,最终完成你对整个管道的建立。换句话会说,定义在Startup类型中旨在用于注册MiddlewareConfigure方法除了采用IApplicationBuilder作为第一个参数之外,它依然可以采用注册的任何一个服务类型作为后续参数的类型。

服务的注册除了现在注册的Startup类型的ConfigureServices方法之外,实际上还具有另一个实现方式,那就是调用IWebHostBuilder定义的ConfigureServices方法。当WebHostBuilder创建出ServiceCollection对象并完成了默认服务的注册后,我们通过调用这个方法所传入的所有Action<IServiceCollection>对象将最终应用到这个ServiceCollection对象上。

csharp
public interface IWebHostBuilder
{
    IWebHostBuilder ConfigureServiecs(Action<IServiceCollection> configureServices);
}

值得一提的是,Startup类型的ConfigureServices方法是允许具有一个IServiceProvider类型的返回值,如果这个方法返回一个具体的ServiceProrivder,那么WebHost将不会利用ServiceCollection来创建ServiceProvider,而是直接使用这个返回的ServiceProvider来调用Startup对象/类型的Configure方法。这实际上是一个很有用的扩展点,使用它可以实现针对第三方DI框架(如UnityCastleNinjectAutoFac等)的集成。

这里我们只是简单的介绍了Asp.Net程序启动的简单过程,具体实现细节属于Asp.Net框架的内容,我们将在后续Asp.Net 程序启动源码和DI源码分析中做详细介绍

2. 依赖服务注册

接下来我们通过一个实例来演示如何利用Startup类型的ConfigureServices来注册服务,以及在Startup类型上的两种依赖注入形式。如下面的代码片段所示,我们定义了两个服务接口(IFooIBar)和对应的实现类型(FooBar)。其中服务Foo是通过调用WebHostBuilderConfigureServices方法进行注册的,而另一个服务Bar的注册则发生在StartupConfigureServices方法上。对于Startup来说,它具有一个类型为IFoo的只读属性,该属性在构造函数利用传入的参数进行初始化,不用说这体现了针对Startup的构造器注入。StartupConfigure方法除了ApplicationBuilder作为第一个参数之外,还具有另一个类型为IBar的参数,我们利用它来演示方法注入。

csharp
public interface IFoo { }
public interface IBar { }
public class Foo : IFoo { }
public class Bar : IBar { }
 
public class Program
{
    public static void Main(string[] args)
    {
        WebHost.CreateDefaultBuilder(args)
            .ConfigureServices(services => services.AddSingleton<IFoo, Foo>())
            .UseStartup<Startup>()
            .Build()
            .Run();
    }
}
public class Startup
{
    public IFoo Foo { get; private set; }
    public Startup(IFoo foo)
    {
        this.Foo = foo;
    }    
    public void ConfigureServices(IServiceCollection services)
    {
        // 最常用的服务注册方式
        services.AddTransient<IBar, Bar>();
    }
    
    public void Configure(IApplicationBuilder app, IBar bar)
    {
        app.Run(async context =>
        {
            context.Response.ContentType = "text/html";
            await context.Response.WriteAsync($"IFoo=>{this.Foo}<br/>");
            await context.Response.WriteAsync($"IBar=>{bar}");
        });
    }
}

StartupConfigure方法中,我们调用IApplicationBulderRun方法注册了一个Middleware,后者将两个注入的服务的类型作为响应的内容输出。

依赖服务的注册与注入

另外,WebHostBuilder在创建ServiceCollection之后,会注册一些默认的服务(如IHostingEnvironmentILoggerFactory等)。这些服务和我们自行注册的服务并没有任何区别,只要我们知道对应的服务类型,就可以通过注入的方式获取并使用它们。

ASP.Net的一些组件已经提供了一些实例的绑定,像AddMvc就是Mvc MiddlewareIServiceCollection上添加的扩展方法。

csharp
public static IMvcBuilder AddMvc(this IServiceCollection services)
{
    if (services == null)
    {
        throw new ArgumentNullException(nameof(services));
    }
 
    var builder = services.AddMvcCore();
 
    builder.AddApiExplorer();
    builder.AddAuthorization();
    AddDefaultFrameworkParts(builder.PartManager);
    ...
}

3. 依赖服务消费

依赖服务之后就可以在需要的位置消费服务了。DI三种注入方式,Asp.Net默认仅支持构造器注入方式和面向约定的方法注入(框架级别使用,如StarupConfig方法)。上面案例中在Startup的构造函数和Config方法分别体现了两种注入方式。

下面我们来演示在Asp.Net项目中Startup之外的位置如何消费DI服务。

3.1 Controller/PageModel

csharp
private ILoginService<ApplicationUser> _loginService;
public AccountController(
  ILoginService<ApplicationUser> loginService)
{
  _loginService = loginService;
}

我们只要在控制器的构造函数里面声明这个参数,ServiceProvider就会把对象注入进来。如果仅在个别Action方法使用注入对象,也可以通过[FromService]方式注入对象。

csharp
public async Task Post([FromServices] ILoginService<ApplicationUser> loginService,User user)
{
    loginService.LoginAsync(user);
    // do something
}

3.2 View

在View中需要用@inject 再声明一下,起一个别名。

html
@using MilkStone.Services;
@model MilkStone.Models.AccountViewModel.LoginViewModel
@inject ILoginService<ApplicationUser>  loginService
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head></head>
<body>
  @loginService.GetUserName()
</body>
</html>

3.3 HttpContext获取实例

HttpContext下有一个RequestedService同样可以用来获取实例对象,不过这种方法一般不推荐。同时要注意GetService<>这是个范型方法,默认如果没有添加Microsoft.Extension.DependencyInjectionusing,是不用调用这个方法的。

csharp
HttpContext.RequestServices.GetService<ILoginService<ApplicationUser>>();

参考文献

4. Autofac

Asp.Net框架的依赖注入基本可以满足一般的日常使用需求,但如果需要使用依赖注入的以下特性则需要借助更为强大的第三方依赖注入框架,其中最流行也最具代表性的当属Autofac.

  • 基于名称的注入
  • 属性注入
  • 子容器
  • 基于动态代理的AOP

Asp.Net框架的依赖注入核心扩展点是IServiceProviderFactory<TContainerBuilder>,第三方依赖注入框架都以此为扩展点。下面我们来快速演示一下Autofac的使用。

程序启动过程中使用Autofac接管依赖注入。

csharp
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseServiceProviderFactory(new AutofacServiceProviderFactory())
        .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })

Startup中声明ConfigureContainer方法并在此注入所需对象,此方法会在ConfigureServices方法执行之后被调用。

csharp
public void ConfigureContainer(ContainerBuilder builder)
{
    builder.RegisterInstance(new RedisHelper(Configuration["RedisConnectionString"])).AsSelf();
    builder.Register(c => new DapperHelper<MySqlConnection>(Configuration["MySqlConnectionString"]))
        .AsSelf();
    builder.RegisterType<MapperConfig>().As<IMapperConfiguration>();
    
    // 程序集扫描注入
    builder.RegisterAssemblyTypes(Assembly.Load(Configuration["DataAccessImplementAssembly"]))
        // 仅注入公有类型
        .PublicOnly()
        // 仅注入 未标记 ExcludeAutofacInjectionAttribute 的类型
        .Where(t => !t.IsDefined(typeof(ExcludeAutofacInjectionAttribute)))
        .AsImplementedInterfaces();

    // 基于Key注入
    builder.RegisterType<Dog>().Keyed<IPet>("Dog");
    builder.RegisterType<Cat>().Keyed<IPet>("Cat");

    // 属性注入
    builder.RegisterType<PetStore>().As<IPetStore>()
        // 启用 Attribute 过滤
        .WithAttributeFiltering()
        // 启用属性将被注入
        .PropertiesAutowired();
}

[AttributeUsage(AttributeTargets.Class)]
public class ExcludeAutofacInjectionAttribute : Attribute
{
}

基于名称注入的对象可以通过Autofac.Features.Indexed.IIndex<K,V>获取。

csharp
public class PetStore : IPetStore
{
  public PetStore(IIndex<string, IPet> pets) 
  { 
      var dog = pets["Dog"];
      var cat = pets["Cat"];
  }
}

Configure方法中获取Autofac注入对象。

csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    var autofacContainer = app.ApplicationServices.GetAutofacRoot();
    var redis = autofacContainer.Resolve<RedisHelper>();
    // 获取命名注入对象
    var dog = autofacContainer.ResolveNamed<IPet>("Dog");

    app.UseRouting();
    app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}

在.Net Worker Service中可以通过如下方式使用Autofac接管DI和注册服务。

csharp
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseServiceProviderFactory(new AutofacServiceProviderFactory())
        .ConfigureServices((hostContext, services) =>services.AddHostedService<Worker>())
        .ConfigureContainer<ContainerBuilder>((context, builder) => builder.RegisterType<Dog>().As<IPet>());

Released under the MIT License.