• ASP.NET Core 一个接口多个实现的依赖注入与动态选择
  • 发布于 2个月前
  • 511 热度
    0 评论
  • 酸梅汤
  • 0 粉丝 8 篇博客
  •   

ASP.NET Core 自带的依赖注入(DI)非常实用,但是当一个接口有多个实现的时候怎么操作呢?运行时能否根据配置选择其中一种实现呢?能不能不用反射呢?很多小伙伴都有这样的疑问。今天我带大家看看如何在ASP.NET Core里不依赖反射,根据配置文件,在运行时动态选择一个接口的具体实现。


首先,这个需求其实来自我自己的博客系统。我的图片存储有两套:Azure Blob和文件系统,因此我写了一个接口,用了2套实现。想要做到能随时切换云存储或本地文件系统来保存博客文章的配图。因为这套代码比较复杂,因此我用一个最简明的例子来演示这个小技巧。

接口定义与两套实现
首先,接口长这样:

public interface IHelloer
{
    string SayHello();
}

有两个实现:

public class HelloerA : IHelloer
{
    public string SayHello()
    {
        return $"Hello from {nameof(HelloerA)}";
    }
}
public class HelloerB : IHelloer
{
    public string SayHello()
    {
        return $"Hello from {nameof(HelloerB)}";
    }
}

注册依赖注入
和注册常规的ASP.NET Core DI完全一样,我们把HelloerA及HelloerB都注册进去:
services.AddTransient<IHelloer, HelloerA>();
services.AddTransient<IHelloer, HelloerB>();

构造函数注入
刚才我们注册了同一接口的两套实现,那么ASP.NET Core运行时候会选择哪个实现呢?好奇可以试试:

public IHelloer Hello { get; set; }
public HomeController(IHelloer hello)
{
    Hello = hello;
}


把结果输出到浏览器:

public IActionResult Index()
{
    var message = Hello.SayHello();
    return Content(message);
}

发现出来的是HelloB,就是我们注册DI时候顺序在最后的那个实现。
那么问题来了,我想要运行时选择HelloA怎么办?请往下看。


构造函数居然还能这样注入
其实,在ASP.NET Core中,当你对一个接口注册了多个实现的时候,构造函数是可以注入一个该接口集合的,这个集合里是所有注册过的实现。

private IEnumerable<IHelloer> Helloers { get; set; }

public HomeController(IEnumerable<IHelloer> helloers)
{
    Helloers = helloers;
}


这样子出来的就是HelloA了:

public IActionResult Index()
{
    var message = Helloers.First().SayHello();
    return Content(message);
}

当然,在项目里,我们绝对是不能这样写死代码的,我们希望通过配置文件来选择具体实现。请往继续往下看。
配置文件
在appsettings.json里加入新配置:

"AppSettings": {
  "CurrentHelloer": "HelloerA"
}

并且建立一个对应的class
public class AppSettings
{
    public string CurrentHelloer { get; set; }
}

同样注册DI及构造器注入

services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
private AppSettings AppSettings { get; set; }
private IEnumerable<IHelloer> Helloers { get; set; }

public HomeController(IEnumerable<IHelloer> helloers, IOptions<AppSettings> settings)
{
    Helloers = helloers;
    AppSettings = settings.Value;
}

这样一来,就可以用LINQ非常方便的根据配置文件,从接口集合中选择对应名字的实现:
public IActionResult Index()
{
    var helloer = Helloers.FirstOrDefault(h => h.GetType().Name == AppSettings.CurrentHelloer);
    var message = helloer?.SayHello();
    return Content(message);
}

现在根据配置文件运行出来的就是HelloA了:

但是这个代码里竟然用了非政治正确的所谓影响性能的反射。为了避免代码被人鄙视,我们可以用一个workaround脱离反射。


不依赖反射
刚才我们用反射无非就是为了在运行时获得具体实现的class的名字。所以其实只要自己给每个class都加上一个名字属性就行了。并且我们可以利用nameof()来增加代码可维护性。
修改接口:

public interface IHelloer
{
    string CurrentName { get; }
    string SayHello();
}

修改实现:

public class HelloerA : IHelloer
{
    public string CurrentName => nameof(HelloerA);

    public string SayHello()
    {
        return $"Hello from {nameof(HelloerA)}";
    }
}

public class HelloerB : IHelloer
{
    public string CurrentName => nameof(HelloerB);

    public string SayHello()
    {
        return $"Hello from {nameof(HelloerB)}";
    }
}

修改LINQ:
public IActionResult Index()
{
    var helloer = Helloers.FirstOrDefault(h => h.CurrentName == AppSettings.CurrentHelloer);
    var message = helloer?.SayHello();
    return Content(message);
}

完美运行:


用户评论