【问题标题】:Validate entities when adding to a navigation property添加到导航属性时验证实体
【发布时间】:2013-01-10 10:28:12
【问题描述】:

我有一个具有如下集合属性的实体:

public class MyEntity
{
    public virtual ICollection<OtherEntity> Others { get; set; }
}

当我通过数据上下文或存储库检索此实体时,我想防止其他人通过使用MyEntity.Others.Add(entity) 将项目添加到此集合中。这是因为我可能希望在将我的实体添加到集合之前执行一些验证代码。我会通过在MyEntity 上提供这样的方法来做到这一点:

public void AddOther(OtherEntity other)
{
    // perform validation code here

    this.Others.Add(other);
}

到目前为止,我已经测试了一些东西,我最终得出的结果是这样的。我在我的实体上创建了一个private 集合并公开了一个public ReadOnlyCollection&lt;T&gt;,所以MyEntity 看起来像这样:

public class MyEntity
{
    private readonly ICollection<OtherEntity> _others = new Collection<OtherEntity>();

    public virtual IEnumerable<OtherEntity>
    {
        get
        {
            return _others.AsEnumerable();
        }
    }
}

似乎正是我正在寻找的,我的单元测试通过了,但我还没有开始做任何集成测试,所以我想知道:

  1. 有没有更好的方法来实现我的目标?
  2. 如果我决定走这条路(如果可行),我将面临哪些影响?

始终感谢您的任何帮助。

编辑 1 我已从使用 ReadOnlyCollection 更改为 IEnumerable 并使用 return _others.AsEnumerable(); 作为我的吸气剂。单元测试再次顺利通过,但我不确定在集成过程中会遇到哪些问题,EF 开始使用相关实体构建这些集合。

编辑 2 所以,我决定尝试创建派生集合(称为 ValidatableCollection)的建议,实现 ICollection,其中我的 .Add() 方法将对之前提供的实体执行验证将其添加到内部集合中。不幸的是,Entity Framework 在构建导航属性时调用了这个方法——所以它并不适合。

【问题讨论】:

  • 你使用什么 .net 版本?
  • @IlyaIvanov 我的项目是 MVC3,所以我使用的是 .NET 4.0。

标签: c# entity-framework validation collections


【解决方案1】:

我会为此目的创建集合类:

OtherEntityCollection : Collection<OtherEntity>
{
    protected override void InsertItem(int index, OtherEntity item)
    {
        // do your validation here
        base.InsertItem(index, item);
    }

    // other overrides
}

这将变得更加严格,因为无法绕过此验证。您可以在documentation 中查看更复杂的示例。

我不确定的一件事是如何让 EF 在具体化数据库中的数据时创建这种具体类型。但正如here 所见,这可能是可行的。

编辑: 如果您想将验证保留在实体内部,您可以通过自定义接口使其成为通用接口,实体将实现该接口,而您的通用集合将调用此接口。

至于 EF 的问题,我认为最大的问题是,当 EF 重新实现集合时,它会为每个项目调用 Add。这然后调用验证,即使该项目不是作为业务规则“添加”,而是作为基础设施行为。这可能会导致奇怪的行为和错误。

【讨论】:

  • 我正在考虑这个问题,但让我失望的是 a) 我想避免为每个实体创建一个派生集合,该集合具有特定的验证,并将此代码保留在实体中定义和 b) 派生集合将如何影响 EF 所做的集合的代理生成?
【解决方案2】:

我建议返回ReadOnlyCollection&lt;T&gt;。我以前在similar scenarios用过,没遇到过问题。

另外,AsEnumerable() 方法也行不通,因为它只是改变了引用的类型,它不会生成一个新的、独立的对象,这意味着这个

MyEntity m = new MyEntity();
Console.WriteLine(m.Others.Count()); //0
(m.Others as Collection<OtherEntity>).Add(new OtherEntity{ID = 1});
Console.WriteLine(m.Others.Count()); //1

将成功插入您的私人收藏。

