c# - Patrón de diseño de repositorio con Dapper
.net design-patterns (2)
Esta es quizás más una pregunta para la revisión del código en lugar del desbordamiento de la pila.
Estoy usando Dapper para un MicroORM para recuperar y guardar datos en SQL Server 2014. Tengo clases DTO en un proyecto DTO que representan los datos recuperados de la base de datos o guardados en la base de datos.
Estoy usando el patrón de repositorio, así que en mi capa de servicio si se requiere un repositorio, estoy usando el constructor DI para inyectar esa dependencia y luego llamar al método en el repositorio para hacer el trabajo.
digamos que tengo 2 servicios llamados CustomerService y CarService.
Luego tengo 2 repositorios, un CustomerRepository y un CarRepository.
Tengo una interfaz que define todos los métodos en cada Repositorio y luego las implementaciones concretas.
A continuación se muestra un método de ejemplo (llamando a un proceso almacenado para hacer el INSERT DB (tenga en cuenta que la variable de cadena real para el proceso almacenado se define como una cadena privada en la parte superior de la clase):
public void SaveCustomer(CustomerDTO custDTO)
{
using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDB"].ConnectionString))
{
db.Execute(saveCustSp, custDTO, commandType: CommandType.StoredProcedure);
}
}
Todo esto funciona bien, pero me encuentro repitiendo el bloque de uso en cada método en cada repositorio. Tengo dos preguntas reales que se detallan a continuación.
¿Hay un mejor enfoque que podría estar usando, tal vez de alguna manera, usando una clase BaseRepository de la que hereda cualquier otro Repositorio y la Base implementaría la instanciación de la conexión DB?
¿Seguiría funcionando bien para múltiples usuarios concurrentes en el sistema?
****ACTUALIZAR****
Basado en la respuesta de Silas, he creado lo siguiente
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);
}
}
}
Sin embargo, en mis repositorios, tengo otros métodos como los siguientes:
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;
}
}
y
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);
}
}
¿Cuál es la forma correcta de agregarlos a mi repositorio Base usando el Tipo T genérico para que pueda devolver cualquier tipo de DTO o cualquier tipo nativo C #
Claro, una función para crear y eliminar su conexión funcionará muy bien.
protected void Execute(Action<IDbConnection> query)
{
using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDB"].ConnectionString))
{
query.Invoke(db);
}
}
Y su sitio de llamadas simplificado:
public void SaveCustomer(CustomerDTO custDTO)
{
Execute(db => db.Execute(saveCustSp, custDTO, CommandType.StoredProcedure));
}
Con valores de retorno:
public T Get<T>(Func<IDbConnection, T> query)
{
using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDB"].ConnectionString))
{
return query.Invoke(db);
}
}
En su sitio de llamadas, simplemente escriba la lógica que desea utilizar.
public IEnumerable<EmployeeDTO> GetEmployeeDetails(int employeeId)
{
return Get<IEnumerable<EmployeeDTO>(db =>
db.Query<EmployeeDTO>(anotherSp, new { EmployeeID = employeeId }, CommandType.StoredProcedure));
}
Esto no es directamente relevante para su pregunta. Pero le sugiero que considere usar DapperExtensions.
Inicialmente, implementé el patrón de repositorio usando Dapper. El inconveniente es que tengo que escribir consultas por todas partes; Fue muy fibroso. Debido a las consultas codificadas, era casi imposible escribir un repositorio genérico.
Recientemente, actualicé mi código para usar DapperExtensions. Esto soluciona muchos problemas.
El siguiente es el repositorio genérico:
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.
}
Como puede ver en el código anterior, la mayoría de los métodos son simplemente envoltorios sobre la clase
DapperExtensionsProxy
subyacente.
DapperExtensionsProxy
internamente también administra UnitOfWork, que puede ver a continuación.
Estas dos clases se pueden combinar sin ningún problema.
Personalmente prefiero mantenerlos separados.
También puede observar que se implementan métodos adicionales como
DeleteById
,
DeleteById
y
DeleteAll
no forman parte de
DapperExtensionsProxy
.
El método
poco.SetDbId
se define en cada clase POCO para establecer su propiedad Identificador.
En mi caso, los identificadores de POCO pueden tener diferentes tipos de datos y nombres.
Lo siguiente es
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;
}
}
El siguiente es el
BasePoco
utilizado anteriormente:
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();
}
}
Esto también usa UnitOfWork, que se explica here .