В мире DDD все крутится вокруг домена. Все бизнес правила живут внутри доменных сущностей либо доменных сервисов. Рассмотрим простой случай: есть сущность Категория, у которой есть несколько Подкатегорий. Также есть бизнес правило: нельзя добавить подкатегорию с именем, равным имени корневой категории (просто для примера). Обычно, мы напишем следующее:
public class Category
{
public Category()
{
_categories = new Collection<Category>();
}
public string Name { get; protected set; }
private readonly ICollection<Category> _categories;
public IEnumerable<Category> Categories { get { return _categories; } }
public void AddCategory(Category category)
{
if (category.Name == Name) throw new InvalidOperationException();
_categories.Add(category);
}
}
NHibernate с легкостью позволяет мапить такие связи, но Entity Framework предполагает только использование конструкций вида:
public class Category
{
public Category()
{
Categories = new Collection<Category>();
}
public ICollection<Category> Categories { get; protected set; }
}
Предложенный EF подход позволяет обратиться к нашей коллекции из любой части приложения, игнорируя необходимые бизнес правила. Что особенно опасно, когда над разными частями приложения работают разные люди.
В сети есть несколько решений маппинга приватных коллекций в EF, но они нам не подходят, т.к. либо мы жестко привязываем домен к инфраструктурному слою, либо городим забор методов, которые не имеют отношения к сущности, а реализуют прослойку для EF.
Решение: использовать собственный тип коллекции, которая позволяет добавлять/удалять объекты только внутри домена. Для этого добавим в сборку домена класс:
public class DomainCollection<T> : Collection<T>
{
[Obsolete("Not supported. Use AddItem(T item)", true)]
public new void Add(T item)
{
throw new NotSupportedException();
}
[Obsolete("Not supported. Use AddItem(T item)", true)]
public new void Insert(int index, T item)
{
throw new NotSupportedException();
}
[Obsolete("Not supported. Use RemoveItem(T item)", true)]
public new void Remove(T item)
{
throw new NotSupportedException();
}
[Obsolete("Not supported. Use RemoveItem(T item)", true)]
public new void RemoveAt(int index)
{
throw new NotSupportedException();
}
[Obsolete("Not supported. Use RemoveItem(T item)", true)]
public new void Clear()
{
throw new NotSupportedException();
}
[Obsolete("Not supported. Use AddItem(T item) or RemoveItem(T item)", true)]
public new T this[int i]
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
internal void AddItem(T item)
{
base.Add(item);
}
internal void RemoveItem(T item)
{
base.Remove(item);
}
}
Мы закрыли все методы добавления/удаления элементов коллекции и добавили два метода для использования внутри домена.
Пример использования:
public class Category
{
public Category()
{
Categories = new DomainCollection<Category>();
}
public string Name { get; protected set; }
public DomainCollection<Category> Categories { get; protected set; }
public void AddCategory(Category category)
{
if (category.Name == Name) throw new InvalidOperationException();
Categories.AddItem(category);
}
}
При попытке использовать методы по умолчанию или методы добавления/удаления за пределами домена VS сообщит нам об этом:
Данный подход не претендует на уникальность и истину в первой инстанции, но для себя я нашел решение, полностью удовлетворяющее всем требованиям. Доступ к коллекции ограничился до минимума, что уменьшает вероятность неправильного использования. Надеюсь, в скором времени EF все же будет поддерживать маппинг приватных свойств, но пока этого не случилось, из всех зол – выбираю меньшее.
Автор: LFedorov