c# - EntityFramework(存储库模式、数据验证、Dto)
问题描述
我一直在整理如何使用 EntityFramework 创建一个 Restful API。问题主要是因为这个 API 应该在很长一段时间内使用,我希望它是可维护的和干净的,具有良好的性能。够了,让我们进入问题。
免责声明: 由于公司政策,不能在这里发布太多,但我会尽量以最好的方式解决这个问题。也只会有代码片段,可能无效。我对 C# 也很陌生,作为一名 JuniorD,我以前从未接触过 API。请原谅我的英语,这是我的第二语言。
每个模型都派生自BaseModel类
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 派生自 UnitOfWorkDiscoverySet 类,该类使用反射初始化存储库属性,还包含用于调用每个 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 类:
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;
}
}
}
问题: 我想验证每个模型和子模型属性/列表中的数据,知道你可能会说这可以使用属性来完成,但验证可能相当复杂,我更喜欢将其分开在它自己的空间中。
我解决这个问题的方法是使用 UnitDiscoverySet 类的反射,在这里我可以找到我正在尝试处理的实体的每个子项,并搜索包含 UnitOfWork 的适当存储库。这无论如何都有效,只需要更多的工作和清理,但由于某种原因,我觉得这是解决问题的作弊/错误方式,而且我也没有得到编译时错误+反射出现成本。
我可以在实体存储库中验证实体的子代,但是我会到处重复自己,而且这个解决方案似乎也不正确。
我不希望这个解决方案过于依赖实体框架,因为我们不会永远使用它。
此解决方案还严重依赖于 DbContext 中的 UpdateEntitiesByBaseModel 方法。所以它只更改应该更改的字段。
不确定我是否像我想象的那样很好地解决了这个问题,但我感谢每一个能让我走上正确道路的贡献。谢谢!
解决方案(编辑): 我最终只将导航属性用于 GET 操作,并将其排除在插入操作中。让一切变得更加灵活和快速,这样我就不需要使用 EF Tracker,它可以将 5000 个实体的插入操作从 13 分钟的操作缩短到 14.3 秒。
解决方案
这个问题可能最好在 CodeReview 中提出,而不是在针对特定代码相关问题的 SO 中提出。你可以问 10 个不同的开发者,得到 10 个不同的答案。:)
反射肯定是有代价的,而且它不是我非常喜欢使用的东西。
我不希望这个解决方案过于依赖实体框架,因为我们不会永远使用它。
这是我在与我合作的开发团队在使用 ORM 时试图应对的应用程序和框架中看到的一个相当普遍的主题。对我来说,从解决方案中抽象出 EF 就像试图抽象出 .Net 的一部分。实际上没有任何意义,因为您失去了对 Entity Framework 提供的大部分灵活性和功能的访问权。它会导致更多、更复杂的代码来处理 EF 可以在本地完成的事情,从而在您重新发明轮子时为错误留出空间,或者留下以后必须解决的空白。您要么信任它,要么不应该使用它。
我可以在实体存储库中验证实体的子代,但是我会到处重复自己,而且这个解决方案似乎也不正确。
这实际上是我在项目中提倡的模式。许多人反对存储库模式,但它是一个很好的模式,可以作为域的边界用于测试目的。(无需设置内存数据库或尝试模拟 DbContext/DbSets)但是,IMO 通用存储库模式是一种反模式。它将实体关注点彼此分开,但是在许多情况下,我们处理的是实体“图”而不是单个实体类型。我没有为每个实体定义存储库,而是选择实际上是每个控制器的存储库。(例如,对于真正常见的实体(例如查找)的存储库。)这背后有两个原因:
- 传递/模拟的依赖引用更少
- 更好地服务于 SRP
- 避免分类数据库操作
我对通用或每个实体存储库的最大问题是,虽然它们似乎符合 SRP(负责单个实体的操作),但我觉得它们违反了它,因为 SRP 只有一个改变的理由。如果我有一个 Order 实体和一个 Order 存储库,我可能有几个应用程序区域加载并与订单交互。与 Order 实体交互的方法现在在几个不同的地方调用,这构成了调整方法的许多潜在原因。您最终要么使用复杂的条件代码,要么使用几种非常相似的方法来服务特定场景。(列出订单的订单、客户订单、商店订单等)在验证实体时,这通常在整个图的上下文中完成,因此将其集中在与图相关的代码中而不是单个实体中是有意义的。这适用于添加/更新/删除等通用基本操作。这在 80% 的情况下有效并节省了工作量,但剩下的 20% 要么必须硬着头皮进入模式,要么导致效率低下和/或容易出错的代码,或者变通方法。在软件设计方面,KISS 应该总是胜过 DNRY。合并到基类等是一种优化,当识别出“相同”功能时,应该随着代码的发展而进行优化。当它作为架构决策预先完成时,我认为这种过早的优化会在“相似”但不“相同”时为开发、性能问题和错误造成障碍
因此,如果我有类似 ManageOrderController 的东西,而不是 OrderRepository 来服务订单,我将有一个 ManageOrderRepository 来服务它。
例如,我喜欢使用 DDD 风格的方法来管理我的存储库在构建中发挥作用的实体,因为它们对数据域是私有的并且可以验证/检索相关实体。因此,典型的存储库将具有:
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})
检索方法(包括常见场景中的“按 ID”)返回 IQueryable,以便调用者可以控制数据的使用方式。这消除了尝试和抽象 EF 可以利用的 Linq 功能的需要,因此调用者可以应用过滤器、执行分页、排序,然后按照他们的需要使用数据。( Select
, Any
, 等) 存储库执行诸如 IsActive 和租赁/授权检查等核心规则。这作为测试的边界,因为模拟只需要返回List<TEntity>.AsQueryable()
或使用异步友好的集合类型包装。(单元测试 .ToListAsync() 使用内存) 存储库还可以作为通过任何适用标准检索任何相关实体的首选场所。这可以被视为潜在的重复,但只有在应用程序的控制器/视图/区域需要更改时才需要更改此存储库。诸如查找之类的常见内容将通过它们自己的存储库提取。这减少了对大量单独存储库依赖项的需求。每个区域都会自行处理,因此此处的更改/优化无需考虑或影响应用程序的其他区域。
“创建”方法管理有关创建实体和将实体关联到 Context 的规则,以确保始终以最低限度的完整和有效状态创建实体。这就是验证发挥作用的地方。传入任何不可为空的值,以及确保如果SaveChanges()
是 Create 之后的下一次调用,实体将有效所需的 FK(键或引用)。
“删除”方法同样出现在这里来管理验证数据状态/授权,并应用一致的行为。(硬删除与软删除、审计等)
我不使用“更新”方法。更新由实体本身的 DDD 方法处理。控制器定义工作单元,使用存储库检索实体,调用实体方法,然后提交工作单元。验证可以在实体级别完成,也可以通过 Validator 类完成。
无论如何,这只是对您可能会采用的 10 种以上方法中的一种方法的总结,并希望强调一些您采取的任何方法都需要考虑的事项。在与 EF 合作时,我的重点是:
- 把事情简单化。(吻> DNRY)
- 利用 EF 所提供的而不是试图隐藏它。
复杂、聪明的代码最终会导致更多的代码,而更多的代码会导致错误、性能问题,并且很难根据您事先没有想到的需求进行调整。(导致更复杂、更多条件路径和更多麻烦)像 EF 这样的框架已经过测试、优化和审查,因此可以利用它们。
推荐阅读
- javascript - 反应重定向单元测试未呈现预期的dom
- git - 是 git config 用户名和电子邮件,再添加一个 ssh 公钥就可以在计算机上使用第二个 Github 帐户吗?
- javascript - BOOTSTRAP v3.3.5 响应式断点和标志
- c# - 如何实现 asp-append-version="true" 到背景图像属性?
- java - Pagable Sort.Order 忽略大小写
- tcp - 为什么我不能通过 HAProxy 将超过 8000 个客户端连接到 MQTT 代理?
- python - Maya 等待 Qt 窗口关闭
- delphi - Delphi EurekaLog 和 OmniThreadLibrary 不兼容?
- ios - 如何检查 facebook 发布的最新版本的 sdk
- redis - 如何定期清理redis条目?