.net控制台应用中使用依赖注入
对依赖注入比较熟悉的小伙伴推荐直接阅读第二节 # 控制台应用
.Net中的依赖注入
使用过ASP.Net Core的小伙伴对以下的代码一定不会陌生。
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSingleton<ILogger>((sp) =>
{
var factory = sp.GetService<ILoggerFactory>();
return factory.CreateLogger("AppLogger");
});
...
}
TestController.cs
public class TestController
{
private ILogger m_Logger;
public TestController(ILogger logger)
{
m_Logger = logger;
}
}
在ASP.Net Core中默认就为我们准备好了依赖注入框架,在Startup中配置好需要用到的类型,后续直接在构造方法里加上就能直接使用了,从而免去了很多新建对象的new语句。
依赖注入能使程序中的耦合度降低,简化对象的创建,几乎已经成为现代.Net程序中必备的东西。那么如果我们不是编写ASP.Net Core,要在其他程序使用依赖注入要怎么做呢?
示例代码
首先我们需要引用微软的扩展包:Microsoft.Extensions.DependencyInjection。然后在程序初始化的时候配置好依赖注入容器。
public class TestConsole
{
/// <summary>
/// 依赖注入容器
/// </summary>
protected IServiceProvider m_Service;
public void Initialize()
{
// 依赖注入配置集合
var serviceArr = new ServiceCollection();
// 像ASP.Net Core一样配置依赖注入
serviceArr.AddScoped<ClassB>();
serviceArr.AddSingleton<ClassA>();
...
// 构建依赖注入容器
m_Service = serviceArr.BuildServiceProvider();
}
public void Run()
{
//需要构建对象时就使用m_Service
ClassA a = m_Service .ServiceProvider.GetRequiredService<ClassA>();
...
}
}
以上就是.Net依赖注入最原始的所有代码。其实步骤非常简单:
- 配置容器里的类型和生命周期
- 调用容器获取对象
基本要点
要使用依赖注入,无论任何依赖注入框架都必须要遵循一些规则。
从头用到尾
依赖注入必须从头用到尾,即从程序的开头开始就要初始化,所有的用到类型都以依赖注入的形式构建。不能说某个模块想用到依赖注入,而其他模块又不用,也许代码上是可以这样实现,但是从整个程序的结构上看来就有点不伦不类的样子,而且如果后面某个没使用依赖注入的模块想获得某个使用依赖注入的类型时将会非常痛苦。
生命周期
.Net依赖注入获取对象的生命周期有3种。由短到长分别是
Transient(瞬时),Scoped(作用域),Singleton(单例)
详情可见以下文档
依赖注入生命周期
Singleton(单例)
单例比较好理解,类型的对象只有1个,每次获取的都是同一个。
Transient(瞬时)
瞬时的也不难理解,类型的对象在每次获取时都会重新创建,所以,每次获取的都是不同的对象。
Scoped(作用域)
作用域就有点难理解了,在IServiceProvider创建的IServiceScope的生命周期中每次获取的对象都是同一个。对应ASP.Net Core中,每个http请求都是一个Scope;对应我们自己写的程序,如下面的代码所示,在using语句中就是一个Scope,即IServiceScope 对象Dispose之前通过它获取的对象都是同一个。
public void Test()
{
...
using (IServiceScope scope = m_Service.CreateScope())
{
try
{
ClassA c = scope.ServiceProvider.GetRequiredService<ClassA>();
c.Test();
if (c is IDisposable dis)
{
dis.Dispose();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
...
}
控制台应用
平时的开发中总会遇到要写控制台应用的时候,比如:定时任务(爬虫)、命令行交互、通讯等场景。众所周知.Net控制台应用创建出来几乎什么都没有,这时候如果我们想在控制台应用中使用依赖注入就需要稍微对程序加一点设计进去。
首先,如果你的控制台应用是用来定时执行一下特定的方法,这里推荐你使用Quartz这个类库。当然如果你和我一样是不怕折腾的程序猿,欢迎你继续往下阅读。
Quartz中使用依赖注入
稍加设计
这里开始我们以定时任务控制台为例子开始踩坑。
首先我们需要将程序抽象一下,把定时任务控制台程序实现为一个类,定时任务就用System.Threading.Timer来实现。
程序雏形
/// <summary>
/// 控制台程序基类
/// </summary>
public abstract class BaseConsole
{
/// <summary>
/// 依赖注入集合
/// </summary>
protected IServiceProvider m_Service;
/// <summary>
/// 配置依赖注入
/// </summary>
/// <param name="services">容器集合</param>
protected abstract void ConfigureServices(IServiceCollection services);
public void Run()
{
// 初始化
Initialize();
while (true)
{
// 阻塞主线程
string input = Console.ReadLine();
// 如果输入了exit就退出
if(input.ToLower() == "exit")
{
break;
}
}
}
/// <summary>
/// 初始化启动配置
/// </summary>
private void Initialize()
{
var serviceArr = new ServiceCollection();
// 配置依赖注入
ConfigureServices(serviceArr);
m_Service = serviceArr.BuildServiceProvider();
}
}
程序的雏形如上所示,后续的实际使用中将我们的具体业务程序类继承BaseConsole,重写ConfigureServices方法以配置我们自己的依赖注入即可。
定时任务
定时任务我们可以用System.Threading.Timer这个类型来实现。
首先,抽象出一个定时任务接口ICron,然后Timer每隔一段时间就以依赖注入的形式创建ICron对象并执行业务。
/// <summary>
/// 定时任务接口
/// </summary>
public interface ICron
{
/// <summary>
/// 任务开始
/// </summary>
/// <remarks>程序开始时执行一次,可进行定时任务的准备工作</remarks>
/// <returns></returns>
Task Start();
/// <summary>
/// 定时任务执行
/// </summary>
/// <remarks>每隔一段时间会以新对象执行该方法</remarks>
/// <returns></returns>
Task<string> Execute();
/// <summary>
/// 程序结束前进行清理工作
/// </summary>
/// <remarks>程序结束时执行一次,可进行定时任务的清理工作</remarks>
/// <returns></returns>
Task Exit();
}
因为程序里的定时任务肯定不会只有一个,这里为了方便就创建了一个定时任务集合的类型方便管理。这里每向集合添加一个定时任务都会以AddScpoed的形式加入到依赖注入中。
/// <summary>
/// 定时任务集合
/// </summary>
public partial class CronCollection : IEnumerable<CronCollection.Cron>
{
/// <summary>
/// 定时任务描述
/// </summary>
public class Cron
{
/// <summary>
/// 执行间隔
/// </summary>
public TimeSpan Interval;
/// <summary>
/// 定时任务逻辑类型
/// </summary>
public Type ExecType;
}
private IServiceCollection m_Services;
/// <summary>
/// 内部集合
/// </summary>
protected List<Cron> m_List;
public CronCollection(IServiceCollection services)
{
m_Services = services;
m_List = new List<Cron>();
}
public CronCollection AddCron<T>(TimeSpan span) where T : class, ICron
{
m_Services.AddScoped<T>();
Cron c = new Cron { Interval = span, ExecType = typeof(T) };
m_List.Add(c);
return this;
}
public CronCollection AddCron<T>(TimeSpan span, Func<IServiceProvider, T> func) where T : class, ICron
{
m_Services.AddScoped<T>(func);
Cron c = new Cron { Interval = span, ExecType = typeof(T) };
m_List.Add(c);
return this;
}
public IEnumerator<Cron> GetEnumerator()
{
return ((IEnumerable<Cron>)m_List).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)m_List).GetEnumerator();
}
}
雏形升级
有了定时任务之后,就需要为雏形加入定时任务让它执行。
/// <summary>
/// 控制台程序类
/// </summary>
public abstract class BaseConsole
{
/// <summary>
/// 依赖注入集合
/// </summary>
protected IServiceProvider m_Service;
/// <summary>
/// 定时任务集合
/// </summary>
protected CronCollection m_Crons;
/// <summary>
/// 定时任务定时器集合
/// </summary>
private List<Timer> m_CronTimers;
public BaseConsole()
{
}
/// <summary>
/// 配置依赖注入
/// </summary>
/// <param name="services">容器集合</param>
protected abstract void ConfigureServices(IServiceCollection services);
/// <summary>
/// 配置定时任务
/// </summary>
/// <param name="crons">定时任务集合</param>
/// <param name="config">配置文件</param>
protected abstract void ConfigureCron(CronCollection crons);
public void Run()
{
// 初始化
Initialize();
// 开启定时任务 or 执行其他业务
CronRun();
while (true)
{
// 阻塞主线程
string input = Console.ReadLine();
// 如果输入了exit就退出
if(input.ToLower() == "exit")
{
break;
}
}
// 释放定时器
CronEnd();
}
/// <summary>
/// 初始化启动配置
/// </summary>
private void Initialize()
{
m_CronTimers = new List<Timer>();
var serviceArr = new ServiceCollection();
// 配置依赖注入
ConfigureServices(serviceArr);
var crons = new CronCollection(serviceArr);
// 配置定时任务
ConfigureCron(crons);
m_Service = serviceArr.BuildServiceProvider();
m_Crons = crons;
}
private async void CronRun()
{
ICron c = default;
foreach (var cron in m_Crons)
{
using (var scope = m_Service.CreateScope())
{
try
{
c = scope.ServiceProvider.GetRequiredService(cron.ExecType) as ICron;
await c.Start();
if (c is IDisposable dis)
{
dis.Dispose();
}
}
catch (Exception ex)
{
Console.WriteLine("CRON start error:" + ex.Message);
}
}
Timer timer = new Timer(ExecCron, cron.ExecType, TimeSpan.FromSeconds(5), cron.Interval);
m_CronTimers.Add(timer);
}
}
private void ExecCron(object state)
{
Type tp = state as Type;
ICron cron = default;
if (tp != null)
{
string print = string.Empty;
using (var scope = m_Service.CreateScope())
{
try
{
cron = scope.ServiceProvider.GetRequiredService(tp) as ICron;
print = cron.Execute().GetAwaiter().GetResult();
if (cron is IDisposable dis)
{
dis.Dispose();
}
}
catch (Exception ex)
{
print = "CRON execute error:" + ex.Message;
}
}
Console.WriteLine(print);
}
}
private void CronEnd()
{
Task[] dispTks = new Task[m_CronTimers.Count];
// 关闭定时器
for (int i = 0; i < m_CronTimers.Count; i++)
{
var timer = m_CronTimers[i];
// 等待正在执行的任务完成
dispTks[i] = Task.Run(async () =>
{
await timer.DisposeAsync();
});
}
Task.WaitAll(dispTks);
// 调用定时任务的结束方法
ICron c;
foreach (var cron in m_Crons)
{
using (var scope = m_Service.CreateScope())
{
try
{
c = scope.ServiceProvider.GetRequiredService(cron.ExecType) as ICron;
c.Exit().Wait();
if (c is IDisposable dis)
{
dis.Dispose();
}
}
catch (Exception ex)
{
Console.WriteLine("CRON exit error:" + ex.Message);
}
}
}
}
}
这里我们把每次获取ICron的代码都写到一个Scope里,所以定时任务的每次执行都是在同一个作用域里面。并且调用完成之后还需要判断这个对象是否需要Dispose,如果需要,则Dispose掉它,保证不占用非托管资源。
正式使用
至此,一个使用依赖注入的微型定时任务控制台应用框架已经实现了。
使用时只需实现ICron接口写我们自己的定时任务业务,然后把这个定时任务类型加到派生自BaseConsole的控制台应用中即可。
/// <summary>
/// 测试定时任务
/// </summary>
public class HelloCron : ICron
{
public async Task<string> Execute()
{
// 假装工作了100ms
await Task.Delay(100);
return "Hello_World";
}
public Task Exit()
{
return Task.CompletedTask;
}
public Task Start()
{
return Task.CompletedTask;
}
}
/// <summary>
/// 派生的控制台应用类
/// </summary>
public class TestConsole : BaseConsole
{
protected override void ConfigureCron(CronCollection crons)
{
// 加入定时任务并配置执行间隔
crons.AddCron<HelloCron>(TimeSpan.FromSeconds(5));
}
protected override void ConfigureServices(IServiceCollection services)
{
// 这里像ASP.Net Core一样配置依赖注入
// services.AddScoped<T>();
}
}
Main方法
class Program
{
static void Main(string[] args)
{
TestConsole console = new TestConsole();
console.Run();
}
}
完善实现
这个雏形,没有日志,没有交互,没有配置文件,作为一个控制台应用是远远不够的。为此可以参考本人对以上代码的进一步完善。
https://github.com/YinRunhao/TimingConsole