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