点击蓝字
关注我
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);
}
完美运行: