项目中依赖注入的使用
多个服务类型在构造函数中注入使用
依赖注入的传染性
依赖注入具有“传染性”,如果一个类的对象是通过DI创建的,那这个类的构造函数中声明的所有服务类型的参数都会被DI赋值;但是如果一个对象是程序员手动创建的,那么这个对象就和DI没有关系,它的构造函数中声明的服务类型参数就不会被自动赋值;.NET的DI默认是构造函数注入。
- 例子:假设我们要实现一个将字符串内容存储到数据库中的功能,并且打印相关的日志,这个时候,我们会建立三个服务,一个日志服务接口,一个获取数据库配置的服务,一个存储数据的服务类和他们各自的实现类,并在Controller中获取三个服务实现功能,我们可以知道,在存储数据的服务ICloudStorage服务中,我们需要一个配置服务类的对象来实现存储,然后在Controller中又需要CloudStorage服务,所以依赖注入是可以多层嵌套的。如下图
图:服务容器中注册相关的服务以及实现类
图:TestController功能模块图
- 我们在Mian方法中把所有的服务类接口和对应的实现类注册到容器中,然后再在controller通过构造函数的形式获取服务即可,.net core 框架会自动分配构造函数中需要的服务类,不管是在controller层的ICloudStorage服务和CloudStorageImpl层的IConfig服务,都会自动获取;
class Program
{
static void Main(string[] args)
{
//框架处理代码,将服务注册到框架容器中
ServiceCollection services = new ServiceCollection();
services.AddScoped<TestController>();
services.AddScoped<ILog, LogImpl>();
services.AddScoped<IConfig, ConfigImpl>();
services.AddScoped<ICloudStorage, CloudStorageImpl>();
using (ServiceProvider sp = services.BuildServiceProvider())
{
var controller = sp.GetRequiredService<TestController>();
controller.Test();
}
}
}
class TestController
{
//日志服务
private readonly ILog _log;
//云存储服务
private readonly ICloudStorage _cloudStorage;
//构造函数(通过构造函数依赖注入,获取服务实例)
public TestController(ILog log, ICloudStorage cloudStorage)
{
_log = log;
_cloudStorage = cloudStorage;
}
public void Test()
{
_log.Debugger("开始存储");
_cloudStorage.Save("hello", "test");
_log.Debugger("结束存储");
}
}
#region 配置服务
interface IConfig
{
string GetConnStr(string conn);
}
class ConfigImpl : IConfig
{
public string GetConnStr(string conn)
{
Console.WriteLine("获取到了数据库连接串123");
return "123";
}
}
#endregion
#region 日志服务
interface ILog
{
void Debugger(string msg);
}
class LogImpl : ILog
{
public void Debugger(string msg)
{
Console.WriteLine($"调试日志:{msg}");
}
}
#endregion
#region 云存储服务
interface ICloudStorage
{
void Save(string content, string path);
}
class CloudStorageImpl : ICloudStorage
{
private readonly IConfig _config;
public CloudStorageImpl(IConfig config)
{
_config = config;
}
public void Save(string content, string path)
{
var conn = _config.GetConnStr("conn");
Console.WriteLine($"向数据库{conn}存储数据:{content},名字为:{path}");
}
}
#endregion
运行结果如下
所以,当从容器中获取了TestController的实例之后,框架会自动的给他需要的所有服务,包括服务里面需要的服务全部自动注入对象实例,这就是依赖注入的"传染性";
-
依赖注入的好处
万一哪天配置服务中的地址不从配置文件中读取,而是转变为从数据库中读取,那么我们的controller层的业务的代码不需要变,而是针对IConfig服务新增一个实例DbConfigImpl实现IConfig接口,然后在框架注册容器的代码中的实现类改成这个DbConfigImpl即可,可以降低模块之间的耦合度,代码修改如下:
可以看到,运行结果已经是新的实现类的逻辑:
- 观察上面的代码,我们把使用服务的过程交给了框架来处理,框架入口处将所有的服务和实现类注册到了ServicesCollection容器中,但是,这里也有一个不方便的点,就是框架的入口注册处必须知道所有的服务类,并且后期有变化,注册的代码都要随之变化,都要知道每个服务接口名以及它对应的实现类的名字,有没有更简便的方法呢?答案是:扩展方法
我们在项目目录下新建一个扩展类LogImplExtension,命名空间改为:Microsoft.Extensions.DependencyInjection,然后增加一个静态的方法AddLogImpl,里面实现具体服务的注;这样,在程序框架的入口处,就可以直接使用services.AddLogImpl()方法;
namespace Microsoft.Extensions.DependencyInjection
{
public static class LogImplExtension
{
public static void AddLogImpl(this IServiceCollection services)
{
services.AddScoped<ILogService, LogImpl>();
}
}
}
-
使用DI(依赖注入)实现多种配置方式的覆盖读取
比如现在有两个类型的配置,本地配置,远程服务上的配置,我们要实现自定义以何种配置优先原则,那么我们就可以通过服务注册优先顺序来实现
如下代码:
namespace DependencyInjectionDemo.Service.ServiceImpl
{
public interface IConfigReader
{
public string GetValue(string name);
}
}
public class LayeredConfigReadImpl : IConfigReader
{
private readonly IEnumerable<IConfigService> _configServices;
public LayeredConfigReadImpl(IEnumerable<IConfigService> configServices)
{
_configServices = configServices;
}
public string GetValue(string name)
{
string value = null;
foreach (var service in _configServices)
{
value = GetValue(name)!=null?GetValue(name):null;
}
return value;
}
}
同时定义一个扩展方法来实现注册
namespace Microsoft.Extensions.DependencyInjection
{
public static class LayeredConfigReadExtension
{
public static void AddLayeredConfigReadImpl(this IServiceCollection services)
{
services.AddScoped<IConfigReader, LayeredConfigReadImpl>();
}
}
}
然后再框架入口的时候,调整配置服务的注册顺序,就可以实现配置覆盖的功能
static void Main(string[] args)
{
ServiceCollection services = new ServiceCollection();
//services.AddScoped<ILogService, LogImpl>();
services.AddLogImpl();
//1.先从环境变量中获取配置
services.AddScoped<IConfigService, EnvVarConfigServiceImpl>();
//2.然后再从指定目录下的文件中获取相关配置
services.AddScoped<IConfigService>(s => new txtConfigServiceImpl() { FilePath = "C:\\Users\\liyumin\\Desktop\\mail.txt" });
services.AddLayeredConfigReadImpl();
services.AddScoped<IMailService, MailServiceImpl>();
using (ServiceProvider sp = services.BuildServiceProvider())
{
var mailService = sp.GetRequiredService<IMailService>();
mailService.Send("hello","liyumin","这是我们的邮件内容");
}
}
运行结果如下
此时如果我们交换配置服务的注册顺序,就会发现,最后读取的是电脑环境变量的值