EntityFramework: (анти)паттерн Repository

Repository Pattern
Репозиторий является посредником между слоем доступа к данным и доменным слоем,
работая как in-memory коллекция доменных обектов. Клиенты создают декларативные
описания запросов и передают их в репозиторий для выполнения.
  — свободный перевод Мартина Фаулера

EntityFraemwork предоставляет нам готовую реализацию паттернов Repository: DbSet<T> и UnitOfWork: DbContext. Но мне часто приходится видеть, как коллеги используют в своих проектах собственную реализацию репозиториев поверх существующих в EntityFraemwork.


Чаще всего используется один из двух подходов:


  1. Generic Repository как попытка абстрагироваться от конкретного ORM.
  2. Repository как набор запросов к выбранной таблице БД (паттерн DAO).

И каждый из этих подходов содержит недостатки.


Generic Repository


При обсуждении архитектуры нового проекта часто звучит вопрос: "А вдруг мы захотим сменить ORM?". И ответом на него обычно бывает предложение: "А давайте сделаем Generic Repository, который будет инкапсулировать в себе взаимодействие с конкретной технологией доступа к данным".


И вот у нас появляется новый слой абстракции, который переводит общеизвестное, хорошо спроектированное и задокументированное API любимого ORM в наше кастомное, спроектированное "на коленке" API без документации.


Типичный интерфейс репозитория выглядит так:


public interface IRepository<T>
{
    T Get(int id);
    void Add(T entity);
    void Remove(T entity);
    IEnumerable<T> GetAll();

    // + какие-то специфичные вещи навроде
    IEnumerable<T> Filter(ICriteria<T> criteria);
    void Load(T entity, Expression<Func<T, TProperty>> property);
}

Зато теперь можно спокойно сменить ORM, если это вдруг понадобится.


На самом деле – нет! А вдруг, при реализации нашего чудо-репозитория мы воспользовались уникальными особенностями конкретного ORM? И при миграции на новый ORM нам придется городить костыли в слое бизнес-логики, чтобы как-то эмулировать то, что предыдущий ORM предоставлял "из коробки".


Получается, чтобы обеспечить спокойную миграцию, мы должны заранее написать реализации нашего репозитория под все популярные библиотеки.


Таким образом, чтобы написать Generic Repository нужно:


  1. Собраться с мыслями.
  2. Спроектировать интерфейс.
  3. Написать реализацию под выбранный в проекте ORM.
  4. Написать реализацию под альтернативные ORM.
  5. Удалить из интерфейса все уникальные фичи каждой библиотеки.
  6. Объяснить товарищам по команде, почему они не могут теперь пользоваться уникальными фичами любимого ORM.
  7. Поддерживать реализации под разные ORM в актуальном состоянии. Ведь фреймворки тоже развиваются!
  8. Объяснить менеджеру, почему Вы тратите на это время вместо выполнения непосредственных задач.

К счастью есть люди, которые уже сделали это за нас. И если Вам действительно необходимо быть независимыми от ORM, Вы можете воспользоваться одной из готовых реализаций. Например из проекта ASP.NET Boilerplate. Помимо Repository в нем есть еще много чего интересного.


Но лучше оставить все как есть. Ведь IDbSet<T> уже содержит весь набор CRUD операций. А так же много чего еще (включая асинхронные операции) за счет наследования от IQueryable<T>.


Repository как набор запросов


Часто люди ошибочно называют репозиторием реализацию другого паттерна — Data Access Object. Либо оба этих паттерна реализуются одним и тем же классом. Тогда в дополнение к CRUD-операциям появляются методы: GetByLogin(), GetByName(), etc.


Пока проект молодой, у нас все хорошо. Запросы лежат по соответствующим файлам. Код структурирован. Но, по мере роста проекта, добавляются новые фичи. А значит и новые запросы. Репозитории пухнут и превращаются в неподдерживаемые чудовища.


Потом появляются запросы, которые джойнят несколько таблиц, и возвращают Data Transfer Object, а не доменный объект. И возникает вопрос, а в какой же репозиторий такие запросы запихнуть? А все потому, что при группировке запросов по таблицам БД, а не по фичам бизнес-логики, нарушается SRP.


В дополнение к этому, методы DAO обладают и другими недостатками:


  • Их трудно тестировать.

Хотя в EF Core эту проблему попытались решить с помощью in-memory DbContext.


  • Они не поддерживают переиспользование и композицию.

Например, если у нас есть интерфейс DAO:


public interface IPostsRepository
{
    IEnumerable<Post> FilterByDate(DateTime date);
    IEnumerable<Post> FilterByTag(string tag);
    IEnumerable<Post> FilterByDateAndTag(DateTime date, string tag);
}

То для реализации FilterByDateAndTag() мы не можем использовать два предыдущих метода.


Так что же делать?


Использовать паттерны Query Builder и Specification.


.NET предоставляет готовую реализацию паттерна Query Builder: IQueryable<T> и набор методов-расширений LINQ.


Давайте проанализируем запросы в нашем проекте. По закону Паретто, 80% запросов будут


  • либо поиском сущности по id: context.Entities.Find(id),
  • либо фильтрацией по единственному полю:
    context.Entities.Where(e => e.Property == value).

Из оставшихся 20% существенная часть будет уникальна для каждого отдельного бизнес-кейса. Такие запросы можно спокойно оставлять внутри сервисов бизнес-логики.


И только по мере рефакторинга следует выносить повторяющиеся части запросов в extension-методы к IQueryable<T>. А повторяющиеся условия — в спецификации.


Specification


Спецификация представляет правила бизнес-логики в виде булевского предиката, принимающего на вход доменную сущность. Таким образом, спецификации поддерживают композицию с помощью булевских операторов.


Фаулери и Эванс определяют спецификацию как:


public interface ISpecification<T>
{
    bool IsSatisfiedBy(T entity);
}

Но такие спецификации невозможно использовать с IQueryable<T>.


В LINQ to Entities в качестве спецификаций используется Expression<Func<T, bool>>. Но такие выражения нельзя комбинировать с помощью булевских операторов и использовать в LINQ to Objects.


Попробуем совместить оба подхода. Добавим метод ToExpression():


public interface ISpecification<T>
{
    bool IsSatisfiedBy(T entity);
    Expression<Func<T, bool>> ToExpression();
}

И базовый класс Specificaiton<T>:


public class Specification<T> : ISpecification<T>
{
    private Func<T, bool> _function;

    private Func<T, bool> Function => _function
        ?? (_function = Predicate.Compile());

    protected Expression<Func<T, bool>> Predicate;

    protected Specification() { }

    public Specification(Expression<Func<T, bool>> predicate)
    {
        Predicate = predicate;
    }

    public bool IsSatisfiedBy(T entity)
    {
        return Function.Invoke(entity);
    }

    public Expression<Func<T, bool>> ToExpression()
    {
        return Predicate;
    }
}

Теперь нам необходимо переопределить булевские операторы &&, || и !. Для этого придется делать достаточно странные вещи. Согласно C# Language Specification [7.11.2], если переопределить операторы: true, false, & и |, то вместо && будет вызван &, а вместо || будет вызван |.


Specification.cs
public static implicit operator Func<T, bool>(Specification<T> spec)
{
    return spec.Function;
}

public static implicit operator Expression<Func<T, bool>>(Specification<T> spec)
{
    return spec.Predicate;
}

public static bool operator true(Specification<T> spec)
{
    return false;
}

public static bool operator false(Specification<T> spec)
{
    return false;
}

public static Specification<T> operator !(Specification<T> spec)
{
    return new Specification<T>(
        Expression.Lambda<Func<T, bool>>(
            Expression.Not(spec.Predicate.Body),
            spec.Predicate.Parameters));
}

public static Specification<T> operator &(Specification<T> left, Specification<T> right)
{
    var leftExpr = left.Predicate;
    var rightExpr = right.Predicate;
    var leftParam = leftExpr.Parameters[0];
    var rightParam = rightExpr.Parameters[0];

    return new Specification<T>(
        Expression.Lambda<Func<T, bool>>(
            Expression.AndAlso(
                leftExpr.Body,
                new ParameterReplacer(rightParam, leftParam).Visit(rightExpr.Body)),
            leftParam));
}

public static Specification<T> operator |(Specification<T> left, Specification<T> right)
{
    var leftExpr = left.Predicate;
    var rightExpr = right.Predicate;
    var leftParam = leftExpr.Parameters[0];
    var rightParam = rightExpr.Parameters[0];

    return new Specification<T>(
        Expression.Lambda<Func<T, bool>>(
            Expression.OrElse(
                leftExpr.Body,
                new ParameterReplacer(rightParam, leftParam).Visit(rightExpr.Body)),
            leftParam));
}

Также нам понадобится подменить аргумент у одного из выражений с помощью ExpressionVisitor:


ParameterReplacer.cs
internal class ParameterReplacer : ExpressionVisitor
{
    readonly ParameterExpression _parameter;
    readonly ParameterExpression _replacement;

    public ParameterReplacer(ParameterExpression parameter, ParameterExpression replacement)
    {
        _parameter = parameter;
        _replacement = replacement;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return base.VisitParameter(_parameter == node ? _replacement : node);
    }
}

И преобразовать Specification<T> в Expresison для использования внутри других выражений:


SpecificationExpander.cs
public class SpecificationExpander : ExpressionVisitor
{
    protected override Expression VisitUnary(UnaryExpression node)
    {
        if (node.NodeType == ExpressionType.Convert)
        {
            MethodInfo method = node.Method;

            if (method != null && method.Name == "op_Implicit")
            {
                Type declaringType = method.DeclaringType;

                if (declaringType.GetTypeInfo().IsGenericType
                    && declaringType.GetGenericTypeDefinition() == typeof(Specification<>))
                {
                    const string name = nameof(Specification<object>.ToExpression);

                    MethodInfo toExpression = declaringType.GetMethod(name);

                    return ExpandSpecification(node.Operand, toExpression);
                }
            }
        }

        return base.VisitUnary(node);
    }

    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        MethodInfo method = node.Method;

