ASP.NET Core入门——使用Autofac实现DI与AOP

3,036 阅读5分钟

内置依赖注入

在依赖注入模式中,依赖一般都需要注册到一个服务容器中,ASP.NET Core 提供了一个内置的服务容器 IServiceProvider。服务依赖需要在应用的 Startup.ConfigureServices 方法中注册。

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddScoped<IMyDependency, MyDependency>();  // <=
}

使用Autofac容器替换默认容器

这里以 ASP.NET Core 3.1为例,ASP.NET Core 1.1 - 2.2用法略有不同。使用VS2019新建一个ASP.NET Core 3.1项目。

  • 首先从Nuget安装Autofac.Extensions.DependencyInjection
  • 修改Program.CreateHostBuilder方法:
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseServiceProviderFactory(new AutofacServiceProviderFactory())   //<=
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}
  • 在Startup类里添加一个ConfigureContainer方法,这是约定名称,在这个方法中可以访问Autofac容器并注册依赖。
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        ...
    }

    public void ConfigureContainer(ContainerBuilder builder)
    {
        // Register your own things directly with Autofac, like:
        builder.RegisterModule(new MyApplicationModule());
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        ...
    }
}

编写服务

接下来我们来编写一个服务测试一下。

首先在项目中新建一个Services文件夹,在文件夹中新建一个接口IMyService:

 public interface IMyService
{
    int DoSomething(int id);
}

再新建一个实现类MyService:

public class MyService : IMyService
{
    public int DoSomething(int id)
    {
        Console.WriteLine("DoSomething:"+id);
        return id;
    }
}

这个类没有什么功能,只是接收一个int值,打印一句话,并返回收到的值。

因为不希望Startup类太长,我们不在ConfigureContainer方法中注册所有服务,新建一个DependencyRegister类:

public class DependencyRegister
{
    public static ContainerBuilder Register(ContainerBuilder builder)
    {
        builder.RegisterType<MyService>().As<IMyService>();
        return builder;
    }
}

这个类有一个静态方法Register,接收ContainerBuilder作为参数,并返回ContainerBuilder,我们在这个类中注册所有的服务,之后在Startup.ConfigureContainer中调用这个方法,返回ContainerBuilder是可能还需要一些调用。

在Startup.ConfigureContainer中:

 public void ConfigureContainer(ContainerBuilder builder)
{
    DependencyRegister.register(builder);
}

接下来新建一个控制器HomeController,在控制器中注入服务:

public class HomeController : Controller
{
    private readonly IMyService _myService;
    public HomeController(IMyService myService)
    {
        this._myService = myService;
    }
    public IActionResult Index()
    {
        var result = _myService.DoSomething(1);
        return View();
    }
}

这样就在ASP.NET Core中使用Autofac实现了依赖注入。但是此时ASP.NET Core只是从容器中创建控制器的构造函数参数,控制器本身并不是从容器中创建的。要想让控制器也从容器中创建需要使用AddControllersAsServices()

public class Startup
{
  // Omitting extra stuff so you can see the important part...
  public void ConfigureServices(IServiceCollection services)
  {
    // Add controllers as services so they'll be resolved.
    services.AddMvc().AddControllersAsServices();
  }

  public void ConfigureContainer(ContainerBuilder builder)
  {
    // If you want to set up a controller for, say, property injection
    // you can override the controller registration after populating services.
    builder.RegisterType<MyController>().PropertiesAutowired();
  }
}

AOP

Autofac使用Autofac.Extras.DynamicProxy实现方法拦截,在Autofac 4.0.0版本之前,使用Autofac.Extras.DynamicProxy2。使用Nuget安装对应版本。

  • 在注册时启动拦截器支持:
builder.RegisterType<MyService>().As<IMyService>().EnableInterfaceInterceptors();
  • 编写拦截器。拦截器是一个继承IInterceptor的类。
public class CallLogger : IInterceptor
{
  TextWriter _output;

  public CallLogger(TextWriter output)
  {
    _output = output;
  }

  public void Intercept(IInvocation invocation)
  {
    _output.WriteLine("Calling method {0} with parameters {1}... ",
      invocation.Method.Name,
      string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray()));

    invocation.Proceed();

