Программист
0,0
рейтинг
16 ноября 2014 в 23:56

Разработка → Авторегистрируемые в Unity репозитории на .net для EF Code first

C#*, .NET*
Привет. Приступим.

Мотивация

  1. Есть проект с Entity framework (>= 5.0.0.0) code first.
  2. Вы любите IoC, но не любите бесконечные регистрации новых сущностей.
  3. В качестве контейнера используется Unity (или есть возможность потратить 10 минут на допиливание исходников под свой контейнер).
  4. Перспектива написания однотипного кода почему-то отпугивает вас.

Итак, что предлагает эта статья. Вы подключаете 2 nuget-пакета, реализуете для своих Entity простой интерфейс IRetrievableEntity<TEntity, TId> (можно упростить задачу, отнаследовавшись от готового класса Entity<TId>), добавляете в код 2 строки регистрации и получаете на выходе полную независимость от DBContext и возможность резолвить репозитории для каждой IRetrievableEntity-сущности с возможностью построения объектно-ориентированных (типизированных) запросов к этим репозиториям. Только посмотрите:
var employeeRepository = container.Resolve<IRepository<Emloyee, int>>();
var employees = employeeRepository.Get(q =>
{
    q = q.Filter(e => e.EmploymentDate >= new DateTime(2014, 9, 1));
    if(excludeFired)
        q = q.Filter(e => !e.Fired);
    q = q.Include(e => e.Department, p => p.Department.Chief)
            .OrderBy(p => p.FirstName);
});

Как быстро начать использовать

Можно использовать репозитории без IoC, получив бонусы построения запросов и изоляции от контекста, но следующий пример и исходники дадут исчерпывающую информацию о наиболее продуктивном и простом применении.
1. Установить пакеты Rikrop.Core.Data и Rikrop.Core.Data.Unity. Первый — в проект с Entity-сущностями, второй — в проект с контекстом БД. Я для примера использовал один проект, получилось следующее:
<packages>
  <package id="EntityFramework" version="5.0.0" targetFramework="net45" />
  <package id="Rikrop.Core.Data" version="1.0.1.0" targetFramework="net45" />
  <package id="Rikrop.Core.Data.Unity" version="1.0.1.0" targetFramework="net45" />
  <package id="Unity" version="3.5.1404.0" targetFramework="net45" />
</packages>

2. Добавить к регистрациям в IoC примерно следующее:
container.RegisterRepositoryContext<MyDbContext>();
//container.RegisterRepositoryContext(s => new MyDbContext(s), "myConStr");
container.RegisterRepositories(typeof(Department).Assembly);

RepositoryContext это обёртка над классом DBContext, соответственно, регистрация принимает generic-параметр наследника от DBContext. Можно регистрировать контекст с именем строки подключения.
Метод-расширение RegisterRepositories принимает на вход Assembly, в которой расположены POCO-объекты, реализующие IRetrievableEntity<TId>.

3. Реализовать для своих POCO IRetrievableEntity. Например:
public class Department : Entity<Int32>, IRetrievableEntity<Department, Int32> {...}
public class Employee : DeactivatableEntity<Int32>, IRetrievableEntity<Employee, Int32> {...}

4. Готово. Можно пользоваться:
var departmentRepository = container.Resolve<IRepository<Department, int>>();
departmentRepository.Save(new Department { Name = "TestDepartment" });
var testDeps = departmentRepository.Get(q => q.Filter(dep => dep.Name.Contains("Test")));

Ошибиться невозможно, поскольку generic-параметры следят за тем, чтобы резолвились правильные репозитории:
// Разрешить IDeactivatableRepository для департамента нельзя (ошибка компиляции), 
// т.к. эта сущность не относледована от DeactivatableEntity.
//var departmentRepository2 = container.Resolve<IDeactivatableRepository<Department, int>>();

5. Если стандартной фунциональности, предлагаемой интерфейсами IRepository<TEntity, in TId> и IDeactivatableRepository<TEntity, in TId> для какой-либо сущности окажется недостаточно, всегда можно расширить существующую реализацию в пару простых шагов. Задаем интерфейс:
public interface IPersonRepository : IDeactivatableRepository<Person, int>
{
    void ExtensionMethod();
}

Добавляем реализацию и обязательно помечем атрибутом:
[Repository(typeof(IPersonRepository))]
public class PersonRepository : DeactivatableRepository<Person, int>, IPersonRepository
{
    public PersonRepository(IRepositoryContext repositoryContext) 
        : base(repositoryContext)
    {
    }

    public void ExtensionMethod()
    {
        // Здесь у вас будет доступ к DBContext
        Console.WriteLine("PersonRepository ExtensionMethod called");
    }
}

Просим Unity найти и зарегистрировать все расширенные репозитории в заданной сборке:
// Пример регистрации "расширенных" репозиториев без указания их типа.
container.RegisterCustomRepositories(typeof(Department).Assembly);

Пользуемся:
// Извлечение "расширенного" репозитория по интерфейсу.
var personRepository = container.Resolve<IPersonRepository>();
personRepository.ExtensionMethod();

При этом без необходимости в расширенных методах всегда можно воспользоваться стандартной реализацией:
// Для класса Person репозиторий зарегистрирован под обоими интерфейсами, поскольку сущность наследуется от DeactivatableEntity.
var personRepository2 = container.Resolve<IRepository<Person, int>>();
var personRepository3 = container.Resolve<IDeactivatableRepository<Person, int>>();


Как это работает

Есть базовая реализация репозитория, которая работает с контекстом через абстракцию IRepositoryContext. Обращение к набору данных из репозитория работает благодаря generic-методам DBContext:
public override DbSet<TEntity> Data { get { return Context.Set<TEntity>(); } }

Ключевым классом для работы с построением запросов к репозиторию служит класс RepositoryQuery. Класс реализует fluent interface и позволяет делать Include по Expression или по текстовому пути (последнее может быть актуально при загрузке свойств дочерних коллекций, когда путь невозможно указать через expression), фильтровать, сортировать, Skip и Take.
Магия регистрации основана на Reflection. При регистрации репозиториев в сборке находятся все классы, отнаследованные от IRetrievableEntity<,>, из них достаются generic-аргументы, строятся новые типы IRepository<,> и Repository<,> с нужными generic-аргументами, дальше всё это регистрируется по свежесозданным через рефлексию типам. Для расширенных репозиториев поиск происходит по атрибуту:
foreach (var repositoryType in assembly.GetTypes().Where(type => type.IsClass))
{
    var repositoryAttribute = repositoryType.GetCustomAttribute<RepositoryAttribute>();
    if (repositoryAttribute != null)
    {
          container.RegisterType(repositoryAttribute.RepositoryInterfaceType, 
                        repositoryType, new TransientLifetimeManager());
     }
}

Проблемы

  1. Только Entity framework и только Unity. Инструмент создавался для наших личных целей и потому довольно трудно найти мотивацию к реализации, например, регистраций для других контейнеров.
  2. Сценарий подходит для использования с единственным DBContext — разные не сможет зарезолвить репозиторий. Это ограничение не распространяется на использование Rikrop.Core.Data без Rikrop.Core.Data.Unity.
  3. Фиксированная версия Unity. Если в Nuget-пакете для 4.0 не указать версию явно, то nuget попытается зарезолвить последнюю версию, несмотря на то, что она несовместима с .net 4. Если кто-нибудь знает способ избавиться от этой проблемы, просьба сообщить в личку.
  4. Только .net 4.0 и 4.5.

Ссылки

