EntityFramework(存储库模式,数据验证,Dto)

时间:2019-05-08 19:26:27

标签: c# asp.net entity-framework rest repository-pattern

我一直在花一些时间来整理如何使用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; }
}

每个存储库都派生自存储库,并具有自己的接口。

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;
        }
    }
}

}

DiscoveryClass:

    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;
    }
}

}

问题: 我想验证每个模型和childmodels属性/列表中的数据,知道您可能会说可以使用属性来完成数据,但是验证可能相当复杂,我更喜欢在自己的空间中将其分开。

我解决此问题的方法是通过使用UnitDiscoverySet类的反射,在这里我可以找到我要处理的实体的每个子级,并搜索包含UnitOfWork的适当存储库。这绝对可行,只需要更多的工作和清理,但是出于某种原因,我觉得这是解决问题的一种作弊/错误方式,而且我也没有得到编译时错误+反射。费用。

我可以在实体存储库中验证实体的子代,但随后我将在整个地方重复一次,这种解决方案似乎也不对。

我不希望这种解决方案过分依赖实体框架,因为没有给出我们将永远使用它的原因。

此解决方案还很大程度上取决于DbContext中的UpdateEntitiesByBaseModel方法。因此,它仅更改应更改的字段。

不确定我是否会尽我所能解决此问题,但我感谢能为我带来正确道路的一切贡献。谢谢!

解决方案(编辑): 我最终仅将导航属性用于GET操作,而将其排除在插入操作之外。使一切变得更加灵活,更快,这样我就不需要使用EF跟踪器,该跟踪器可以从13分钟的操作(最多14.3秒)对5000个实体进行插入操作。

1 个答案:

答案 0 :(得分:1)

可能最好在CodeReview中问这个问题,而不是针对特定代码相关问题的SO。您可以问10个不同的开发人员,并获得10个不同的答案。 :)

反射肯定是有代价的,我根本不想使用它。

  

我不希望这种解决方案过分依赖实体框架,   因为我们没有永远使用它。

这是我在与我一起工作的开发团队尝试应对的应用程序和框架中遇到的一个相当普遍的主题。对我而言,从解决方案中抽象出EF就像试图抽象出.Net的一部分。从根本上讲没有意义,因为您放弃了对Entity Framework提供的大部分灵活性和功能的访问。它导致了更多,更复杂的代码来处理EF本身可以做的事情,从而在您重新发明轮子时留有bug的余地,或者留下了以后必须解决的空白。您要么信任它,要么不应该使用它。

  

我可以在实体存储库中验证实体的子代,但是   然后我会在整个地方重复我自己,而这个解决方案   看起来也不对。

这实际上是我提倡的项目模式。许多人都反对Repository模式,但是这是一个很好的模式,可以用作测试的域边界。 (无需建立内存数据库或尝试模拟DbContext / DbSets)。但是,IMO通用存储库模式是一种反模式。它将实体关注点彼此分开,但是在许多情况下,我们处理的是实体“图”,而不是单个实体类型。与其定义每个实体的存储库,不如选择有效的每个控制器存储库。 (例如,具有用于真正普通实体(例如查找)的存储库。)这背后有两个原因:

  • 更少的依赖引用可以传递/模拟
  • 更好地服务于SRP
  • 避免数据库管理操作

我对通用或实体存储库最大的问题是,尽管它们似乎符合SRP(负责单个实体的操作),但我认为它们违反了SRP,因为SRP只是有一个更改理由。如果我有一个Order实体和一个Order存储库,则我可能在应用程序的多个区域中加载和与订单交互。现在,与Order实体进行交互的方法在几个不同的地方被调用,这构成了调整方法的许多潜在原因。您最终将获得复杂的条件代码,或者使用几种非常相似的方法来满足特定的情况。 (列出订单的订单,客户的订单,商店的订单等)。在验证实体时,通常是在整个图的上下文中完成,因此有意义的是将其集中在与图相关的代码中,而不是单个的实体。这适用于通用基本操作,例如添加/更新/删除。 80%的时间可以正常工作并节省精力,但是剩下的20%要么不得不陷入模式中,导致效率低下和/或易于出错的代码,或者解决方法。吻。在软件设计方面,应始终胜过D.N.R.Y。合并到基类等中是一种优化,应该在识别“相同”功能时随着代码的发展而完成。当作为架构决策预先完成时,我认为这种过早的优化会在“相似”而不是“相同”的行为组合在一起时导致开发,性能问题和错误的障碍,从而导致条件代码在边缘情况下蔓延开来。

因此,如果我有诸如ManageOrderController之类的东西,则可以使用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功能,以便调用者可以应用过滤器,执行分页,排序,然后按需使用数据。 ({SelectAny等)。存储库强制执行核心规则,例如IsActive和租用/授权检查。这是测试的边界,因为模拟仅需返回List<TEntity>.AsQueryable()或使用异步友好的集合类型包装即可。 (Unit-testing .ToListAsync() using an in-memory)信息库还可以作为通过任何适用标准检索任何相关实体的理想去处。这可以看作是潜在的重复,但是仅当需要更改应用程序的控制器/视图/区域时才需要更改此存储库。诸如查找之类的常见内容将通过其自己的存储库提取。这减少了对大量单个存储库依赖项的需求。每个区域都需要照顾好自己,因此此处的更改/优化无需考虑或影响应用程序的其他区域。

“创建”方法管理围绕将实体创建和关联到上下文的规则,以确保始终以最小的完整和有效状态创建实体。这就是验证起作用的地方。任何不可为空的值都会与FK(键或引用)一起传入,以确保如果SaveChanges()是Create之后的下一个调用,则该实体将有效。

“删除”方法类似地出现在这里,以管理验证数据状态/授权并应用一致的行为。 (硬删除与软删除,审核等)

我不使用“更新”方法。更新由实体本身上的DDD方法处理。控制器定义工作单元,使用存储库检索实体,调用实体方法,然后提交工作单元。验证可以在实体级别或通过Validator类完成。

无论如何,这只是您可能会采用的超过10种方法中的一种的总结,并希望突出显示一些您要采用的方法要考虑的事情。在使用EF时,我的重点是:

  1. 保持简单。 (K.I.S.S.> D.N.R.Y)
  2. 利用EF必须提供的功能,而不是尝试隐藏它。

复杂,灵巧的代码最终会导致更多的代码,而更多的代码会导致bug,性能问题,并使其难以适应您从未想到的需求。 (导致更多的复杂性,更多的条件路径和更多的麻烦)EF之类的框架已经过测试,优化和审查,因此可以充分利用它们。