EntityFramework and ReadOnlyCollection
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.
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
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!).
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!