Привет. Приступим.
Итак, что предлагает эта статья. Вы подключаете 2 nuget-пакета, реализуете для своих Entity простой интерфейс IRetrievableEntity<TEntity, TId> (можно упростить задачу, отнаследовавшись от готового класса Entity<TId>), добавляете в код 2 строки регистрации и получаете на выходе полную независимость от DBContext и возможность резолвить репозитории для каждой IRetrievableEntity-сущности с возможностью построения объектно-ориентированных (типизированных) запросов к этим репозиториям. Только посмотрите:
1. Установить пакеты Rikrop.Core.Data и Rikrop.Core.Data.Unity. Первый — в проект с Entity-сущностями, второй — в проект с контекстом БД. Я для примера использовал один проект, получилось следующее:
2. Добавить к регистрациям в IoC примерно следующее:
RepositoryContext это обёртка над классом DBContext, соответственно, регистрация принимает generic-параметр наследника от DBContext. Можно регистрировать контекст с именем строки подключения.
Метод-расширение RegisterRepositories принимает на вход Assembly, в которой расположены POCO-объекты, реализующие IRetrievableEntity<TId>.
3. Реализовать для своих POCO IRetrievableEntity. Например:
4. Готово. Можно пользоваться:
Ошибиться невозможно, поскольку generic-параметры следят за тем, чтобы резолвились правильные репозитории:
5. Если стандартной фунциональности, предлагаемой интерфейсами IRepository<TEntity, in TId> и IDeactivatableRepository<TEntity, in TId> для какой-либо сущности окажется недостаточно, всегда можно расширить существующую реализацию в пару простых шагов. Задаем интерфейс:
Добавляем реализацию и обязательно помечем атрибутом:
Просим Unity найти и зарегистрировать все расширенные репозитории в заданной сборке:
Пользуемся:
При этом без необходимости в расширенных методах всегда можно воспользоваться стандартной реализацией:
Ключевым классом для работы с построением запросов к репозиторию служит класс RepositoryQuery. Класс реализует fluent interface и позволяет делать Include по Expression или по текстовому пути (последнее может быть актуально при загрузке свойств дочерних коллекций, когда путь невозможно указать через expression), фильтровать, сортировать, Skip и Take.
Магия регистрации основана на Reflection. При регистрации репозиториев в сборке находятся все классы, отнаследованные от IRetrievableEntity<,>, из них достаются generic-аргументы, строятся новые типы IRepository<,> и Repository<,> с нужными generic-аргументами, дальше всё это регистрируется по свежесозданным через рефлексию типам. Для расширенных репозиториев поиск происходит по атрибуту:
Мотивация
- Есть проект с 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.