Pull to refresh

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

Reading time 5 min
Views 6.9K
Привет. Приступим.

Мотивация

  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.

Ссылки

Tags:
Hubs:
+12
Comments 11
Comments Comments 11

Articles