0

我有一个具有集合属性的实体,看起来像这样:

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. 如果我决定走这条路(如果可行),我将面临什么影响?

始终感谢您的任何帮助。

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

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

4

6 回答 6

2

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

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

    // other overrides
}

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

我不确定的一件事是如何让 EF 在实现数据库中的数据时创建这种具体类型。但这可能是可行的,如此处所示

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

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

于 2013-01-10T11:00:01.963 回答
1

我建议返回ReadOnlyCollection<T>. 我过去在类似的场景中使用过它,我没有遇到任何问题。

此外,该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

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

于 2013-01-10T11:00:11.933 回答
1

您不应该使用AsEnumerable()on 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,他们将返回一个副本

于 2013-01-10T11:00:15.000 回答
1

您在这里有两个选择:

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

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

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

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);   
  }
}
于 2013-01-10T11:01:08.240 回答
0

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

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

样品..

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

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

于 2013-01-10T11:21:02.150 回答
0

终于有了一个合适的工作解决方案,这就是我所做的。我将更改为更具可读性的内容,例如MyEntity我想阻止老师教的学生数量超过他们可以处理的地方。OtherEntityTeacherStudent

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

public interface IValidatableEntity
{
    void Validate();
}

然后我在 my 上实现这个接口,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 时的生活变得更加轻松。

于 2013-01-10T16:23:15.057 回答