Click here to Skip to main content
15,891,905 members
Articles / Programming Languages / C#

Generic Repository Framework (Generic Unit of Work)

Rate me:
Please Sign up or sign in to vote.
4.83/5 (19 votes)
30 Apr 2014CPOL6 min read 40.9K   1.7K   45   3
Generic Repository Framework (Generic Unit of Work)

Introduction

In my work, I have encountered the problem with access to different repository types in the same application. The problem concerns ADO.NET and EF. The solution I have been looking for should allow me in the future to easily extend the existing functionality, maintain existing one. I also wanted to be able to validate data before storing data into database. I want to share with you my investigations and conclusions.

I have spent some time trying to resolve the problem with different units of work attached to any defined framework that will be in use. I could not find a general approach that will have persistent ignorance. I am also strongly focused on TDD and easy making solution testable. Generally, I did not want to think what will be the repository (ADO, EF or WebService).

I will present the solution on Portal project. Portal allows to do some actions on it, but we will focus only on portal user and its contact information. Creating Portal I have tried to follow SOLID principals. I have used Dependency Injection with Autofac. This project does not show the complete working solution, just some guidelines and small examples to support my boring talk. In code examples, I have skipped null object validation to shorten the code. Sorry, you have to do it by yourself. The database schema is below:

Image 1

Services

I will start with services.

Services are set of classes (you can call them controllers or managers) that will process domain object to and from repository.

Image 2

We have general service interface that give us a method to validate if passed object is valid to be stored in repository. We have also two interfaces to our domain objects: User and ContactInfo. There are typical CRUD methods. The implementation of IServiceBase<T> is as below:

C#
    public abstract class ServiceBase<P, U, T> : IServiceBase<T>
    where T : class
    where P : IUnitOfWorkProvider<U>
    where U : IUnitOfWork
{
    private readonly IValidator<T> _validator;
    private readonly P _unitOfWorks;

    private U _readOnly = default(U);


    public ServiceBase(P unitOfWorks, IValidator<T> validator)
    {
        _validator = validator;
        _unitOfWorks = unitOfWorks;
    }

    protected U GetTransactionalUnitOfWork()
    {
        var unitOfWork = _unitOfWorks.GetReadOnly();
        return unitOfWork;
    }

    protected U GetReadOnlyUnitOfWork()
    {
        if (_readOnly == null)
        {
            _readOnly = _unitOfWorks.GetReadOnly();
        }
        return _readOnly;
    }

    protected void Validate(T entity)
    {
        var result = _validator.Validate(entity);
        if (!result.IsValid)
        {
                    throw new CustomValidationException(result.Errors);
        }
    }

    public bool IsValid(T entity)
    {
        if (entity == null)
        {
            return false;
        }
        var result = _validator.Validate(entity);
        return result.IsValid;
    }
}

As it was expected, constructor takes validator as a parameter.

Image 3

Except that, there is also IUnitOfWorkProvider<IUnitOfWork>. Unit of work provider gives us unit of work for each action we want to perform in service with repository.

Image 4Image 5

In this case, this will be IPortalUnitOfWorkProvider. We can read from the same unit of work on all services, but for sure we need new unit of work to perform modification. Unit of work can assure that all actions taken will be wrapped in transaction.

It can be confusing what is PortalUnitOfWork. I will define it as a part of repository that is related with portal as general (database dbo.* tables). If we are selling stuff in our platform, we can expect some ProductUnitOfWork (and probably corresponding schema on database).

Image 6Image 7

IPortalUnitOfWork provides access to repositories on which transaction can be performed. In our case, it will be IUserRepository and IContactInfoRepository. Few more words about unit of work. Generic unit of work allow to just save changes in existing transaction. And its portal implementation provides repositories transaction concerns. Unit of works differs depending on repository type.

Now we have unit of work but we miss IPortalUnitOfWokProvider implementation. Generic unit of work provider looks like below:

C#
public abstract class UnitOfWorkProvider<T> : IUnitOfWorkProvider<T> where T : IUnitOfWork
{

    private T _readOnly = default(T); 
     
    
    public T GetReadOnly()
    {
        if (_readOnly == null)
        {
            _readOnly = GetNew();
        }
        return _readOnly;
    }
    
    public T GetTransactional()
    {
        return GetNew();
    } 
    
    protected abstract T GetNew();
    
    
    public void Dispose()
    {
        if (_readOnly != null)
        {
            _readOnly.Dispose();
        }
    }
 }

I came from the assumption that my unit of work provider will be singleton inside application/http request. Therefore, I am reusing one existing connection to all reads from repository. Custom implementation for portal just overrides GetNew() method.

Each repository type implements its own UnitOfWork that implement IUnitOfWork as well as IPortalUnitOfWork.

Now we can go back to services, that have in their constructor IUnitOfWorkProvider that provides IUnitOfWork. Service classes implementation is the same for all types of repository, are repository independent.

Image 8
C#
public class UserService : ServiceBase<IUnitOfWorkProvider<IPortalUnitOfWork>, IPortalUnitOfWork, IUser>, IUserService
    {
                private readonly IUserRepository _readOnlyRepository;
        
    public UserService(IPortalUnitOfWorkProvider provider, IUserValidator validator)
            : base(provider, validator)
        {
                        _readOnlyRepository = GetReadOnlyUnitOfWork().UserRepository;
        }
        
        public IUser AddOrUpdate(IUser user)
        {
            using (var unitOfWork = GetTransactionalUnitOfWork())
            {
                var result = AddOrUpdate(unitOfWork.UserRepository, user);
                unitOfWork.Save();
                return result;
            }
        }
                     public IUser AddOrUpdate(IUserRepository repository, IUser user)
        {
            Validate(user);
            return repository.AddOrUpdate(user);
        }
 
        public IUser GetBy(string logon)
        {
            return _readOnlyRepository.GetBy(logon);
        }
}

public class ContactInfoService : ServiceBase<IUnitOfWorkProvider<IPortalUnitOfWork>, IPortalUnitOfWork, IContactInfo>, IContactInfoService
    {
         
        private readonly IContactInfoRepository _readOnlyRepository;
         
        public ContactInfoService(IPortalUnitOfWorkProvider provider, IContactInfoValidator validator)
            : base(provider, validator)
        {
            _readOnlyRepository = GetReadOnlyUnitOfWork().ContactInfoRepository;
        }
 
        
    public IEnumerable<IContactInfo> GetUserContactInfosBy(string login)
        {
            return _readOnlyRepository.GetAllBy(login);
        }
 
        public void DeleteAllBy(IContactInfoRepository repository, IUser user)
        {
            repository.DeleteAllBy(user);
        }
 
                public IContactInfo AddOrUpdate(IContactInfoRepository repository, IUser user, IContactInfo contactInfo)
        {
            Validate(contactInfo);
            return repository.AddOrUpdate(user, contactInfo);
        }
 
            public IContactInfo AddOrUpdate(IUser user, IContactInfo contactInfo)
        {
            using (var unitOfWork = GetTransactionalUnitOfWork())
            {
                var result = AddOrUpdate(_readOnlyRepository, user, contactInfo);
                unitOfWork.Save();
                return result;
            }
        }
} 

The read-only repository is created in constructor. As I have mentioned, unit of work provider keeps one connection open for all reads. Each modification is done on new unit of work. Validation of the domain model is done before passing the value into repository, so changing repository does not harm domain objects validation.

One more thing worth mentioning is UserInfo domain entity.

Image 9

UserInfo combines user and its contact information for domain needs. As domain entity combines two repository models, we want access to them to be transparent and handle this through UserInfoService.

C#
public class UserInfoService : IUserInfoService
    { 
        private readonly IContactInfoService _contactInfoService;
        private readonly IUserInfoCreator _cretor;
        private readonly IPortalUnitOfWorkProvider _unitOfWorks;
        private readonly IUserService _userService;
 
    public UserInfoService(IPortalUnitOfWorkProvider unitOfWorks, 
    IUserService userService, IContactInfoService contactInfoService, IUserInfoCreator cretor)
        {
            _unitOfWorks = unitOfWorks;
            _userService = userService;
            _cretor = cretor;
            _contactInfoService = contactInfoService;
        }
        public IUserInfo GetBy(IUser user)
        {
            if (user == null)
            {
                return null;
            }
            var info = _cretor.From(user);
            if (info == null)
            {
                return null;
            }
 
            var infos = _contactInfoService.GetUserContactInfosBy(user.Login);
            if (infos != null)
            {
                info.PrimaryPhone = infos.Where(x => x.Type == ContactInfoType.Phone && x.IsDefault).FirstOrDefault();
                info.SecondaryPhone = infos.Where(x => x.Type == ContactInfoType.Phone && !x.IsDefault).FirstOrDefault();
                info.PrimaryEmail = infos.Where(x => x.Type == ContactInfoType.Email && x.IsDefault).FirstOrDefault();
                info.SecondaryEmail = infos.Where(x => x.Type == ContactInfoType.Email && !x.IsDefault).FirstOrDefault();
            }
            return info;
        }
 
                public bool IsValid(IUserInfo userInfo)
        {
            if (userInfo == null)
            {
                return false;
            }
 
            if (!_userService.IsValid(userInfo.User))
            {
                return false;
            }
 
            if (!_contactInfoService.IsValid(userInfo.PrimaryEmail))
            {
                return false;
            }
            if (!_contactInfoService.IsValid(userInfo.PrimaryPhone))
            {
                return false;
            }
 
            return true;
        }
 
        public void Save(IUserInfo userInfo)
        {
            if (userInfo == null)
            {
                return;
            }
 
            using (var unitOfWork = _unitOfWorks.GetTransactional())
            {
                var user = _userService.AddOrUpdate(unitOfWork.UserRepository, userInfo.User);
                _contactInfoService.DeleteAllBy(unitOfWork.ContactInfoRepository, user);
                _contactInfoService.AddOrUpdate(unitOfWork.ContactInfoRepository, user, userInfo.PrimaryPhone);
                _contactInfoService.AddOrUpdate(unitOfWork.ContactInfoRepository, user, userInfo.SecondaryPhone);
                _contactInfoService.AddOrUpdate(unitOfWork.ContactInfoRepository, user, userInfo.PrimaryEmail);
                _contactInfoService.AddOrUpdate(unitOfWork.ContactInfoRepository, user, userInfo.SecondaryEmail);
                unitOfWork.Save();
            }
        }
    }

If we need user information, the proper data are retrieved from repository and combined into domain accepted entity. We are using existing services to retrieve User and ContactInfo of user. Doing this though existing services, we are sure that no logic that is defined for each model is broken.

Similar to adding and updating. New transactional unit of work is created and corresponding repositories are passed to services. Same as above, we are sure that validation before processing is consistent.

Entity Framework

This section provides guidelines on how to add EntityFramework repository to make it as a working solution.

Unit of work should provide Context and allow to save changes that has been made on current context. Our Portal unit of work should also provide ContactInfoRepository as well as UserRepository.

Image 10

First, we define general UnitOfWork that will allow to process any EF DbContext.

C#
public abstract class UnitOfWork<T> : IUnitOfWork, IDisposable where T : DbContext
{
    private readonly T _context;
    private bool _disposed = false;
    private DbContextTransaction _transaction = null;

    protected T Context
    {
        get { return _context; }
    }

    public UnitOfWork(T entities)
    {
        _context = entities;
        _transaction = _context.Database.BeginTransaction();
    }
    
    public void Save()
    {
        try
        {
            _context.SaveChanges();
            _transaction.Commit();
        }
        catch
        {
            _transaction.Rollback();
            throw;
        }
        finally
        {
            _transaction = _context.Database.BeginTransaction();
        }
     }
     
    protected virtual void Dispose(bool disposing)
    {
        if (!this._disposed)
        {
            if (disposing)
            {
                _transaction.Dispose();
                _context.Dispose();
            }
        }
        this._disposed = true;
    }
    
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

Implementation of PortalUnitOfWorkProvider just overrides GetNew() method that will return PortalUnitOfWork.

C#
public class PortalUnitOfWorkProvider : UnitOfWorkProvider<IPortalUnitOfWork>, IPortalUnitOfWorkProvider
    {
        protected override IPortalUnitOfWork GetNew()
        {
            return new PortalUnitOfWork();
        }
    }

UnitOfWork implements IUnitOfWork as well as IPortalUnitOfWork. Generic unit of work is as below:

C#
public abstract class UnitOfWork<T> : IUnitOfWork, IDisposable where T : DbContext
{
    private readonly T _context;
    private bool _disposed = false;
    private DbContextTransaction _transaction = null;

    protected T Context
    {
        get { return _context; }
    }

    public UnitOfWork(T entities)
    {
        _context = entities;
        _transaction = _context.Database.BeginTransaction();
    }

    public void Save()
    {
        try
        {
            _context.SaveChanges();
            _transaction.Commit();
        }
        catch
        {
            _transaction.Rollback();
            throw;
        }
        finally
        {
            _transaction = _context.Database.BeginTransaction();
        }
    }
 
    protected virtual void Dispose(bool disposing)
    {
        if (!this._disposed)
        {
            if (disposing)
            {
                _transaction.Dispose();
                _context.Dispose();
            }
        }
        this._disposed = true;
    }
 
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

And its Portal version:

C#
public class PortalUnitOfWork : UnitOfWork<PortalEntities>, IPortalUnitOfWork
{
    private IContactInfoRepository _contactInfoRepository;
    private IUserRepository _userRepository;
    
    public IContactInfoRepository ContactInfoRepository
    {
        get
        {
            if (_contactInfoRepository == null)
            {
                _contactInfoRepository = new ContactInfoRepository(Context);
            }
            return _contactInfoRepository;
        }
    }
    
    public IUserRepository UserRepository
    {
        get
        {
            if (_userRepository == null)
            {
                _userRepository = new UserRepository(Context);
            }
            return _userRepository;
        }
    }
    
    public PortalUnitOfWork()
        : base(new PortalEntities())
    {
    }
}

To show complete solution below are Portal repositories implementations.

It must be remembered not to allow repository for _context.SaveChanges(). This will brake unit of work that is over repository. SaveChanges are called in service after all operations in transaction are done.

C#
public class UserRepository : GenericRepository<PortalEntities, User>, IUserRepository
    {
        public UserRepository(PortalEntities entities)
            : base(entities)
        {
        }
 
        public IUser GetBy(string login)
        {
            if (string.IsNullOrWhiteSpace(login))
            {
                return null;
            }
 
            var user = Get(u => u.Login.ToLower() == login.ToLower()).FirstOrDefault();
            return user;
        }
 
        public IUser AddOrUpdate(IUser user)
        {
 
            if (user == null)
            {
                return null;
            }
 
            var current = Get(u => u.Login == user.Login).FirstOrDefault();
            if (current != null)
            {
                user.MapUpdate<IUser, User>(current);
                Update(current);
                return current;
            }
            else
            {
                current = new User();
                user.MapUpdate<IUser, User>(current);
                base.Insert(current);
            }
            return current;
        } 
    }

ADO.NET

This section provides guidelines on how to add ADO.NET repository to make it as working solution.

Unit of work should provide Connection and Transaction property and allow to save changes that have been made on current transaction. Our Portal unit of work should also provide ContactInfoRepository as well as UserRepository.

Image 11

Generic UnitOfWork class is shown below:

C#
public abstract class UnitOfWork : IUnitOfWork, IDisposable
    {
        private readonly SqlConnection _connection;
        private SqlTransaction _transaction;
        private bool _disposed = false;
 
        public SqlConnection Connection
        {
            get { return _connection; }
        }
 
        public SqlTransaction Transaction
        {
            get { return _transaction; }
        }
 
        public UnitOfWork(string connectionString)
        {
            _connection = new SqlConnection(connectionString);
            _connection.Open();
            _transaction = _connection.BeginTransaction();
        }

        public void Save()
        {
            try
            {
                _transaction.Commit();
                _transaction = _connection.BeginTransaction();
            }
            catch
            {
                _transaction.Rollback();
                throw;
            }
            finally
            {
                _transaction.Dispose();
                _transaction = _connection.BeginTransaction();
            }
        }
 
        protected virtual void Dispose(bool disposing)
        {
            if (!this._disposed)
            {
                if (disposing)
                {
                    _transaction.Dispose();
                    _connection.Dispose();
                }
            }
            this._disposed = true;
        }
 
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

Implementation of PortalUnitOfWorkProvider just overrides GetNew() method that will return PortalUnitOfWork. PortalUnitOfWorkProvider accepts connection string as an input parameter.

C#
public class PortalUnitOfWorkProvider : UnitOfWorkProvider<IPortalUnitOfWork>, IPortalUnitOfWorkProvider
    {
        private readonly string _connectionString;
        
        public PortalUnitOfWorkProvider(string connectionString)
        {
            _connectionString = connectionString;
        }

        protected override IPortalUnitOfWork GetNew()
        {
            return new PortalUnitOfWork(_connectionString);
        } 
    }

UnitOfWork implements IUnitOfWork as well as IPortalUnitOfWork. Generic unit of work is as below:

C#
public abstract class UnitOfWork : IUnitOfWork, IDisposable
    {
        private readonly SqlConnection _connection;
        private SqlTransaction _transaction;
        private bool _disposed = false;
 
        public SqlConnection Connection
        {
            get { return _connection; }
        }
 
        public SqlTransaction Transaction
        {
            get { return _transaction; }
        }
 
        public UnitOfWork(string connectionString)
        {
            _connection = new SqlConnection(connectionString);
            _connection.Open();
            _transaction = _connection.BeginTransaction();
        }
 
        public void Save()
        {
            try
            {
                _transaction.Commit();
                _transaction = _connection.BeginTransaction();
            }
            catch
            {
                _transaction.Rollback();
                throw;
            }
            finally
            {
                _transaction.Dispose();
                _transaction = _connection.BeginTransaction();
            }
        }
 
        protected virtual void Dispose(bool disposing)
        {
            if (!this._disposed)
            {
                if (disposing)
                {
                    _transaction.Dispose();
                    _connection.Dispose();
                }
            }
            this._disposed = true;
        }
 
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

And its Portal version:

C#
public class PortalUnitOfWork : UnitOfWork, IPortalUnitOfWork
    {
        private IContactInfoRepository _contactInfoRepository;
        private IUserRepository _userRepository;
 
        public IContactInfoRepository ContactInfoRepository
        {
            get
            {
                if (_contactInfoRepository == null)
                {
                    _contactInfoRepository = new ContactInfoRepository(Connection);
                }
                return _contactInfoRepository;
            }
        }
 
        public IUserRepository UserRepository
        {
            get
            {
                if (_userRepository == null)
                {
                    _userRepository = new UserRepository(this);
                }
                return _userRepository;
            }
        }
 
        public PortalUnitOfWork(string connectionString)
            : base(connectionString)
        {
        }
    }

To show complete solution below are Portal repositories implementations.

C#
public class UserRepository : GenericRepository, IUserRepository
{
    private PortalUnitOfWork _unitOfWork;
    