        if (method.Name == nameof(ISpecification<object>.ToExpression))
        {
            Type declaringType = method.DeclaringType;
            Type[] interfaces = declaringType.GetTypeInfo().GetInterfaces();

            if (interfaces.Any(i => i.GetTypeInfo().IsGenericType
                && i.GetGenericTypeDefinition() == typeof(ISpecification<>)))
            {
                return ExpandSpecification(node.Object, method);
            }
        }

        return base.VisitMethodCall(node);
    }

    private Expression ExpandSpecification(Expression instance, MethodInfo toExpression)
    {
        return Visit((Expression)GetValue(Expression.Call(instance, toExpression)));
    }

    // http://stackoverflow.com/a/2616980/1402923
    private static object GetValue(Expression expression)
    {
        var objectMember = Expression.Convert(expression, typeof(object));
        var getterLambda = Expression.Lambda<Func<object>>(objectMember);
        return getterLambda.Compile().Invoke();
    }
}

Теперь мы сможем


— тестировать наши спецификации:


public class UserIsActiveSpec : Specification<User>
{
    public UserIsActiveSpec()
    {
        Predicate = u => u.IsActive;
    }
}

var spec = new UserIsActiveSpec();
var user = new User { IsActive = true };

Assert.IsTrue(spec.IsSatisfiedBy(user));

— комбинировать наши спецификации:


public class UserByLoginSpec : Specification<User>
{
    public UserByLoginSpec(string login)
    {
        Predicate = u => u.Login == login;
    }
}

public class UserCombinedSpec : Specification<User>
{
    public UserCombinedSpec(string login)
        : base(new UserIsActive() && new UserByLogin(login))
    {
    }
}

— использовать их в LINQ to Entities:


var spec = new UserByLoginSpec("admin");

context.Users.Where(spec.ToExpression());

// или даже так (за счет implicit conversion в Expression)
context.Uses.Where(new UserByLoginSpec("admin") || new UserByLoginSpec("user"));

Если Вам не нравится магия с операторами, Вы можете воспользоваться готовой реализацией Specification из ASP.NET Boilerplate. Или использовать PredicateBuilder из LinqKit.


Методы расширения к IQueryable


Альтернативой спецификациям могут послужить методы-расширения к IQueryable<T>. Например:


public static IQueryable<Post> FilterByAuthor(
    this IQueryable<Post> posts, int authorId)
{
    return posts.Where(p => p.AuthorId = authorId);
}

public static IQueryable<Comment> FilterTodayComments(
    this IQueryable<Comment> comments)
{
    DateTime today = DateTime.Now.Date;

    return comments.Where(c => c.CreationTime > today)
}

Comment[] comments = context.Posts
    .FilterByAuthor(authorId)    // it's OK
    .SelectMany(p => p.Comments
        .AsQueryable()
        .FilterTodayComments())  // will throw Exception
    .ToArray();

Проблема здесь в том, что если первый extension-метод сработает как ожидается, то для второго будет выброшен Exception. Потому что он вызывается внутри Expression Tree переданного в SelectMany(). А LINQ to Entities не может это обработать.


Попытаемся исправить ситуацию. Для этого нам потребуется:


  • ExpressionVisitor, который раскроет наши extension-методы.
  • Декоратор для IQueryable<T>, который вызовет наш ExpressionVisitor.
  • Метод расширения AsExpandable(), который обернет IQueryable<T> в наш декоратор.
  • Аттрибут [Expandable], котоым мы будем помечать extension-методы для раскрытия.
    Ведь Where() или Select() тоже extension-методы, а их раскрывать не надо.

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ExpandableAttribute : Attribute { }

QueryableExtensions.cs
public static IQueryable<T> AsExpandable<T>(this IQueryable<T> queryable)
{
    return queryable.AsVisitable(new ExtensionExpander());
}

public static IQueryable<T> AsVisitable<T>(
    this IQueryable<T> queryable, params ExpressionVisitor[] visitors)
{
    return queryable as VisitableQuery<T>
        ?? VisitableQueryFactory<T>.Create(queryable, visitors);
}

Теперь нам необходимо реализовать интерфейсы IQueryable<T> и IQueryProvider:


public interface IQueryable<T>
{
    IEnumerator GetEnumerator();     // from IEnumerable
    IEnumerator<T> GetEnumerator();  // from IEnumerable<T>
    Type ElementType { get; }        // from IQueryable
    Expression Expression { get; }   // from IQueryable
    IQueryProvider Provider { get; } // from IQueryable
}

VisitableQuery.cs
internal class VisitableQuery<T> : IQueryable<T>, IOrderedQueryable<T>, IOrderedQueryable
{
    readonly ExpressionVisitor[] _visitors;
    readonly IQueryable<T> _queryable;
    readonly VisitableQueryProvider<T> _provider;

    internal ExpressionVisitor[] Visitors => _visitors;
    internal IQueryable<T> InnerQuery => _queryable;

    public VisitableQuery(IQueryable<T> queryable, params ExpressionVisitor[] visitors)
    {
        _queryable = queryable;
        _visitors = visitors;
        _provider = new VisitableQueryProvider<T>(this);
    }

    Expression IQueryable.Expression => _queryable.Expression;

    Type IQueryable.ElementType => typeof(T);

    IQueryProvider IQueryable.Provider => _provider;

    public IEnumerator<T> GetEnumerator()
    {
        return _queryable.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return _queryable.GetEnumerator();
    }
}

public interface IQueryProvider
{
    IQueryable CreateQuery(Expression expression);
    IQueryable<TElement> CreateQuery<TElement>(Expression expression);
    object Execute(Expression expression);
    TResult Execute<TResult>(Expression expression);
}

VisitableQueryProvider.cs
internal class VisitableQueryProvider<T> : IQueryProvider
{
    readonly VisitableQuery<T> _query;

    public VisitableQueryProvider(VisitableQuery<T> query)
    {
        _query = query;
    }

    IQueryable<TElement> IQueryProvider.CreateQuery<TElement>(Expression expression)
    {
        expression = _query.Visitors.Visit(expression);
        return _query.InnerQuery.Provider
            .CreateQuery<TElement>(expression)
            .AsVisitable(_query.Visitors);
    }

    IQueryable IQueryProvider.CreateQuery(Expression expression)
    {
        expression = _query.Visitors.Visit(expression);
        return _query.InnerQuery.Provider.CreateQuery(expression);
    }

    TResult IQueryProvider.Execute<TResult>(Expression expression)
    {
        expression = _query.Visitors.Visit(expression);
        return _query.InnerQuery.Provider.Execute<TResult>(expression);
    }

    object IQueryProvider.Execute(Expression expression)
    {
        expression = _query.Visitors.Visit(expression);
        return _query.InnerQuery.Provider.Execute(expression);
    }
}

VisitorExtensions.cs
internal static class VisitorExtensions
{
    public static Expression Visit(this ExpressionVisitor[] visitors, Expression node)
    {
        if (visitors != null)
        {
            foreach (var visitor in visitors)
            {
                node = visitor.Visit(node);
            }
        }
        return node;
    }
}

Но тут есть одна маленькая особенность. Для подержки асинхронных операций, таких как ToListAsync(), EntityFramework и EF Core определяют дополнительные интерфейсы: IDbAsyncEnumerable<T> и IAsyncEnumerable<T>. Поэтому лучше воспользоваться готовой реализацией. Она сделана на основе ExpandableQuery из LinqKit, но позволяет использовать любой ExpressionVisitor.


И, наконец, сам ExpressionVisitor:


ExtensionExpander.cs
public class ExtensionExpander : ExpressionVisitor
{
    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        MethodInfo method = node.Method;

        if (method.IsDefined(typeof(ExtensionAttribute), true)
            && method.IsDefined(typeof(ExpandableAttribute), true))
        {
            ParameterInfo[] methodParams = method.GetParameters();
            Type queryableType = methodParams.First().ParameterType;
            Type entityType = queryableType.GetGenericArguments().Single();

            object inputQueryable = MakeEnumerableQuery(entityType);

            object[] arguments = new object[methodParams.Length];

            arguments[0] = inputQueryable;

            var argumentReplacements = new List<KeyValuePair<string, Expression>>();

            for (int i = 1; i < methodParams.Length; i++)
            {
                try
                {
                    arguments[i] = GetValue(node.Arguments[i]);
                }
                catch (InvalidOperationException)
                {
                    ParameterInfo paramInfo = methodParams[i];
                    Type paramType = paramInfo.GetType();

                    arguments[i] = paramType.GetTypeInfo().IsValueType
                        ? Activator.CreateInstance(paramType) : null;

                    argumentReplacements.Add(
                        new KeyValuePair<string, Expression>(paramInfo.Name, node.Arguments[i]));
                }
            }

            object outputQueryable = method.Invoke(null, arguments);

            Expression expression = ((IQueryable)outputQueryable).Expression;

            Expression realQueryable = node.Arguments[0];

            if (!typeof(IQueryable).IsAssignableFrom(realQueryable.Type))
            {
                MethodInfo asQueryable = _asQueryable.MakeGenericMethod(entityType);
                realQueryable = Expression.Call(asQueryable, realQueryable);
            }

            expression = new ExtensionRebinder(
                inputQueryable, realQueryable, argumentReplacements).Visit(expression);

            return Visit(expression);
        }
        return base.VisitMethodCall(node);
    }

    private static object MakeEnumerableQuery(Type entityType)
    {
        return _queryableEmpty.MakeGenericMethod(entityType).Invoke(null, null);
    }

    private static readonly MethodInfo _asQueryable = typeof(Queryable)
        .GetMethods(BindingFlags.Static | BindingFlags.Public)
        .First(m => m.Name == nameof(Queryable.AsQueryable) && m.IsGenericMethod);

    private static readonly MethodInfo _queryableEmpty = (typeof(ExtensionExpander))
        .GetMethod(nameof(QueryableEmpty), BindingFlags.Static | BindingFlags.NonPublic);

    private static IQueryable<T> QueryableEmpty<T>()
    {
        return Enumerable.Empty<T>().AsQueryable();
    }

    // http://stackoverflow.com/a/2616980/1402923
    private static object GetValue(Expression expression)
    {
        var objectMember = Expression.Convert(expression, typeof(object));
        var getterLambda = Expression.Lambda<Func<object>>(objectMember);
        return getterLambda.Compile().Invoke();
    }
}

