18

I use EntityFramewotk and code first approach. So, I describe my model like this:

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). EntityFramework requires all Collections have ICollection<T> interface, and it has Add method (to materialize results) and Remove method, and others. 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. Because I can always cast to ICollection and call prohibited method.

So, my question is:

  • Is there a way to work with read-only collections in EntityFramework?
Backs
  • 24,430
  • 5
  • 58
  • 85
  • Possible duplicate of [Entity Framework read only collections](http://stackoverflow.com/questions/11191103/entity-framework-read-only-collections) – Machet Feb 10 '16 at 19:17

4 Answers4

25

In EF Core, you can encapsulate collections and achieve true domain modeling by using backing fields. So, you can define your collection as a private field and expose it as a public readonly property like below as _parents and Parents.

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.

Note that _parents is discovered as a backing-field by ef core's convention.

Ehsan Mirsaeedi
  • 6,924
  • 1
  • 41
  • 46
  • 2
    This seems to be the perfect solution and deserves more votes. It impossible to alter the collection outside of the class (even by force casting). And its still possible to eager load using `Include(p => p.Parents)`. PS: I also had to add some code into my `DbContext.OnModelCreating` to get this working for my particular needs => `modelBuilder.Entity().HasMany(g => g.Parents)` – Oliver Pearmain Jun 30 '20 at 08:44
10

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. 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

ssmith
  • 8,092
  • 6
  • 52
  • 93
1

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?)

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:

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. When you need to return a Person somewhere, you return IPerson instead of 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!).

Community
  • 1
  • 1
Matías Fidemraizer
  • 63,804
  • 18
  • 124
  • 206
  • @Backs You can check your Q&A overtime if someone adds another answer anyway ;) BTW, I believe EF is very limited in some areas and this is a good example of its limitations... – Matías Fidemraizer Aug 18 '15 at 06:07
1

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.

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.

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:

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

Change any Include statements in case you are not using LazyLoading

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

Happy DDD!

Kaspars Ozols
  • 6,967
  • 1
  • 20
  • 33