    public UserRepository(PortalUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    
    public IUser GetBy(string login)
    {
        if (string.IsNullOrWhiteSpace(login))
        {
            return null;
        }
        
        using (var command = new SqlCommand())
        {
            command.Connection = _unitOfWork.Connection;
            command.Transaction = _unitOfWork.Transaction;
            command.CommandType = CommandType.Text;
            command.CommandText = "SELECT id, login, firstname, lastname FROM [dbo].[User] WHERE login = @login ";
            
            command.Parameters.Add("@login", SqlDbType.VarChar);
            command.Parameters["@login"].Value = login;
            
            var table = ExecuteToDataTable(command, "User");
            var entity = GetEntity(table);
            return entity;
        }
    }
    
    public IUser AddOrUpdate(IUser user)
    {
        if (user == null)
        {
            return null;
        }
        
        using (var command = new SqlCommand())
        {
            command.Connection = _unitOfWork.Connection;
            command.Transaction = _unitOfWork.Transaction;
            command.CommandType = CommandType.Text;
            command.CommandText = "MERGE [dbo].[User] AS ex USING _
            (SELECT @id As ID, @login as Login, @firstname as firstname, _
            @lastname as lastname) AS u ON ex.login = u.login WHEN MATCHED _
            THEN UPDATE SET ex.firstname = @firstname, ex.lastname =@lastname _
            WHEN NOT MATCHED THEN INSERT(login,firstname, lastname) _
            VALUES(@login, @firstname, @lastname); SELECT id, login, _
            firstname, lastname FROM [dbo].[User] WHERE login = @login ";
            
            command.Parameters.Add("@id", SqlDbType.Int);
            command.Parameters["@id"].Value = user.Id;
            
            command.Parameters.Add("@login", SqlDbType.VarChar);
            command.Parameters["@login"].Value = user.Login;
            
            command.Parameters.Add("@firstname", SqlDbType.VarChar);
            command.Parameters["@firstname"].Value = user.FirstName;
            
            command.Parameters.Add("@lastname", SqlDbType.VarChar);
            command.Parameters["@lastname"].Value = user.LastName;
            
            var table = ExecuteToDataTable(command, "User");
            var entity = GetEntity(table);
            return entity;
        }
    }
    
    public IUser GetEntity(DataTable dataTable)
    {
        var entities = GetEntities(dataTable);
        if (entities != null)
        {
            return entities.FirstOrDefault();
        }
        return null;
    }
    
    public IEnumerable<IUser> GetEntities(DataTable dataTable)
    {
        if (dataTable == null || dataTable.Rows.Count == 0)
        {
            return null;
        }
        var entities = Mapper.DynamicMap<IDataReader, List<User>>(dataTable.CreateDataReader());
        return entities;
    } 
}  

Tools

In the next sections, you can find implementation for EF and ADO.NET repository. To accomplish this, I have to introduce few tools that I am using to help myself.

AutoMapper

If in code you find something with mapping, you can be 100% sure that there is AutoMapper in use. You can find it here. “AutoMapper is a simple little library built to solve a deceptively complex problem - getting rid of code that mapped one object to another.” – and I can fully agree with it.

FluentFalidation

To have one common way to validate domain object, I have started using FluentValidator. You can find it on CodePlex here. How to use it? As you have noticed, all domain entities are defined by interfaces. So our User validator looks like this:

C#
public class UserValidator : BaseValidator<IUser>, IUserValidator
{    
    public UserValidator()
    {
        RuleFor(x => x.Login)
            .NotEmpty()
            .Length(0, 50);
        RuleFor(x => x.FirstName)
            .Length(0, 50);
        RuleFor(x => x.LastName)
            .Length(0, 50);
    }
}

It is integrated with ASP.NET MVC and Web API 2. Cool :)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
Poland Poland
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Question_transaction = _connection.BeginTransaction(); again ? Pin
fengzijun16-Mar-16 21:47
fengzijun16-Mar-16 21:47 
AnswerRe: _transaction = _connection.BeginTransaction(); again ? Pin
Leszek Koc31-May-16 21:56
Leszek Koc31-May-16 21:56 
QuestionExcellent Pin
bartss24-Apr-15 2:51
bartss24-Apr-15 2:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.