Привет. Приступим.
Мотивация
- Есть проект с Entity framework (>= 5.0.0.0) code first.
- Вы любите IoC, но не любите бесконечные регистрации новых сущностей.
- В качестве контейнера используется Unity (или есть возможность потратить 10 минут на допиливание исходников под свой контейнер).
- Перспектива написания однотипного кода почему-то отпугивает вас.
Итак, что предлагает эта статья. Вы подключаете 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());
}
}
Проблемы
- Только Entity framework и только Unity. Инструмент создавался для наших личных целей и потому довольно трудно найти мотивацию к реализации, например, регистраций для других контейнеров.
- Сценарий подходит для использования с единственным DBContext — разные не сможет зарезолвить репозиторий. Это ограничение не распространяется на использование Rikrop.Core.Data без Rikrop.Core.Data.Unity.
- Фиксированная версия Unity. Если в Nuget-пакете для 4.0 не указать версию явно, то nuget попытается зарезолвить последнюю версию, несмотря на то, что она несовместима с .net 4. Если кто-нибудь знает способ избавиться от этой проблемы, просьба сообщить в личку.
- Только .net 4.0 и 4.5.
Ссылки
- GitHub Rikrop.Core.Data
- GitHub Rikrop.Core.Data.Unity
- Отдельное спасибо lexwings за соавторство в коде и консультации по Unity.
Автор: Vadimyan