EntityFramework:(アンチ)リポジトリパターン

リポジトリパターン

リポジトリは、データアクセスレイヤーとドメインレイヤーの間の仲介です。

ドメインオブジェクトのメモリ内コレクションとして機能します。 クライアントは宣言型を作成します

リクエストの説明とそれらを実行のためにリポジトリに渡します。

-マーティン・ファウラーによる無料翻訳

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%









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 .







. :









[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 %.













All Articles