添加到导航属性时验证实体

时间:2013-01-10 10:28:13

标签: c# entity-framework validation collections

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

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<T>,因此MyEntity看起来像这样:

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

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

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

  1. 有没有更好的方法来实现我正在寻找的东西?
  2. 如果我决定走这条路线(如果可行的话),我会面临什么影响?
  3. 永远感谢您的帮助。

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

    编辑2 因此,我决定尝试创建一个实现ValidatableCollection的派生集合(称之为ICollection),我的.Add()方法会执行{{1}}在将实体添加到内部集合之前对其提供的验证。不幸的是,实体框架在构建导航属性时会调用此方法 - 因此它并不适合。

6 个答案:

答案 0 :(得分:2)

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

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。然后,这会调用验证,即使项目未作为业务规则“添加”,也可以作为基础架构行为。这可能会导致奇怪的行为和错误。

答案 1 :(得分:1)

我建议回到ReadOnlyCollection<T>。我过去在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

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

答案 2 :(得分:1)

您不应在AsEnumerable()上使用HashSet,因为可以通过将其转换为ICollection<OtherEntity>

来轻松修改集合
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,他们将返回一个副本

答案 3 :(得分:1)

这里有两个选项:

1)您当前使用的方式:将集合公开为ReadOnlyCollection<OtherEntity>并在MyEntity类中添加方法以修改该集合。这非常好,但考虑到您在使用该集合的类中添加OtherEntity集合的验证逻辑,因此如果您使用{{1的集合在项目的其他地方,您可能需要复制验证代码,以及代码味道(DRY):P

2)要解决这个问题,最好的方法是创建一个实现OtherEntity的自定义OtherEntityCollection类,以便在那里添加验证逻辑。它非常简单,因为您可以创建一个简单的OtherEntityCollection对象,该对象包含一个真正实现集合操作的ICollection<OtherEntity>实例,因此您只需要验证插入:。

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

List<OtherEntity>

答案 4 :(得分:0)

如果没有setter,EF无法映射属性。甚至private set { }需要一些配置。保持模型为POCO,像DTO一样简单

常见的方法是在保存之前创建包含针对模型的验证逻辑的分离服务层。

代表..

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

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

答案 5 :(得分:0)

最后有一个合适的工作解决方案,这就是我所做的。我会将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!");
        }
    }
}

现在我如何调用validate。我在实体上下文中覆盖.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时变得更加轻松。