Вадим Мартынов @Vadimyan
карма
13,0
рейтинг 0,0
Программист
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (11)

  • +3
    Так и не понял, какая проблема решалась. И какая решилась, впрочем, тоже.
    • 0
      Возможно, я слишком увлёкся технической составляющей.
      1. Уход от явного использования DBContext, который используется только в регистрации.
      2. CRUD над источником данных, сразу зарегистрированный в контейнере для любой сущности, реализующей определенный интерфейс.
      3. Возможность строить нетривиальные запросы к репозиторию, которые транслируются в IQueryable.
      4. И всё это без написания кода самих репозиториев: generic даёт достаточно функциональности.

      Очень удобно для новых и особенно некрупных (до 100.000 строк) проектов, когда модель данных меняется в процессе уточнений — можно написать POCO-объект и к нему сразу появится готовый репозиторий в контейнере. Пишите только бизнес-логику, не инфраструктуру.
      • 0
        1. С какой целью
        2. Сводится в п.1
        3. Сводится к п.1
        4. Снова сводится к п.1

        Итого, какая проблама решается «уходом от явного использования DBContext»?
        • 0
          Почему не стоит использовать явно в бизнес-логике конкретную технологию доступа к данным?
          Во-первых, можно создать другую реализацию IRepository и работать с другим источником. Достаточно будет реализовать репозитории, не потребуется переписывать весь код, который использовал DBContext. Скрее всего достаточно будет поправить 2 класса — RepositoryBase и RepositoryContext.
          Во-вторых (сводится к п.1), можно легко замокать репозиторий и написать юнит-тест на бизнес-логику.
          В-третьих, простота расширения. Из реализации на GitHub я удалил часть интерфейса IRepository, связанного с использованием автомаппинга и извлечения недоменных объектов, связанных через AutoMapper с Entity, но достаточно добавить этот код в одном месте и он станет доступен для всех сущностей.

          Я согласен, что репозиторий сам по себе является спорным шаблоном, есть UoF и CQRS, но статья немного о другом.
          • 0
            > Почему не стоит использовать явно в бизнес-логике конкретную технологию доступа к данным?
            Ну так для того, чтобы не использовать конкретную технологию, достаточно абстрагировать DbContext за интерфейсом — и все.

            (все, конечно, не так прямолинейно, но в общих чертах работает)
            • 0
              не получится — интерфейс никак не описывает Lazy Loading и Change Tracking. Поэтому другая реализация интерфейса легко ломает приложение.
              • 0
                Мне чаще всего нужно работать с detached entities, поэтому и то, и другое меня обычно волнует мало.
          • +1
            Поведение Вашей реализации репозитория целиком и полностью определяется именно поведением «конкретной технологии доступа к данным» в частности, при добавления в репозиторий entity со связанными entity сохранится весь граф. Это не является типичным поведением репозитория, очевидным из его интерфейса. Наивная реализация интерфейса репозитория на другой «технологии доступа к данным» легко поломает работающее приложение.

            «Почему не стоит использовать явно в бизнес-логике конкретную технологию доступа к данным?» в чем тут проблема то? Хотите скрыть persistence от БЛ — скрывайте, но при чем тут репозитарий. Хотите чтобы любая ORM репозитарии представляла — тпк они это давно переросли, на UoW сидят уже. Хотите ужать все ORM до репозитариев и переключать их — на здоровье, если Вам репозитариев хватает. Фронт борьбы давно ушел дальше.

            Про мокание
            EF нельзя полностью замокать именно потому, что он предоставляет такую функциональность, как Change Tracking, Lazy Loading, In-Memory collections которые поощряют неявное их использование и написание полноценного теста с моками потребовало бы отслеживания кучи вызовов. Поэтому я первым делом указал, что Ваша реализация репозитория «течет» сторонними эффектами и мокание его в простых тестах не означает работоспособность в более сложном сценарии. А течет она из-за того самого change tracking в EF, который как раз и не мешает этот самый EF мокать.

            Про расширение.
            Да, действительно, если сначала образать EF, то потом получившееся можно расширить. Чем сильнее обрезать, тем проще расширить. Обычно так. Исключения встречаются тоже.

            Репозиторий вообщето совсем не спорбый шаблон. А вот занятие по превращению UoW в набор репозиториев — спорное.
  • 0
    -
  • 0
    Нашёл довольно интересные идеи о репозиториях в соседней статье. Во-первых, созданием репозиториев управляет UoW, что подразумевает использование одного контекста всегда, Во-вторых, репозиторий представляется как интерфейс доступа к IQueryable Table. Вполне вероятно, что следующая версия библиотеки эти идеи реализует.
    • 0
      Комментарии к «соседней статье», где показывается несостоятельность этой идеи, вас, видимо, не смутили?

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.