首页 > 解决方案 > 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 秒。

标签: c#asp.netentity-frameworkrestrepository-pattern

解决方案


这个问题可能最好在 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 合作时,我的重点是:

  1. 把事情简单化。(吻> DNRY)
  2. 利用 EF 所提供的而不是试图隐藏它。

复杂、聪明的代码最终会导致更多的代码,而更多的代码会导致错误、性能问题,并且很难根据您事先没有想到的需求进行调整。(导致更复杂、更多条件路径和更多麻烦)像 EF 这样的框架已经过测试、优化和审查,因此可以利用它们。


推荐阅读