简体   繁体   English

EntityFramework 和 ReadOnlyCollection

[英]EntityFramework and ReadOnlyCollection

I use EntityFramewotk and code first approach.我使用 EntityFramewotk 和代码优先方法。 So, I describe my model like this:所以,我这样描述我的 model:

class Person
{
    public long Id { get;set; }
    public string Name { get;set; }
    public ICollection<Person> Parents { get;set; }
}

But, my domain logic don't allow to modify Parents collection (add, delete), it must be readonly (just for example).但是,我的域逻辑不允许修改 Parents 集合(添加、删除),它必须是只读的(仅作为示例)。 EntityFramework requires all Collections have ICollection<T> interface, and it has Add method (to materialize results) and Remove method, and others. EntityFramework 要求所有 Collections 都具有ICollection<T>接口,并且它具有Add方法(用于具体化结果)和Remove方法等。 I can create my own collection with explicit implementation of interface:我可以通过接口的显式实现来创建自己的集合:

public class ParentsCollection : ICollection<Person>
{
    private readonly HashSet<Person> _collection = new HashSet<Person>();
    void ICollection<Person>.Add(Person item)
    {
        _collection.Add(item);
    }

    bool ICollection<Person>.Remove(Person item)
    {
        return _collection.Remove(item);
    }

    //...and others
}

This hides Add and Remove methods, but does not protect at all.这隐藏了AddRemove方法,但根本没有保护。 Because I can always cast to ICollection and call prohibited method.因为我总是可以转换为 ICollection 并调用禁止的方法。

So, my question is:所以,我的问题是:

  • Is there a way to work with read-only collections in EntityFramework?有没有办法在 EntityFramework 中使用只读 collections?

You can expose private collection properties to EF, allowing for mapping and querying, while still keeping your domain object's members and relationships properly encapsulated. 您可以将私有集合属性公开给EF,允许映射和查询,同时仍然保持域对象的成员和关系正确封装。 It's a bit messy, but it works: 这有点乱,但它有效:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }

    public IEnumerable<Order> Orders
    {
        get { return _orders.AsEnumerable(); }
    }

    private List<Order> _orders { get; set; }

    public Customer()
    {
        _orders = new List<Order>();
    }

    public static Expression<Func<Customer, ICollection<Order>>> OrderMapping
    {
        get { return c => c._orders; }
    }
}

Mapping then uses: 映射然后使用:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);
    modelBuilder.Entity<Customer>().HasMany(Customer.OrderMapping);
}

This approach is described further here: http://ardalis.com/exposing-private-collection-properties-to-entity-framework 这里进一步描述了这种方法: http//ardalis.com/exposing-private-collection-properties-to-entity-framework

In EF Core, you can encapsulate collections and achieve true domain modeling by using backing fields . 在EF Core中,您可以使用支持字段封装集合并实现真正的域建模。 So, you can define your collection as a private field and expose it as a public readonly property like below as _parents and Parents . 因此,您可以将您的集合定义为私有字段,并将其作为公共只读属性公开,如下所示为_parentsParents

class Person
{
    public long Id { get;set; }
    public string Name { get;set; }
    private List<Person> _parents = new List<Person>();
    public IReadOnlyCollection<Person> Parents => _parents.AsReadOnly();
    public void AddParent(Parent parent){
        _parents.Add(parent); 
    }
}

As you can see, Parents is a read-only collection and consumers are not allowed to modify it. 如您所见,Parents是一个只读集合,不允许消费者对其进行修改。

Note that _parents is discovered as a backing-field by ef core's convention. 请注意,_parents被ef core的约定发现为后备字段。