【讨论】:

    【解决方案3】:

    您不应该在HashSet 上使用AsEnumerable(),因为可以通过将集合转换为ICollection&lt;OtherEntity&gt; 轻松修改集合

    var values = new MyEntity().Entities;
    ((ICollection<OtherEntity>)values).Add(new OtherEntity());
    

    尝试返回列表的副本,例如

    return new ReadOnlyCollection<OtherEntity>(_others.ToList()).AsEnumerable();
    

    这样可以确保用户在尝试修改异常时会收到异常。您可以将ReadOnlyCollection 公开为返回类型,而不是IEnumerable,以便用户清楚和方便。在 .NET 4.5 中添加了一个新接口 IReadOnlyCollection

    除了某些组件依赖于 List 突变之外,您不会遇到大的集成问题。如果用户调用 ToList 或 ToArray,他们将返回一个副本

    【讨论】:

      【解决方案4】:

      你有两个选择:

      1) 您当前使用的方式:将集合公开为ReadOnlyCollection&lt;OtherEntity&gt;,并在MyEntity 类中添加方法来修改该集合。这很好,但考虑到您正在为仅使用该集合的类中添加OtherEntity 集合的验证逻辑,因此如果您在其他地方使用OtherEntity 集合该项目,您可能需要复制验证代码,这是代码异味(DRY):P

      2) 要解决这个问题,最好的方法是创建一个自定义的OtherEntityCollection 类来实现ICollection&lt;OtherEntity&gt;,这样您就可以在那里添加验证逻辑。这真的很简单,因为您可以创建一个简单的 OtherEntityCollection 对象,其中包含一个真正实现集合操作的List&lt;OtherEntity&gt; 实例,因此您只需要验证插入:。

      编辑:如果您需要对多个实体进行自定义验证,您应该创建一个自定义集合,该集合接收执行该验证的其他对象。我修改了下面的例子,但是创建一个泛型类应该不难:

      class OtherEntityCollection : ICollection<OtherEntity>  
      {
        OtherEntityCollection(Predicate<OtherEntity> validation)
        {
          _validator = validator;
        } 
      
        private List<OtherEntity> _list = new List<OtherEntity>();
        private Predicate<OtherEntity> _validator;
        public override void Add(OtherEntity entity)   
        {
           // Validation logic
           if(_validator(entity))
             _list.Add(entity);   
        }
      }
      

      【讨论】:

      • 我正在考虑这个问题,但让我失望的是 a) 我想避免为每个实体创建一个派生集合,该集合具有特定的验证,并将此代码保留在实体中定义和 b) 派生集合将如何影响 EF 所做的集合的代理生成?
      • 然后你可以在集合类之外提取验证码。查看我的编辑并告诉我您的想法
      • 这看起来更像它,目前正在玩这个。
      • 我使用了一个简单的谓词验证器来简化,但如果验证逻辑很复杂,您可以使用自定义对象。
      【解决方案5】:

      EF 不能在没有 setter 的情况下映射属性。甚至private set { } 也需要一些配置。保持模型为 POCO,Plain-Old 为 DTO

      常见的方法是创建单独的服务层,其中包含在保存之前针对您的模型的验证逻辑。

      样品..

      public void AddOtherToMyEntity(MyEntity myEntity, OtherEntity otherEntity)
      {
          if(myService.Validate(otherEntity)
          {
            myEntity.Others.Add(otherEntity);
          }
          //else ...
      }
      

      ps。您可以阻止编译器执行某些操作,但不能阻止其他编码器。只是让你的代码明确地说“不要直接修改实体集合,直到它通过验证”

      【讨论】:

        【解决方案6】:

        终于有一个合适的工作解决方案,这就是我所做的。我会将MyEntityOtherEntity 更改为更易读的内容,例如TeacherStudent,我想阻止老师教的学生数量超出他们的承受能力。

        首先,我为我打算以这种方式验证的所有实体创建了一个名为IValidatableEntity 的接口,如下所示:

        public interface IValidatableEntity
        {
            void Validate();
        }
        

        然后我在我的Student 上实现这个接口,因为我在添加到Teacher 的集合时验证这个实体。

        public class Student : IValidatableEntity
        {
            public virtual Teacher Teacher { get; set; }
        
            public void Validate()
            {
                if (this.Teacher.Students.Count() > this.Teacher.MaxStudents)
                {
                    throw new CustomException("Too many students!");
                }
            }
        }
        

        现在谈谈我如何调用验证。我在我的实体上下文中覆盖 .SaveChanges() 以获取添加的所有实体的列表,并为每个调用验证 - 如果它失败,我只需将其状态设置为分离以防止它被添加到集合中。因为我使用异常(此时我仍然不确定)作为我的错误消息,所以我 throw 将它们删除以保留堆栈跟踪。

        public override int SaveChanges()
        {
            foreach (var entry in ChangeTracker.Entries())
            {
                if (entry.State == System.Data.EntityState.Added)
                {
                    if (entry.Entity is IValidatableEntity)
                    {
                        try
                        {
                            (entry.Entity as IValidatableEntity).Validate();
                        }
                        catch
                        {
                            entry.State = System.Data.EntityState.Detached;
        
                            throw; // preserve the stack trace
                        }
                    }
                }
            }
        
            return base.SaveChanges();
        }
        

        这意味着我将我的验证代码很好地隐藏在我的实体中,这将使我在单元测试期间模拟我的 POCO 时变得更加轻松。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-05-26
          • 2023-03-27
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多