概念
依赖注入(dependency injection DI)是控制反转(inversion of control, IOC)思想的实现方式。
依赖注入简化模块的组装过程,降低模块之间的耦合度
IOC目的
IOC的目的:“怎样创建xxx对象”------>“我要xxx对象”
不关注细节,只关注业务
IOC实现方式
- 服务定位器 serviceLocator
- 依赖注入 DI
DI几个概念
- 服务(service): 你向框架要的对象
- 注册服务
- 服务容器:负责管理注册的服务
- 查询服务:创建对象及关联对象
- 对象生命周期:Transient(瞬态)、scoped(范围)、singleton(单例)
.net中使用DI
1 服务定位器
来看个例子
private static void Main(string[] args)
{
// 之前的调用方法
ITestService test = new TestService1();
test.Name = "hahha";
test.SayHi();
ITestService test2 = new TestService2();
test2.Name = "ttt";
test2.SayHi();
}
public interface ITestService
{
public string Name { get; set; }
public void SayHi();
}
public class TestService1 : ITestService
{
public string Name { get; set; }
public void SayHi()
{
Console.WriteLine($"hello {Name}");
}
}
public class TestService2 : ITestService
{
public string Name { get; set; }
public void SayHi()
{
Console.WriteLine($"你好 {Name}");
}
}
- 在.net中,是根据类型来获取和注册服务的。类型可以分别值服务类型(service type)和实现类型(implementation type).这两者可能相同,也可能不同。服务类型可以是接口,也可以是类,建议面向接口编程,更灵活。
- .net IOC组件取名为DependencyInjection,但它包含serviceLocator的功能。
- 通过nuget添加DependencyInjection包
private static void Main(string[] args)
{
// ioc写法
ServiceCollection serviceollection = new ServiceCollection();
serviceollection.AddTransient<TestService1>();
using (ServiceProvider sp = serviceollection.BuildServiceProvider())
{
TestService1 t1 = sp.GetService<TestService1>();
t1.Name = "ioc1";
t1.SayHi();
}
}
生命周期
- 给类构造函数中打印,看看不同生命周期的对象创建,使用serviceProvider.CreateScope()创建Scope。
Transient 每次都创建一个新的对象
private static void Main(string[] args)
{
// ioc写法
ServiceCollection serviceollection = new ServiceCollection();
serviceollection.AddTransient<TestService1>(); // 瞬时生命周期 每次都创建一个新的对象
using (ServiceProvider sp = serviceollection.BuildServiceProvider())
{
TestService1 t1 = sp.GetService<TestService1>();
t1.Name = "ioc1";
t1.SayHi();
TestService1 t2 = sp.GetService<TestService1>(); // 再次创建对象
Console.WriteLine(object.ReferenceEquals(t1, t2));// 看看是否是同一个对象
}
}
Singleton 每次都创建同一个对象
private static void Main(string[] args)
{
// ioc写法
ServiceCollection serviceollection = new ServiceCollection();
serviceollection.AddSingleton<TestService1>(); // 单例生命周期 每次都创建同一个对象
using (ServiceProvider sp = serviceollection.BuildServiceProvider())
{
TestService1 t1 = sp.GetService<TestService1>();
t1.Name = "ioc1";
t1.SayHi();
TestService1 t2 = sp.GetService<TestService1>(); // 再次创建对象
Console.WriteLine(object.ReferenceEquals(t1, t2));// 看看是否是同一个对象
}
}
private static void Main(string[] args)
{
// ioc写法
ServiceCollection serviceollection = new ServiceCollection();
serviceollection.AddScoped<TestService1>(); // 范围生命周期 同一个范围内创建的对象相同,不同范围不同
TestService1 test;
using (ServiceProvider sp = serviceollection.BuildServiceProvider())
{
using (var scope1 = sp.CreateScope())
{
TestService1 t1 = scope1.ServiceProvider.GetService<TestService1>();
TestService1 t2 = scope1.ServiceProvider.GetService<TestService1>();
Console.WriteLine(object.ReferenceEquals(t1, t2));// t1和t2是同一个
test = t1;
}
using (var scope2 = sp.CreateScope())
{
TestService1 t3 = scope2.ServiceProvider.GetService<TestService1>();
TestService1 t4 = scope2.ServiceProvider.GetService<TestService1>();
Console.WriteLine(object.ReferenceEquals(t3, t4));// 同一个对象
Console.WriteLine(object.ReferenceEquals(test, t4));// 不是同一个
}
}
}
- 如果一个类实现了IDisposable接口,则离开作用域之后容器会自动调用dispose方法。
- 不要在长生命周期的对象中引用比他生命周期短的对象。在asp.net core 中,这样做默认会抛异常。
- 生命周期的选择:如果类无状态,建议为Singleton;如果类有状态,且有scope控制,建议为scope的,因为通常scope控制下的代码都是在同一个线程中运行,没有并发修改问题;在使用transient的时候要谨慎。
- .net注册服务的重载方法很多,请自行看文档学习。
其他注册方法
- T GetService 如果获取不到对象,则返回null
static void Main(string[] args)
{
ServiceCollection services = new ServiceCollection();
services.AddScoped<ITestService, TestService1>(); // 更推荐这种写法 前面是服务类型,后面是实现类型
using (ServiceProvider provider = services.BuildServiceProvider())
{
using (IServiceScope scope = provider.CreateScope())
{
// 这里必须和上面注册的服务类型一致 如果找不到则返回null
ITestService testService = scope.ServiceProvider.GetService<ITestService>();
// 这里把ITestService换成TestService1, 会返回null 因为找不到注册的服务
// ITestService testService = scope.ServiceProvider.GetService<ITestService>();
Console.WriteLine(testService.GetType());
}
}
}
2. object GetService(Type serviceType)
static void Main(string[] args)
{
ServiceCollection services = new ServiceCollection();
services.AddScoped<ITestService, TestService1>(); // 更推荐这种写法 前面是服务类型,后面是实现类型
using (ServiceProvider provider = services.BuildServiceProvider())
{
using (IServiceScope scope = provider.CreateScope())
{
ITestService testService = (ITestService)scope.ServiceProvider.GetService(typeof(ITestService));
Console.WriteLine(testService.GetType());
}
}
}
- T GetRequiredSerice 如果获取不到对象,则抛异常
- object GetRequiredSerice(Type serviceType)
- IEnumerable GetServices 适用于可能有很多满足条件的服务
static void Main(string[] args)
{
ServiceCollection services = new ServiceCollection();
services.AddScoped<ITestService, TestService1>(); // 更推荐这种写法 前面是服务类型,后面是实现类型
services.AddScoped<ITestService, TestService2>(); // 更推荐这种写法 前面是服务类型,后面是实现类型
using (ServiceProvider provider = services.BuildServiceProvider())
{
using (IServiceScope scope = provider.CreateScope())
{
IEnumerable<ITestService> testServices = scope.ServiceProvider.GetServices<ITestService>();
foreach (var item in testServices)
{
Console.WriteLine(item.GetType());
}
}
}
}
这里有个需要注意的点:注册多个服务时,如果用GetRequiredSerice()去获取服务,只会获取到最新的。
- IEnumerable GetServices
2 依赖注入 DI
- 依赖注入是有传染性的,如果一个类的对象是通过DI创建的,那么这个类的构造函数中声明的所有服务类型的参数都会被DI赋值;但是如果一个对象是程序员手动创建的,那么这个对象和DI没有关系,它的构造函数中声明的服务类型参数就不会被自动赋值。
- .NET的DI默认是构造函数注入
例子:编写一个类,连接数据库做插入操作,并且记录服务日志(模拟输出),把Dao、日志都放入单独的服务类。
using Microsoft.Extensions.DependencyInjection;
internal class Program
{
private static void Main(string[] args)
{
// 声明服务容器
ServiceCollection services = new ServiceCollection();
// 注册需要的服务
services.AddScoped<ILog, Log>();
services.AddScoped<IStorage, Strorage>();
services.AddScoped<Controller>();
services.AddScoped<IConfig, Config>();
using (ServiceProvider provider = services.BuildServiceProvider())
{
// 获取服务
Controller controller = provider.GetRequiredService<Controller>();
controller.Test();
}
}
class Controller
{
private readonly ILog log;
private readonly IStorage storage;
public Controller(ILog log, IStorage storage)
{
this.log = log;
this.storage = storage;
}
public void Test()
{
this.log.WriteLog("开始写日志了");
this.storage.Save("hahhaha", "file1");
this.log.WriteLog("日志结束了");
}
}
interface ILog
{
public void WriteLog(string message);
}
public class Log : ILog
{
public void WriteLog(string message)
{
Console.WriteLine(message);
}
}
interface IConfig
{
public string GetValue();
}
public class Config : IConfig
{
public string GetValue()
{
return "server1";
}
}
interface IStorage
{
public void Save(string content, string name);
}
class Strorage : IStorage
{
private readonly IConfig config;
public Strorage(IConfig config)
{
this.config = config;
}
public void Save(string content, string name)
{
string server = config.GetValue();
Console.WriteLine($"upload {content} to {server}, and its name is {name}");
}
}
}
案例
发送邮件
建三个类库,一个控制台程序。LogServices类库负责记录日志;MailServices类库负责发送邮件;ConfigServices类库负责获取配置信息;SendMailConsole控制台程序负责调用,发送邮件。
项目结构如下图所示:
IConfigService接口
using System;
namespace ConfigServices
{
public interface IConfigService
{
public string GetValue(string name);
}
}
ConfigService实现类
using System;
namespace ConfigServices
{
public class ConfigService : IConfigService
{
public string GetValue(string name)
{
return name;
}
}
}
ILogService接口
using System;
namespace LogServices
{
public interface ILogService
{
public void LogError();
public void LogInfo(string message);
}
}
LogService实现类
using System;
namespace LogServices
{
public class LogService : ILogService
{
public void LogError()
{
Console.WriteLine("error");
}
public void LogInfo(string message)
{
Console.WriteLine($"info: {message}");
}
}
}
IMailService接口
using System;
namespace MailServices
{
public interface IMailService
{
public void Send(string title, string recipients, string body);
}
}
MailService实现类
using System;
using LogServices;
using ConfigServices;
namespace MailServices
{
public class MailService : IMailService
{
private readonly ILogService logService;
private readonly IConfigService configService;
public MailService(ILogService logService, IConfigService configService)
{
this.logService = logService;
this.configService = configService;
}
public void Send(string title, string recipients, string body)
{
this.logService.LogInfo("开始发送邮件了");
string username = this.configService.GetValue("UserName");
Console.WriteLine($"真的发送邮件了: {username}, {title}, {recipients}, {body}");
this.logService.LogInfo("邮件发送完成");
}
}
}
SendMailConsole 控制台代码
using MailServices;
using LogServices;
using ConfigServices;
using Microsoft.Extensions.DependencyInjection;
namespace SendMailConsole;
class Program
{
static void Main(string[] args)
{
ServiceCollection services = new ServiceCollection();
services.AddScoped<IMailService, MailService>();
services.AddScoped<ILogService, LogService>();
services.AddScoped<IConfigService, ConfigService>();
using (var provider = services.BuildServiceProvider())
{
IMailService mailService = provider.GetRequiredService<IMailService>();
mailService.Send("案例一", "mzz", "imissyou");
}
}
}
这样的实现方式还是有一些不完美,因为在控制台程序中需要自己注册服务和实现类,这就要求你很清楚服务和实现类是谁,而我们期望的结果是可以直接用,不用很清楚细节。下面我们通过写扩展类的方法来修改一下上面的写法。
分别给上面的三个类库都增加一个扩展类,在扩展类里面注册服务,在控制台程序中直接调用这个扩展方法即可。
ConfigServiceExtension 扩展类
using System;
using ConfigServices;
namespace Microsoft.Extensions.DependencyInjection
{
public static class ConfigServiceExtension
{
public static void AddConfigService(this IServiceCollection services)
{
services.AddScoped<IConfigService, ConfigService>();
}
}
}
控制台程序代码
using MailServices;
using Microsoft.Extensions.DependencyInjection;
namespace SendMailConsole;
class Program
{
static void Main(string[] args)
{
ServiceCollection services = new ServiceCollection();
//services.AddScoped<IMailService, MailService>();
//services.AddScoped<ILogService, LogService>();
//services.AddScoped<IConfigService, ConfigService>();
services.AddMailService();
services.AddLogService();
services.AddConfigService();
using (var provider = services.BuildServiceProvider())
{
IMailService mailService = provider.GetRequiredService<IMailService>();
mailService.Send("案例一", "mzz", "imissyou");
}
}
}
注意:因为我们希望扩展方法可以让ServiceCollection对象直接调用,所以扩展类的命名空间必须和ServiceCollection一致,即Microsoft.Extensions.DependencyInjection。