Entity Framework Code First и Domain Driven Design: защищаем коллекции

в 12:09, , рубрики: .net, DDD, entity framework, Песочница, метки: ,

В мире 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 сообщит нам об этом:
Entity Framework Code First и Domain Driven Design: защищаем коллекции
Entity Framework Code First и Domain Driven Design: защищаем коллекции

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

Автор: LFedorov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js