    _output.WriteLine("Done: result was {0}.", invocation.ReturnValue);
  }
}
  • 注册拦截器。拦截器要注册到Autofac容器中。
builder.Register(c => new CallLogger(Console.Out));
  • 使用拦截器。将Intercept特性标注在类或接口上对方法进行拦截。
[Intercept(typeof(CallLogger))]
    public class MyService : IMyService
    {
        public int DoSomething(int id)
        {
            Console.WriteLine("DoSomething:"+id);
            return 0;
        }
    }

拦截器成功调用:

实现一个SpringBoot风格的Cacheable特性

在SpringBoot中,有一个@Cacheable注解,将这个注解标注在方法上,会对方法的返回结果进行缓存,想要在ASP.NET Core中实现一个类似的功能。

首先我们需要一个自定义的特性,这个特性标注在方法上,将这个方法标注为需要缓存。然后,需要一个拦截器,在调用方法时查看这个方法的特性是否需要缓存,如果需要缓存,就将结果放入缓存,并在下次访问时从缓存中取数据。

自定义特性Cacheable

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class CacheableAttribute:Attribute
{
}

新建拦截器CheckCache,这个拦截器检查方法是否需要缓存

public class CheckCache : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        var cacheable =invocation.MethodInvocationTarget.GetCustomAttributestypeof(CacheableAttribute), true).Length > 0;
        if(cacheable)
        {
            //执行方法前先检查缓存,如果缓存中有数据直接返回
        }
        invocation.Proceed();
        if (cacheable)
        {
            //执行方法后将方法返回值写入缓存
        }
    }
}

注册这个拦截器:

builder.RegisterType<CheckCache>();

为了操作缓存,我们新建一个接口ICache,方便缓存与具体的实现解耦:

public interface ICache
{
    object Get(string key);
    void Put(string key, object value);
}

这个接口很简单,只有两个方法,分别是查询缓存与插入缓存。

为了简单,写一个没有任何操作的缓存实现NoOpCache:

public class NoOpCache : ICache
{
    public object Get(string key)
    {
        Console.WriteLine("查询缓存:" + key);
        return null;
    }
    public void Put(string key, object value)
    {
        Console.WriteLine("插入缓存:" + key);
    }
}

这个缓存类啥也不干,既不会插入值,也不会返回值,只是为了测试,实际情况中可能要实现MemeryCache或RedisCache等。

注册这个类:

builder.RegisterType<NoOpCache>().As<ICache>().EnableInterfaceInterceptors();

将缓存服务注入到CheckCache类中,并使用:

public class CheckCache : IInterceptor
{
    private readonly ICache _cache;
    public CheckCache(ICache cache)
    {
        this._cache = cache;
    }
    public void Intercept(IInvocation invocation)
    {
        var cacheable =invocation.MethodInvocationTarget.GetCustomAttributestypeof(CacheableAttribute), true).Length > 0;
        string key = "";
        if(cacheable)
        {
            //执行方法前先检查缓存,如果缓存中有数据直接返回
            
            key = invocation.Method.Name + string.Join(", ",invocation.Arguments.Select(a => (a ??"").ToString()).ToArray());
            var cacheValue = _cache.Get(key);
            if(cacheValue!=null)
            {
            if(invocation.Method.ReturnType.IsInstanceOfTpe(cacheValue))
                {
                    invocation.ReturnValue = cacheValue;
                    return;
                }
            }
        }
        invocation.Proceed();
        if (cacheable)
        {
            //执行方法后将方法返回值写入缓存
            _cache.Put(key, invocation.ReturnValue);
        }
    }
}

在这里我们简单的使用方法名加参数名拼成一个key来使用,在SpringBoot中实际上可以使用表达式来指定key,这里我们为了简单就这么实现了,后面可以再改进。

最后将特性加到服务上:

[Intercept(typeof(CallLogger))]
[Intercept(typeof(CheckCache))]
public class MyService : IMyService
{
    [Cacheable]
    public int DoSomething(int id)
    {
        Console.WriteLine("DoSomething:"+id);
        return id;
    }
}

这样在执行DoSomething时就会查询缓存,如果缓存中有数据就用缓存中的数据,否则就执行方法,并将返回值插入缓存: