Repository и UnitOfWork в 2020 году

Содержание

Слайд 2

Что такое репозиторий Абстракция от хранилища данных Интерфейс для добавления и

Что такое репозиторий

Абстракция от хранилища данных
Интерфейс для добавления и удаления аналогичный

коллекциям
Поиск объектов по декларативным запросам
Слайд 3

Реализация (Вон Вернон) public class HibernateCalendarEntryRepository implements CalendarEntryRepository { @Override public

Реализация (Вон Вернон)

public class HibernateCalendarEntryRepository
implements CalendarEntryRepository {
@Override
public void add (CalendarEntry aCalendarEntry)

{
try {
this.session().saveOrUpdate(aCalendarEntry);
}
catch ( ConstraintViolationException е ) {
throw new IllegalStateException("CalendarEntry is not unique.", е);
}
}
}
Слайд 4

UnitOfWork Управляет записью изменений для набора объектов, изменяемых в одной бизнес-транзакции

UnitOfWork

Управляет записью изменений
для набора объектов,
изменяемых в одной бизнес-транзакции

Слайд 5

Опрос Кто считает, что эти паттерны актуальны (нужна своя реализация)? Кто считает, что нет?

Опрос

Кто считает, что эти паттерны актуальны (нужна своя реализация)?
Кто считает, что

нет?
Слайд 6

Какие плюсы дает репозиторий: Изоляция доступа к данным в одном месте

Какие плюсы дает репозиторий:

Изоляция доступа к данным в одном месте
Работа с

зависимыми сущностями через репозиторий агрегата
Инкапсуляция специфики SQL для конкретной базы
Простота тестирования
Слайд 7

Пример многие-ко-многим: модель public class Product { public ICollection ProductCategories {

Пример многие-ко-многим: модель

public class Product
{
public ICollection ProductCategories { get; set;

}
}
public class ProductCategory
{
public int ProductId { get; set; }
public int CategoryId { get; set; }
public Product Product { get; set; }
public Category Category { get; set; }
}
Слайд 8

Удаление var product = await _uow.ProductRepository .GetWithCategoriesAsync(request.ProductId); if (product == null)

Удаление

var product = await _uow.ProductRepository
.GetWithCategoriesAsync(request.ProductId);
if (product == null)
throw

new NotFoundException("Deleted product not found.");
// Delete categories before product
_uow.ProductCategoryRepository.RemoveRange(product.ProductCategories);
_uow.ProductRepository.Remove(product);
await _uow.SaveChangesAsync();
Слайд 9

Удаление, чего хочется var product = await _uow.ProductRepository.Remove(request.ProductId); await _uow.SaveChangesAsync();

Удаление, чего хочется

var product = await _uow.ProductRepository.Remove(request.ProductId);
await _uow.SaveChangesAsync();

Слайд 10

Корень агрегации: обновление var product = await _uow.ProductRepository .GetWithProductCategories(request.ProductId); // Delete

Корень агрегации: обновление

var product = await _uow.ProductRepository
.GetWithProductCategories(request.ProductId);
// Delete not exists

categories
// Add new categories
await _uow.SaveChangesAsync();
Слайд 11

Удаление категорий //delete not existing in DTO categories foreach (var category

Удаление категорий

//delete not existing in DTO categories
foreach (var category in product.ProductCategories

.Where(x => !newCategoryIds.Contains(x.CategoryId)))
{
product.ProductCategories.Remove(category);
}
Слайд 12

Добавление новых категорий //new categories foreach (var categoryId in newCategoryIds.Except(currentCategoryIds)) {

Добавление новых категорий

//new categories
foreach (var categoryId in
newCategoryIds.Except(currentCategoryIds))
{
product.ProductCategories.Add(new ProductCategory


{
CategoryId = categoryId,
ProductId = product.Id
});
}
Слайд 13

Обновление, чего хочется var product = await _uow.ProductRepository.Update(request.Product); await _uow.SaveChangesAsync();

Обновление, чего хочется

var product = await _uow.ProductRepository.Update(request.Product);
await _uow.SaveChangesAsync();

Слайд 14

Абстракция ;) public interface IAppUnitOfWork : IUnitOfWork { AppDbContext Context { get; } }

Абстракция ;)

public interface IAppUnitOfWork : IUnitOfWork
{
AppDbContext Context { get;

}
}
Слайд 15

Часто стремятся к такому public interface IUnitOfWork { IRepository UsersRepository {

Часто стремятся к такому

public interface IUnitOfWork
{
IRepository UsersRepository { get; }
IProductRepository ProductsRepository

{ get; }
Task SaveChangesAsync();
}
Слайд 16

Но получается примерно так public interface IUnitOfWork { IRepository UsersRepository {

Но получается примерно так

public interface IUnitOfWork
{
IRepository UsersRepository { get; }
IProductRepository ProductsRepository

{ get; }
Task SaveChangesAsync();
DbSet Users { get; }
DbSet Products { get; }
}
Слайд 17

Или вот так public interface IAvailableRepository : IRepository { Task >

Или вот так

public interface IAvailableRepository : IRepository
{
Task> GetAllAvailableAsync();
IQueryable

AllAvailable { get; } // DbSet
}
Слайд 18

И скатывается к вот-такому public interface IUnitOfWork { IQueryable Users {

И скатывается к вот-такому

public interface IUnitOfWork
{
IQueryable Users { get; }

IQueryable Products { get; }
Task SaveChanges();
}
Слайд 19

Или даже такому public interface IUnitOfWork { DbSet Users { get;

Или даже такому

public interface IUnitOfWork
{
DbSet Users { get; }
DbSet

Products { get; }
Task SaveChanges();
}
Слайд 20

Что имеем на практике Изоляция: CRUD код, который должен быть в

Что имеем на практике

Изоляция: CRUD код, который должен быть в репозитории,

протекает в вызывающий код
Создаются репозитории не только для агрегатов
SQL никто мало кто пишет
Для EF Core есть InMemory (для остальных – SQLite::memory)
Слайд 21

Реализация Repository public class Repository where T : class { protected

Реализация Repository

public class Repository where T : class
{
protected readonly DbSet

DbSet;
public Repository(AppDbContext context)
{
DbSet = context.Set();
}
}
Слайд 22

Еще одна реализация Repository public class Repository where T : class

Еще одна реализация Repository

public class Repository where T : class
{

protected readonly AppDbContext Context;
public Repository(AppDbContext context)
{
DbContext = context;
}
}
Слайд 23

И так тоже бывает ☺ public abstract class AbstractRepository { protected

И так тоже бывает ☺

public abstract class AbstractRepository {
protected

readonly AppDbContext Context;
protected AbstractRepository(AppDbContext context) {
Context = context;
}
}
public class Repository : AbstractRepository {
protected readonly DbSet DbSet;
public Repository(AppDbContext context) : base(context) {
DbSet = context.Set();
}
}
Слайд 24

Взгляд из угла ORM A DbContext instance represents a combination of

Взгляд из угла ORM

A DbContext instance represents a combination of

the Unit Of Work and Repository patterns
Слайд 25

Плюсы реализации Repository поверх ORM

Плюсы реализации Repository поверх ORM

Слайд 26

Репозиторий и ORM создает сложность Нужно писать дополнительный инфраструктурный уровень Нужно

Репозиторий и ORM создает сложность

Нужно писать дополнительный инфраструктурный уровень
Нужно думать как

сделать выборки с Include
Нужно думать как отключить ChangeTraching для запросов
Нужно думать как сделать универсальные выборки чтобы не плодить много методов
что возвращать из репозитория: IQueryable или Ienumerable
Нужно думать что использовать в контроллерах, репозитории или сервисы
А если сервис только пробрасывает методы репозитория?
Слайд 27

Ну может хотя бы запросы? public class Product { public int

Ну может хотя бы запросы?

public class Product
{
public int Quantity

{ get; set; }
public bool IsAvailable { get; set;}
public string Name { get; set; }
}
Слайд 28

Репозиторий public class ProductRepository { public async Task > GetProductsByName(string name)

Репозиторий

public class ProductRepository
{
public async Task> GetProductsByName(string name)
{
return await

_context.Products
.Where(x => x.IsAvailable && x.Quantity > 0 && // дубль
x.Name.Contains(name))
.ToListAsync();
}
public async Task> GetAllProducts()
{
return await _context.Products
.Where(x => x.IsAvailable && x.Quantity > 0) // дубль
.ToListAsync();
}
}
Слайд 29

Метод для инкапсуляции запроса protected IQueryable GetAvailableProducts() { return _context.Products.Where(x =>

Метод для инкапсуляции запроса

protected IQueryable GetAvailableProducts()
{
return _context.Products.Where(x => x.IsAvailable

&& x.Quantity > 0);
}
Слайд 30

Его использование в других запросах public async Task > GetProductsByName(string name)

Его использование в других запросах

public async Task> GetProductsByName(string name)
{
return await

GetAvailableProducts()
.Where(x => x.Name.Contains(name))
.ToListAsync();
}
public async Task> GetAllProducts()
{
return await GetAvailableProducts().ToListAsync();
}
Слайд 31

Спецификация – классика public interface ISpecification { bool IsSatisfiedBy(T obj); }

Спецификация – классика

public interface ISpecification
{
bool IsSatisfiedBy(T obj);
}

Слайд 32

Universal.Autofilter спецификация public class Product { public static Spec AvailableSpec =

Universal.Autofilter спецификация

public class Product
{
public static Spec AvailableSpec =
new

Spec(x => x.IsAvailable && x.Quantity > 0);
public static Spec ByNameSpec(string name)
{
return new Spec(x => x.Name.Contains(name));
}
}
https://github.com/denis-tsv/AutoFilter
Слайд 33

Все продукты public class ProductController { public async Task > GetAllProducts()

Все продукты

public class ProductController
{
public async Task> GetAllProducts()
{
return await

_context.Products
.Where(Product.AvailableSpec)
.ToListAsync();
}
}
Слайд 34

Комбинация спецификаций public async Task > GetProductsByName(string name) { return await

Комбинация спецификаций

public async Task> GetProductsByName(string name)
{
return await _context.Products
.Where(Product.AvailableSpec &&

Product.ByNameSpec(name))
.ToListAsync();
}
Слайд 35

LinqSpec – класс для каждой спецификации public abstract class Specification {

LinqSpec – класс для каждой спецификации

public abstract class Specification
{
public abstract

Expression> ToExpression();
}
Слайд 36

Реализация LinqSpec public class AvailableProductSpecification : Specification { public override Expression

Реализация LinqSpec

public class AvailableProductSpecification : Specification {
public override Expression> ToExpression()

{
return x => x.IsAvailable && x.Quantity > 0;
}
}
public class ProductByNameSpecification : Specification {
private string _name;
public ProductByNameSpecification(string name) {
_name = name;
}
public override Expression> ToExpression() {
return x => x.Name.Contains(_name);
}
}
Слайд 37

Репозиторий Не нужен как абстракция источника данных Не нужен для избавления от дублирования в запросах

Репозиторий

Не нужен как абстракция источника данных
Не нужен для избавления от дублирования

в запросах
Слайд 38

Чистая архитектура Дядя Боб: ORM – это инфраструктура, он которой нужно

Чистая архитектура

Дядя Боб: ORM – это инфраструктура, он которой нужно абстрагироваться.
Ага,

может быть новая роль Repository и UnitOfWork – абстракция для ORM, а не базы?
Слайд 39

Получение списка через ORM internal class GetProductsQueryHandler { private readonly AppDbContext

Получение списка через ORM

internal class GetProductsQueryHandler
{
private readonly AppDbContext _context;
public

GetProductsQueryHandler(AppDbContext context)
{
_context = context;
}
public async Task> HandleAsync(GetProductsQuery query)
{
return await _context.Products.ToListAsync();
}
}
Слайд 40

Получение списка через репозиторий public interface IUnitOfWork { IQueryable Products {

Получение списка через репозиторий

public interface IUnitOfWork
{
IQueryable Products { get; }
}
public

class EFUnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
public EFUnitOfWork(AppDbContext context)
{
_context = context;
}
public IQueryable Products => _context.Products;
}
Слайд 41

Хендлер с UnitOfWork вместо контекта internal class GetProductsQueryHandler { private readonly

Хендлер с UnitOfWork вместо контекта

internal class GetProductsQueryHandler
{
private readonly IUnitOfWork _uow;

public GetProductsQueryHandler(IUnitOfWork uow)
{
_uow = uow;
}
public async Task> HandleAsync(GetProductsQuery query)
{
return await _uow.Products.ToListAsync();
}
}
Слайд 42

Не все так просто ☹ using Microsoft.EntityFrameworkCore; internal class GetProductsQueryHandler {

Не все так просто ☹

using Microsoft.EntityFrameworkCore;
internal class GetProductsQueryHandler
{
public async Task>

HandleAsync(GetProductsQuery query)
{
return await _uof.Products.ToListAsync();
}
}
Слайд 43

Как переопределить Extension Method? Новый класс QueryableExecutor Service Locator

Как переопределить Extension Method?

Новый класс QueryableExecutor
Service Locator

Слайд 44

QuerableExecutor public interface IQueryableExecutor { Task > ToListAsync (IQueryable source); //SingleAsync

QuerableExecutor

public interface IQueryableExecutor
{
Task> ToListAsync(IQueryable source);
//SingleAsync
//и остальные асинхронные методы

по необходимости
}
public class QueryableExecutor : IQueryableExecutor
{
public async Task> ToListAsync(IQueryable source)
{
return EntityFrameworkQueryableExtensions.ToListAsync(source);
}
}
Слайд 45

Хендлер с IQueryableExecutor internal class GetProductsQueryHandler { public GetProductsQueryHandler(IUnitOfWork uow, IQueryableExecutor

Хендлер с IQueryableExecutor

internal class GetProductsQueryHandler
{
public GetProductsQueryHandler(IUnitOfWork uow,
IQueryableExecutor executor)

{
_uow = uow;
_executor = executor;
}
public async Task> HandleAsync(GetProductsQuery query)
{
var query = _uof.Products;
return await _executor.ToListAsync(query);
}
}
Слайд 46

Не забываем про тестирование public class InMemoryQueryableExecutor : IQueryableExecutor { public

Не забываем про тестирование

public class InMemoryQueryableExecutor : IQueryableExecutor
{
public async Task>

ToListAsync(IQueryable source)
{
return source.ToList();
}
//SingleAsync итд
}
Слайд 47

ServiceLocator и новые extension методы public static class QueryableExtensions { //Инициализация

ServiceLocator и новые extension методы

public static class QueryableExtensions
{
//Инициализация в конфигурации

приложения или тесте
public static IQueryableExecutor QueryableExecutor { get; set; }
public static Task> ToListAsync(this IQueryable source)
{
return QueryableExecutor.ToListAsync(source);
}
//SingleAsync итд
}
Слайд 48

Хендлер без QuerableExecutor using Infrastructure.Interfaces; internal class GetProductsQueryHandler { private readonly

Хендлер без QuerableExecutor

using Infrastructure.Interfaces;
internal class GetProductsQueryHandler
{
private readonly IUnitOfWork _uof;
public

GetProductsQueryHandler(IUnitOfWork uof)
{
_uof = uof;
}
public async Task> HandleAsync(GetProductsQuery query)
{
return await _uof.Products.ToListAsync();
}
}
Слайд 49

При миграции на другой ORM //EF 6 context.Blogs .Include(b => b.Posts.Select(p

При миграции на другой ORM

//EF 6
context.Blogs
.Include(b => b.Posts.Select(p => p.Comments))

.ToList();
//EF Core
context.Blogs
.Include(b => b.Posts).ThenInclude(p => p.Comments)
.ToList();
//EF 6 и EF Core
context.Blogs.Include(“Posts.Comments”).ToList();
Слайд 50

Миграция на другой ORM Надо учитывать API тех ORM, между которыми

Миграция на другой ORM

Надо учитывать API тех ORM, между которыми хотим

заложить возможность перехода
Надо продать эти задачи менеджеру/заказчику ;)
Слайд 51

Итого UnitOfWork для абстракция ORM Не нужна, если не планируется переход

Итого UnitOfWork для абстракция ORM

Не нужна, если не планируется переход на

другой ORM
Переход на другой ORM не планируется никогда ☺
Слайд 52

Как сделать DAL без Repository и UnitOfWork Сборка Infrastructure.Interfaces - интерфейс

Как сделать DAL без Repository и UnitOfWork

Сборка Infrastructure.Interfaces - интерфейс IDbContext


Нет реализации OnModelCreating, зависимой от базы
По возможности чистая архитектура (без EFCore.MsSql)
Все нужные свойства и методы EF контекста (ChangeTracker, DbSet)
Сборка DataAccess.MsSql (Postgres, …) – то, что зависит от базы
AppDbContext - реализация интерфейса IDbContext
Миграции, так их проще добавлять
Дублирующиеся запросы – спецификации и их комбинации
Слайд 53

Infrastructure.Interfaces public interface IDbContext { DbSet Products { get; } Task

Infrastructure.Interfaces

public interface IDbContext
{
DbSet Products { get; }
Task SaveChangesAsync
(CancellationToken

cancellationToken = default);
}
Слайд 54

Или два контекста для Read и CUD public interface IReadDbContext {

Или два контекста для Read и CUD

public interface IReadDbContext
{

DbSet Products { get; }
}
public interface IDbContext : IReadDbContext
{
ChangeTracker ChangeTracker { get; }
Task SaveChangesAsync();
}
Слайд 55

DataAccess.MsSql public class AppDbContext : IDbContext { DbSet Products { get;

DataAccess.MsSql

public class AppDbContext : IDbContext
{
DbSet Products { get; set; }
protected

override void OnModelCreating
(ModelBuilder builder)
{
//
}
}
Слайд 56

Если нужны EF.Functions (полнотекстовый поиск) Нужна ли поддержка нескольких баз одновременно?

Если нужны EF.Functions (полнотекстовый поиск)

Нужна ли поддержка нескольких баз одновременно?
Да
Делаем абстракции

и свои реализации для каждой базы
Нет
Обходимся без оберток ☺
При переходе на другую базу – переписываем
Слайд 57

Если дублируется логика сохранения Инициализация ChangedAt+ChangedBy Перегрузка SaveChanges у контекста Пост-процессор в пайплайне обработки запроса (MediatR)

Если дублируется логика сохранения

Инициализация ChangedAt+ChangedBy
Перегрузка SaveChanges у контекста
Пост-процессор в пайплайне обработки

запроса (MediatR)
Слайд 58

Модель данных public class AuditableEntity { public DateTime CreatedAt { get;

Модель данных

public class AuditableEntity
{
public DateTime CreatedAt { get; set; }

public int CreatedBy { get; set; }
public DateTime? ModifiedAt { get; set; }
public int? ModifiedBy { get; set; }
}
public class Entity : AuditableEntity
{
}
Слайд 59

Перегрузка SaveChanges у контекста public override Task SaveChangesAsync() { foreach (var

Перегрузка SaveChanges у контекста

public override Task SaveChangesAsync() {
foreach (var entry

in ChangeTracker.Entries()) {
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedBy = _currentUserService.UserId;
entry.Entity.CreatedAt = _dateTime.Now;
break;
case EntityState.Modified:
entry.Entity.ModifiedBy = _currentUserService.UserId;
entry.Entity.ModifiedAt = _dateTime.Now;
break;
}
}
return base.SaveChangesAsync(cancellationToken);
}
Слайд 60

Интерфейс для отметки об изменениях public interface IChangeDataRequest { } public

Интерфейс для отметки об изменениях

public interface IChangeDataRequest
{
}
public class ChangeEntityRequest : IReqiest,

IChangeDataRequest
{
}
public class PostProcessor :
IRequestPostProcessor
where TRequest : IChangeDataRequest
{
}
Слайд 61

Хендлер обновления Entity (MediatR) public class ChangeEntityHandler : IRequestHandler { public

Хендлер обновления Entity (MediatR)

public class ChangeEntityHandler :
IRequestHandler
{
public async Task

Handle(ChangeEntityRequest request)
{
var entity = await _context.FindAsync(request.Id);
Mapper.Map(request, entity);
//не вызываем SaveChanges
}
}
Слайд 62

Пост-процессор IChangeDataRequest запросов public async Task Process(TRequest request, TResponse response) {

Пост-процессор IChangeDataRequest запросов

public async Task Process(TRequest request, TResponse response)
{

_context.ChangeTracker.Entries().ToList()
.ForEach(x => {
if (x.State == EntityState.Added)
{
x.Entity.CreatedBy = _currentUserService.UserId;
x.Entity.CreatedAt = _dateTime.Now;
}
if (x.State == EntityState.Modified)
{
x.Entity.ModifiedBy = _currentUserService.UserId;
x.Entity.ModifiedAt = _dateTime.Now;
}
});
await _context.SaveChangesAsync();
}
Слайд 63

Итого, отказ от Repository и UnitOfWork Избавляет от мук выбора: Использовать

Итого, отказ от Repository и UnitOfWork

Избавляет от мук выбора:
Использовать в контроллерах

репозитории или сервисы
Возвращать из репозитория IQueryable или IEnumerable
Как сделать универсальные запросы вместо множества методов
Итд
Избавляет от дополнительного слоя абстракций, который:
Протекает
Не привносит ничего полезного
Требует времени и сил на разработку
Слайд 64

А что говорит Вон Вернон? От репозиториев будет польза только если

А что говорит Вон Вернон?

От репозиториев будет польза только если у

вас есть агрегаты
Если нет агрегатов – используйте DAO (CRUD для таблиц)
Именно это делает ORM
Логика типа каскадного удаления в репозитории – спорный вопрос ☺
Автору больше нравится помещать ее туда
Но это его личный выбор!
Полезный кейс – отношения 1-1 между таблицами
Не настроить каскадное удаление
На практике редко встречается
Слайд 65

Мораль Не только пишем код по образцам дядек из умных книжек

Мораль

Не только пишем код по образцам дядек из умных книжек
Но читаем

комментарии к нему ;)
И думаем своей головой!
Слайд 66

Полезные ссылки по теме Что такое репозиторий Фаулер , Эванс Спецификация

Полезные ссылки по теме

Что такое репозиторий
Фаулер , Эванс
Спецификация
AutoFilter, не нужен

отдельный класс для спецификации, есть в nuget
LinqSpec, отдельный класс для спецификации, есть в nuget
Доклад Максима Аршинова на DotNext про Linq в Enterprise
Cross-cutting concerns
Перегрузка SaveChanges у контекста
Аршинов, доклад Быстрорастворимое проектирование про декораторы
MediatR – пайплайн путем цепочки вывозов методов, nuget пакет
Cqrs In Practiсe – пример велика для пайплайна из декораторов
Слайд 67

Холивар про репозиторий Нет – автор книги «EF Core in Action»

Холивар про репозиторий

Нет – автор книги «EF Core in Action»
Нет –

автор EntityFramework.CommonTools
Нет – Jason Taylor (он говорит, что автор MediatR тоже против)
Да – Владимир Хориков, в блоге часто встречается репозиторий
Да – ведущий разработчик Бындю софт
Слайд 68

Пример проекта с репозиториями и без них https://github.com/denis-tsv/DataAccessWithoutRepositoryAndUnitOfWork http://bit.ly/no-repository

Пример проекта с репозиториями и без них

https://github.com/denis-tsv/DataAccessWithoutRepositoryAndUnitOfWork
http://bit.ly/no-repository

Слайд 69

Опрос Кто изменил мнение и считает, что он Repoisitory и UnitOfWork

Опрос

Кто изменил мнение и считает, что он Repoisitory и UnitOfWork больше

не нужны?
А кто остался при своем и думает что они нужны?
Слайд 70

Вопрос на подумать ☺ public class UpdateProductCommandHandler : IRequestHandler { protected

Вопрос на подумать ☺

public class UpdateProductCommandHandler : IRequestHandler
{
protected override async

Task Handle(UpdateProductCommand request)
{
var product = await _dbContext.Products.FindAsync(request.ProductId);
_mapper.Map(request.ProductDto, product);
await _dbContext.SaveChangesAsync();
}
}