Repository Design Pattern with Dapper

2019-01-04 02:34发布

This is maybe more a question for code review rather than stack overflow.

I am using Dapper for a MicroORM to retrieve and Save Data to SQL Server 2014. I have got DTO classes in a DTO Proj that represent the Data retrieved from the DB or saved to the DB.

I am using the Repository Pattern so at my Service layer if a repository is required I am using constructor DI to inject that dependency and then call the method on the Repository to do the work.

so let say I have 2 services called CustomerService and CarService.

I then have 2 Repositories a CustomerRepository and a CarRepository.

I have an interface which defines all the methods in each Repository and then the concrete implementations.

An example method is shown below (calling a Stored Proc to do the DB INSERT (note the actual string variable for the stored proc is defined as a private string at the top of the class):

    public void SaveCustomer(CustomerDTO custDTO)
    {
        using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDB"].ConnectionString))
        {
            db.Execute(saveCustSp, custDTO, commandType: CommandType.StoredProcedure);
        }
    }

This all works fine but I am finding myself repeating the using block in every method in every repository. I have two real questions outlined below.

Is there a better approach which I could be using perhaps somehow using a BaseRepository class which every other Repository inherits from and the Base would implement the instantiation of the DB connection?

Would that still work ok for multiple concurrent Users on the system?

****UPDATE****

Based on Silas answer I have created the following

public interface IBaseRepository
{
    void Execute(Action<IDbConnection> query);
}

public class BaseRepository: IBaseRepository
{
        public void Execute(Action<IDbConnection> query)
        {
            using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDB"].ConnectionString))
            {
                query.Invoke(db);
            }
        }
}

However, in my repositories, I have other methods such as the below:

    public bool IsOnlyCarInStock(int carId, int year)
    {
        using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDB"].ConnectionString))
        {
            var car = db.ExecuteScalar<int>(anotherStoredSp, new { CarID = carId, Year = year },
                                commandType: CommandType.StoredProcedure);

            return car > 0 ? true : false;
        }
    }

and

    public IEnumerable<EmployeeDTO> GetEmployeeDetails(int employeeId)
    {
        using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDB"].ConnectionString))
        {
            return db.Query<EmployeeDTO>(anotherSp, new { EmployeeID = employeeId },
                                commandType: CommandType.StoredProcedure);
        }
    }

What is the correct way to add these to my Base repository using Generic Type T so I could return any type of DTO or any C# Native type

2条回答
Root(大扎)
2楼-- · 2019-01-04 02:45

This is not directly relevant to your question. But I suggest you consider using DapperExtensions.

Initially, I did implemented Repository pattern using Dapper. The drawback was that, I have to write queries all over; it was very stringy. Due to hard coded queries it was near to impossible to write generic repository.

Recently, I upgraded my code to use DapperExtensions. This fixes lot many issues.

Following is the generic repository:

public abstract class BaseRepository<T> where T : BasePoco
{
    internal BaseRepository(IUnitOfWork unitOfWork)
    {
        dapperExtensionsProxy = new DapperExtensionsProxy(unitOfWork);
    }

    DapperExtensionsProxy dapperExtensionsProxy = null;

    protected bool Exists()
    {
        return (GetCount() == 0) ? false : true;
    }

    protected int GetCount()
    {
        var result = dapperExtensionsProxy.Count<T>(null);
        return result;
    }

    protected T GetById(Guid id)
    {
        var result = dapperExtensionsProxy.Get<T>(id);
        return result;
    }
    protected T GetById(string id)
    {
        var result = dapperExtensionsProxy.Get<T>(id);
        return result;
    }

    protected List<T> GetList()
    {
        var result = dapperExtensionsProxy.GetList<T>(null);
        return result.ToList();
    }

    protected void Insert(T poco)
    {
        var result = dapperExtensionsProxy.Insert(poco);
    }

    protected void Update(T poco)
    {
        var result = dapperExtensionsProxy.Update(poco);
    }

    protected void Delete(T poco)
    {
        var result = dapperExtensionsProxy.Delete(poco);
    }

    protected void DeleteById(Guid id)
    {
        T poco = (T)Activator.CreateInstance(typeof(T));
        poco.SetDbId(id);
        var result = dapperExtensionsProxy.Delete(poco);
    }
    protected void DeleteById(string id)
    {
        T poco = (T)Activator.CreateInstance(typeof(T));
        poco.SetDbId(id);
        var result = dapperExtensionsProxy.Delete(poco);
    }

    protected void DeleteAll()
    {
        var predicateGroup = new PredicateGroup { Operator = GroupOperator.And, Predicates = new List<IPredicate>() };
        var result = dapperExtensionsProxy.Delete<T>(predicateGroup);//Send empty predicateGroup to delete all records.
    }

As you can see in above code, most of the methods are just wrapper over underlying DapperExtensionsProxy class. DapperExtensionsProxy internally also manages UnitOfWork which you can see below. These two classes can be combined without any issue. I personally prefer to keep them separate.

You can also notice that additional methods Exists, DeleteById, and DeleteAll are implemented those are not part of DapperExtensionsProxy.

Method poco.SetDbId is defined in each POCO class to set its Identifier property. In my case, identifiers of POCOs may have different datatypes and names.

Following is DapperExtensionsProxy:

internal sealed class DapperExtensionsProxy
{
    internal DapperExtensionsProxy(IUnitOfWork unitOfWork)
    {
        this.unitOfWork = unitOfWork;
    }

    IUnitOfWork unitOfWork = null;

    internal int Count<T>(object predicate) where T : BasePoco
    {
        var result = unitOfWork.Connection.Count<T>(predicate, unitOfWork.Transaction);
        return result;
    }

    internal T Get<T>(object id) where T : BasePoco
    {
        var result = unitOfWork.Connection.Get<T>(id, unitOfWork.Transaction);
        return result;
    }

    internal IEnumerable<T> GetList<T>(object predicate, IList<ISort> sort = null, bool buffered = false) where T : BasePoco
    {
        var result = unitOfWork.Connection.GetList<T>(predicate, sort, unitOfWork.Transaction, null, buffered);
        return result;
    }

    internal IEnumerable<T> GetPage<T>(object predicate, int page, int resultsPerPage, IList<ISort> sort = null, bool buffered = false) where T : BasePoco
    {
        var result = unitOfWork.Connection.GetPage<T>(predicate, sort, page, resultsPerPage, unitOfWork.Transaction, null, buffered);
        return result;
    }

    internal dynamic Insert<T>(T poco) where T : BasePoco
    {
        var result = unitOfWork.Connection.Insert<T>(poco, unitOfWork.Transaction);
        return result;
    }

    internal void Insert<T>(IEnumerable<T> listPoco) where T : BasePoco
    {
        unitOfWork.Connection.Insert<T>(listPoco, unitOfWork.Transaction);
    }

    internal bool Update<T>(T poco) where T : BasePoco
    {
        var result = unitOfWork.Connection.Update<T>(poco, unitOfWork.Transaction);
        return result;
    }

    internal bool Delete<T>(T poco) where T : BasePoco
    {
        var result = unitOfWork.Connection.Delete<T>(poco, unitOfWork.Transaction);
        return result;
    }

    internal bool Delete<T>(object predicate) where T : BasePoco
    {
        var result = unitOfWork.Connection.Delete<T>(predicate, unitOfWork.Transaction);
        return result;
    }
}

Following is the BasePoco used above:

public abstract class BasePoco
{
    Guid pocoId = Guid.NewGuid();

    public Guid PocoId { get { return pocoId; } }

    public virtual void SetDbId(object id)
    {//Each POCO should override this method for specific implementation.
        throw new NotImplementedException("This method is not implemented by Poco.");
    }

    public override string ToString()
    {
        return PocoId + Environment.NewLine + base.ToString();
    }
}

This also uses UnitOfWork which is explained here.

查看更多
Summer. ? 凉城
3楼-- · 2019-01-04 03:06

Sure, a function to create and dispose your Connection will work great.

protected void Execute(Action<IDbConnection> query)
{
    using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDB"].ConnectionString))
    {
        query.Invoke(db);
    }
}

And your simplified call site:

public void SaveCustomer(CustomerDTO custDTO)
{
    Execute(db => db.Execute(saveCustSp, custDTO, CommandType.StoredProcedure));
}

With Return Values:

public T Get<T>(Func<IDbConnection, T> query)
{
    using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDB"].ConnectionString))
    {
        return query.Invoke(db); 
    }
}

In your call site, just write the logic you wish to use.

public IEnumerable<EmployeeDTO> GetEmployeeDetails(int employeeId)
{
    return Get<IEnumerable<EmployeeDTO>(db => 
        db.Query<EmployeeDTO>(anotherSp, new { EmployeeID = employeeId }, CommandType.StoredProcedure));
}
查看更多
登录 后发表回答