在C#中使用依赖注入-生命周期控制

在使用依赖注入的过程当中,除了应用设计模式注意代码的变化隔离之外,另外一个重要的内容就是生命周期控制

每次获取都是新的实例

前文中用到的方式都是这样的效果。在容器中每次获取同一个接口的实现,每次获取到的都是不同的实例。读者可以翻阅一下先前的示例代码回顾一下。

单例模式

单例模式也是一种常见的设计模式,这种设计模式。主要是为了解决某些特定需求时不希望特定的实例过多,而采用单个实例的设计模式。

在C#之中,最为容易理解的一种单例模式的应用便是静态成员,这点显而易见,以下获取系统时间的代码。便是一种单例模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
using System.Threading;

namespace Use_Dependency_Injection_With_Lifetime_Scope_Control
{
public static class Demo1
{
public static void Run()
{
Console.WriteLine($"第一次获取时间:{DateTime.Now}");
Thread.Sleep(1000);
Console.WriteLine($"第二次获取时间:{DateTime.Now}");
Thread.Sleep(1000);
Console.WriteLine($"第三次获取时间:{DateTime.Now}");
}
}
}

每隔一秒钟获取一次系统时间。DateTime.Now是DateTime类型提供的静态属性。在C#语言之中这可以被看做一种单例模式。

但是,存在一个问题,那就是单元测试的可行性。简单来说,这段代码的运行结果会随着时间的变化而变化,每次运行的结果都不相同,这样通常来说是不可测的。因此,应用依赖注入进行一下改造。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using Autofac;
using System;
using System.Threading;

namespace Use_Dependency_Injection_With_Lifetime_Scope_Control
{
public static class Demo2
{
public static void Run()
{
var cb = new ContainerBuilder();
cb.RegisterType<StaticClockByOneTime>()
.As<IClock>()
.SingleInstance();
var container = cb.Build();
var clock = container.Resolve<IClock>();
Console.WriteLine($"第一次获取时间:{clock.Now}");
Thread.Sleep(1000);
clock = container.Resolve<IClock>();
Console.WriteLine($"第二次获取时间:{clock.Now}");
Thread.Sleep(1000);
clock = container.Resolve<IClock>();
Console.WriteLine($"第三次获取时间:{clock.Now}");
}

public interface IClock
{
/// <summary>
/// 获取当前系统时间
/// </summary>
DateTime Now { get; }
}

public class StaticClockByOneTime : IClock
{
private DateTime _firstTime = DateTime.MinValue;
public DateTime Now
{
get
{
if (_firstTime == DateTime.MinValue)
{
_firstTime = DateTime.Now;
}

return _firstTime;
}
}
}
}
}

简要分析。通过改造之后引入了新的接口获取当前系统时间。由于接口的存在,我们可以替换接口的实现。

此处使用了一个有趣的实现StaticClockByOneTime。简单来说,这个实例如果获取过一次时间之后,时间就不会变化。

为这个特性作支撑的,便是SingleInstance这个方法。此方法将StaticClockByOneTime注册时标记为了“单例”。因此,从容器中获取IClock实例时始终得到的是同一个实例。就这样,便即实现了单例,又实现了可以自主控制时间的需求。

读者可以将上文代码中的SingleInstance代码去掉来体验单例和非单例运行结果的区别。

生命周期内单例

上文的单例是一种全局性的单例配置。只要容器建立起来,在容器内就是完全单例的。但在实际的应用场景中可能需要在某个特定生命周期内的单例,也可以成为局部单例。

业务需求

以下实例代码都将完成如下定义的一个业务场景:从A账号转账给B账号,转账数额为C,则A账号减少数额C,B账号增加数额C。

有关联的输出日志

转账影响了两个账号余额,现在考虑输出两条余额更新的日志,并且在日志中需要包含相同的转账流水号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
using Autofac;
using System;
using System.Collections.Generic;

namespace Use_Dependency_Injection_With_Lifetime_Scope_Control
{
public static class Demo3
{
public static void Run()
{
var cb = new ContainerBuilder();
cb.RegisterType<AccountBll>().As<IAccountBll>();
cb.RegisterType<AccountDal>().As<IAccountDal>();
cb.RegisterType<ConsoleLogger>().As<ILogger>()
.InstancePerLifetimeScope();
var container = cb.Build();

using (var beginLifetimeScope = container.BeginLifetimeScope())
{
var accountBll = beginLifetimeScope.Resolve<IAccountBll>();
accountBll.Transfer("yueluo", "newbe", 333);
accountBll.Transfer("yueluo", "newbe", 333);
}
}

public interface ILogger
{
void BeginScope(string scopeTag);
void Log(string message);
}

public class ConsoleLogger : ILogger
{
private string _currenctScopeTag;

public void BeginScope(string scopeTag)
{
_currenctScopeTag = scopeTag;
}

public void Log(string message)
{
Console.WriteLine(string.IsNullOrEmpty(_currenctScopeTag)
? $"输出日志:{message}"
: $"输出日志:{message}[scope:{_currenctScopeTag}]");
}
}

public interface IAccountBll
{
/// <summary>
/// 转账
/// </summary>
/// <param name="fromAccountId">来源账号Id</param>
/// <param name="toAccountId">目标账号Id</param>
/// <param name="amount">转账数额</param>
void Transfer(string fromAccountId, string toAccountId, decimal amount);
}

public class AccountBll : IAccountBll
{
private readonly ILogger _logger;
private readonly IAccountDal _accountDal;

public AccountBll(
ILogger logger,
IAccountDal accountDal)
{
_logger = logger;
_accountDal = accountDal;
}

public void Transfer(string fromAccountId, string toAccountId, decimal amount)
{
_logger.BeginScope(Guid.NewGuid().ToString());
var fromAmount = _accountDal.GetBalance(fromAccountId);
var toAmount = _accountDal.GetBalance(toAccountId);
fromAmount -= amount;
toAmount += amount;
_accountDal.UpdateBalance(fromAccountId, fromAmount);
_accountDal.UpdateBalance(toAccountId, toAmount);
}
}

public interface IAccountDal
{
/// <summary>
/// 获取账户的余额
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
decimal GetBalance(string id);

/// <summary>
/// 更新账户的余额
/// </summary>
/// <param name="id"></param>
/// <param name="balance"></param>
void UpdateBalance(string id, decimal balance);
}

public class AccountDal : IAccountDal
{
private readonly ILogger _logger;

public AccountDal(
ILogger logger)
{
_logger = logger;
}

private readonly Dictionary<string, decimal> _accounts = new Dictionary<string, decimal>
{
{"newbe",1000},
{"yueluo",666},
};

public decimal GetBalance(string id)
{
return _accounts.TryGetValue(id, out var balance) ? balance : 0;
}

public void UpdateBalance(string id, decimal balance)
{
_logger.Log($"更新了 {id} 的余额为 {balance}");
_accounts[id] = balance;
}
}
}
}

简要分析。以上代码的关键点:

  1. 在注册ILogger时,注册为了生命周期内单例。
  2. 在获取IAccountBll时,开启了一个生命周期,那么在这个生命周期内获取的ILogger实例都是同一个。
  3. IAccountBll内使用ILogger记录了转账流水号。

读者可以尝试将InstancePerLifetimeScope去除,观察运行效果的不同。

使用相同的数据库事务

转账从现有的代码结构而言,需要开启数据库事务才能够确保在数据入库时是无误的。从三层结构的角度来说,通常需要调用多个具有修改数据库数据功能的DAL方法时,将会开启事务从而确保这些DAL方法的执行是正确的。

为了实现这个特性,首先准备一些基础的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
using System;
using System.Data;

namespace Use_Dependency_Injection_With_Lifetime_Scope_Control
{
/// <summary>
/// 能够直接执行语句的数据库链接
/// </summary>
public interface IExecuteSqlDbConnection : IDbConnection
{
/// <summary>
/// 执行数据库语句
/// </summary>
/// <param name="sql"></param>
/// <param name="ps"></param>
/// <param name="dbTransaction"></param>
void ExecuteSql(string sql, object[] ps, IDbTransaction dbTransaction = null);
}

/// <summary>
/// 只会向控制台输出内容的数据库连接
/// </summary>
public class ConsoleDbConnection : IExecuteSqlDbConnection
{
public delegate ConsoleDbConnection Factory();

public void Dispose()
{
Console.WriteLine("数据库连接:释放");
}

public IDbTransaction BeginTransaction()
{
return new ConsoleOutDbTransaction(this, IsolationLevel.Unspecified);
}

public IDbTransaction BeginTransaction(IsolationLevel il)
{
return new ConsoleOutDbTransaction(this, il);
}

public void Close()
{
Console.WriteLine("数据库连接:关闭");
}

public void ChangeDatabase(string databaseName)
{
throw new NotSupportedException();
}

public IDbCommand CreateCommand()
{
throw new NotSupportedException();
}

public void Open()
{
throw new NotSupportedException();
}

public string ConnectionString { get; set; }

public int ConnectionTimeout
{
get { throw new NotSupportedException(); }
}

public string Database
{
get { throw new NotSupportedException(); }
}

public ConnectionState State
{
get { throw new NotSupportedException(); }
}

public void ExecuteSql(string sql, object[] ps, IDbTransaction dbTransaction = null)
{
if (dbTransaction == null)
{
Console.WriteLine($"无事务执行:{string.Format(sql, ps)}");
}
else
{
Console.WriteLine($"有事务执行:{string.Format(sql, ps)}");
}
}
}

/// <summary>
/// 只会向控制台输出内容的事务
/// </summary>
public class ConsoleOutDbTransaction : IDbTransaction
{
public ConsoleOutDbTransaction(IDbConnection connection, IsolationLevel isolationLevel)
{
Connection = connection;
IsolationLevel = isolationLevel;
}

public void Dispose()
{
Console.WriteLine("事务:释放");
}

public void Commit()
{
Console.WriteLine("事务:提交");
}

public void Rollback()
{
Console.WriteLine("事务:回滚");
}

public IDbConnection Connection { get; }
public IsolationLevel IsolationLevel { get; }
}
}

具备了数据库链接和事务的基础类后,假设我们不采用生命周期控制的方案。那么一种实现方案如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
using Autofac;
using System;
using System.Collections.Generic;
using System.Data;

namespace Use_Dependency_Injection_With_Lifetime_Scope_Control
{
public static class Demo4
{
public static void Run()
{
var cb = new ContainerBuilder();
cb.RegisterType<AccountBll>().As<IAccountBll>();
cb.RegisterType<AccountDal>().As<IAccountDal>();
cb.RegisterType<DbFactory>().As<IDbFactory>();
cb.RegisterType<ConsoleDbConnection>().AsSelf();
var container = cb.Build();

using (var beginLifetimeScope = container.BeginLifetimeScope())
{
var accountBll = beginLifetimeScope.Resolve<IAccountBll>();
accountBll.Transfer("yueluo", "newbe", 333);
}
}

public interface IDbFactory
{
IExecuteSqlDbConnection CreateDbConnection();
}

public class DbFactory : IDbFactory
{
private readonly ConsoleDbConnection.Factory _factory;

public DbFactory(
ConsoleDbConnection.Factory factory)
{
this._factory = factory;
}

public IExecuteSqlDbConnection CreateDbConnection()
{
return _factory();
}
}

public interface IAccountBll
{
void Transfer(string fromAccountId, string toAccountId, decimal amount);
}

public class AccountBll : IAccountBll
{
private readonly IDbFactory _dbFactory;
private readonly IAccountDal _accountDal;

public AccountBll(
IDbFactory dbFactory,
IAccountDal accountDal)
{
_dbFactory = dbFactory;
_accountDal = accountDal;
}

public void Transfer(string fromAccountId, string toAccountId, decimal amount)
{
using (var dbConnection = _dbFactory.CreateDbConnection())
{
using (var transaction = dbConnection.BeginTransaction())
{
try
{
var fromAmount = _accountDal.GetBalance(fromAccountId);
var toAmount = _accountDal.GetBalance(toAccountId);
fromAmount -= amount;
toAmount += amount;
_accountDal.UpdateBalance(fromAccountId, fromAmount, dbConnection, transaction);
_accountDal.UpdateBalance(toAccountId, toAmount, dbConnection, transaction);
transaction.Commit();
}
catch (Exception)
{
transaction.Rollback();
throw;
}
}
}
}
}

public interface IAccountDal
{
/// <summary>
/// 获取账户的余额
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
decimal GetBalance(string id);

/// <summary>
/// 更新账户的余额
/// </summary>
/// <param name="id"></param>
/// <param name="balance"></param>
/// <param name="dbConnection"></param>
/// <param name="dbTransaction"></param>
void UpdateBalance(string id, decimal balance, IExecuteSqlDbConnection dbConnection = null, IDbTransaction dbTransaction = null);
}

public class AccountDal : IAccountDal
{
private readonly IDbFactory _dbFactory;

public AccountDal(
IDbFactory dbFactory)
{
_dbFactory = dbFactory;
}

private readonly Dictionary<string, decimal> _accounts = new Dictionary<string, decimal>
{
{"newbe",1000},
{"yueluo",666},
};

public decimal GetBalance(string id)
{
return _accounts.TryGetValue(id, out var balance) ? balance : 0;
}

public void UpdateBalance(string id, decimal balance, IExecuteSqlDbConnection dbConnection = null, IDbTransaction dbTransaction = null)
{
if (dbConnection == null)
{
dbConnection = _dbFactory.CreateDbConnection();
dbConnection.ExecuteSql("更新语句:更新 {0} 余额为 {1}", new object[] { id, balance });
_accounts[id] = balance;

}
else
{
dbConnection.ExecuteSql("更新语句:更新 {0} 余额为 {1}", new object[] { id, balance }, dbTransaction);
_accounts[id] = balance;
}

}
}
}
}

