Search code examples
c#design-patternsdapper

Dependency injection management with 'Unit of work' and 'Repository' design patterns using SqlConnection instance


A lot of time, I face a situation where I want to execute a number of statements without re-opening a connection.

So my practice so far is to create a 'unit of work' class, open a connection and pass it to all the repositories.

Here is an example of code that I have:

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

Although, I can unit test the repositories without a problem, I have 2 dependencies in the function: EmailSMSTemplateRepository and UserInformationRepository.

What is the best practice? What other way is there to share the connection, take note also that I need some class to dispose of the connection when the object is being disposed or if there is an error.

By the way, I use Dapper for the application, same micro-ORM that was used to build this site.


Solution

  • In the past I've approached this via the use of a Connection Factory -> Data Session -> Unit of Work -> Repository pattern.

    Something like -

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

    One weak point was that I never did find a better way to pass the unit of work to the repo layer besides requiring them to implement a receiving property and then populated it before use. It means it would be easy for a dev to forget that step and run into errors.

    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", .....);
        }
    }