使用 SqlConnection 实例的 'Unit of work' 和 'Repository' 设计模式的依赖注入管理
Dependency injection management with 'Unit of work' and 'Repository' design patterns using SqlConnection instance
很多时候,我遇到这样的情况,我想在不重新打开连接的情况下执行一些语句。
所以我目前的做法是创建一个 'unit of work' class,打开一个连接并将其传递给所有存储库。
这是我的代码示例:
public class BasicEmailUnitOfWork : IBasicEmailUnitOfWork
{
private readonly IDNTConnectionFactory _conn;
public BasicEmailUnitOfWork(IDNTConnectionFactory connection)
{
_conn = connection;
}
public (string, string, string, string, string) RenderEmailTemplate(string emailTemplateEventName, int userId)
{
string userPhone = String.Empty;
string userEmail = String.Empty;
string emailTitletranslatedContent = String.Empty;
string emailBodytranslatedContent = String.Empty;
string smstranslatedContent = String.Empty;
try
{
IDbConnection connectionDb = _conn.GetConnection();
IEmailSMSTemplateRepository _emailSMSTemplateRepository = new EmailSMSTemplateRepository(connectionDb);
IUserInformationRepository _userInformationRepository = new UserInformationRepository(connectionDb);
List<EmailSMSTemplateTDO> emailSmsesList = _emailSMSTemplateRepository.GetAllTemplates(emailTemplateEventName);
UserInfoDTO userInfoDTO = _userInformationRepository.GetAllInformation(userId);
userPhone = userInfoDTO.Phone;
userEmail = userInfoDTO.Email;
foreach (EmailSMSTemplateTDO emailTemplate in emailSmsesList)
{
emailTitletranslatedContent = TranslateContent(connectionDb, userInfoDTO, emailTemplate.EmailTitle);
emailBodytranslatedContent = TranslateContent(connectionDb, userInfoDTO, emailTemplate.EmailBody);
if (emailTemplate.IsSMS)
smstranslatedContent = TranslateContent(connectionDb, userInfoDTO, emailTemplate.SMSBody);
}
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
_conn.CloseConnection();
}
return (userPhone, userEmail,emailTitletranslatedContent, emailBodytranslatedContent, smstranslatedContent);
}
private string TranslateContent(IDbConnection connectionDb, UserInfoDTO userInfoDTO, string content)
{
InterpreterContext intContext = new InterpreterContext();
intContext.user = userInfoDTO;
intContext.Content = content;
IExpression emailSMSContentInterpreter = new EmailSMSContentInterpreter(connectionDb);
emailSMSContentInterpreter.Interpret(intContext);
return emailSMSContentInterpreter.BodyContentOutPut;
}
}
尽管我可以毫无问题地对存储库进行单元测试,但函数中有 2 个依赖项:EmailSMSTemplateRepository
和 UserInformationRepository
。
最佳做法是什么?还有什么其他方法可以共享连接,另请注意,我需要一些class来在处理对象或出现错误时处理连接。
顺便说一句,我在应用程序中使用了 Dapper,与用于构建此站点的微型 ORM 相同。
就个人而言,保持连接打开并重复使用它存在很多危险,尤其是在多个用户上下文中。插错机率很高
至于你说的“不重新打开连接”,不“重新打开”的目的是什么。我问这个是因为 SQL 连接在你处理它们时实际上并没有被处理掉。连接被清除并进入引擎盖下的池中。这意味着您不会招致任何开销“重新实例化”池中的对象。您的代码具有处置安全性,但没有开销。注意我假设对象创建开销的想法是可感知的问题。
这就是为什么 Microsoft 每次都让您关闭它们,但在引擎盖下创建了池。是的,池对象可以超时,但是你向它扔东西的速度足够快,它不会达到超时(默认 == 30 秒)。
注意:即使是 Dapper 也没有做任何特别的事情来真正破坏连接对象(即他们没有绕过 .NET 并重新发明轮子)。
根据上述内容,最佳做法是允许对象 Dispose 并返回到池中。我知道这似乎有点违反直觉,但一旦你意识到有一个内部池对象,它就有意义了。
如果您想要一个关于连续 运行 多个命令的思想实验,您可以考虑将命令创建为工作单元并快速喂养它们。您需要在回购协议中进行一些命令管理,但这比创建单个连接并等待直到您完成要处理的对象(因为处理失败,在某些情况下,可能是 BAAAAADDDD)更合理。
过去我通过使用连接工厂 -> 数据会话 -> 工作单元 -> 存储库模式来解决这个问题。
类似于 -
public interface IUnitOfWork : IDisposable
{
Guid Id { get; }
IDbConnection Connection { get; }
IDbTransaction Transaction { get; }
void Begin();
void Commit();
void Rollback();
}
public sealed class UnitOfWork : IUnitOfWork
{
internal UnitOfWork(IDbConnection connection)
{
_id = Guid.NewGuid();
_connection = connection;
}
private readonly IDbConnection _connection = null;
private IDbTransaction _transaction = null;
private readonly Guid _id;
IDbConnection IUnitOfWork.Connection => _connection;
IDbTransaction IUnitOfWork.Transaction => _transaction;
Guid IUnitOfWork.Id => _id;
public void Begin()
{
_transaction = _connection.BeginTransaction();
}
public void Commit()
{
_transaction.Commit();
Dispose();
}
public void Rollback()
{
_transaction.Rollback();
Dispose();
}
public void Dispose()
{
_transaction?.Dispose();
_transaction = null;
}
}
//define enum's that describe various databases you might want to connect to
//the db type/engine is an implementation detail handled by the connection factory
public enum DatabaseInstance
{
YourDatabase,
SomeOtherDatabase,
AThirdDatabase
}
//abstracts the 'how do i connect to the database?' question
public interface IDbConnectionFactory
{
IDbConnection GetConnection(DatabaseInstance database);
}
//for instance -
public class SqlDbConnectionFactory : IDbConnectionFactory
{
public IDbConnection GetConnection(DatabaseInstance database)
{
//return a IDbConnection for a given DatabaseInstance value
//possible implementations could be storing a DatabaseInstance->Connectionstring map dictionary that is populated in the constructor
}
}
public interface IDataAccessSession : IDisposable
{
IUnitOfWork UnitOfWork { get; }
void StartSession(DatabaseInstance databaseInstance);
}
//one possible implementation - use a connection factory and requested instance to create an IUnitOfWork for consumers to use
public sealed class DataAccessSession : IDataAccessSession
{
private IDbConnection _connection = null;
private readonly IDbConnectionFactory _dbConnectionFactory;
public DalSession(IDbConnectionFactory dbConnectionFactory)
{
_dbConnectionFactory = dbConnectionFactory;
}
public void StartSession(DatabaseInstance databaseInstance)
{
_connection = _dbConnectionFactory.GetConnection(databaseInstance);
_connection.Open();
UnitOfWork = new UnitOfWork(_connection);
}
public IUnitOfWork UnitOfWork { get; private set; }
public void Dispose()
{
UnitOfWork.Dispose();
_connection.Dispose();
}
}
//then there's an extension method (in practice a helper overload per DatabaseInstance as well
//starts a session, passes the unit of work to the consumer task, commit/rollsback, and then disposes
public static class DataAccessSessionExtensions
{
public static async Task<T> RunTransactionAsync<T>(this IDataAccessSession dataAccessSession, DatabaseInstance instance, Func<IUnitOfWork, Task<T>> functionToRun, bool rollbackOnException=false)
{
dataAccessSession.StartSession(instance);
dataAccessSession.UnitOfWork.Begin();
try
{
var result = await functionToRun(dataAccessSession.UnitOfWork);
dataAccessSession.UnitOfWork.Commit();
return result;
}
catch (SqlException)
{
if(rollbackOnException)
dataAccessSession.UnitOfWork.Rollback();
else
dataAccessSession.UnitOfWork.Commit();
throw;
}
}
}
//then in your dependency injection container (asp.net core here)
....
//if you needed to say connect to mongo for one and sql for another this would need a bit further refinement as the MS DI Api doesn't handle that well, you'd need to do something like pass collections of connection factory/instance->connections instead as one possible solution
//this can be a singleton as it doesn't store live connections, just how to map requests to new connections
services.AddSingleton<IDbConnectionFactory>(i => new SqlDbConnectionFactory(/*pass in a values for connection strings per data base instance defined*/));
//should be scoped so that a new instance is created/disposed per DI request
services.AddScoped<IDalSession, DalSession>();
....
//finally usage would be like so
public class SomeService : ISomeService
{
private readonly IDataAccessSession _dataAccessSession;
private readonly ISomeRepo _someRepo;
Private readonly ISomeOtherRepo _someOtherRepo
public SomeService (IDataAccessSession dataAccessSession, ISomeRepo someRepo, ISomeOtherRepo someOtherRepo)
{
_dataAccessSession = dataAccessSession;
_someRepo = someRepo;
_someOtherRepo = someOtherRepo;
}
// at this point all needed repo's are handed a single UnitOfWork that represents the connection and transaction and a session that will clean up afterwards
public async Task<SomeResult> DoSomeThing(string param1, int param2)
{
var result = await _dataAccessSession.RunTransactionAsync(DatabaseInstance.Whatever, async uow =>
{
//critical - forget this and things will go boom
_someRepo.UnitOfWork = uow;
_someOtherRepo.UnitOfWork = uow;
var repo1Value = await _someRepo.SomeAction(param1);
var repo2Value = await _someOtherRepo.GetAThing(param2);
return new SomeResult(repo1Value, repo2Value);
});
return Task.FromResult(result);
}
}
一个弱点是,除了要求他们实现接收 属性 然后在使用前填充它之外,我从来没有找到更好的方法将工作单元传递给 repo 层。这意味着开发人员很容易忘记该步骤并 运行 出错。
public interface IHasUnitOfWork
{
IUnitOfWork UnitOfWork { get; set; }
}
public interface IDataRepo : IHasUnitOfWork
{
}
public interface ISomeRepo : IDataRepo
{
Task UpdateSomethingAsync();
}
//at the time of implementing the repo the promise is that UnitOfWork will be non-null with a valid open connection
//that is ready to exec against and that if a sql exception is thrown a rollback will be initiated (if flag is set)
//though note that if multiple repos are sharing this unit of work and a different one throws, this repo's calls will also rollback as it considers all actions to be under a single transaction scope per IDataAccessSession
public SomeRepo : ISomeRepo
{
public IUnitOfWork UnitOfWork { get; set; }
public async Task UpdateSomethingAsync()
{
await UnitOfWork.Connection.ExecuteAsync("sproc", .....);
}
}
很多时候,我遇到这样的情况,我想在不重新打开连接的情况下执行一些语句。
所以我目前的做法是创建一个 'unit of work' class,打开一个连接并将其传递给所有存储库。
这是我的代码示例:
public class BasicEmailUnitOfWork : IBasicEmailUnitOfWork
{
private readonly IDNTConnectionFactory _conn;
public BasicEmailUnitOfWork(IDNTConnectionFactory connection)
{
_conn = connection;
}
public (string, string, string, string, string) RenderEmailTemplate(string emailTemplateEventName, int userId)
{
string userPhone = String.Empty;
string userEmail = String.Empty;
string emailTitletranslatedContent = String.Empty;
string emailBodytranslatedContent = String.Empty;
string smstranslatedContent = String.Empty;
try
{
IDbConnection connectionDb = _conn.GetConnection();
IEmailSMSTemplateRepository _emailSMSTemplateRepository = new EmailSMSTemplateRepository(connectionDb);
IUserInformationRepository _userInformationRepository = new UserInformationRepository(connectionDb);
List<EmailSMSTemplateTDO> emailSmsesList = _emailSMSTemplateRepository.GetAllTemplates(emailTemplateEventName);
UserInfoDTO userInfoDTO = _userInformationRepository.GetAllInformation(userId);
userPhone = userInfoDTO.Phone;
userEmail = userInfoDTO.Email;
foreach (EmailSMSTemplateTDO emailTemplate in emailSmsesList)
{
emailTitletranslatedContent = TranslateContent(connectionDb, userInfoDTO, emailTemplate.EmailTitle);
emailBodytranslatedContent = TranslateContent(connectionDb, userInfoDTO, emailTemplate.EmailBody);
if (emailTemplate.IsSMS)
smstranslatedContent = TranslateContent(connectionDb, userInfoDTO, emailTemplate.SMSBody);
}
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
_conn.CloseConnection();
}
return (userPhone, userEmail,emailTitletranslatedContent, emailBodytranslatedContent, smstranslatedContent);
}
private string TranslateContent(IDbConnection connectionDb, UserInfoDTO userInfoDTO, string content)
{
InterpreterContext intContext = new InterpreterContext();
intContext.user = userInfoDTO;
intContext.Content = content;
IExpression emailSMSContentInterpreter = new EmailSMSContentInterpreter(connectionDb);
emailSMSContentInterpreter.Interpret(intContext);
return emailSMSContentInterpreter.BodyContentOutPut;
}
}
尽管我可以毫无问题地对存储库进行单元测试,但函数中有 2 个依赖项:EmailSMSTemplateRepository
和 UserInformationRepository
。
最佳做法是什么?还有什么其他方法可以共享连接,另请注意,我需要一些class来在处理对象或出现错误时处理连接。
顺便说一句,我在应用程序中使用了 Dapper,与用于构建此站点的微型 ORM 相同。
就个人而言,保持连接打开并重复使用它存在很多危险,尤其是在多个用户上下文中。插错机率很高
至于你说的“不重新打开连接”,不“重新打开”的目的是什么。我问这个是因为 SQL 连接在你处理它们时实际上并没有被处理掉。连接被清除并进入引擎盖下的池中。这意味着您不会招致任何开销“重新实例化”池中的对象。您的代码具有处置安全性,但没有开销。注意我假设对象创建开销的想法是可感知的问题。
这就是为什么 Microsoft 每次都让您关闭它们,但在引擎盖下创建了池。是的,池对象可以超时,但是你向它扔东西的速度足够快,它不会达到超时(默认 == 30 秒)。
注意:即使是 Dapper 也没有做任何特别的事情来真正破坏连接对象(即他们没有绕过 .NET 并重新发明轮子)。
根据上述内容,最佳做法是允许对象 Dispose 并返回到池中。我知道这似乎有点违反直觉,但一旦你意识到有一个内部池对象,它就有意义了。
如果您想要一个关于连续 运行 多个命令的思想实验,您可以考虑将命令创建为工作单元并快速喂养它们。您需要在回购协议中进行一些命令管理,但这比创建单个连接并等待直到您完成要处理的对象(因为处理失败,在某些情况下,可能是 BAAAAADDDD)更合理。
过去我通过使用连接工厂 -> 数据会话 -> 工作单元 -> 存储库模式来解决这个问题。
类似于 -
public interface IUnitOfWork : IDisposable
{
Guid Id { get; }
IDbConnection Connection { get; }
IDbTransaction Transaction { get; }
void Begin();
void Commit();
void Rollback();
}
public sealed class UnitOfWork : IUnitOfWork
{
internal UnitOfWork(IDbConnection connection)
{
_id = Guid.NewGuid();
_connection = connection;
}
private readonly IDbConnection _connection = null;
private IDbTransaction _transaction = null;
private readonly Guid _id;
IDbConnection IUnitOfWork.Connection => _connection;
IDbTransaction IUnitOfWork.Transaction => _transaction;
Guid IUnitOfWork.Id => _id;
public void Begin()
{
_transaction = _connection.BeginTransaction();
}
public void Commit()
{
_transaction.Commit();
Dispose();
}
public void Rollback()
{
_transaction.Rollback();
Dispose();
}
public void Dispose()
{
_transaction?.Dispose();
_transaction = null;
}
}
//define enum's that describe various databases you might want to connect to
//the db type/engine is an implementation detail handled by the connection factory
public enum DatabaseInstance
{
YourDatabase,
SomeOtherDatabase,
AThirdDatabase
}
//abstracts the 'how do i connect to the database?' question
public interface IDbConnectionFactory
{
IDbConnection GetConnection(DatabaseInstance database);
}
//for instance -
public class SqlDbConnectionFactory : IDbConnectionFactory
{
public IDbConnection GetConnection(DatabaseInstance database)
{
//return a IDbConnection for a given DatabaseInstance value
//possible implementations could be storing a DatabaseInstance->Connectionstring map dictionary that is populated in the constructor
}
}
public interface IDataAccessSession : IDisposable
{
IUnitOfWork UnitOfWork { get; }
void StartSession(DatabaseInstance databaseInstance);
}
//one possible implementation - use a connection factory and requested instance to create an IUnitOfWork for consumers to use
public sealed class DataAccessSession : IDataAccessSession
{
private IDbConnection _connection = null;
private readonly IDbConnectionFactory _dbConnectionFactory;
public DalSession(IDbConnectionFactory dbConnectionFactory)
{
_dbConnectionFactory = dbConnectionFactory;
}
public void StartSession(DatabaseInstance databaseInstance)
{
_connection = _dbConnectionFactory.GetConnection(databaseInstance);
_connection.Open();
UnitOfWork = new UnitOfWork(_connection);
}
public IUnitOfWork UnitOfWork { get; private set; }
public void Dispose()
{
UnitOfWork.Dispose();
_connection.Dispose();
}
}
//then there's an extension method (in practice a helper overload per DatabaseInstance as well
//starts a session, passes the unit of work to the consumer task, commit/rollsback, and then disposes
public static class DataAccessSessionExtensions
{
public static async Task<T> RunTransactionAsync<T>(this IDataAccessSession dataAccessSession, DatabaseInstance instance, Func<IUnitOfWork, Task<T>> functionToRun, bool rollbackOnException=false)
{
dataAccessSession.StartSession(instance);
dataAccessSession.UnitOfWork.Begin();
try
{
var result = await functionToRun(dataAccessSession.UnitOfWork);
dataAccessSession.UnitOfWork.Commit();
return result;
}
catch (SqlException)
{
if(rollbackOnException)
dataAccessSession.UnitOfWork.Rollback();
else
dataAccessSession.UnitOfWork.Commit();
throw;
}
}
}
//then in your dependency injection container (asp.net core here)
....
//if you needed to say connect to mongo for one and sql for another this would need a bit further refinement as the MS DI Api doesn't handle that well, you'd need to do something like pass collections of connection factory/instance->connections instead as one possible solution
//this can be a singleton as it doesn't store live connections, just how to map requests to new connections
services.AddSingleton<IDbConnectionFactory>(i => new SqlDbConnectionFactory(/*pass in a values for connection strings per data base instance defined*/));
//should be scoped so that a new instance is created/disposed per DI request
services.AddScoped<IDalSession, DalSession>();
....
//finally usage would be like so
public class SomeService : ISomeService
{
private readonly IDataAccessSession _dataAccessSession;
private readonly ISomeRepo _someRepo;
Private readonly ISomeOtherRepo _someOtherRepo
public SomeService (IDataAccessSession dataAccessSession, ISomeRepo someRepo, ISomeOtherRepo someOtherRepo)
{
_dataAccessSession = dataAccessSession;
_someRepo = someRepo;
_someOtherRepo = someOtherRepo;
}
// at this point all needed repo's are handed a single UnitOfWork that represents the connection and transaction and a session that will clean up afterwards
public async Task<SomeResult> DoSomeThing(string param1, int param2)
{
var result = await _dataAccessSession.RunTransactionAsync(DatabaseInstance.Whatever, async uow =>
{
//critical - forget this and things will go boom
_someRepo.UnitOfWork = uow;
_someOtherRepo.UnitOfWork = uow;
var repo1Value = await _someRepo.SomeAction(param1);
var repo2Value = await _someOtherRepo.GetAThing(param2);
return new SomeResult(repo1Value, repo2Value);
});
return Task.FromResult(result);
}
}
一个弱点是,除了要求他们实现接收 属性 然后在使用前填充它之外,我从来没有找到更好的方法将工作单元传递给 repo 层。这意味着开发人员很容易忘记该步骤并 运行 出错。
public interface IHasUnitOfWork
{
IUnitOfWork UnitOfWork { get; set; }
}
public interface IDataRepo : IHasUnitOfWork
{
}
public interface ISomeRepo : IDataRepo
{
Task UpdateSomethingAsync();
}
//at the time of implementing the repo the promise is that UnitOfWork will be non-null with a valid open connection
//that is ready to exec against and that if a sql exception is thrown a rollback will be initiated (if flag is set)
//though note that if multiple repos are sharing this unit of work and a different one throws, this repo's calls will also rollback as it considers all actions to be under a single transaction scope per IDataAccessSession
public SomeRepo : ISomeRepo
{
public IUnitOfWork UnitOfWork { get; set; }
public async Task UpdateSomethingAsync()
{
await UnitOfWork.Connection.ExecuteAsync("sproc", .....);
}
}