Могу ли я скрыть свои поля ICollection<T>, когда у меня есть отображение "один ко многим" только в коде EF4?

Классы моего домена, которые имеют однозначные сопоставления, обычно принимают следующую форму (непроверенный код):

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, которые я видел, представляют публичную коллекцию ICollection, когда имеют дело с отношениями "один ко многим".

Есть ли способ сохранить и восстановить мои коллекции, разоблачая их? Если нет, то, похоже, что мои доменные объекты будут спроектированы так, чтобы соответствовать требованиям ORM, что, похоже, противоречит духу усилий. Представление ICollection (с его методами Add и т. Д.) Не выглядит особенно чистым и не будет моим подходом по умолчанию.

Обновить

Нашел этот пост, который предполагает, что это было невозможно в мае. Конечно, на постере Microsoft действительно говорилось, что они "решительно рассматривают возможность его реализации" (я надеюсь, что это так), и у нас уже полгода, так что, может быть, был какой-то прогресс?

3 ответа

Решение

Я обнаружил, что все, что было сделано, EF требует ICollection<T> быть публичным. Я думаю, это потому, что когда объекты загружаются из базы данных, отображение ищет свойство коллекции, получает коллекцию и затем вызывает Add Метод коллекции для добавления каждого из дочерних объектов.

Я хотел убедиться, что добавление было сделано с помощью метода родительского объекта, поэтому создал решение обернуть коллекцию, перехватить добавление и направить его на мой предпочтительный метод добавления.

Расширение List и другие типы коллекций было невозможно, потому что Add метод не виртуальный. Одним из вариантов является продление Collection класс и переопределить InsertItem метод.

Я сосредоточился только на Add, Remove, а также Clear функции ICollection<T> интерфейс, как те, которые могут изменить коллекцию.

Во-первых, это моя базовая оболочка коллекции, которая реализует ICollection<T> Интерфейс Поведение по умолчанию такое же, как и у обычной коллекции. Однако вызывающая сторона может указать альтернативную Add метод, который будет вызван. Кроме того, вызывающая сторона может обеспечить Add, Remove, Clear операции не допускаются путем установки альтернатив null, Это приводит к NotSupportedException быть брошенным, если кто-то пытается использовать метод.

Выдача исключения не так хороша, как предотвращение доступа в первую очередь. Тем не менее, код должен быть проверен (проверен модулем), и исключение будет найдено очень быстро, и будет произведено соответствующее изменение кода.

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();
    }

}

Учитывая этот базовый класс, мы можем использовать его двумя способами. Примеры использования оригинальных почтовых объектов.

1) Создайте определенный тип упакованной коллекции (например, List) открытый класс 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
    }

Это может быть использовано:

 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) Дайте коллекцию для упаковки, используя

 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;
    }
}

который можно использовать следующим образом:

{public ICollection Orders {get {return _wrappedOrders; }} // Публичные методы.

 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);
}

Есть несколько других способов позвонить WrappedCollection конструкторы Например, чтобы переопределить add, но оставьте remove и clear как обычно

private WrappedListCollection<Order> _orders = new WrappedListCollection(this.AddOrder,  (Order o) => _orders.RemoveFromInnerCollection(o), () => _orders.ClearInnerCollection());

Я согласен, что было бы лучше, если бы EF не требовала, чтобы коллекция была общедоступной, но это решение позволяет мне контролировать изменение моей коллекции.

Для проблемы предотвращения доступа к коллекции для запросов вы можете использовать подход 2) выше и установить WrappedCollection GetEnumerator метод бросить NotSupportedException, Тогда ваш GetOrder метод может оставаться как есть. Более аккуратный метод, однако, может заключаться в том, чтобы выставить свернутую коллекцию. Например:

 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;
    }
}

Тогда звонок в GetOrder метод станет

_orders.InnerCollection.Where(x => x.Id == id).Single();

Еще один способ сделать это - создать связанный интерфейс для каждого из ваших POCO, чтобы отображать только то, что вы хотите, за пределами уровней персистентности / домена. Вы также можете связать свой класс DbContext с тем, чтобы также скрыть и контролировать доступ к коллекциям DbSet. Как выясняется, свойства DbSet могут быть защищены, и построитель модели подберет их при создании таблиц, но при попытке доступа к коллекциям они будут нулевыми. Фабричный метод (в моем примере, CreateNewContext) можно использовать вместо конструктора, чтобы получить интерфейс DbContext для скрытия коллекций DbSet.

При написании кода требуется немало дополнительных усилий, но если важно скрыть детали реализации в POCO, это сработает.

ОБНОВЛЕНИЕ: Оказывается, вы МОЖЕТЕ заполнить DBSets, если они защищены, но не напрямую в DBContext. Они не могут быть агрегированными корнями (т. Е. Доступность сущности должна быть через коллекцию в одной из общедоступных сущностей DBSet). Если важно скрыть реализацию DBSet, описанный мной шаблон интерфейса по-прежнему актуален.

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's interface

   // Follow this pattern also for Order/IOrder

}

Если вы измените название своего _orders Сбор к названию таблицы заказов в вашей базе данных, это должно работать. EF отображает имена таблиц / полей в коллекции / свойства по соглашению. Если вы хотите использовать другое имя, вы можете отредактировать сопоставления в файле edmx.

AFAIK, вы можете просто оставить приватный модификатор как есть. Коллекции не должны быть публичными.

Другие вопросы по тегам