I'm building pretty big application and I wonder how should I share the code between asp.net mvc website and standalone (self running) server application. I've got some idea how I should do this but I'm not sure if I'm doing this right. I've got Domain Entities, Services and Repositories that will be shared between asp.mvc app and server app. Services will perform transactions on multiple repositories using IDbTransaction (I can't use ORM, pure ado.net and mysql).

IDbContext.cs

public interface IDbContext : IDisposable
{
    IUnitOfWork CreateUnitOfWork();
}

IDbContextFactory.cs

public interface IDbContextFactory
{
    IDbContext Create();
}

IRepositoryFactory.cs

public interface IRepositoryFactory
{
    T GetRepository<T>(IDbContext context, IUnitOfWork unitOfWork = null)
        where T : class;
}

IUnitOfWork.cs

   public interface IUnitOfWork : IDisposable
    {
        bool IsInTransaction { get; }
        void BeginTransaction();
        void BeginTransaction(IsolationLevel isolation);
        void CommitTransaction();
        void RollbackTransaction();
    }

Now the MySql implementation

MysqlDbContext.cs

 public class MysqlDbContext : IDbContext
    {
        #region Fields
        private MySqlConnection _connection;
        private bool _disposed;

        #endregion

        #region Constructor

        public MysqlDbContext(MysqlConnectionFactory connectionFactory)
        {
            //Todo: create and open the connection
            _connection = connectionFactory.Create();
            if (_connection.State != ConnectionState.Open)
                _connection.Open();
        }

        #endregion

        public MySqlConnection Connection
        {
            get { return _connection; }
        }

        public IUnitOfWork CreateUnitOfWork()
        {
            return new MysqlUnitOfWork(_connection);
        }

        public void Dispose()
        {
            Dispose(true);
        }

        public void Dispose(bool disposing)
        {
            if (!disposing)
                return;

            /*
             * Dispose resources 
             */
            if(_connection != null)
            {
                Console.WriteLine("Dispose connection!");
                _connection.Dispose();
                _connection = null;
            }

            //Set disposed
            _disposed = true;
        }


    }

MysqlDbContextFactory.cs

public class MysqlDbContextFactory : IDbContextFactory
{
    private MysqlConnectionFactory _connectionFactory;

    public MysqlDbContextFactory(MysqlConnectionFactory connectionFactory)
    {
        _connectionFactory = connectionFactory;
    }

    public IDbContext Create()
    {
        return new MysqlDbContext(_connectionFactory);
    }
}

MysqlRepositoryFactory.cs

public class MysqlRepositoryFactory : IRepositoryFactory
{
    #region Constructor

    public MysqlRepositoryFactory()
    {

    }

    #endregion

    public TRepository GetRepository<TRepository>(IDbContext context, IUnitOfWork unitOfWork = null)
        where TRepository : class
    {
        if (typeof(TRepository) == typeof(IUserRepository))
            return (new UserRepository(context, unitOfWork)) as TRepository;


        return default(TRepository);
    }
}

MysqlUnitOfWork.cs

public class MysqlUnitOfWork : IUnitOfWork
{
    private readonly MySqlConnection _connection;
    private MySqlTransaction _transaction;
    private bool _disposed;

    #region Constructor

    public MysqlUnitOfWork(MySqlConnection connection)
    {
        if (connection.State != ConnectionState.Open)
            throw new ApplicationException("Cannot begin transaction. Connection is not opened!");

        _connection = connection;
    }

    #endregion

    #region Properties

    public bool IsInTransaction
    {
        get
        {
            if (_transaction != null)
                return true;

            return false;
        }
    }

    public MySqlTransaction Transaction
    {
        get
        {
            return _transaction;
        }
    }

    #endregion

    public void BeginTransaction()
    {
        BeginTransaction(IsolationLevel.ReadCommitted);
    }

    public void BeginTransaction(IsolationLevel isolation)
    {
        if (_transaction != null)
            throw new ApplicationException("Cannot begin a new transaction while an existing transaction is still running. Please commit or rollback the existing transaction before starting a new one");

        _transaction = _connection.BeginTransaction(isolation);
    }

    public void CommitTransaction()
    {
        if (_transaction == null)
            throw new ApplicationException("Cannot commit transaction while there is no transaction running");

        _transaction.Commit();
    }

    public void RollbackTransaction()
    {
        if (_transaction == null)
            throw new ApplicationException("Cannot rollback transaction while there is no transaction running");

        _transaction.Rollback();
    }

    public void Dispose()
    {
        Dispose(true);
    }

    public void Dispose(bool disposing)
    {
        if (!disposing)
            return;

        if(_transaction != null)
        {
            _transaction.Dispose();
            _transaction = null;
        }

        _disposed = true;
    }
}