ExtensionRebinder.cs
internal class ExtensionRebinder : ExpressionVisitor
{
    readonly object _originalQueryable;
    readonly Expression _replacementQueryable;
    readonly List<KeyValuePair<string, Expression>> _argumentReplacements;

    public ExtensionRebinder(
        object originalQueryable, Expression replacementQueryable,
        List<KeyValuePair<string, Expression>> argumentReplacements)
    {
        _originalQueryable = originalQueryable;
        _replacementQueryable = replacementQueryable;
        _argumentReplacements = argumentReplacements;
    }

    protected override Expression VisitConstant(ConstantExpression node)
    {
        return node.Value == _originalQueryable ? _replacementQueryable : node;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        if (node.NodeType == ExpressionType.MemberAccess
            && node.Expression.NodeType == ExpressionType.Constant
            && node.Expression.Type.GetTypeInfo().IsDefined(typeof(CompilerGeneratedAttribute)))
        {
            string argumentName = node.Member.Name;

            Expression replacement = _argumentReplacements
                .Where(p => p.Key == argumentName)
                .Select(p => p.Value)
                .FirstOrDefault();

            if (replacement != null)
            {
                return replacement;
            }
        }
        return base.VisitMember(node);
    }
}

Теперь мы сможем


— использовать extension-методы внутри Expression Tree:


[Expandable]
public static IQueryable<Post> FilterByAuthor(
    this IEnumerable<Post> posts, int authorId)
{
    return posts.AsQueryable().Where(p => p.AuthorId = authorId);
}

[Expandable]
public static IQueryable<Comment> FilterTodayComments(
    this IEnumerable<Comment> comments)
{
    DateTime today = DateTime.Now.Date;

    return comments.AsQueryable().Where(c => c.CreationTime > today)
}

Comment[] comments = context.Posts
    .AsExpandable()
    .FilterByAuthor(authorId)    // it's OK
    .SelectMany(p => p.Comments
        .FilterTodayComments())  // it's OK too
    .ToArray();

TL; DR


Уважаемые коллеги, не пытайтесь абстрагироваться от выбранного Вами фреймворка! Как правило, фреймворк уже предоставляет достаточный уровень абстракции. А иначе зачем он нужен?




Полный код расширений доступен на GitHub: EntityFramework.CommonTools, и в NuGet:


PM> Install-Package EntityFramework.CommonTools

PM> Install-Package EntityFrameworkCore.CommonTools



Update


Добавил я бенчмарки в свой проект.


DatabaseQueryBenchmark.cs
public class DatabaseQueryBenchmark
{
    readonly DbConnection _connection = Context.CreateConnection();

    [Benchmark(Baseline = true)]
    public object RawQuery()
    {
        using (var context = new Context(_connection))
        {
            DateTime today = DateTime.Now.Date;

            return context.Users
                .Where(u => u.Posts.Any(p => p.Date > today))
                .FirstOrDefault();
        }
    }

    [Benchmark]
    public object ExpandableQuery()
    {
        using (var context = new Context(_connection))
        {
            return context.Users
                .AsExpandable()
                .Where(u => u.Posts.FilterToday().Any())
                .ToList();
        }
    }

    readonly Random _random = new Random();

    [Benchmark]
    public object NotCachedQuery()
    {
        using (var context = new Context(_connection))
        {
            int[] postIds = new[] { _random.Next(), _random.Next() };

            return context.Users
                .Where(u => u.Posts.Any(p => postIds.Contains(p.Id)))
                .ToList();
        }
    }
}

Результаты получаются такие примерно
          Method |        Median |     StdDev | Scaled | Scaled-SD |
---------------- |-------------- |----------- |------- |---------- |
        RawQuery |   555.6202 μs | 15.1837 μs |   1.00 |      0.00 |
 ExpandableQuery |   644.6258 μs |  3.7793 μs |   1.15 |      0.03 | <<<
  NotCachedQuery | 2,277.7138 μs | 10.9754 μs |   4.06 |      0.10 |

Похоже, все кэшируется как надо. Просадка на компиляцию запроса получается в 15-30 %.




