Can I hide my ICollection <T> fields when I have a one-to-many mapping in EF4 only code?
My domain classes that have one-to-many mappings usually have the following form (unverified code):
public Customer Customer { // Public methods. public Order AddOrder(Order order) { _orders.Add(order); } public Order GetOrder(long id) { return _orders.Where(x => x.Id).Single(); } // etc. // Private fields. private ICollection<Order> _orders = new List<Order>(); } EF4 Code Samples I saw the publication of a public ICollection when dealing with one-to-many relationships.
Is there any way to save and restore my collections with their disclosure? If not, it looks like my domain objects will be designed in accordance with ORM requirements, which seem to contradict the spirit of the undertaking. Detecting ICollection (using Add methods, etc.) Does not seem particularly clean and will not be my default approach.
Update
I found this post , which suggests that this was not possible in May. Of course, the Microsoft poster said that they "are strongly considering the possibility of its implementation" (I would have hoped so), and we are six months old, so maybe there has been some progress?
I found that everything that was done, EF requires that ICollection<T> be publicly available. I think this is because when objects are loaded from the database, the mapping looks for the collection property, gets the collection, and then calls the Add method of the collection to add each of the child objects.
I wanted to make sure that the addition was done using the method of the parent object, so a solution was created to wrap the collection, catch the addition and direct it to my preferred add method.
Extending List and other types of collections was not possible because the Add method is not virtual. One option is to extend the Collection class and override the InsertItem method.
I focused only on the Add , Remove and Clear functions of the ICollection<T> interface, as those that can modify the collection.
Firstly, this is my basic collection, which implements the ICollection<T> interface ICollection<T> default behavior is a regular collection. However, the caller may specify an alternative Add method to be called. In addition, the caller can ensure that Add , Remove , Clear operations are not resolved by setting null alternatives. The result is a NotSupportedException if someone tries to use this method.
Throwing an exception is not as good as preventing access in the first place. Nevertheless, the code must be tested (checked by the module), and the exception will be found very quickly and the corresponding code change will be performed.
public abstract class WrappedCollectionBase<T> : ICollection<T> { private ICollection<T> InnerCollection { get { return GetWrappedCollection(); } } private Action<T> addItemFunction; private Func<T, bool> removeItemFunction; private Action clearFunction; /// <summary> /// Default behaviour is to be like a normal collection /// </summary> public WrappedCollectionBase() { this.addItemFunction = this.AddToInnerCollection; this.removeItemFunction = this.RemoveFromInnerCollection; this.clearFunction = this.ClearInnerCollection; } public WrappedCollectionBase(Action<T> addItemFunction, Func<T, bool> removeItemFunction, Action clearFunction) : this() { this.addItemFunction = addItemFunction; this.removeItemFunction = removeItemFunction; this.clearFunction = clearFunction; } protected abstract ICollection<T> GetWrappedCollection(); public void Add(T item) { if (this.addItemFunction != null) { this.addItemFunction(item); } else { throw new NotSupportedException("Direct addition to this collection is not permitted"); } } public void AddToInnerCollection(T item) { this.InnerCollection.Add(item); } public bool Remove(T item) { if (removeItemFunction != null) { return removeItemFunction(item); } else { throw new NotSupportedException("Direct removal from this collection is not permitted"); } } public bool RemoveFromInnerCollection(T item) { return this.InnerCollection.Remove(item); } public void Clear() { if (this.clearFunction != null) { this.clearFunction(); } else { throw new NotSupportedException("Clearing of this collection is not permitted"); } } public void ClearInnerCollection() { this.InnerCollection.Clear(); } public bool Contains(T item) { return InnerCollection.Contains(item); } public void CopyTo(T[] array, int arrayIndex) { InnerCollection.CopyTo(array, arrayIndex); } public int Count { get { return InnerCollection.Count; } } public bool IsReadOnly { get { return ((ICollection<T>)this.InnerCollection).IsReadOnly; } } public IEnumerator<T> GetEnumerator() { return InnerCollection.GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return InnerCollection.GetEnumerator(); } } Given this base class, we can use it in two ways. The examples use the original post objects.
1) Create a specific type of wrapped collection (for example, List ) Public class WrappedListCollection: WrappedCollectionBase, IList {private List innerList;
public WrappedListCollection(Action<T> addItemFunction, Func<T, bool> removeItemFunction, Action clearFunction) : base(addItemFunction, removeItemFunction, clearFunction) { this.innerList = new List<T>(); } protected override ICollection<T> GetWrappedCollection() { return this.innerList; } <...snip....> // fill in implementation of IList if important or don't implement IList } Then it can be used:
public Customer Customer { public ICollection<Order> Orders {get { return _orders; } } // Public methods. public void AddOrder(Order order) { _orders.AddToInnerCollection(order); } // Private fields. private WrappedListCollection<Order> _orders = new WrappedListCollection<Order>(this.AddOrder, null, null); } 2) Give a collection to be wrapped with
public class WrappedCollection<T> : WrappedCollectionBase<T> { private ICollection<T> wrappedCollection; public WrappedCollection(ICollection<T> collectionToWrap, Action<T> addItemFunction, Func<T, bool> removeItemFunction, Action clearFunction) : base(addItemFunction, removeItemFunction, clearFunction) { this.wrappedCollection = collectionToWrap; } protected override ICollection<T> GetWrappedCollection() { return this.wrappedCollection; } } which can be used as follows:
{public ICollection Orders {get {return _wrappedOrders; }} // Public methods.
public void AddOrder(Order order) { _orders.Add(order); } // Private fields. private ICollection<Order> _orders = new List<Order>(); private WrappedCollection<Order> _wrappedOrders = new WrappedCollection<Order>(_orders, this.AddOrder, null, null); } There are other ways to call WrappedCollection constructors, WrappedCollection example, to override the addition, but save the deletion and clear as usual
private WrappedListCollection<Order> _orders = new WrappedListCollection(this.AddOrder, (Order o) => _orders.RemoveFromInnerCollection(o), () => _orders.ClearInnerCollection()); I agree that it would be better if EF did not require the collection to be publicly available, but this solution allows me to control the modification of my collection.
In order to prevent access to the collection for the request, you can use the 2) approach above and set the WrappedCollection GetEnumerator method to throw a NotSupportedException . Then your GetOrder method can remain as it is. However, a simpler method is to expose the wrapped collection. For example:
public class WrappedCollection<T> : WrappedCollectionBase<T> { public ICollection<T> InnerCollection { get; private set; } public WrappedCollection(ICollection<T> collectionToWrap, Action<T> addItemFunction, Func<T, bool> removeItemFunction, Action clearFunction) : base(addItemFunction, removeItemFunction, clearFunction) { this.InnerCollection = collectionToWrap; } protected override ICollection<T> GetWrappedCollection() { return this.InnerCollection; } } Then the call in the GetOrder method will become
_orders.InnerCollection.Where(x => x.Id == id).Single(); Another way to achieve this is to create a related interface for each of your POCOs to expose only what you want outside the persistence / domain layers. You can also bind your DbContext class to hide and control access to DbSet collections. As it turned out, DbSet properties can be protected, and the creator of the model will pick them up when creating tables, but when you try to access the collections, they will be empty. The factory method (in my example, CreateNewContext) can be used instead of the constructor to get the conjugate DbContext to hide the DbSet collections.
There is a lot of extra effort in coding, but if you hide implementation details in POCOs, it will work.
UPDATE: It turns out that you can fill in DBS files if they are protected, but not directly in the DBC text. They cannot be aggregate roots (i.e., the object must be accessible through a collection in one of the public DBSet objects). If it is important to hide the DBSet implementation, the interface interface described by me still matters.
public interface ICustomer { void AddOrder(IOrder order); IOrder GetOrder(long id); } public Customer : ICustomer { // Exposed methods: void ICustomer.AddOrder(IOrder order) { if (order is Order) orders.Add((Order)order); else throw new Exception("Hey! Not a mapped type!"); } IOrder ICustomer.GetOrder(long id) { return orders.Where(x => x.Id).Single(); } // public collection for EF // The Order class definition would follow the same interface pattern illustrated // here for the Customer class. public ICollection<Order> orders = new List<Order>(); } public interface IMyContext { IEnumerable<ICustomer> GetCustomers(); void AddCustomer(ICustomer customerObject); ICustomer CreateNewCustomer() } public class MyContext : DbContext, IMyContext { public static IMyContext CreateNewContext() { return new MyContext(); } public DbSet<Customer> Customers {get;set;} public DbSet<Order> Orders {get;set;} public IEnumerable<ICustomer> GetCustomers() { return Customers; } public void AddCustomer(ICustomer customerObject) { if (customerObject is Customer) Customers.Add((Customer)customerObject); else throw new Exception("Hey! Not a mapped type"); } public ICustomer CreateNewCustomer() { return Customers.Create(); } // wrap the Removes, Finds, etc as necessary. Remember to add these to the // DbContext interface // Follow this pattern also for Order/IOrder } If you change the name of your _orders collection to the name of the order table in your database, this should work. EF matches table and field names to collections / properties by convention. If you want to use a different name, you can edit the mappings in the edmx file.
AFAIK you can just leave the private modifier as it is. Collections do not have to be publicly available.