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

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

每次获取都是新的实例

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

单例模式

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

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


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#语言之中这可以被看做一种单例模式。

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


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。

有关联的输出日志

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


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方法的执行是正确的。

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


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; }
    }
}

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


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层实现比较麻烦。

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


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更加深入的生命周期控制:参考链接

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

本文示例代码地址

教程链接

在C#中使用依赖注入-三层结构

在C#中使用依赖注入-工厂模式和工厂方法模式

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