Метки:
Поделиться публикацией
Похожие публикации
Комментарии 122
  • +4
    Нуу… Это вообще почти холиварный вопрос, использовать или не использовать РЕПО и свой юнитофворк поверх контекста. Считаю что вообще то ещё должен быть базовый класс для доменной сущности. А оборачивать или нет прокси контекст EF в РЕПО дело вкуса. Главное четко разделять слои от сервисов. DDD тут главное.
    • +3
      базовый класс для доменной сущности

      Ну это как-то ортогонально созданию кастомного репозитория.


      А вот насчет Repository и UnitOfWork — либо через год они превратятся в неподдерживаемое нечто (времени на рефакторинг не хватило). Либо со временем окажется, что Вы пишете на собственном фреймворке поверх EF (времени хватило).


      А еще, такие репозитории — это пустая абстракиця. В них нет ни бизнес-логики, ни инфраструктурной логики. Это как адаптер. Только он используется не для интеграции двух разных модулей. А для интеграции EF и кода, который еще даже не написан.

      • +2
        А вот насчет Repository и UnitOfWork — либо через год они превратятся в неподдерживаемое нечто (времени на рефакторинг не хватило).

        Как говорится, по себе людей не судят.
    • +2
      да, да, да! супер.
      жаль, те кому это надо прочитать, не прочитают. ведь и так понятно, что в папочке DAL должен лежать UserRepository… с тыщей методов.
      • +1
        Зато теперь можно тыкнуть их носом в эту статью =)

        У меня за последние три года несколько раз случались холивары на эту тему с товарищами по команде.
        Причем в разных конторах. Вот, решил оформить свои мысли в статью.
        • +2
          Некоторые часто увлекаются, им нужно больше абстракций. Вот вроде уже абстрагировались ORMом от базы данных так нет же, давайте еще и от ORMа абстрагируемся. Вот если представить что да, появилась удобная, работающая абстракция над ORMом, вот от чего будут такие программисты абстрагироваться дальше?
          • +1

            ORM в целом не абстракция от базы данных, а репозиторий — абстракция от системы хранения.

            • 0
              ORM в целом не абстракция от базы данных
              У вас тут «не» лишнее?
              • +1

                То, что некоторые ОРМ позволяют асбтрагироваться от конкретной РСУБД парой строчек в конфиге, не означает что ОРМ — абстракция от БД. Наоборот, ОРМ обеспечивает связь объектной и реляционной моделей. Реляционная модель в ОРМ не под капотом, а входной параметр.

            • 0

              Я бы абстрагировал программистов:)

          • +3
            Поддерживаю вашу борьбу с Repository, при живом EF в проекте. Но вот Specification по-моему, это оверхед, да еще и с переопределением операторов.
            • +2

              Тут я пожалуй соглашусь. Мне самому комфортнее с extension-методами. Особенно, если дополнить их возможностью вызова из ExpressionTree, как описано в конце статьи.


              Спецификации — это не способ облегчить жизнь разработчику, а скорее способ оформить все "по феншую" (DDD, TDD, переиспользование и т.п.).


              И еще иногда (очень редко) возникает потребность динамически комбинировать условия с выборки помощью OR. Для AND все просто — добавляем в цепочку запроса несколько .Where()
              А для OR или спецификация, или PredicateBuilder из LinqKit

              • +1

                Есть ещё LinqSpecs. Ваша реализация очень похожа на неё. Я некоторое время назад тоже сравнивал варианты реализации спецификаций через extension'ы и expression'ы: https://habrahabr.ru/post/325280/

                • +2
                  Значит, мне надо было поглубже копать Хабр, прежде чем свой велосипед писать =)
                  • 0
                    Напротив, не первый раз замечаю, если несколько человек независимо «изобретают» одно и тоже, значит идея здравая:)
                    • 0
                      На последнем проекте мы смогли элегантно решить проблему поиска по связанным сущностям с помощью вот такого трюка: habrahabr.ru/post/313394. EF Core больше не выбрасывает NotSupported, что позволяет использовать спецификации не только к целевым сущностям, но и к связанным. Возможно, будет полезно.
              • +3

                Насколько эта expression-магия наносит урон по производительности? Сможет ли EF кэшировать запросы в таком случае?

                • +2

                  Это Вы про extension-методы? Хороший вопрос на самом деле. Надо проверять.


                  Но, чисто умозрительно, кэширование должно работать. Магия не встраивается внутрь EF. Она работает как декоратор вокруг LINQ IQueryable. И тот Expression, который содержится во внутреннем IQueryable из EntityFramework, уже не содержит никакой магии. Ну и компиляция SQL происходит из нормального ExpressionTree.


                  В принципе, когда мы добавляем в цепочку вызовов еще один LINQ-метод:


                  query.Where(...).Select(...).OrderBy(...)

                  мы точно так же модифицируем IQueryable.Expression.

                  • 0

                    Alex_ME, в общем, добавил я бенчмарки в свой проект. См. обновление статьи.


                    Просадка на компиляцию запроса получается в 15-30 %. А не в несколько раз, как без кеширования. И все равно укладывается в одну миллисекунду.

                  • +2
                    Repository как набор запросов

                    Удобен тем, что можно сделать реализацию Repository специально для тестирования, которые отдают те или иные тестовые данные. Так вы проверите бизнес-логику.

                    Тестировать сами Repository, как правило, нужды нет, так как в них и логики то особой нет. Но то что есть можно протестировать, как вы правильно заметили, с помощью in-memory DbContext.

                    То для реализации FilterByDateAndTag() мы не можем использовать два предыдущих метода.

                    В чем проблема Nullable?

                    IEnumerable<Post> FilterByDateAndTag(DateTime? date, string tag);
                    

                    • 0
                      реализацию Repository специально для тестирования

                      Только вот замокать репозиторий с достаточно богатым API — та еще задачка. Лучше уж In-Memory Context. А вот IQueryable замокать элементарно — new List<T>().

                      • +3
                        Вы сами же предлагаете использовать все преимущества ORM, которые не замокаются через List, а учитывая особенности трансляции LINQ в SQL, замокать в List не получится и подавно. Это красивая утопия.

                        Репозитории делаются максимально простыми не в целях смены ORM или СУБД. А в целях создания искусственных ограничений, превращая БД в абстрактное хранилище данных. И в целях уменьшить или свести к нулю проблемы тестирования.

                        Соответственно, для 80-90% запросов, можно прекрасно прожить в рамках этого ограничения. А оставшиеся узкие места оптимизировать специальным образом, например, создавая специализированные реализации репозиториев для некоторых сущностей вместо Generic Repository.

                        И это всё также хорошо работает как со спецификациями, так и Query Extensions или Query Object.

                        Вот с чем не поспоришь, так это DAL, описанный вами в статье — действительно зло.
                        • +1
                          Так я не говорю, что репозиторий зло. Репозиторий это отличная абстракция, и она у нас уже есть — IDbSet.

                          А вот от догмы, что все запросы к MyEntity должны лежать в MyEntityRepository нужно отступить.
                          • +3
                            Generic Repository отлично выполняет роль барьера, за которым может стоять что угодно. Почему многие сразу впадают в крайность и думают, что интерфейс репозитория служит для замены одной ORM на другую? А если у меня появится несколько источников данных? А если я хочу в целях оптимизации для некоторых работ вообще использовать Dapper? Нет, IDbSet не отличная абстракция и вообще не абстракция репозитория. Извините, но нет :)
                            • –1
                              Ну, IDbSet предоставляет CRUD и расширенную фильтрацию. Все по Фаулеру =)
                              А что оптимизировать Dapper-ом в GenericRepository — CRUD или специфические запросы?
                              Если запросы, то это уже не Repository. Это DAO, или QueryHandler из CQRS, или вообще какой-то сервис. Короче, абстракции протекают.
                              • +1
                                Ну а Generic Repository не предоставляют CRUD и фильтрацию на IQueryable? Только у вас в таком случае есть возможность написать полностью свою реализацию, и данные брать где угодно, а значит есть возможность провести любую оптимизацию, какая только вздумается и разделить источники данных. С IDbSet вы прибиваете себя именно к EF во всех смыслах. CRUD тоже может быть весьма специфичен.

                                Абстракции начинают «протекать», когда одно пытаются натянуть на другое. Необходимо ограничить обязанности и чётко понимать, для чего вводится абстракция. Когда это звучит вот так: «репозиторий для замены ORM» — это совершенно некорректная задача и обязанность. Зачем его заменять вообще? Абстракция репозитория это абстракция хранилище данных. Она прекрасно мокается, не зависит от внутренностей и багов ORM, которые можно подпереть в случае чего костыликом до лучших времён. Репозиторий может быть прокси, вычисляющим время запроса, вести аудит и поддерживать любую инфраструктуру. На маленьких проектах это может и не заметно, и там можно обойтись IDbSet, или вообще просто пробрасывать DbContext, чего мелочиться?
                                • 0
                                  IDbSet — всего лишь интерфейс. И реализовать его не сложнее чем кастомный. Единственный косяк, что лежит он в assembly EntityFramework. Ну так до Microsoft начало доходить. И теперь они выпускают по два пакета: MyPackage.dll и MyPackage.Abstractions.dll. Может и до EntityFramework-а доберутся.
                                  • +2
                                    IDbSet сильно привязан к EF: Attach, Find — эти методы уместны именно для EF, и не имеют ничего общего с концепцией репозитория. Косяк в том, что одно выдаётся за другое, только за похожесть :) Опять же. Почему всё таки, не отдавать тогда просто DbContext везде? Там ещё больше ORM-specific полезных вещей.
                                    • 0

                                      А можно не завязываться на DbContext, а разделить чтение и запись и для чтения использовать QueryObject. Если в проекте только реляционная, то IQueryable уже есть и не надо ничего своего изобретать.

                                    • 0
                                      Я так вижу. Если вы хотите работать именно с ORM и завязываться на ORM, не предусматривая никакой возможности для развития системы за рамками выбранного компонента, то городить абстракции не нужно конечно. И нормально будет передавать DbContext, используя все плюшки. Query Extensions неплохо инкапсулируют код запросов, позволяя вставлять их в подзапросы. Спецификации вообще не жизнеспособны для запросов, колоссальный оверхед с минимальным выхлопом. Но они хороши для системы валидации, например, или для системы проверки прав досутпа. Но всё это ограничивается небольшим размером и сложностью проектов.
                                    • +3
                                      Когда это звучит вот так: «репозиторий для замены ORM» — это совершенно некорректная задача и обязанность.

                                      А если я хочу в целях оптимизации для некоторых работ вообще использовать Dapper?

                                      А это разве не замена ORM, хоть и для отдельных мест?


                                      Кстати, как Вы реализуете Generic Repository с IQueryable для Dapper? Как я понимаю, Dapper работает с чистыми SQL-запросами, а не LINQ. Реализуете свой LINQ-провайдер?


                                      И если Вы хотите возможность подключить Dapper или иной источник данных, то Ваш репозиторий не должен иметь торчащего наружу IQueryable, иначе как подключить что-то, что в IQueryable не умеет.


                                      Честно говоря, я вообще не могу сходу придумать, какой интерфейс должен иметь репозиторий, чтобы можно было реализовать и EF, и Dapper. Описанные Specification так же не подходят, они реализованы на Expression. Самим транслировать в SQL? Потеряется плюсы Dapper, тогда уж лучше Linq2Db взять. Единственное, что приходит в голову — тот ужасный DAO с кучей методов.

                                      • 0

                                        Поэтому есть смысл использовать QueryObject, а не Repository и возвращать DTO, а не доменные объекты. Expression<Func<TDto, bool>> подойдёт в качестве спецификации. Для Dapper придётся написать свой Visitor для разбора выражения, но это не так сложно, если Dapper используется точечно.


                                        А вот если тяжелой ORM в проекте нет и все на Dapper, то проще скопировать архитектуру StackOverflow.

                                        • 0

                                          Не могу понять один момент. Как я понимаю, основной профит Dapper — то, что он имеет минимальный оверхед, и запросы самописные, и материализация быстрая. Если мы будем разбирать выражения и генерировать SQL то мы потеряем в производительности. И, возможно, рано или поздно, критические места придется вынести в самописные скрипты или вообще вызов хранимой процедуры. Куда в таком случае поместить этот вызов? В репозиторий GetSomeFooWithBar?


                                          Поэтому есть смысл использовать QueryObject, а не Repository и возвращать DTO, а не доменные объекты.

                                          А где связь между QueryObject и возвращением DTO? Разве он не может содержать SQL-код, который извлечет из БД доменный объект?

                                          • +3
                                            В EF самое тормозное — материализация объектов, ибо Reflection. На сколько тормозным будет разбор деревьев зависит от вас. В реальности проще всего написать реализацию только под те выражения, которые вы собираетесь поддерживать, а все остальное NotSupportedException.

                                            QueryObject может возвращать что угодно. Мы пришли к выводу, что в read-части DTO лучше: легче запрос, меньше затрат на материализацию, нет проблем с сериализацией и накладных расходов на дополнительный маппинг, нет опасности нарваться на lazy-load и связанные коллекции (опытный разработчик сразу такое заметит, а junior — нет).

                                            Доменный объект чаще нужен по id во write-подсистеме. Его проще получать из DbContext'а или своего UoW, если есть в наличии. Специально на эту тему не думали, получилось из практики. Возможно, другие проекты с другой спецификой нужно проектировать иначе.
                            • +3
                              Только вот замокать репозиторий с достаточно богатым API

                              Нужно разделять репозитории по смыслу. Чтобы сохранялся in-memory контекст — делается UnitOfWork и через него происходит получение репозиториев разных типов.

                              Вот пример от мелкомягких: implementing-the-repository-and-unit-of-work-patterns.

                              Плюсов много. К примеру когда я хочу добавить индексы в таблицу — я пересматриваю все запросы и сразу оптом их добавляю. Все запросы в одном месте, не нужно искать их по коду сервисов.
                              • +2
                                Я вообще сторонник группировки кода по фичам, а не по паттернам, которые этот код реализует. Как компромисс — можно сделать репоизтории partial, и раскидать их по каталоагм с фичами. Так можно и все запросы посмотреть по сущности, и в огромном файле не запутаться.
                                • 0
                                  В примере от MS на каждую фичу свой репозиторий а общий контекс обеспечивается
                                  с помощью UnitOfWork. Зачем partial?

                                  Я в одном из проектов разнес репозитории по модулям (т.е. в каждом из модулей только те репозитории, которые с ним связаны). Модули можно использовать независимо или объединять (при этом они могут использовать один контекст, если нужно, к примеру один модуль зависит от другого).

                                  Для меня важно что этот подход рекомендуемый, он понятен всем. Мне не пришлось придумывать свой велосипед.
                                  • +1

                                    Вообще забавный пример. Там предлагают передавать в .Include() строковые названия. Прощай Type Safety. Да и репозитории там не по фичам, а по сущностями. А что если UserRepository используется вообще везде, но с вызовом разных методов?


                                    А Репозиторий по фичам называется уже "сервис" =).

                                    • –1
                                      Вообще забавный пример. Там предлагают передавать в .Include() строковые названия. Прощай Type Safety.

                                      Ну так Include() в EF и принимает строки. А вот в EF Core уже expression.


                                      А что если UserRepository используется вообще везде, но с вызовом разных методов?

                                      Несколько репозиториев по решаемым задачам? У меня в проекте были слишком распухшие репозитории и сервисы, возможно, такое разделение хорошая мысль, но не уверен на 100%.

                          • 0
                            Nullable?

                            Тогда уж лучше сразу


                            IEnumerable<Post> Filter(PostCriteria criteria);
                            
                            class PostCriteria
                            {
                                public int? AuthorId { get; set; }
                                public DateTime? Date { get; set; }
                                public string Tag { get; set; }
                                // и т.п.
                            }

                            Но я про другое. Для extension-методов:


                            .FilterByDateAndTag(date, tag)
                            // эквивалентно
                            .FilterByDate(date).FilterByTag(tag)

                            И их можно использовать в подзапросах в в отличие от..


                            context.Users.Select(user => new
                            {
                                User = user,
                                Posts = user.Posts.FilterByDate(today).ToList(),
                            })
                            • 0
                              Тогда уж лучше сразу

                              Зависит от количества параметров. Если их много и много разных вариаций вызова — то да, объединяем в пакет.

                              Я не стал изобретать велосипед, делаю по рекомендациям от MS: использование Repository и UnitOfWork. Они же сами рекомендуют такой способ работы с данными.

                              Минус вашего подхода — у вас все запросы разбросаны по слою сервисов (бизнес-логики). И когда вы захотите добавить индексы — вы долго и нудно в профайлере будете выискивать тормозные запросы, после чего добавлять индексы. Или же будете лазить по всей бизнес логике, выискивать там запросы и смотреть какие индексы вам нужно добавить.
                              • +1
                                И когда вы захотите добавить индексы — вы долго и нудно в профайлере будете выискивать тормозные запросы, после чего добавлять индексы.
                                Так разве это не лучший способ узнать где нужны индексы? Зачем заниматься сомнительной работой гадая какой нужен индекс для какого запроса. Есть инструмент, бери и используй, не гадай.
                                • 0
                                  Профайлер — это крайняя мера, когда вы что-то пропустили. А так лучше все индексы закладывать в архитектуру, для этого нужно знать все свои запросы.
                                  • 0
                                    Мое мнение, по поводу индексов, это преувеличенная причина.
                                    Как уже писали в комментариях, зато в репозитарии можно добавить доп функциональность типа логирования, прав доступа и т. д.
                              • 0

                                Оффтоп: как ведет себя ToList() внутри лямбды в Select? Всегда думал, что ToList производит материализацию запроса, а что происходит когда он в середине запроса?

                                • +1

                                  Никак не ведет себя, он при трансляции в SQL игнорируется.


                                  Видимый эффект — свойство в анонимном классе получается не IEnumerable<>, а List<>

                                  • 0

                                    Спасибо. Спасли от возможного лишнего выстрела в ногу.

                            • +2
                              Мнение 1.
                              Repository Pattern with C# and Entity Framework, Done Right



                              Мнение 2.
                              Pluralsight — Entity Framework in the Enterprise Julie Lerman говорит что долго использовала R паттерн, и до сих пор многие из ее знакомых его используют. Это как привычка.

                              Для меня это место хранения часто используемых запросов к сущности.
                              • +1

                                Я тоже смотрел это видео перед написанием статьи =) На первый взгляд, там все правильно и красиво. Единственный вопрос — а зачем?


                                место хранения часто используемых запросов к сущности

                                А вот тут уже суровая реальность: Вы будете выносить запросы в репозиторий по мере рефакторинга. А Ваш коллега просто напихает от балды. Со временем все равно класс распухнет. Потому что его ответственность "часто используемые запросы" слишком размыта. Группировать запросы надо. Но по бизнес-кейсам, а не по сущностями.

                                • 0
                                  Со временем все равно класс распухнет.

                                  Это проблема процесса, а не паттерна. Причем проблема, легко подбираемая на первом же code review.
                              • 0
                                А вдруг, при реализации нашего чудо-репозитория мы воспользовались уникальными особенностями конкретного ORM?


                                А какая разница, какими особенностями мы воспользовались? У нас есть интерфейс, который говорит, что если попросить User UserOfId(UserId userId) то получим пользователя. Внутри конкретного репозитория это может быть сделано как угодно, главное, что в итоге есть User.

                                Очень многие используют репозиторий для получения вообще любых данных, хотя по идее репозиторий описывает то, что нужно бизнес-логике.

                                В интерфейсе репозитория, и в реализации соответственно, мы создаем такие методы, которые нам нужны для применения каких-то изменений к моделям. Если нам где-то нужно вывести постраничный список пользователей с разными условиями — это не имеет отношения к сохранению данных этих пользователей или манипуляцией их списком как одним целым. И в этой ситуации нам не нужны никакие репозитории, можно хоть прямо в базу запросы слать.

                                Интерфейс репозитория, это как документация, он сообщает нам, что, например, где-то есть бизнес-процесс, которому нужно получить всех пользователей с датой регистрации не позднее какого-то числа. И это нужно не потому, что мы хотим вывести это где-то в интерфейсе, а потому что мы будем проводить какие-то манипуляции с такими пользователями, принимать какое-то бизнес-решение. Если же нужно просто выводить список ограниченный по дате, используем то что удобно.
                                • 0
                                  А как понять, должен быть метод именно в репозитории или еще где?
                                  Почему не прикреплять этот метод к обработчику бизнес-процесса?

                                  Вот например есть бизнес-процесс: модератор хабов просматривает список пользователей по дате, и принимает решение — выдавать ли инвайт. Где поместить метод фильтрации пользователей?
                                  • 0
                                    Ответил ниже, сорри.
                                    • +1
                                      Можно еще так ответить. Представьте, что вы используете разделение на Command и Query в слое сервисов, даже если вы этого не делаете. И вот те обращения к БД, которые вы делали бы в Command если бы они у вас были, вы делаете через Репозиторий. А все остальные обращения — так как вам удобно.
                                      • 0

                                        О как! Ну, такой подход имеет право на жизнь. Можно правда еще поизвращаться чуть-чуть:
                                        Сделать репозиторий как partial class, и растащить UserRepository.cs по папкам, соответствующим разным фичам. Что угодно, только бы не нарушать SRP =)

                                  • 0
                                    Поместить там, где хочется. Начиная от обращения к базе прямо в контроллере заканчивая особым QueryRepository который содержит все эти запросы, которые мы часто где-то используем для вывода данных пользователю. По-умолчанию я бы выбрал эти данные где-то в сервисе или запросе (запросе из слоя сервисов, а не БД).
                                    Момент просмотра модератором списка хабов, это еще не момент выполнения какого-то кода в бизнес-логике.
                                    • 0
                                      Итак, вы предлагает пользоваться стандартным апи. Хорошо.
                                      Как быть с правами? Например, у меня в приложении своя система прав. Все через стандартный «репозиторий» ходят мимо прав?
                                      • +2

                                        А у Вас что, права на сущности распространяются, а не на бизнес-операции?
                                        Обычно система прав живет в слое сервисов. А их можно сделать уже такими, как Вы захотите.
                                        Ну или вообще AOP.

                                        • +1

                                          А репозиторий и есть доменный сервис. Конкретные реализации в инфраструктурном слое, а интерфейс в доменном.

                                          • 0
                                            И на сущности и на бизнес операции. Стандартная система прав — это всегда объект, субъект и операция (низкоуровневая, crud etc).
                                            И тут я вас не очень понял, как вы предлагаете обойтись без объекта.
                                            • +1

                                              Допустим:
                                              Руководитель отдела может оперировать только своими сотрудниками.


                                              Если мы добавим фильтрацию в репозиторий, то в сервисе уже будут уже отфильтрованные сотрудники. Если реализовывать в сервисе — то нужно цеплять эту фильтрацию везде, где надо (разумеется, можно вынести в Extension метод или еще куда-нибудь) и постараться не забыть и ничего не сломать в процессе.

                                          • +3

                                            Мне нравится репозиторий как набор запросов.
                                            Только не один гигантский на весь проект, а набор маленьких по сущностям и фичам.


                                            В чем преимущество


                                            • легко мокать (если они маленькие).
                                            • сразу видно какие именно операции с базой могут выполняться

                                            При этом необязательно отказываться от всего описанного в статье: репозиторий вполне может возвращать IQueryable, и к нему можно применять спецификацию.


                                            Обобщенный репозиторий плох тем, что он не ограничивает никак ни чтение данных (т.к. доступна вся таблица), ни запись.


                                            Свой же репозиторий может в методе All() возвращать не всю таблицу, а только доступные пользователю записи, например.


                                            Также репозиторий как набор запросов позволяет легко и безболезненно для остального кода проводить оптимизацию отдельных методов, перенося их в базу как функции или вьюшки.

                                            • +1

                                              Но это не репозиторий — это DAL-сервис.

                                              • 0

                                                Ок, спасибо за поправку. Я употребил "репозиторий" в смысле объекта для доступа к данным в хранилище.

                                              • 0

                                                Набор запросов — это как раз спецификации. Каноническая реализация должна отвечать требованиям компонуемости, т.е. Если есть ASpec и BSpec, то можно сделать aAndB = aSpec && bSpec; и aOrB = aSpec || bSpec; В .NET expressions не без особой уличной магии отлично выполняют роль спецификаций.

                                                • 0

                                                  Я имел ввиду объект, который содержит методы типа ПолучитьВсехАктивныхПользователей, ОбновитьОнлайнСтатус, и т.п., которые отражают предметную область.


                                                  Спецификации — это (компонуемое) описание операций над набором данных, по сути + универсальный метод их исполнения.

                                              • +3
                                                Считаю, что в идеале бизнес-слой не должен ни использовать DbSet как репозиторий, ни ссылаться на EntityFramework вообще. Это должно остаться в конкретной реализации DAL.
                                                • 0

                                                  Как я уже писал в комментарии выше, нужно различать зависимость между архитектурными слоями и зависимость от NuGet пакетов. Если бы EntityFramework был разделен на два пакета: абстракции и реализация, как сейчас Microsoft делает для многих пакетов под dotnet core, то можно было бы "собрать" слой домена из интерфейса репозитория в пакете EntityFramework.Abstractions и набора доменных сущностей в Вашей сборке. А слой DAL — собственно сам EF.


                                                  Сейчас конечно интерфейс IDbSet лежит прямо в EF, и не совсем подходит в качестве классического репозитория. Но кастомный репозиторий получится еще хуже, по крайней мере если Вы не потратите существенное время на проектирование.

                                                  • +9
                                                    Использовать интерфейс описанный в EF — не значит абстрагироваться от EF. Этот интерфейс нужен был разработчикам EF для их внутренних целей(например позволить в будущем менять конкретные типы возвращаемые за ним), а не для того чтобы его кто-то другой реализовывал.

                                                    Никакой домен не должен ссылаться на EF.Abstractions, потому что с точки зрения домена нет EF, есть рейсы, покупатели, брони и прочие скидки.

                                                    Честная слоёная архитектура подразумевает сильную избыточность. Вы же хотите срезать углы на каждом повороте. Так не бывает. У вас либо честные слои, разрезанная ответственность и тестируемость в изоляции и сильная избыточность кода, либо макароны из зависимостей на какие то библиотеки в домене, но зато быстрая разработка.

                                                    На крупных кроссплатформенных проектах ничего подобного в принципе не позволительно. Любая зависимость на внешнюю систему (EF в вашем случае) ОБЯЗАТЕЛЬНО абстрагируется архитектурным швом. Потому что нет никаких гарантий что на платформе X она в принципе поддерживается. Верить можно в libc и изредка в pthreads. Хотя хардварщики наверное и тут плюнули бы мне в лицо :)

                                                    Почитайте немного про кольцевые архитектуры (типа Clean Architecture, но она не единственная), и про Dependency Inversion.
                                                    • 0
                                                      Тут я с Вами согласен. Мне не удалось поучаствовать в кроссплатформных проектах. Потому что до недавнего времени .NET и кроссплатформа были несовместимы =)

                                                      Другой вопрос, что код с зависимостью от внешних абстракций ничего не теряет в поддерживаемости и тестируемости. Он теряет в гибкости. А нужна ли Вам такая гибкость?

                                                      И при начале разработки надо ответить на вопрос, мы действительно собираемся менять операционную систему / БД / ORM?

                                                      Если у нас «коробочное решение», то ответ — Да. А если внутренний сервис, то скорее всего «Нет».
                                                      • 0
                                                        И при начале разработки надо ответить на вопрос, мы действительно собираемся менять операционную систему / БД / ORM?

                                                        Неправильно сформулированный вопрос в контексте обсуждения :) Правильный: "мы действительно собираемся изолировать бизнес-логику от инфрастрктуры или нет?"

                                                        • 0

                                                          Ну, я Вам про цели, а Вы мне про средства. Тогда уж — какие преимущества мы получим, если изолирует бизнес-логику от инфраструктуры? И важны ли кто преимущества для данного конкретного проекта.

                                                          • 0

                                                            Упрощение поддержки и развития. Собственно ради этого большинство архитектурных решений и принимается.

                                                            • 0

                                                              Да, но за каждый новый слой абстракции надо платить. И будет ли от этого упрощение поддержки — зависит от проекта.

                                                              • +1

                                                                Конечно зависит. Эмпирическое правило, которое вывел для себя: если при инфраструктурных изменениях нужно лезть туда, где в целом лежит бизнес-логика, то пора изолировать. Равно как и наоборот. При этом следует отличать изменения и дополнения. Грубо, изолировать надо когда сложно следовать принципам SOLID, прежде всего SRP и OCP.

                                                  • +2
                                                    почему? в каждом проекте с самописными репозиториями я видел классы на 1к+ строк. в единственном виденном проекте на спецификациях был структурированный, сгруппированный код, в котором легко ориентироваться и вносить изменения. и ни разу я не встречал случая смены реализации DAL. это если отталкиваться только от опыта, что субъективно.

                                                    если объективно — то какие преимущества дает абстракция над EF?
                                                    • +3
                                                      Попробуйте представить это иначе. Не абстракция над EF, а класс с помощью EF реализующий контракт бизнес уровня на доступ к данным.

                                                      Если вы изначально проектируете от доменного уровня, вам вообще не важно, EF там или файлы с json вычитываются с SMB шары. Домен просто хочет IUserDataStore. И что прикольно, пока в начале проекта вы пишете доменный уровень — у вас в тестах на моках всё будет крутиться безо всяких EF и в принципе баз данных.
                                                      • +1
                                                        но зачем тогда EF? использовать его только как источник данных избыточно, тот же даппер делает это намного проще.
                                                        • 0
                                                          А если вам хватает Dapper, чтобы реализовать ваш IUserDataStore, то может на нем и стоит остановиться.

                                                          Абстракция уровня доступа к данных как раз об этом — бизнесу не должно быть важно, как берутся данные.
                                                        • 0
                                                          >вам вообще не важно, EF там или файлы с json вычитываются с SMB шары.

                                                          Нет, важно. Никакое вычитывание с SMB, прости господи, «шары», и никакой json не даст того, зачем к типичному энтерпрайзному проекту прикрепляют базу — MS SQL, разумеется. Ни производительности, ни надёжности, ни возможностей.
                                                          • 0

                                                            К типичному энтерпрайзному проекту базу применяют по умолчанию. MS SQ не по умолчанию — как минимум есть Oracle. Но такой типичный энтерпрайзный проект быстро превращается то ли в спагетти, то ли в лазанью, если в нём абстракции текут или их вообще нет, если у одного класса 100500 ответственностей, а постановки бизнес-задач вынуждены оперировать операциями типа "загрузить из таблицы customer пользователей, у которых год не было активностей и для каждого сделать http-запрос на домен very-cool-sms-provider.com POST /send/{customer.phone/} "{message: "забыл про нас? ща напомним"}"

                                                    • 0
                                                      Ох как классно расписано. Я пытался сделать что-то вроде спецификаций, но для такой красоты не хватило, наверное, владения языком. Спасибо!
                                                      • +1
                                                        Не пиара ради. На днях как раз сделал реализацию спецификации для .NET Standard 2.0
                                                        github.com/NoMoreLoad/CoreSpecs
                                                        Забавно получилось)
                                                        • 0
                                                          А если create/update/delete требует выполнить много операций для некоторых сущностей?

                                                          • Сохранить версию сущности в специальную таблицу.
                                                          • Обновить audit-лог.
                                                          • Обновить связанные сущности.


                                                          А есть ещё логика импорта/экспорта сущностей, которая может быть довольно нетривиальна.

                                                          • +1

                                                            Если нет цели абстрагироваться от EntityFramework, то такие задачи удобнее всего решать на уровне DbContext. Например, в моем велосипеде такое есть.


                                                            А если такая побочная логика слишком нетривиальна, то лучше вообще использовать CommandHandler из CQRS. Там по крайней мере можно запрятать каждый аспект (история, аудит лог и т.п.) в свой декоратор вокруг CommandHandler. А иначе все это осядет в нашем многострадальном репоизтории.

                                                            • 0
                                                              В репоизториях. У меня есть базовый репозиторий и много репозиториев-наследников для каждой сущности. Где можно переопределять логику. Если всё пихать в DbContext, то будет куча методов, типа DeleteUser, DeleteGroup и т.п. А так у меня правильное удаление групп и юзеров лежит в UserRepository.Delete и GroupRepostiroy.Delete удобно использовать, легко поддерживать не надо усложнять себе жизнь.
                                                              • 0

                                                                А как связаны UserRepository и GroupRepostiroy, если надо удалять пользователей вместе с группой? Через DI, или эвенты?


                                                                Похоже, мы не поняли друг друга. Под "решать на уровне DbContext" я понимаю вот что


                                                                Скрытый текст
                                                                class User : IAuditable { }
                                                                
                                                                public override int SaveChanges()
                                                                { 
                                                                    foreach (var auditable in ChangeTracker.Entries().OfType<IAuditable>())
                                                                    {
                                                                        // do whatever you want
                                                                    }
                                                                    return base.SaveChanges();
                                                                }

                                                                Если не хочется такое руками писать — есть EntityFramework.Triggers


                                                                или CommandHandler
                                                                class DeleteUserHandler : ICommandHandler<DeleteUserCommand>
                                                                {
                                                                   public void Handle(DeleteUserCommand command)
                                                                   {
                                                                        // do whatever you want
                                                                   }
                                                                }

                                                                Проблема базового репозитория в том, что в нем сразу сосредоточено много побочной логики (весь общий код, который может понадобиться в наследниках). Причем в начале проекта эта проблема не заметна.


                                                                В CommandHandler же только одна операция. И можно легко написать декораторы:


                                                                Скрытый текст
                                                                class AuditableCommandHandler<TCommand> : ICommandHandler<TCommand>
                                                                {
                                                                    public AuditableCommandHandler(ICommandHandler<TCommand> inner) { }
                                                                }
                                                          • 0

                                                            Считаю, что как-только проект перестает CRUD, то Generic Repository должен вводиться принудительно. И не в замене ORM дело, а в полном абстрагировании от системы персистентности. Их под капотом может быть даже несколько одновременно. А может и вообще не быть (для тестов, например).


                                                            Удалить из интерфейса все уникальные фичи каждой библиотеки.

                                                            Не то, что удалить, а даже вводить их не надо.


                                                            Объяснить товарищам по команде, почему они не могут теперь пользоваться уникальными фичами любимого ORM

                                                            Объяснить товарищам где они могут увидеть свою любимую ORM

                                                            • +2
                                                              Считаю, что как-только проект перестает CRUD, то Generic Repository должен вводиться принудительно. И не в замене ORM дело, а в полном абстрагировании от системы персистентности. Их под капотом может быть даже несколько одновременно. А может и вообще не быть (для тестов, например).

                                                              На бумаге это все выглядит гладко, но как реализовать запросы? 99% Generic Repository имеют LINQ-интерфейс, но не все системы персистентности его поддерживают. Если вы хотите подключить Dapper, Elastic Search (под последний что-то есть, но так себе)?


                                                              Как реализовать интерфейс запросов, независимый от системы хранения данных?


                                                              Только в случае DAO легко видится: есть запрос GetActiveUsers, одна реализация с LINQ+EF, другая на чистом SQL к Dapper...

                                                              • 0

                                                                Классические спецификации, тоже являющиеся частью домена, внутри реализации репозитория преобразуются в LINQ, SQL, HTTP, вызовы Redis, чего угодно в общем.

                                                            • 0

                                                              Главная проблема подхода "работать с EF напрямую, отправляя запросы из бизнес-логики" — сложность замены схемы хранилища. Например, ввода версионности. Или перехода на EAV. Или перехода с EAV… Пока весь проект не будет переписан — он даже не скомпилируется.


                                                              Ладно еще одному таком переписыванием заниматься, но когда команда параллельно разрабатывает новые фичи — можно вешаться.

                                                              • 0
                                                                хорошо, допустим даже вы не будете менять ОРМ потом но где тогда размещать все эти ГетБайАйди? В DBContext? Это искажает его ответственность.
                                                              • 0
                                                                вот эту статью смотрели? cpratt.co/truly-generic-repository мне показалось очень толково
                                                                • +1
                                                                  Больше абстракций богу абстракций! Это уже не репозиторий даже. Это как Session в Nhibernate.
                                                                  Репозиторий это коллекция доменных объектов. А тут у нас все вперемешку лежит. Да еще и UoW сбоку приклепали. Не скажу, что получилось неудобно. Но это скорее про уменьшение boilerplate, чем про разделение ответственностей.
                                                                • 0
                                                                  > В LINQ to Entities в качестве спецификаций используется Expression<Func<T, bool>>. Но такие выражения нельзя комбинировать с помощью булевских операторов и использовать в LINQ to Objects.
                                                                  > Попробуем совместить оба подхода. Добавим метод ToExpression():

                                                                  Какой в этом смысл? Не лучше ли просто определить методы расширения на Expression<Func<>>?
                                                                  • 0

                                                                    Можно и так. Как раз этот подход реализуется в LinqKit

                                                                  • –1
                                                                    Спасибо, хорошая статья! Тоже отказались от репозиториев, когда нужна быстрая разработка и внедрение лишние слои только отнимают время (это же не только разработка — длительные обсуждения тоже). Есть слой BL — использует EF.
                                                                    • –1
                                                                      CURD is a dairy product obtained by coagulating milk in a process called curdling. The coagulation can be caused by adding rennet or any edible acidic substance such as lemon juice or vinegar, and then allowing it to sit. The increased acidity causes the milk proteins (casein) to tangle into solid masses, or curds.
                                                                      image
                                                                      • 0
                                                                        Если реализовать базовый generic класс для repository, который покрывает базовые потребности CRUD + пробросить наружу нужные интерфейсы DbSet. То можно получить крайне тонкую обертку над EF и отвязать бизнес логику от доступа к данным. Для сложных задач по доступу данных мы можем наследоваться от generic класса и реализовать нужные методы. Ярким примером жесткой связки бизнес логики и логики хранения данных может послужить ASP.NET Identity. В проекте, где используется, к приему Dapper и EF вообще не нужен, вам нужно будет переопределить множество моделей + реализовать собственные storage. Этот тот самый пример, когда жесткое связывание хранения данных и бизнес логики дает осложнения потом.
                                                                        • +1
                                                                          От себя добавлю, что ОРМ бывает меняется в начале разработки проекта, когда тестируются различные варианты. А после этого уже незачем избавляться от удобной абстракции. + различный добавочный функционал по обновлению каких-либо полей внутри Update\Delete.
                                                                          • +5

                                                                            Автор не потрудился почитать описание паттерна Репозиторий, какие проблемы он решает, а так же не понял где у него проблема в принципе(в разных источниках). Хотя с QueryObject еще не все так плохо и они вполне жизнеспособны, но в другом месте.


                                                                            Начну с хорошего:


                                                                            • В папочке с QueryObject в принципе легче ориентороваться.
                                                                            • Их проще писать, проще внедрять в сервисы.
                                                                            • Проще вводить новые версии.
                                                                            • Проще моки писать в тестах — не надо все зависимости репозотория переопределять.
                                                                            • Еще отлично встроятся в read часть вашего приложения, если вы запилили CQRS.
                                                                              На этом все.

                                                                            Теперь о плохом. Репозиторий.
                                                                            В первую очередь Репозиторий — это интерфейс коллекции доменных объектов. Он абстрагирует нашу систему хранения данных. А ORM — Это вообще не про это. ORM про маппинг сток из базе на объекты.
                                                                            Эти 2 паттерна решают разные проблемы и используются по разному.


                                                                            Даже Фарулер говорит:


                                                                            A Repository mediates between the domain and data mapping layers.
                                                                            https://martinfowler.com/eaaCatalog/repository.html

                                                                            Data Mapping Layer — это как раз таки Ваша ORM.


                                                                            Использование класса из ORM вместо интерфейса репозитория — это циклическая зависимость в вашем коде. Если вы захотите заменить систему хранения данных — вы попали. Хотя в некоторых случаях такое упрощение в полне себе решение. Никому эти репозитории в блоге или сайте визитке не нужны.


                                                                            Пример из реального проекта.
                                                                            Одним из решений для задачи — было запилить EventSourcing. В качестве первого решения, было решено использовать MySQL как хранилище данных. Так было проще начать.
                                                                            Со временем оказалось что:


                                                                            • все события просто напросто физически не влезут в него, хард не резиновый.
                                                                            • использовать MySQL финансово не выгодно.

                                                                            По этому было решено переехать на DynamoDB. Если бы я использовал использовал репозитории ORM — мне бы пришлось выпилить его полностью. А так — я просто запилил новый класс, имплементировал пару методов и готово — в продакшин.


                                                                            Это то что касается Репозиториев.


                                                                            Теперь про количество методов в Репозиториях.
                                                                            Репозиторий используется совместно с ОРМ. Сейчас мы обратим внимание на то что он используется именно для Маппинга Доменных Объектов. И возвращает коллекцию доменных объектов.
                                                                            Большинство методов, от которых растет Репозиторий и на которые грешит автор — это методы для чтения данных.
                                                                            Так вот. Если вы подумали хорошо, когда проектировали ваше приложение — то наверное запилили бы CQRS. Тогда в вашем слое C были бы UseCase, которые использовали бы Репозитории с ОРМ.
                                                                            А в слое Q были бы ваши QueryObject или что по проще, без ОРМ. Он там нафиг не впился.


                                                                            Другими словами — Репозитории с ОРМ вам нужны там, где бизнесс логика. Там где просто вывод данных — вам и ОРМ не впилась, используйте SQL.


                                                                            Вы можете посмотреть видео Marco Pivetta "Doctrine Best Practices". Не надо плеваться что это Доктрина, принципы в ОРМ одни и те же.


                                                                            А нафиг оно вообще все надо? Зачем усложнять?
                                                                            Все эти дела с перозиториями, орм, интерфейсам, и т.д. и т.п. порой появляются на ровном месте.
                                                                            Ваше решение должно быть обосновано, внимание, ТРЕБОВАНИЯМИ К ПРОЕКТУ, а не личными хотелками.
                                                                            Если вы пишите блог — не надо там квери обьекты городить или отдельные репозитории вводить. Берите репозиторий ОРМ и в продакшин. А если у вас что-то больше — обязательно проверьте не будет ли меняться система хранения данных в конкретной части приложения. Ведь мы можем писать в мускуль, а клиенту отдавать из редиса. И то и то можно спрятать в репозитории. И когда прийдет время заменить имплементацию на что-то еще — вы не будете плакать.


                                                                            Для лучшего понимания рекомендовал бы посмотреть видео Евгения Кривошеева "Осознанность проектирования" и "Как не угробить архитектуру сразу же". Можно найти тут же на хабре. Там еще интересное.

                                                                            • 0

                                                                              Ну, во-первых, не знаю как другие ОРМ, а EntityFramework предоставляет сразу несколько паттернов: DataMapper, QueryBuilder, Repository, UnitOfWork. И незачем их переизобретать.


                                                                              Во-вторых не класс из ОРМ вместо репозитория, а интерфейс из ОРМ.


                                                                              А в остальном согласен.

                                                                              • +2

                                                                                Да немного сумбурно получилось. Но я не призываю ничего переизобретать.
                                                                                Речь про то что надо думать о зависимостях в проекте.


                                                                                Я тут капитана очевидность включу не на долго:
                                                                                Обычно создается класс наследующий репозиторий из ОРМ и имплементирующий наш интерфейс.
                                                                                Но интерфейс определяется в самом приложении. Нельзя использовать интерфейс из ОРМ. Т.к. это будет нарушение принципа инверсии зависимостей.


                                                                                Формулировка:
                                                                                • Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
                                                                                • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

                                                                                Первый пункт как раз об этом.
                                                                                кэп офф.


                                                                                И это конфликтует с Вашим выводом:


                                                                                Уважаемые коллеги, не пытайтесь абстрагироваться от выбранного Вами фреймворка! Как правило, фреймворк уже предоставляет достаточный уровень абстракции. А иначе зачем он нужен?

                                                                                Я это к тому что — не стоит выдавать такие утверждения. В интернетах полно неопытных ребят. И вы можете наблюдать их в комментариях к статье тоже. И если они без понимания проблемы рванут прикручивать ваше решение — это до добра не доведет.


                                                                                Самое печельное, что они искренне уверены, что они знают как надо использовать фреймворк.
                                                                                http://sergeykorol.ru/blog/competence/

                                                                                • 0
                                                                                  Только на практике чаще бывает наоборот. По крайней мере в .NET.
                                                                                  — Сначала люди начитаются туториалов, и пишут свой абстрактный репозиторий. Почему-то им кажется, что они-то спроектируют его лучше уже готовых.
                                                                                  — Потом начинают использовать его в других проектах внутри конторы.
                                                                                  — Потом оформляют в NuGet пакет.

                                                                                  И чем зависимость от своего пакета абстракций лучше зависимости от пакета абстракций Microsoft? Как-то NIH-синдромом попахивает. Это даже безотносительно затрат на поддержку своей реализации.
                                                                                  • +3

                                                                                    Ну так и качается скил :)


                                                                                    Сначала мы не знаем ничего (Неосознанная Некомпетентность).
                                                                                    Читаем туториал. С абсолютной уверенностью, что мы делаем лучшую реализацию в мире, запиливаем это в проект.
                                                                                    Глаза горят, добавляем соседу в проект. Выделяем в пакет.
                                                                                    Потом, набив шишки в 2-3 проектах, идем искать ответы на новые вопросы.
                                                                                    Оказывается что проблем то не мало.


                                                                                    И тут мы понимаем что чего-то не знаем (Осознанная Некомпетентность).
                                                                                    Начинаем усиленно читать, смотреть конференции. Всеми путями получаем как можно больше знаний.
                                                                                    И когда познаем дзен — переходим в состояние Осознанной Компетенции.
                                                                                    Умеем использовать Репозитории, пилить CQRS и EventSourcing.
                                                                                    Видим правильные проблемы. И умеем их правильно решать.


                                                                                    Это естественный процесс. И очень длительный, если конечно у вас под рукой нет компетентного наставника.
                                                                                    Я не один проект написал с жирными репозиториями, пока научился этому.
                                                                                    Главное задавать как можно больше вопросов и искать ответы. Критически мыслить.


                                                                                    А зависимость от своего интерфейса лучше тем, что:


                                                                                    1. это наш интерфейс, и только мы можем его менять. Никакой сторонний пакет вдруг не скажет что метод депрекейтед или изменит порядок аргументов. Даже если что-то и поменяется — внутри нашего приложения ничего не изменится, т.к. мы только изменим имплементацию этого интерфейса а одном месте.
                                                                                    2. в интерфейсе будут только те методы, которые нам действительно нужны. Тогда когда мы заходим переехать с MySQL на NoSql — мы будем знать точно обьем работ.
                                                                                    3. это просто тестировать

                                                                                    NIH синдром — это если бы мы пилили действительно всю ОРМ с дата мепперами и юнит оф ворк. А с Репозиторием чаще всего получается как я описал в комменте выше. Т.е. затраты на поддержку === 0. Ведь мы по факту используем то что предоставляет фреймворк, просто внутрь нашего приложения не просочится инородный интерфейс.


                                                                                    Весь паттерн Репозиторий — это про интерфейс, а не про реализацию.

                                                                            • +2
                                                                              Потом появляются запросы, которые джойнят несколько таблиц, и возвращают Data Transfer Object, а не доменный объект. И возникает вопрос, а в какой же репозиторий такие запросы запихнуть?

                                                                              А зачем нам привязывать репозиторий к одной из таблиц?
                                                                              Если сущность имеет зависимости, например User требует UserSettings, Roles то мы просто джойним зависимости и возвращаем User вместе с ними.
                                                                              Никакого DTO тут не нужно.

                                                                              • 0
                                                                                Я имею в виду когда нужен не граф связанных сущностей, а именно что dto. Например коммент + логин автора, а не коммент+автор
                                                                              • 0
                                                                                Таким образом, чтобы написать Generic Repository нужно:
                                                                                Собраться с мыслями.
                                                                                Спроектировать интерфейс.
                                                                                Написать реализацию под выбранный в проекте ORM.
                                                                                Написать реализацию под альтернативные ORM


                                                                                А зачем вообще в таком случае использовать какую бы то ни было ORM? Почему бы просто не использовать запросы на чистом SQL в реализации самого репозитория в таком случае? Не холивара ради пишу, реально не понимаю и хочу понять. Традиционный аргумент о привлечении дополнительного слоя абстракции ради независимости от конкретных СУБД вроде как не в счёт т.к. всё-равно у нас есть пункты «Спроектировать интерфейс» и «Написать реализацию под альтернативные ORM».
                                                                                • 0

                                                                                  Запросы на чистом SQL писать тяжело, а вычитывать из DataReader — нудно и многословно. И все проверки — только в рантайме.

                                                                                  • 0
                                                                                    Верный вопрос и предложение. CQRS как раз об этом.
                                                                                    Только не надо добавлять методы с использующие SQL в репозиторий. Это совершенно другая зона ответственности. И назначение другое.
                                                                                    В целом тут на лицо не правильный подход к использованию Репозитория как паттерна. Отсюда и не верное и переусложненное решение.
                                                                                    • 0

                                                                                      В этом случае у вас у репозитория будет две отвественности — коллекция объектов и маппинг объектов на СУБД. Другое дело, что для маппинга необязательно использовать универсальные ОРМ, а можно написать свой маппинг с чистым SQL

                                                                                    • 0
                                                                                      gnaeus, cкажите, в файле Specification.cs на строке возвращающей результат указано нечто неожиданное:
                                                                                      public static bool operator true(Specification<T> spec)
                                                                                      {
                                                                                          return false;
                                                                                      }
                                                                                      

                                                                                      Подскажите, это тонкий расчёт или ошибка?
                                                                                      • 0

                                                                                        Это черная магия конечно =) Впрочем, все в соответствии с C# Language Specification [7.11.2], как я уже упомянул в статье.


                                                                                        The operation x && y is evaluated as T.false(x) ? x : T.&(x, y)
                                                                                        The operation x || y is evaluated as T.true(x) ? x : T.|(x, y)

                                                                                        Мы хотим, чтобы выполнялась всегда правая часть тернарного оператора. Поэтому и operator true(), и operator false() должны возвращать false.

                                                                                      • +1
                                                                                        В итоге все становиться очень похоже на то как сделано в django Managers и Q-objects

                                                                                        Manager — расширение над QuerySet, со своими методами, Q-objects — те самая спецификации, которые можно комбинировать и использовать для запросов.
                                                                                        • +1
                                                                                          Все замечательно, если у вас одна БД или БД одного типа. А если легаси система, или несколько разных систем и бизнес-объект надо собирать из разных мест?
                                                                                          • 0

                                                                                            А вот это как раз "fair use". Ваш репозиторий — это не адаптер над ORM, который транслирует методы практически один-в-один с точностью до переименования. А фасад поверх API нескольких разнородных систем.


                                                                                            Но это если сложность Вашей бизнес-логики оправдывает внедрение DDD. Ведь снаружи Ваш репозиторий — это in-memory коллекция. А внутри — преобразование доменной модели в несколько других + вызов нескольких внешних систем + возможно, координатор распределенных транзакций.


                                                                                            А если надо просто сделать REST-сервис поверх пары БД и одного SOAP (как это обычно и бывает), тогда проще CQRS.

                                                                                            • 0

                                                                                              CQRS и Repository никак друг друга не исключают. Как Repository не подразумевает DDD.


                                                                                              Repository по сути локальная абстракция для хранилища данных — независимая от реализации хранилища представление данных для клиента репозитория в виде in-memory коллекции сущностей домена. Что часто конкретная реализация сводится к трансляции методов ORM (вернее ORM-библиотеки, кроме собственно ORM, реализующей обычно кучу паттернов, среди которых есть и что-то похожее на классический Repository) — лишь частный, пускай и частый случай.


                                                                                              Основная цель репозитория в моем понимании — не возможность быстро поменять слой хранения, а очистить бизнес-логику от деталей реализации слоя хранения. Разнести две сложных концепции в разные слои исключительно с односторонней зависимостью.

                                                                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.