EntityFramework(存储库模式、数据验证、Dto)
EntityFramework(Repository Pattern, Data Validation, Dto's)
我花了一些时间来整理如何使用 EntityFramework 创建 Restful API。这个问题主要是因为这个 API 应该在很长一段时间内使用,我希望它是可维护和干净的,具有良好的性能。够了,让我们进入问题。
免责声明:
由于公司政策,这里不能 post 太多,但我会尽量以最好的方式解决问题。也只有代码片段,可能无效。我对 C# 也很陌生,作为一名 JuniorD,我以前从未接触过 API。请原谅我的英语,这是我的第二语言。
Every 模型派生自 BaseModel class
public class BaseModel
{
[Required]
public Guid CompanyId { get; set; }
public DateTime CreatedDateTime { get; set; }
[StringLength(100)]
public string CreatedBy { get; set; }
public DateTime ChangedDateTime { get; set; }
[StringLength(100)]
public string ChangedBy { get; set; }
public bool IsActive { get; set; } = true;
public bool IsDeleted { get; set; }
}
public class Carrier : BaseModel
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public Guid CarrierId { get; set; }
public int CarrierNumber { get; set; }
[StringLength(100)]
public string CarrierName { get; set; }
[StringLength(100)]
public string AddressLine { get; set; }
public Guid? PostOfficeId { get; set; }
public PostOffice PostOffice { get; set; }
public Guid? CountryId { get; set; }
public Country Country { get; set; }
public List<CustomerCarrierLink> CustomerCarrierLinks { get; set; }
}
每个存储库都派生自 Repository 并有自己的接口。
public class CarrierRepository : Repository<Carrier>, ICarrierRepository
{
public CarrierRepository(CompanyMasterDataContext context, UnitOfWork unitOfWork) : base(context, unitOfWork) { }
#region Helpers
public override ObjectRequestResult<Carrier> Validate(Carrier carrier, List<string> errorMessages)
{
var errorMessages = new List<string>();
if(carrier != null)
{
var carrierIdentifier = (carrier.CarrierName ?? carrier.CarrierNumber.ToString()) ?? carrier.CarrierGLN;
if (string.IsNullOrWhiteSpace(carrier.CarrierName))
{
errorMessages.Add($"Carrier({carrierIdentifier}): Carrier name is null/empty");
}
}
else
{
errorMessages.Add("Carrier: Cannot validate null value.");
}
return CreateObjectResultFromList(errorMessages, carrier); // nonsense
}
}
UnitOfWork 派生自 classUnitOfWorkDiscoverySet,此 class 使用反射初始化存储库属性,还包含一个方法 (OnBeforeChildEntityProcessed) 用于调用每个 OnBeforeChildEntityProcessed。
public class UnitOfWork : UnitOfWorkDiscoverySet
{
public UnitOfWork(CompanyMasterDataContext context)
: base(context){}
public CarrierRepository Carriers { get; internal set; }
public PostOfficeRepository PostOffices { get; internal set; }
public CustomerCarrierLinkRepository CustomerCarrierLinks { get; internal set; }
}
public IRepository<Entity> where Entity : BaseModel
{
ObjectRequestResult<Entity> Add(Entity entity);
ObjectRequestResult<Entity> Update(Entity entity);
ObjectRequestResult<Entity> Delete(Entity entity);
ObjectRequestResult<Entity> Validate(Entity entity);
Entity GetById(Guid id);
Guid GetEntityId(Entity entity);
}
public abstract class Repository<Entity> : IRepository<Entity> where Entity : BaseModel
{
protected CompanyMasterDataContext _context;
protected UnitOfWork _unitOfWork;
public Repository(CompanyMasterDataContext context, UnitOfWork unitOfWork)
{
_context = context;
_unitOfWork = unitOfWork;
}
public ObjectRequestResult<Entity> Add(Entity entity)
{
if (!EntityExist(GetEntityId(entity)))
{
try
{
var validationResult = Validate(entity);
if (validationResult.IsSucceeded)
{
_context.Add(entity);
_context.UpdateEntitiesByBaseModel(entity);
_context.SaveChanges();
return new ObjectRequestResult<Entity>()
{
ResultCode = ResultCode.Succceeded,
ResultObject = entity,
Message = OBJECT_ADDED
};
}
return validationResult;
}
catch (Exception exception)
{
return new ObjectRequestResult<Entity>()
{
ResultCode = ResultCode.Failed,
ResultObject = entity,
Message = OBJECT_NOT_ADDED,
ErrorMessages = new List<string>()
{
exception?.Message,
exception?.InnerException?.Message
}
};
}
}
return Update(entity);
}
public virtual ObjectRequestResult Validate(Entity entity)
{
if(entity != null)
{
if(!CompanyExist(entity.CompanyId))
{
return EntitySentNoCompanyIdNotValid(entity); // nonsense
}
}
return EntitySentWasNullBadValidation(entity); // nonsense
}
}
DbContext class:
public class CompanyMasterDataContext : DbContext {
public DbSet<PostOffice> PostOffices { get; set; }
public DbSet<Carrier> Carriers { get; set; }
public DbSet<Company> Companies { get; set; }
public DbSet<CustomerCarrierLink> CustomerCarrierLinks { get; set; }
public UnitOfWork Unit { get; internal set; }
public CompanyMasterDataContext(DbContextOptions<CompanyMasterDataContext> options)
: base(options)
{
Unit = new UnitOfWork(this);
}
public void UpdateEntitiesByBaseModel(BaseModel baseModel)
{
foreach (var entry in ChangeTracker.Entries())
{
switch (entry.State)
{
case EntityState.Added:
entry.CurrentValues["CompanyId"] = baseModel.CompanyId;
entry.CurrentValues["CreatedDateTime"] = DateTime.Now;
entry.CurrentValues["CreatedBy"] = baseModel.CreatedBy;
entry.CurrentValues["IsDeleted"] = false;
entry.CurrentValues["IsActive"] = true;
Unit.OnBeforeChildEntityProcessed(entry.Entity, enumEntityProcessState.Add);
break;
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.CurrentValues["ChangedDateTime"] = DateTime.Now;
entry.CurrentValues["ChangedBy"] = baseModel.ChangedBy;
entry.CurrentValues["IsDeleted"] = true;
Unit.OnBeforeChildEntityProcessed(entry.Entity, enumEntityProcessState.Delete);
break;
case EntityState.Modified:
if (entry.Entity != null && entry.Entity.GetType() != typeof(Company))
entry.CurrentValues["CompanyId"] = baseModel.CompanyId;
entry.CurrentValues["ChangedDateTime"] = DateTime.Now;
entry.CurrentValues["ChangedBy"] = baseModel.ChangedBy;
Unit.OnBeforeChildEntityProcessed(entry.Entity, enumEntityProcessState.Update);
break;
}
}
}
}
发现类:
public abstract class UnitOfWorkDiscoverySet
{
private Dictionary<Type, object> Repositories { get; set; }
private CompanyMasterDataContext _context;
public UnitOfWorkDiscoverySet(CompanyMasterDataContext context)
{
_context = context;
InitializeSets();
}
private void InitializeSets()
{
var discoverySetType = GetType();
var discoverySetProperties = discoverySetType.GetProperties();
Repositories = new Dictionary<Type, object>();
foreach (var child in discoverySetProperties)
{
var childType = child.PropertyType;
var repositoryType = childType.GetInterfaces()
.Where( i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IRepository<>))
.FirstOrDefault();
if (repositoryType != null)
{
var repositoryModel = repositoryType.GenericTypeArguments.FirstOrDefault();
if (repositoryModel != null)
{
if (repositoryModel.IsSubclassOf(typeof(BaseModel)))
{
var repository = InitializeProperty(child); //var repository = child.GetValue(this);
if (repository != null)
{
Repositories.Add(repositoryModel, repository);
}
}
}
}
}
}
private object InitializeProperty(PropertyInfo property)
{
if(property != null)
{
var instance = Activator.CreateInstance(property.PropertyType, new object[] {
_context, this
});
if(instance != null)
{
property.SetValue(this, instance);
return instance;
}
}
return null;
}
public void OnBeforeChildEntityProcessed(object childObject, enumEntityProcessState processState)
{
if(childObject != null)
{
var repository = GetRepositoryByObject(childObject);
var parameters = new object[] { childObject, processState };
InvokeRepositoryMethod(repository, "OnBeforeEntityProcessed", parameters);
}
}
public void ValidateChildren<Entity>(Entity entity, List<string> errorMessages) where Entity : BaseModel
{
var children = BaseModelUpdater.GetChildModels(entity);
if(children != null)
{
foreach(var child in children)
{
if(child != null)
{
if (child.GetType() == typeof(IEnumerable<>))
{
var list = (IEnumerable<object>) child;
if(list != null)
{
foreach (var childInList in list)
{
ValidateChild(childInList, errorMessages);
}
}
}
ValidateChild(child, errorMessages);
}
}
}
}
public void ValidateChild(object childObject, List<string> errorMessages)
{
if(childObject != null)
{
var repository = GetRepositoryByObject(childObject);
var parameters = new object[] { childObject, errorMessages };
InvokeRepositoryMethod(repository, "Validate", parameters);
}
}
public void InvokeRepositoryMethod(object repository, string methodName, object[] parameters)
{
if (repository != null)
{
var methodToInvoke = repository.GetType().GetMethod(methodName);
var methods = repository.GetType().GetMethods().Where(x => x.Name == methodName);
if (methodToInvoke != null)
{
methodToInvoke.Invoke(repository, parameters);
}
}
}
public object GetRepositoryByObject(object objectForRepository)
{
return Repositories?[objectForRepository.GetType()];
}
public object GetObject<Entity>(Type type, Entity entity) where Entity : BaseModel
{
var childObjects = BaseModelUpdater.GetChildModels(entity);
foreach (var childObject in childObjects)
{
if (childObject.GetType().FullName == type.FullName)
{
return childObject;
}
}
return null;
}
}
}
问题:
我想验证每个模型和子模型中的数据 properties/list,知道你可能会说这可以使用属性来完成,但验证可能相当复杂,我更喜欢将其分开 space。
我解决这个问题的方法是使用来自 UnitDiscoverySet class 的反射,在这里我可以找到我正在尝试处理的实体的每个子实体,并搜索包含 UnitOfWork 的适当存储库。这无论如何都有效,只需要更多的工作和清理,但出于某种原因,我觉得这是 cheating/wrong 解决问题的方法,而且我也没有遇到编译时错误 + 反射有代价。
我可以在实体存储库中验证实体的子实体,但我会到处重复自己,这个解决方案似乎也不正确。
我不希望这个解决方案过于依赖 entityframework,因为我们不会永远使用它。
此解决方案还严重依赖于 DbContext 中的 UpdateEntitiesByBaseModel 方法。所以它只更改应该更改的字段。
不确定我是否像我想的那样很好地解决了这个问题,但我感谢每一个能引导我走上正确道路的贡献。谢谢!
解决方案(编辑):
我最终只将导航属性用于 GET 操作,而将其排除在插入操作之外。让一切变得更加灵活和快速,这样我就不需要使用 EF Tracker,它使 5000 个实体的插入操作从 13 分钟的操作缩短到 14.3 秒。
最好在 CodeReview 中提出这个问题,而不是针对特定 code-related 问题的 SO。你可以问 10 个不同的开发者,得到 10 个不同的答案。 :)
反射肯定是有代价的,我也不是很喜欢用它。
I don't want this solution to depend to heavy on entityframework,
since it's not given that we will use this forever.
这是我在应用程序和框架中看到的一个相当常见的主题,与我合作的开发团队在使用 ORM 时试图应对这些应用程序和框架。对我来说,从解决方案中抽象出 EF 就像试图抽象出 .Net 的一部分。这实际上毫无意义,因为您失去了对 Entity Framework 提供的大部分灵活性和功能的访问权。它会导致更多、更复杂的代码来处理 EF 可以在本机执行的事情,从而在您 re-invent 控制方向盘时为错误留下空间,或者留下以后必须解决的漏洞。您要么信任它,要么不应该使用它。
I could validate children of the entity in the entity repository, but
then i would be repeating myself all over the place, and this solution
don't seem right either.
这实际上是我在项目中提倡的模式。许多人反对存储库模式,但它是一个很好的模式,可以作为用于测试目的的域边界。 (无需设置 in-memory 数据库或尝试模拟 DbContext/DbSets)但是,IMO 通用存储库模式是 anti-pattern。它将实体关注点彼此分开,但是在许多情况下,我们处理的是实体 "graphs" 而不是单个实体类型。我没有为每个实体定义存储库,而是选择了一些实际上是每个控制器的存储库的东西。 (使用真正常见实体的存储库,例如查找。)这背后有两个原因:
- 传递/模拟的依赖引用更少
- 更好地服务 SRP
- 避免 pigeon-holing 数据库操作
我对通用或 per-entity 存储库的最大问题是,虽然它们似乎符合 SRP(负责单个实体的操作),但我觉得它们违反了它,因为 SRP 是关于只有一个改变的理由。如果我有一个 Order 实体和一个 Order 存储库,我可能有应用程序的多个区域来加载订单并与订单交互。与 Order 实体交互的方法现在在几个不同的地方被调用,这构成了调整方法的许多潜在原因。您最终要么使用复杂的条件代码,要么使用几种非常相似的方法来服务于特定场景。 (用于列出订单的订单、客户订单、商店订单等)在验证实体时,这通常是在整个图形的上下文中完成的,因此将其集中在与图形相关的代码中而不是单独的代码中是有意义的实体。这适用于像 Add/Update/Delete 这样的通用基础操作。 80% 的时间这有效并且省力,但剩下的 20% 要么必须让 shoe-horned 进入模式,导致低效的 and/or error-prone 代码,要么 work-arounds . K.I.S.S。在软件设计方面,应该始终胜过 D.N.R.Y。整合到基础 classes 等是一种优化,当识别出 "identical" 功能时,应该随着代码的发展而完成。当 up-front 作为架构决策完成时,我认为这种过早的优化会在 "similar" 而不是 "identical" 行为组合在一起导致条件代码时为开发、性能问题和错误创造障碍为边缘情况爬行。
因此,如果我有类似 ManageOrderController 的东西,我将有一个 ManageOrderRepository 来提供订单,而不是 OrderRepository 来提供订单。
例如,我喜欢使用 DDD 风格的方法来管理我的存储库在构建中发挥作用的实体,因为它们对数据域是私有的,并且可以 validate/retrieve 相关实体。因此,典型的存储库将具有:
IQueryable<TEntity> GetTEntities()
IQueryable<TEntity> GetTEntityById(id)
IQueryable<TRelatedEntity> GetTRelatedEntities()
TEntity CreateTEntity({all required properties/references})
void DeleteTEntity(entity)
TChildEntity CreateTChildEntity(TEntity, {all required properties/references})
检索方法,包括 "By ID" 因为它是常见场景,return IQueryable 以便调用者可以控制数据的使用方式。这消除了尝试和抽象 EF 可以利用的 Linq 功能的需要,以便调用者可以应用过滤器、执行分页、排序,然后按他们需要的方式使用数据。 (Select
、Any
等)存储库执行核心规则,例如 IsActive 和 tenancy/authorization 检查。这作为测试的边界,因为模拟只需要 return List<TEntity>.AsQueryable()
或用 async-friendly 集合类型包装。 () 存储库还充当 go-to 位置,用于通过适用的任何标准检索任何相关实体。这可以被视为潜在的重复,但仅当应用程序的 controller/view/area 需要更改时才需要更改此存储库。诸如查找之类的常见内容将通过他们自己的存储库提取。这减少了对大量独立存储库依赖项的需求。每个区域都会自行处理,因此 changes/optimizations 这里不需要考虑或影响应用程序的其他区域。
"Create" 方法管理有关创建实体和将实体关联到 Context 的规则,以确保实体始终以最低限度的完整和有效状态创建。这就是验证发挥作用的地方。任何不是 null-able 的值都会被传入,连同 FK(键或引用)一起传递,以确保如果 SaveChanges()
是 Create 之后的下一个调用,实体将是有效的。
"Delete" 方法同样用于管理验证数据 state/authorization,并应用一致的行为。 (硬删除与软删除、审计等)
我不使用 "Update" 方法。更新由实体本身的 DDD 方法处理。控制器定义工作单元,使用存储库检索实体,调用实体方法,然后提交工作单元。验证可以在实体级别完成,也可以通过验证器 class.
无论如何,这只是您可能采用的 10 多种方法中的一种的总结,并希望强调您采用任何方法时需要考虑的一些事项。在使用 EF 时,我的重点是:
- 保持简单。 (K.I.S.S. > D.N.R.Y)
- 充分利用 EF 提供的功能,而不是试图隐藏它。
复杂、聪明的代码最终会导致更多的代码,而更多的代码会导致错误、性能问题,并且难以针对您未曾想到的需求进行调整 up-front。 (导致更复杂、更多条件路径和更多麻烦)像 EF 这样的框架已经过测试、优化和审查,因此可以利用它们。
我花了一些时间来整理如何使用 EntityFramework 创建 Restful API。这个问题主要是因为这个 API 应该在很长一段时间内使用,我希望它是可维护和干净的,具有良好的性能。够了,让我们进入问题。
免责声明: 由于公司政策,这里不能 post 太多,但我会尽量以最好的方式解决问题。也只有代码片段,可能无效。我对 C# 也很陌生,作为一名 JuniorD,我以前从未接触过 API。请原谅我的英语,这是我的第二语言。
Every 模型派生自 BaseModel class
public class BaseModel
{
[Required]
public Guid CompanyId { get; set; }
public DateTime CreatedDateTime { get; set; }
[StringLength(100)]
public string CreatedBy { get; set; }
public DateTime ChangedDateTime { get; set; }
[StringLength(100)]
public string ChangedBy { get; set; }
public bool IsActive { get; set; } = true;
public bool IsDeleted { get; set; }
}
public class Carrier : BaseModel
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public Guid CarrierId { get; set; }
public int CarrierNumber { get; set; }
[StringLength(100)]
public string CarrierName { get; set; }
[StringLength(100)]
public string AddressLine { get; set; }
public Guid? PostOfficeId { get; set; }
public PostOffice PostOffice { get; set; }
public Guid? CountryId { get; set; }
public Country Country { get; set; }
public List<CustomerCarrierLink> CustomerCarrierLinks { get; set; }
}
每个存储库都派生自 Repository 并有自己的接口。
public class CarrierRepository : Repository<Carrier>, ICarrierRepository
{
public CarrierRepository(CompanyMasterDataContext context, UnitOfWork unitOfWork) : base(context, unitOfWork) { }
#region Helpers
public override ObjectRequestResult<Carrier> Validate(Carrier carrier, List<string> errorMessages)
{
var errorMessages = new List<string>();
if(carrier != null)
{
var carrierIdentifier = (carrier.CarrierName ?? carrier.CarrierNumber.ToString()) ?? carrier.CarrierGLN;
if (string.IsNullOrWhiteSpace(carrier.CarrierName))
{
errorMessages.Add($"Carrier({carrierIdentifier}): Carrier name is null/empty");
}
}
else
{
errorMessages.Add("Carrier: Cannot validate null value.");
}
return CreateObjectResultFromList(errorMessages, carrier); // nonsense
}
}
UnitOfWork 派生自 classUnitOfWorkDiscoverySet,此 class 使用反射初始化存储库属性,还包含一个方法 (OnBeforeChildEntityProcessed) 用于调用每个 OnBeforeChildEntityProcessed。
public class UnitOfWork : UnitOfWorkDiscoverySet
{
public UnitOfWork(CompanyMasterDataContext context)
: base(context){}
public CarrierRepository Carriers { get; internal set; }
public PostOfficeRepository PostOffices { get; internal set; }
public CustomerCarrierLinkRepository CustomerCarrierLinks { get; internal set; }
}
public IRepository<Entity> where Entity : BaseModel
{
ObjectRequestResult<Entity> Add(Entity entity);
ObjectRequestResult<Entity> Update(Entity entity);
ObjectRequestResult<Entity> Delete(Entity entity);
ObjectRequestResult<Entity> Validate(Entity entity);
Entity GetById(Guid id);
Guid GetEntityId(Entity entity);
}
public abstract class Repository<Entity> : IRepository<Entity> where Entity : BaseModel
{
protected CompanyMasterDataContext _context;
protected UnitOfWork _unitOfWork;
public Repository(CompanyMasterDataContext context, UnitOfWork unitOfWork)
{
_context = context;
_unitOfWork = unitOfWork;
}
public ObjectRequestResult<Entity> Add(Entity entity)
{
if (!EntityExist(GetEntityId(entity)))
{
try
{
var validationResult = Validate(entity);
if (validationResult.IsSucceeded)
{
_context.Add(entity);
_context.UpdateEntitiesByBaseModel(entity);
_context.SaveChanges();
return new ObjectRequestResult<Entity>()
{
ResultCode = ResultCode.Succceeded,
ResultObject = entity,
Message = OBJECT_ADDED
};
}
return validationResult;
}
catch (Exception exception)
{
return new ObjectRequestResult<Entity>()
{
ResultCode = ResultCode.Failed,
ResultObject = entity,
Message = OBJECT_NOT_ADDED,
ErrorMessages = new List<string>()
{
exception?.Message,
exception?.InnerException?.Message
}
};
}
}
return Update(entity);
}
public virtual ObjectRequestResult Validate(Entity entity)
{
if(entity != null)
{
if(!CompanyExist(entity.CompanyId))
{
return EntitySentNoCompanyIdNotValid(entity); // nonsense
}
}
return EntitySentWasNullBadValidation(entity); // nonsense
}
}
DbContext class:
public class CompanyMasterDataContext : DbContext {
public DbSet<PostOffice> PostOffices { get; set; }
public DbSet<Carrier> Carriers { get; set; }
public DbSet<Company> Companies { get; set; }
public DbSet<CustomerCarrierLink> CustomerCarrierLinks { get; set; }
public UnitOfWork Unit { get; internal set; }
public CompanyMasterDataContext(DbContextOptions<CompanyMasterDataContext> options)
: base(options)
{
Unit = new UnitOfWork(this);
}
public void UpdateEntitiesByBaseModel(BaseModel baseModel)
{
foreach (var entry in ChangeTracker.Entries())
{
switch (entry.State)
{
case EntityState.Added:
entry.CurrentValues["CompanyId"] = baseModel.CompanyId;
entry.CurrentValues["CreatedDateTime"] = DateTime.Now;
entry.CurrentValues["CreatedBy"] = baseModel.CreatedBy;
entry.CurrentValues["IsDeleted"] = false;
entry.CurrentValues["IsActive"] = true;
Unit.OnBeforeChildEntityProcessed(entry.Entity, enumEntityProcessState.Add);
break;
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.CurrentValues["ChangedDateTime"] = DateTime.Now;
entry.CurrentValues["ChangedBy"] = baseModel.ChangedBy;
entry.CurrentValues["IsDeleted"] = true;
Unit.OnBeforeChildEntityProcessed(entry.Entity, enumEntityProcessState.Delete);
break;
case EntityState.Modified:
if (entry.Entity != null && entry.Entity.GetType() != typeof(Company))
entry.CurrentValues["CompanyId"] = baseModel.CompanyId;
entry.CurrentValues["ChangedDateTime"] = DateTime.Now;
entry.CurrentValues["ChangedBy"] = baseModel.ChangedBy;
Unit.OnBeforeChildEntityProcessed(entry.Entity, enumEntityProcessState.Update);
break;
}
}
}
}
发现类:
public abstract class UnitOfWorkDiscoverySet
{
private Dictionary<Type, object> Repositories { get; set; }
private CompanyMasterDataContext _context;
public UnitOfWorkDiscoverySet(CompanyMasterDataContext context)
{
_context = context;
InitializeSets();
}
private void InitializeSets()
{
var discoverySetType = GetType();
var discoverySetProperties = discoverySetType.GetProperties();
Repositories = new Dictionary<Type, object>();
foreach (var child in discoverySetProperties)
{
var childType = child.PropertyType;
var repositoryType = childType.GetInterfaces()
.Where( i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IRepository<>))
.FirstOrDefault();
if (repositoryType != null)
{
var repositoryModel = repositoryType.GenericTypeArguments.FirstOrDefault();
if (repositoryModel != null)
{
if (repositoryModel.IsSubclassOf(typeof(BaseModel)))
{
var repository = InitializeProperty(child); //var repository = child.GetValue(this);
if (repository != null)
{
Repositories.Add(repositoryModel, repository);
}
}
}
}
}
}
private object InitializeProperty(PropertyInfo property)
{
if(property != null)
{
var instance = Activator.CreateInstance(property.PropertyType, new object[] {
_context, this
});
if(instance != null)
{
property.SetValue(this, instance);
return instance;
}
}
return null;
}
public void OnBeforeChildEntityProcessed(object childObject, enumEntityProcessState processState)
{
if(childObject != null)
{
var repository = GetRepositoryByObject(childObject);
var parameters = new object[] { childObject, processState };
InvokeRepositoryMethod(repository, "OnBeforeEntityProcessed", parameters);
}
}
public void ValidateChildren<Entity>(Entity entity, List<string> errorMessages) where Entity : BaseModel
{
var children = BaseModelUpdater.GetChildModels(entity);
if(children != null)
{
foreach(var child in children)
{
if(child != null)
{
if (child.GetType() == typeof(IEnumerable<>))
{
var list = (IEnumerable<object>) child;
if(list != null)
{
foreach (var childInList in list)
{
ValidateChild(childInList, errorMessages);
}
}
}
ValidateChild(child, errorMessages);
}
}
}
}
public void ValidateChild(object childObject, List<string> errorMessages)
{
if(childObject != null)
{
var repository = GetRepositoryByObject(childObject);
var parameters = new object[] { childObject, errorMessages };
InvokeRepositoryMethod(repository, "Validate", parameters);
}
}
public void InvokeRepositoryMethod(object repository, string methodName, object[] parameters)
{
if (repository != null)
{
var methodToInvoke = repository.GetType().GetMethod(methodName);
var methods = repository.GetType().GetMethods().Where(x => x.Name == methodName);
if (methodToInvoke != null)
{
methodToInvoke.Invoke(repository, parameters);
}
}
}
public object GetRepositoryByObject(object objectForRepository)
{
return Repositories?[objectForRepository.GetType()];
}
public object GetObject<Entity>(Type type, Entity entity) where Entity : BaseModel
{
var childObjects = BaseModelUpdater.GetChildModels(entity);
foreach (var childObject in childObjects)
{
if (childObject.GetType().FullName == type.FullName)
{
return childObject;
}
}
return null;
}
}
}
问题: 我想验证每个模型和子模型中的数据 properties/list,知道你可能会说这可以使用属性来完成,但验证可能相当复杂,我更喜欢将其分开 space。
我解决这个问题的方法是使用来自 UnitDiscoverySet class 的反射,在这里我可以找到我正在尝试处理的实体的每个子实体,并搜索包含 UnitOfWork 的适当存储库。这无论如何都有效,只需要更多的工作和清理,但出于某种原因,我觉得这是 cheating/wrong 解决问题的方法,而且我也没有遇到编译时错误 + 反射有代价。
我可以在实体存储库中验证实体的子实体,但我会到处重复自己,这个解决方案似乎也不正确。
我不希望这个解决方案过于依赖 entityframework,因为我们不会永远使用它。
此解决方案还严重依赖于 DbContext 中的 UpdateEntitiesByBaseModel 方法。所以它只更改应该更改的字段。
不确定我是否像我想的那样很好地解决了这个问题,但我感谢每一个能引导我走上正确道路的贡献。谢谢!
解决方案(编辑): 我最终只将导航属性用于 GET 操作,而将其排除在插入操作之外。让一切变得更加灵活和快速,这样我就不需要使用 EF Tracker,它使 5000 个实体的插入操作从 13 分钟的操作缩短到 14.3 秒。
最好在 CodeReview 中提出这个问题,而不是针对特定 code-related 问题的 SO。你可以问 10 个不同的开发者,得到 10 个不同的答案。 :)
反射肯定是有代价的,我也不是很喜欢用它。
I don't want this solution to depend to heavy on entityframework, since it's not given that we will use this forever.
这是我在应用程序和框架中看到的一个相当常见的主题,与我合作的开发团队在使用 ORM 时试图应对这些应用程序和框架。对我来说,从解决方案中抽象出 EF 就像试图抽象出 .Net 的一部分。这实际上毫无意义,因为您失去了对 Entity Framework 提供的大部分灵活性和功能的访问权。它会导致更多、更复杂的代码来处理 EF 可以在本机执行的事情,从而在您 re-invent 控制方向盘时为错误留下空间,或者留下以后必须解决的漏洞。您要么信任它,要么不应该使用它。
I could validate children of the entity in the entity repository, but then i would be repeating myself all over the place, and this solution don't seem right either.
这实际上是我在项目中提倡的模式。许多人反对存储库模式,但它是一个很好的模式,可以作为用于测试目的的域边界。 (无需设置 in-memory 数据库或尝试模拟 DbContext/DbSets)但是,IMO 通用存储库模式是 anti-pattern。它将实体关注点彼此分开,但是在许多情况下,我们处理的是实体 "graphs" 而不是单个实体类型。我没有为每个实体定义存储库,而是选择了一些实际上是每个控制器的存储库的东西。 (使用真正常见实体的存储库,例如查找。)这背后有两个原因:
- 传递/模拟的依赖引用更少
- 更好地服务 SRP
- 避免 pigeon-holing 数据库操作
我对通用或 per-entity 存储库的最大问题是,虽然它们似乎符合 SRP(负责单个实体的操作),但我觉得它们违反了它,因为 SRP 是关于只有一个改变的理由。如果我有一个 Order 实体和一个 Order 存储库,我可能有应用程序的多个区域来加载订单并与订单交互。与 Order 实体交互的方法现在在几个不同的地方被调用,这构成了调整方法的许多潜在原因。您最终要么使用复杂的条件代码,要么使用几种非常相似的方法来服务于特定场景。 (用于列出订单的订单、客户订单、商店订单等)在验证实体时,这通常是在整个图形的上下文中完成的,因此将其集中在与图形相关的代码中而不是单独的代码中是有意义的实体。这适用于像 Add/Update/Delete 这样的通用基础操作。 80% 的时间这有效并且省力,但剩下的 20% 要么必须让 shoe-horned 进入模式,导致低效的 and/or error-prone 代码,要么 work-arounds . K.I.S.S。在软件设计方面,应该始终胜过 D.N.R.Y。整合到基础 classes 等是一种优化,当识别出 "identical" 功能时,应该随着代码的发展而完成。当 up-front 作为架构决策完成时,我认为这种过早的优化会在 "similar" 而不是 "identical" 行为组合在一起导致条件代码时为开发、性能问题和错误创造障碍为边缘情况爬行。
因此,如果我有类似 ManageOrderController 的东西,我将有一个 ManageOrderRepository 来提供订单,而不是 OrderRepository 来提供订单。
例如,我喜欢使用 DDD 风格的方法来管理我的存储库在构建中发挥作用的实体,因为它们对数据域是私有的,并且可以 validate/retrieve 相关实体。因此,典型的存储库将具有:
IQueryable<TEntity> GetTEntities()
IQueryable<TEntity> GetTEntityById(id)
IQueryable<TRelatedEntity> GetTRelatedEntities()
TEntity CreateTEntity({all required properties/references})
void DeleteTEntity(entity)
TChildEntity CreateTChildEntity(TEntity, {all required properties/references})
检索方法,包括 "By ID" 因为它是常见场景,return IQueryable 以便调用者可以控制数据的使用方式。这消除了尝试和抽象 EF 可以利用的 Linq 功能的需要,以便调用者可以应用过滤器、执行分页、排序,然后按他们需要的方式使用数据。 (Select
、Any
等)存储库执行核心规则,例如 IsActive 和 tenancy/authorization 检查。这作为测试的边界,因为模拟只需要 return List<TEntity>.AsQueryable()
或用 async-friendly 集合类型包装。 (
"Create" 方法管理有关创建实体和将实体关联到 Context 的规则,以确保实体始终以最低限度的完整和有效状态创建。这就是验证发挥作用的地方。任何不是 null-able 的值都会被传入,连同 FK(键或引用)一起传递,以确保如果 SaveChanges()
是 Create 之后的下一个调用,实体将是有效的。
"Delete" 方法同样用于管理验证数据 state/authorization,并应用一致的行为。 (硬删除与软删除、审计等)
我不使用 "Update" 方法。更新由实体本身的 DDD 方法处理。控制器定义工作单元,使用存储库检索实体,调用实体方法,然后提交工作单元。验证可以在实体级别完成,也可以通过验证器 class.
无论如何,这只是您可能采用的 10 多种方法中的一种的总结,并希望强调您采用任何方法时需要考虑的一些事项。在使用 EF 时,我的重点是:
- 保持简单。 (K.I.S.S. > D.N.R.Y)
- 充分利用 EF 提供的功能,而不是试图隐藏它。
复杂、聪明的代码最终会导致更多的代码,而更多的代码会导致错误、性能问题,并且难以针对您未曾想到的需求进行调整 up-front。 (导致更复杂、更多条件路径和更多麻烦)像 EF 这样的框架已经过测试、优化和审查,因此可以利用它们。