UserService.cs

public class UserService : IUserService
{
    #region Fields

    private readonly IDbContextFactory _dbContextFactory;
    private readonly IRepositoryFactory _repositoryFactory;

    #endregion

    #region Constructor

    public UserService(IDbContextFactory dbContextFactory, IRepositoryFactory repositoryFactory)
    {
        _dbContextFactory = dbContextFactory;
        _repositoryFactory = repositoryFactory;
    }

    #endregion

    public void Test()
    {
        using(var context = _dbContextFactory.Create())
        {
            var userRepository = _repositoryFactory.GetRepository<IUserRepository>(context);

            userRepository.Create(new User
            {
                Username = "admin",
                Password = "admin",
                Email = "admin@bidit.com",
            });
        }
    }

    public void TestTransaction()
    {
        using(var context = _dbContextFactory.Create())
        {
            using(var uow = context.CreateUnitOfWork())
            {
                var userRepository = _repositoryFactory.GetRepository<IUserRepository>(context, uow);

                //Begin new transaction (defualt isolation level: ReadCommited)
                uow.BeginTransaction();

                try
                {
                    userRepository.Create(new User
                    {
                        Username = "rlydontknow",
                        Password = "12345",
                        Email = "rlydontknow@bidit.com",
                    });

                    //Commit transaction
                    uow.CommitTransaction();
                }
                catch
                {
                    //Rollback transaction on error
                    uow.RollbackTransaction();
                    throw;
                }

            }
        }
    }
}

IDbContextFactory and IRepositoryFactory will be injected into service.

What do you think about my solution?

Instead of an ApplicationException, your application would be better off throwing an InvalidOperationException, since that actually tells someone who uses your library a bit more about the problem.

Why do you even have a variable _disposed if it is only ever assigned to once as soon as the objects do get disposed? It is never read from.

MysqlRepositoryFactory is not specific to your MySQL implementation at all - just call it your RepositoryFactory. Apart from that, GetRepository<IUserRepository> should not work in the current version, because it is limited to classes?

Those nitpicks aside, I would argue that the responsibility of creating, committing and disposing the unit of work should be somewhere else. The way your design works, you could not ever use the same unit of work (and transaction) for two or more service calls. Furthermore, you would end up creating a lot of duplicate code if you do that for every public method of every service. One usual way of solving that with ASP.NET is to create the unit of work once for every ASP.NET request, inject it into the services/repositories directly, and commit it in Application_EndRequest. In a Windows service or WinForms app, you could still scope the unit of work for every logical action in - which might still span over more than one service call.

  • Well.. This is sample code and missing some stuff. About using the same unit of work accross multiple services - good point. MysqlRepositoryFactory returns Mysql implementation of UserRepository. I will re-design my code and post some changes tommorow. Got 12h shift at work today so no chance to make those changes today :D – RlyDontKnow Dec 13 '13 at 13:31

I took hangy advice and rewritten my code totally. Now it looks like that.

IDbContext.cs

public interface IDbContext : IDisposable
{
    IUnitOfWork CreateUnitOfWork();
}

IUnitOfWork.cs/

public interface IUnitOfWork : IDisposable
{
    void Commit();
    void Rollback();
}

IConnectionFactory.cs

public interface IConnectionFactory
{
    IDbConnection Create();
}

MysqlConnectionFactory.cs

public class MysqlConnectionFactory : IConnectionFactory
{
    private string _connectionString;

    public MysqlConnectionFactory(string connectionString)
    {
        _connectionString = connectionString;
    }

    public string ConnectionString
    {
        get
        {
            return _connectionString;
        }
    }


    public IDbConnection Create()
    {
        var connection = new MySqlConnection(_connectionString);
        connection.Open();

        return connection;
    }
}

MysqlDbContext.cs

public class MysqlDbContext : IDbContext
{
    private IDbConnection _connection;
    private MysqlUnitOfWork _currentUnitOfWork;

    public MysqlDbContext(IConnectionFactory connectionFactory)
    {
        //Create new connection
        _connection = connectionFactory.Create();
        if (_connection.State != ConnectionState.Open)
            _connection.Open();
    }

    public IDbConnection Connection
    {
        get
        {
            return _connection;
        }
    }

    public MysqlUnitOfWork CurrentUnitOfWork
    {
        get
        {
            return _currentUnitOfWork;
        }
    }


    public IUnitOfWork CreateUnitOfWork()
    {
        if (_currentUnitOfWork != null)
            throw new InvalidOperationException("Cannot create new unit of work. Commit or rollback current one.");

        _currentUnitOfWork = new MysqlUnitOfWork(_connection);

        return _currentUnitOfWork;
    }