Short answer: no . 简答: And it would be strange that you could that ( well, NHibernate can set private class fields and this means that you can expose it using a public property encapsulating the field as read-only collection... well, you can workaround this situation in EF too: Entity Framework Many to many through containing object . BTW, I wouldn't suggest you this approach, because how could you add new parents if it's a private property? ) 你可以这样做很奇怪( 好吧,NHibernate可以设置私有类字段,这意味着你可以使用将字段封装为只读集合的​​公共属性来公开它......好吧,你可以在EF中解决这种情况也是: 实体框架很多人通过包含对象 。顺便说一句,我不建议你采用这种方法,因为如果它是私有财产,你怎么能添加新的父母?

Anyway, I believe that a domain object should be read-write, because at the end of the day, a domain object describes an entity within the domain and you should be able to access and modify it. 无论如何,我认为域对象应该是可读写的,因为在一天结束时,域对象描述域中的实体,您应该能够访问和修改它。

An alternate solution is designing an interface to expose Parents as IReadOnlyList<Person> , and also an IPerson with all Person members excepting Parents , and return Person as IPerson : 一种替代的解决方案是设计以暴露的接口ParentsIReadOnlyList<Person> ,并且也是一个IPerson与所有Person成员以外Parents ,并返回Person作为IPerson

public interface IHasParents
{
    IReadOnlyList<Person> Parents { get; }
}

public interface IPerson : IHasParents
{
    long Id { get; set; }
    string Name { get; set; }
}

And implement IPerson implicitly on Person excepting Parents that would be implemented explicitly . 并且在除了明确实现的Parents之外的Person隐式实施IPerson When you need to return a Person somewhere, you return IPerson instead of Person : 当你需要在某个地方返回一个Person ,你会返回IPerson而不是Person

public IPerson CreatePerson(string name, IEnumerable<Persons> parents)
{
    Person person = new Person { Name = name, Parents = parents };

    // Persistence stuff

    return person;
}

You can argue that you may be able to downcast IPerson to Person , but at this point I would answer telling you that you need to follow your own coding conventions: if you defined that you never return Person but IPerson then I would do it this way in the entire code base, and if you need a write-capable Parents property, then you return Person instead (and you avoid a cast later!). 你可以说你可以将IPersonPerson ,但是在这一点上我会回答告诉你你需要遵循自己的编码约定:如果你定义你永远不会返回Person但是IPerson那么我会这样做在整个代码库中,如果你需要一个可写入的Parents属性,那么你将返回Person (并且你以后会避免演员!)。

Well, there is a way. 嗯,有一种方法。 It ain't pretty as it adds extra stuff on your domain model, but I just checked and it works. 它并不漂亮,因为它在您的域模型上添加了额外的东西,但我只是检查它的工作原理。

All credits go to Owen Craig. 所有学分归Owen Craig所有。

http://owencraig.com/mapping-but-not-exposing-icollections-in-entity-framework/ http://owencraig.com/mapping-but-not-exposing-icollections-in-entity-framework/

Example in a nut shell 坚果壳中的例子

Imagine we have existing model with Organization => Employees already set up. 想象一下,我们已经建立了Organization => Employees的现有模型。

To apply this technique, we need to change Organization model a bit: 要应用此技术,我们需要稍微更改组织模型:

// this is the main collection that will be persisted, mark it as protected
protected virtual ICollection<Employee> EmployeesInternal { get; private set; } = new List<Employee>();

// this will expose collection contents to public, seemingly unneccessary `Skip` statement will prevent casting back to Collection
public IEnumerable<Employee> Employees => EmployeesInternal.Skip(0);

// this is property accessor that will be used to define model and/or in `Include` statements, could be marked as internal if your domain/persistance/services are in the same assembly
public static Expression<Func<Organization, ICollection<Employee>>> EmployeeAccessor = f => f.EmployeesInternal;

Change fluent config in your database context: 在数据库上下文中更改fluent配置:

modelBuilder.Entity<Organization>().HasMany(Organization.EmployeeAccessor).WithRequired();

Change any Include statements in case you are not using LazyLoading 如果您不使用LazyLoading,请更改任何Include语句

var organizations = organizationRepository.GetAll().Include(Organization.EmployeeAccessor)

Happy DDD! 快乐的DDD!

If you, like me, are stuck using .Net Framework for a while longer, you can make this work in Entity Framework 6.x.如果您和我一样,长时间无法使用 .Net Framework,则可以在 Entity Framework 6.x 中进行这项工作。 It isn't pretty, and you only get runtime write protection on your readonly collections, but here is one way to go about it.它并不漂亮,您只能在只读集合上获得运行时写保护,但这是一种解决方法。

I don't recommended this, but if you must adhere so strongly to DDD, and only manipulate collections through Roots / Aggregates, well, don't say I didn't warn you...我不建议这样做,但是如果您必须如此强烈地坚持 DDD,并且只能通过 Roots/Aggregates 操作集合,那么,不要说我没有警告您...

NOT RECOMMENDED : http://edo-van-asseldonk.blogspot.com/2012/03/readonly-collections-with-entity.html不推荐http : //edo-van-asseldonk.blogspot.com/2012/03/readonly-collections-with-entity.html

Our ReadonlyList wound up a bit more involved.我们的 ReadonlyList 涉及更多。 To fully protect against changes (including Add, Insert, Remove, and their collection-based siblings) you need something like this:要完全防止更改(包括添加、插入、删除及其基于集合的同级),您需要这样的东西:

/// <remarks>
///     This class should be removed after migration to EF Core, which supports IReadonlyCollection.
/// </remarks>
/// <summary>
///     Represents a strongly typed read-only list of objects that can be accessed by index. Provides methods to search,
///     sort, and manipulate lists.
/// </summary>
/// <typeparam name="T">The type of elements in the list.</typeparam>
public class ReadonlyList<T> : List<T>
{
    public ReadonlyList(IEnumerable<T> collection)
        : base(collection)
    {
    }

    /// <summary>
    /// Not supported.
    /// </summary>
    public new void Add(T item)
    {
        base.Add(item);
        throw new NotSupportedException(GetNotSupportedExceptionMessage(nameof(Add)));
    }

    /// <summary>
    /// Not supported.
    /// </summary>
    public new void AddRange(IEnumerable<T> collection)
    {
        throw new NotSupportedException(GetNotSupportedExceptionMessage(nameof(AddRange)));
    }

    /// <summary>
    /// Not supported.
    /// </summary>
    public new void Insert(int index, T item)
    {
        throw new NotSupportedException(GetNotSupportedExceptionMessage(nameof(Insert)));
    }

    /// <summary>
    /// Not supported.
    /// </summary>
    public new void InsertRange(int index, IEnumerable<T> collection)
    {
        throw new NotSupportedException(GetNotSupportedExceptionMessage(nameof(InsertRange)));
    }

    /// <summary>
    /// Not supported.
    /// </summary>
    public new void Remove(T item)
    {
        throw new NotSupportedException(GetNotSupportedExceptionMessage(nameof(Remove)));
    }

    /// <summary>
    /// Not supported.
    /// </summary>
    public new void RemoveAll(Predicate<T> match)
    {
        throw new NotSupportedException(GetNotSupportedExceptionMessage(nameof(RemoveAll)));
    }

    /// <summary>
    /// Not supported.
    /// </summary>
    public new void RemoveAt(int index)
    {
        throw new NotSupportedException(GetNotSupportedExceptionMessage(nameof(RemoveAt)));
    }

    private string GetNotSupportedExceptionMessage(string memberName)
    {
        return $"Member {memberName} is not supported in {nameof(ReadonlyList<T>)}.";
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM