.net - unit - Patrón de repositorio y mapeo entre modelos de dominio y Entity Framework
repository pattern c# mvc (6)
Mis repositorios tratan y proporcionan persistencia para un modelo de dominio enriquecido. No quiero exponer la anémica entidad de datos de Entity Framework a mis capas de negocios, así que necesito alguna forma de mapeo entre ellas.
Si usa Entity Framework, puede mapear el propio Rich Domain Model.
He respondido la pregunta similar "Consejos sobre el mapeo de entidades para objetos de dominio" recientemente.
He estado usando NHibernate y sé que en Entity Framework también puedes especificar reglas de mapeo desde las tablas de DB hasta tus objetos POCO. Es un esfuerzo adicional desarrollar otra capa de abstracción sobre las entidades de Entity Framework. Deje que el ORM sea responsable de todas las mappings , el seguimiento del estado, la unidad de trabajo y la implementación del mapa de identidad , etc. Los ORM modernos saben cómo manejar todos estos problemas.
AutoMapper podría usarse para la situación opuesta (asignación a entidades de datos) pero no al crear modelos de dominio.
Tienes toda la razón.
Automapper es útil cuando una entidad se puede mapear en otra sin dependencias adicionales (por ejemplo, repositorios, servicios, ...).
... ¿dónde definiría el mapeo entre un
Department
y unDepartmentDataEntity
?
Lo pondría en DepartmentRepository
y agregaría el método IList<Department> FindByCompany(int companyId)
para recuperar los departamentos de la compañía.
Podría proporcionar más métodos de mapeo en el
CompanyRepository
, pero esto pronto se volverá complicado. Pronto habría métodos de mapeo duplicados en todo el sistema.¿Cuál es un mejor enfoque para el problema anterior?
Si es necesario obtener una lista de Department
para otra entidad, se debe agregar un nuevo método a DepartmentRepository
y simplemente usarlo donde se necesite.
Mis repositorios tratan y proporcionan persistencia para un modelo de dominio enriquecido. No quiero exponer la anémica entidad de datos de Entity Framework a mis capas de negocios, así que necesito alguna forma de mapeo entre ellas.
En la mayoría de los casos, construir una instancia de modelo de dominio a partir de una entidad de datos requiere el uso de constructores y métodos parametrizados (ya que es rico). No es tan simple como una coincidencia de propiedad / campo. AutoMapper podría usarse para la situación opuesta (asignación a entidades de datos) pero no al crear modelos de dominio.
Debajo está el núcleo de mi patrón de repositorio.
La clase EntityFrameworkRepository
funciona con dos tipos genéricos:
-
TDomainModel
: el modelo de dominio enriquecido -
TEntityModel
: Entidad de datos de Entity Framework
Se definen dos métodos abstractos:
-
ToDataEntity(TDomainModel)
: para convertir a entidades de datos (para los métodosAdd()
yUpdate()
) -
ToDomainModel(TEntityModel)
: para construir modelos de dominio (para el métodoFind()
).
Las implementaciones concretas de estos métodos definirían el mapeo requerido para el repositorio en cuestión.
public interface IRepository<T> where T : DomainModel
{
T Find(int id);
void Add(T item);
void Update(T item);
}
public abstract class EntityFrameworkRepository<TDomainModel, TEntityModel> : IRepository<TDomainModel>
where TDomainModel : DomainModel
where TEntityModel : EntityModel
{
public EntityFrameworkRepository(IUnitOfWork unitOfWork)
{
// ...
}
public virtual TDomainModel Find(int id)
{
var entity = context.Set<TEntityModel>().Find(id);
return ToDomainModel(entity);
}
public virtual void Add(TDomainModel item)
{
context.Set<TEntityModel>().Add(ToDataEntity(item));
}
public virtual void Update(TDomainModel item)
{
var entity = ToDataEntity(item);
DbEntityEntry dbEntityEntry = context.Entry<TEntityModel>(entity);
if (dbEntityEntry.State == EntityState.Detached)
{
context.Set<TEntityModel>().Attach(entity);
dbEntityEntry.State = EntityState.Modified;
}
}
protected abstract TEntityModel ToDataEntity(TDomainModel domainModel);
protected abstract TDomainModel ToDomainModel(TEntityModel dataEntity);
}
Aquí hay un ejemplo básico de una implementación de repositorio:
public interface ICompanyRepository : IRepository<Company>
{
// Any specific methods could be included here
}
public class CompanyRepository : EntityFrameworkRepository<Company, CompanyTableEntity>, ICompanyRepository
{
protected CompanyTableEntity ToDataEntity(Company domainModel)
{
return new CompanyTable()
{
Name = domainModel.Name,
City = domainModel.City
IsActive = domainModel.IsActive
};
}
protected Company ToDomainModel(CompanyTableEntity dataEntity)
{
return new Company(dataEntity.Name, dataEntity.IsActive)
{
City = dataEntity.City
}
}
}
Problema:
Una Company
puede estar compuesta de muchos Departments
. Si quiero cargarlos ansiosamente desde el CompanyRepository
Company
cuando obtengo una Company
, ¿dónde definiría la asignación entre un Department
y un DepartmentDataEntity
?
Podría proporcionar más métodos de mapeo en el CompanyRepository
, pero esto pronto se volverá complicado. Pronto habría métodos de mapeo duplicados en todo el sistema.
¿Cuál es un mejor enfoque para el problema anterior?
Al igual que las publicaciones anteriores han dicho. Probablemente sea mejor esperar hasta que los repositorios realicen el mapeo real ... PERO me gusta trabajar con auto mapeador. Proporciona una forma muy fácil de asignar objetos a otros objetos. Para cierta separación de preocupaciones, también puede definir las asignaciones en un proyecto separado. Estas asignaciones también son genéricas / basadas en tipo:
- Especifique el tipo base en la asignación y defina cómo llena el tipo de destino
- Donde necesite el mapeo, simplemente llame a Mapper.Map (baseTypeObject, DestinationTypeObject)
- Automapper debería manejar el resto
Esto podría hacer el truco, si entendí tu pregunta correctamente.
Con el marco de la entidad, en todas las capas en mi humilde opinión, generalmente es una mala idea convertir modelos de entidades a otras formas de modelos (modelos de dominio, objetos de valor, modelos de vista, etc.) y viceversa, excepto solo en la capa de aplicación porque lo hará pierda muchas capacidades EF que solo puede lograr a través de los objetos de la entidad, como la pérdida de seguimiento de cambios y la pérdida de consultas de LINQ.
Es mejor hacer la asignación entre la capa de repositorio y la capa de aplicación. Mantenga los modelos de entidad en la capa de repositorio.
Me gusta usar métodos de extensión personalizados para hacer la asignación entre los objetos de Entidad y Dominio.
- Puede llamar fácilmente a los métodos de extensión para otras entidades cuando están dentro de una entidad que lo contiene.
- Puede tratar fácilmente las colecciones creando extensiones IEnumerable <>.
Un simple ejemplo:
public static class LevelTypeItemMapping
{
public static LevelTypeModel ToModel(this LevelTypeItem entity)
{
if (entity == null) return new LevelTypeModel();
return new LevelTypeModel
{
Id = entity.Id;
IsDataCaptureRequired = entity.IsDataCaptureRequired;
IsSetupRequired = entity.IsSetupRequired;
IsApprover = entity.IsApprover;
Name = entity.Name;
}
}
public static IEnumerable<LevelTypeModel> ToModel(this IEnumerable<LevelTypeItem> entities)
{
if (entities== null) return new List<LevelTypeModel>();
return (from e in entities
select e.ToModel());
}
}
... y los usa así .....
using (IUnitOfWork uow = new NHUnitOfWork())
{
IReadOnlyRepository<LevelTypeItem> repository = new NHRepository<LevelTypeItem>(uow);
record = repository.Get(id);
return record.ToModel();
records = repository.GetAll(); // Return a collection from the DB
return records.ToModel(); // Convert a collection of entities to a collection of models
}
No es perfecto, pero es muy fácil de seguir y muy fácil de reutilizar.
No me gusta mapear manualmente, así que para mapear estoy usando http://valueinjecter.codeplex.com/
Supongamos que tiene el siguiente objeto de acceso a datos ...
public class AssetDA
{
public HistoryLogEntry GetHistoryRecord(int id)
{
HistoryLogEntry record = new HistoryLogEntry();
using (IUnitOfWork uow = new NHUnitOfWork())
{
IReadOnlyRepository<HistoryLogEntry> repository = new NHRepository<HistoryLogEntry>(uow);
record = repository.Get(id);
}
return record;
}
}
que devuelve una entidad de datos de entrada de registro de historial. Esta entidad de datos se define de la siguiente manera ...
public class HistoryLogEntry : IEntity
{
public virtual int Id
{ get; set; }
public virtual int AssetID
{ get; set; }
public virtual DateTime Date
{ get; set; }
public virtual string Text
{ get; set; }
public virtual Guid UserID
{ get; set; }
public virtual IList<AssetHistoryDetail> Details { get; set; }
}
Puede ver que la propiedad Details
referencia a otra entidad de datos AssetHistoryDetail
. Ahora, en mi proyecto, necesito asignar estas entidades de datos a los objetos del modelo de Dominio que se usan en mi lógica comercial. Para hacer el mapeo he definido métodos de extensión ... Sé que es un antipatrón porque es un lenguaje específico, pero lo bueno es que aísla y rompe las dependencias entre sí ... sí, esa es la belleza de eso. Entonces, el mapeador se define de la siguiente manera ...
internal static class AssetPOMapper
{
internal static HistoryEntryPO FromDataObject(this HistoryLogEntry t)
{
return t == null ? null :
new HistoryEntryPO()
{
Id = t.Id,
AssetID = t.AssetID,
Date = t.Date,
Text = t.Text,
UserID = t.UserID,
Details = t.Details.Select(x=>x.FromDataObject()).ToList()
};
}
internal static AssetHistoryDetailPO FromDataObject(this AssetHistoryDetail t)
{
return t == null ? null :
new AssetHistoryDetailPO()
{
Id = t.Id,
ChangedDetail = t.ChangedDetail,
OldValue = t.OldValue,
NewValue = t.NewValue
};
}
}
y eso es más o menos. Todas las dependencias están en un solo lugar. Entonces, cuando llame a un objeto de datos desde la capa de lógica de negocios, dejaría que LINQ
hiciera el resto ...
var da = new AssetDA();
var entry = da.GetHistoryRecord(1234);
var domainModelEntry = entry.FromDataObject();
Tenga en cuenta que puede definir lo mismo para asignar objetos del Modelo de dominio a las entidades de datos.