    public void Dispose()
    {
        Dispose(true);
    }

    public void Dispose(bool disposing)
    {
        if (!disposing)
            return;

        if(_connection != null)
        {
            _connection.Dispose();
            _connection = null;
        }
    }
}

MysqlRepository.cs

public abstract class MysqlRepository
{
    private readonly MysqlDbContext _context;

    public MysqlRepository(IDbContext context)
    {
        if (context.GetType() != typeof(MysqlDbContext))
            throw new InvalidOperationException("Incorrect context type. MysqlDbContext required!");

        _context = (MysqlDbContext)context;
    }

    public MysqlDbContext Context
    {
        get
        {
            return _context;
        }
    }

    protected IDbConnection Connection
    {
        get
        {
            if (_context != null)
                return _context.Connection;

            return null;
        }
    }

    protected IDbTransaction Transaction
    {
        get
        {
            if (_context != null && _context.CurrentUnitOfWork != null)
                return _context.CurrentUnitOfWork.Transaction;

            return null;
        }
    }
}

MysqlUnitOfWork.cs

public class MysqlUnitOfWork : IUnitOfWork
{
    private IDbTransaction _transaction;
    private bool _commited;

    public MysqlUnitOfWork(IDbConnection connection, IsolationLevel isolation = IsolationLevel.ReadCommitted)
    {
        if (connection == null)
            throw new ArgumentNullException("connection");

        if (connection.State != ConnectionState.Open)
            throw new InvalidOperationException("Cannot create unit of work. Connection to the database is not estabilished!");

        //Begin new transaction
        _transaction = connection.BeginTransaction(isolation);

    }

    public IDbTransaction Transaction
    {
        get
        {
            return _transaction;
        }
    }

    public void Commit()
    {
        if (_transaction == null)
            throw new InvalidOperationException("Cannot commit transaction while there is no transaction running");

        _transaction.Commit();

        _commited = true;
    }

    public void Rollback()
    {
        if (_transaction == null)
            throw new InvalidOperationException("Cannot rollback transaction while there is no transaction running");

        if (_commited)
            throw new InvalidOperationException("Cannot rollback already commited transaction");

        _transaction.Rollback();
    }

    public void Dispose()
    {
        Dispose(true);   
    }

    public void Dispose(bool disposing)
    {
        if (!disposing)
            return;

        if(_transaction != null)
        {
            //Automatically rollback if not commited
            if (!_commited)
                _transaction.Rollback();

            _transaction.Dispose();
            _transaction = null;
        }

    }


}

UserRepository.cs

public class UserRepository : MysqlRepository, IUserRepository
{
    public UserRepository(IDbContext context)
        : base(context)
    {

    }

    public void Create(User user)
    {
        var parameters = new
        {
            username = user.Username,
            password = user.Password,
            email = user.Email,
        };

        user.Id = (uint)Connection.Query<ulong>("INSERT INTO users (username, password, email) VALUES (@username, @password, @email); select last_insert_id();", parameters).First();
    }
}

UserSerivce.cs

public class UserService : IUserService
{
    private readonly IUserRepository _userRepository;

    public UserService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }



    public void Create()
    {
        var user = new User
        {
            Username = "rlydontknow",
            Password = "12345",
            Email = "rlydontknow@sharpbid.com"
        };

        _userRepository.Create(user);

        Console.WriteLine("New user created! User id: " + user.Id);

    }
}

Program.cs

class Program
{
    private static IConnectionFactory _connectionFactory;

    static void Main(string[] args)
    {
        _connectionFactory = new MysqlConnectionFactory("Server=localhost;Database=sharpbid;Uid=root;Pwd=maniek1;");


        SomeLogicalOperation();

        Console.ReadLine();
    }

    static void SomeLogicalOperation()
    {
        using(var context = GetContext())
        {
            var userRepository = new UserRepository(context);

            /*
             *  Do some stuff without transaction
             */

            //Begin transaction
            using (var uow = context.CreateUnitOfWork())
            {
                var userService = new UserService(userRepository);

                userService.Create();

                //Commit transaction
                uow.Commit();
            }
        }
    }

    static IDbContext GetContext()
    {
        return new MysqlDbContext(_connectionFactory);
    }
}

It's just example code. Might missing few things.

  • Why pass in a IDbcontext to the MysqlRepository if the conditions are it requires a MysqlDbContext anyway. Why not just have it require a MysqlDbContext? – dreza Dec 14 '13 at 7:06
  • Right... my bad. :P – RlyDontKnow Dec 14 '13 at 13:47

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.