简要分析,上例代码中关键点:

IAccountDal.UpdateBalance支持传入数据库链接和事务对象,这样在IAccountBll既可以开启事务确保方法在一个事务内执行,也可以不开启事务,进行分事务执行。

这样做的缺点也比较明显。DAL层实现比较麻烦。

假如参照上文中“日志”的处理方案,将数据库链接和事务作为生命周期内单例来控制,实现起来将更加方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
using Autofac;
using System;
using System.Collections.Generic;
using System.Data;

namespace Use_Dependency_Injection_With_Lifetime_Scope_Control
{
public static class Demo5
{
public static void Run()
{
var cb = new ContainerBuilder();
cb.RegisterType<AccountBll>().As<IAccountBll>();
cb.RegisterType<AccountDal>().As<IAccountDal>();
cb.RegisterType<DbFactory>().As<IDbFactory>()
.InstancePerLifetimeScope();
cb.RegisterType<ConsoleDbConnection>().AsSelf();
var container = cb.Build();

using (var beginLifetimeScope = container.BeginLifetimeScope())
{
var accountBll = beginLifetimeScope.Resolve<IAccountBll>();
accountBll.Transfer("yueluo", "newbe", 333);
}
}

public interface IDbFactory
{
IExecuteSqlDbConnection CreateDbConnection();
}

public class DbFactory : IDbFactory
{
private readonly ConsoleDbConnection.Factory _factory;

public DbFactory(
ConsoleDbConnection.Factory factory)
{
this._factory = factory;
}

private IExecuteSqlDbConnection _connection;
public IExecuteSqlDbConnection CreateDbConnection()
{
return _connection ?? (_connection = new TransactionOnceDbConnection(_factory()));
}
}

/// <summary>
/// 除非上次事务结束,否则只会开启一次事务的链接
/// </summary>
public class TransactionOnceDbConnection : IExecuteSqlDbConnection
{
private readonly IExecuteSqlDbConnection _innerConnection;
private IDbTransaction _innerDbTransaction;
public TransactionOnceDbConnection(
IExecuteSqlDbConnection innerConnection)
{
_innerConnection = innerConnection;
}

public void Dispose()
{
_innerConnection.Dispose();
}

public IDbTransaction BeginTransaction()
{
if (_innerDbTransaction != null)
{
return _innerDbTransaction;
}
return _innerDbTransaction = _innerConnection.BeginTransaction();
}

public IDbTransaction BeginTransaction(IsolationLevel il)
{
if (_innerDbTransaction != null)
{
return _innerDbTransaction;
}
return _innerDbTransaction = _innerConnection.BeginTransaction(il);
}

public void Close()
{
_innerConnection.Close();
}

public void ChangeDatabase(string databaseName)
{
_innerConnection.ChangeDatabase(databaseName);
}

public IDbCommand CreateCommand()
{
return _innerConnection.CreateCommand();
}

public void Open()
{
_innerConnection.Open();
}

public string ConnectionString
{
get => _innerConnection.ConnectionString;
set => _innerConnection.ConnectionString = value;
}

public int ConnectionTimeout => _innerConnection.ConnectionTimeout;

public string Database => _innerConnection.Database;

public ConnectionState State => _innerConnection.State;
public void ExecuteSql(string sql, object[] ps, IDbTransaction dbTransaction = null)
{
_innerConnection.ExecuteSql(sql, ps, _innerDbTransaction ?? dbTransaction);
}
}

public interface IAccountBll
{
void Transfer(string fromAccountId, string toAccountId, decimal amount);
}

public class AccountBll : IAccountBll
{
private readonly IDbFactory _dbFactory;
private readonly IAccountDal _accountDal;

public AccountBll(
IDbFactory dbFactory,
IAccountDal accountDal)
{
_dbFactory = dbFactory;
_accountDal = accountDal;
}

public void Transfer(string fromAccountId, string toAccountId, decimal amount)
{
using (var dbConnection = _dbFactory.CreateDbConnection())
{
using (var transaction = dbConnection.BeginTransaction())
{
try
{
var fromAmount = _accountDal.GetBalance(fromAccountId);
var toAmount = _accountDal.GetBalance(toAccountId);
fromAmount -= amount;
toAmount += amount;
_accountDal.UpdateBalance(fromAccountId, fromAmount);
_accountDal.UpdateBalance(toAccountId, toAmount);
transaction.Commit();
}
catch (Exception)
{
transaction.Rollback();
throw;
}
}
}
}
}

public interface IAccountDal
{
/// <summary>
/// 获取账户的余额
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
decimal GetBalance(string id);

/// <summary>
/// 更新账户的余额
/// </summary>
/// <param name="id"></param>
/// <param name="balance"></param>
void UpdateBalance(string id, decimal balance);
}

public class AccountDal : IAccountDal
{
private readonly IDbFactory _dbFactory;

public AccountDal(
IDbFactory dbFactory)
{
this._dbFactory = dbFactory;
}

private readonly Dictionary<string, decimal> _accounts = new Dictionary<string, decimal>
{
{"newbe",1000},
{"yueluo",666},
};

public decimal GetBalance(string id)
{
return _accounts.TryGetValue(id, out var balance) ? balance : 0;
}

public void UpdateBalance(string id, decimal balance)
{
var dbConnection = _dbFactory.CreateDbConnection();
dbConnection.ExecuteSql("更新语句:更新 {0} 余额为 {1}", new object[] { id, balance });
_accounts[id] = balance;
}
}
}
}

简要分析,上例代码关键点:

  1. 通过装饰模式实现了TransactionOnceDbConnection,支持一次开启事务之后,后续操作都使用相同事务。
  2. 修改了DbFactory,实现一次开启链接之后,就是用相同链接的特性。
  3. IDbFactory标记为生命周期内单例。
  4. 在使用IAccountBll时,开启了一个生命周期。

这样改造之后,DAL实现时,就不需要关系事务到底是否开启没有,只需要直接执行相关操作即可。

总结

在使用依赖注入的时候,生命周期控制是一个相当重要的课题。读者需要在实践中注意分析。

以上示例代码都是基于较为简单的业务场景与基础代码实现,实际操作中不一定是如此,读者需要在实践中注意分析。

本文由于采用了Autofac作为主要的依赖注入框架,因此生命周期控制方式也采用了框架相关的函数。实际上,绝大多数框都提供了以上提及的生命周期控制方式。在实践中,读者可以找寻相关框架的文档,了解如何应用框架进行生命周期控制。

关于Autofac更加深入的生命周期控制:参考链接

至此,该系列文章也已完结,希望读者能够从中获益。

本文示例代码地址

教程链接

来吧,信仰充值!