How do I use Moq and DbFunctions in unit tests to prevent a NotSupportedException?

前端 未结 7 1710
借酒劲吻你
借酒劲吻你 2020-12-05 23:05

I\'m currently attempting to run some unit tests on a query that is running through the Entity Framework. The query itself runs without any issues on the live version, but t

7条回答
  •  情书的邮戳
    2020-12-05 23:36

    There is a way to do it. Since unit testing of business logic is generally encouraged, and since it is perfectly OK for business logic to issue LINQ queries against application data, then it must be perfectly OK to unit test those LINQ queries.

    Unfortunately, DbFunctions feature of Entity Framework kills our ability to unit test code that contains LINQ queries. Moreover, it is architecturally wrong to use DbFunctions in business logic, because it couples business logic layer to a specific persistence technology (which is a separate discussion).

    Having said that, our goal is the ability to run LINQ query like this:

    var orderIdsByDate = (
        from o in repo.Orders
        group o by o.PlacedAt.Date 
             // here we used DateTime.Date 
             // and **NOT** DbFunctions.TruncateTime
        into g
        orderby g.Key
        select new { Date = g.Key, OrderIds = g.Select(x => x.Id) });
    

    In unit test, this will boil down to LINQ-to-Objects running against a plain array of entities arranged in advance (for example). In a real run, it must work against a real ObjectContext of Entity Framework.

    Here is a recipe of achieving it - although, it requires a few steps of yours. I'm cutting down a real working example:

    Step 1. Wrap ObjectSet inside our own implementation of IQueryable in order to provide our own intercepting wrapper of IQueryProvider.

    public class EntityRepository : IQueryable where T : class
    {
        private readonly ObjectSet _objectSet;
        private InterceptingQueryProvider _queryProvider = null;
    
        public EntityRepository(ObjectSet objectSet)
        {
            _objectSet = objectSet;
        }
        IEnumerator IEnumerable.GetEnumerator()
        {
            return _objectSet.AsEnumerable().GetEnumerator();
        }
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return _objectSet.AsEnumerable().GetEnumerator();
        }
        Type IQueryable.ElementType
        {
            get { return _objectSet.AsQueryable().ElementType; }
        }
        System.Linq.Expressions.Expression IQueryable.Expression
        {
            get { return _objectSet.AsQueryable().Expression; }
        }
        IQueryProvider IQueryable.Provider
        {
            get
            {
                if ( _queryProvider == null )
                {
                    _queryProvider = new InterceptingQueryProvider(_objectSet.AsQueryable().Provider);
                }
                return _queryProvider;
            }
        }
    
        // . . . . . you may want to include Insert(), Update(), and Delete() methods
    }
    

    Step 2. Implement the intercepting query provider, in my example it is a nested class inside EntityRepository:

    private class InterceptingQueryProvider : IQueryProvider
    {
        private readonly IQueryProvider _actualQueryProvider;
    
        public InterceptingQueryProvider(IQueryProvider actualQueryProvider)
        {
            _actualQueryProvider = actualQueryProvider;
        }
        public IQueryable CreateQuery(Expression expression)
        {
            var specializedExpression = QueryExpressionSpecializer.Specialize(expression);
            return _actualQueryProvider.CreateQuery(specializedExpression);
        }
        public IQueryable CreateQuery(Expression expression)
        {
            var specializedExpression = QueryExpressionSpecializer.Specialize(expression);
            return _actualQueryProvider.CreateQuery(specializedExpression);
        }
        public TResult Execute(Expression expression)
        {
            return _actualQueryProvider.Execute(expression);
        }
        public object Execute(Expression expression)
        {
            return _actualQueryProvider.Execute(expression);
        }
    }
    

    Step 3. Finally, implement a helper class named QueryExpressionSpecializer, which would replace DateTime.Date with DbFunctions.TruncateTime.

    public static class QueryExpressionSpecializer
    {
        private static readonly MethodInfo _s_dbFunctions_TruncateTime_NullableOfDateTime = 
            GetMethodInfo>>(d => DbFunctions.TruncateTime(d));
    
        private static readonly PropertyInfo _s_nullableOfDateTime_Value =
            GetPropertyInfo>>(d => d.Value);
    
        public static Expression Specialize(Expression general)
        {
            var visitor = new SpecializingVisitor();
            return visitor.Visit(general);
        }
        private static MethodInfo GetMethodInfo(TLambda lambda) where TLambda : LambdaExpression
        {
            return ((MethodCallExpression)lambda.Body).Method;
        }
        public static PropertyInfo GetPropertyInfo(TLambda lambda) where TLambda : LambdaExpression
        {
            return (PropertyInfo)((MemberExpression)lambda.Body).Member;
        }
    
        private class SpecializingVisitor : ExpressionVisitor
        {
            protected override Expression VisitMember(MemberExpression node)
            {
                if ( node.Expression.Type == typeof(DateTime?) && node.Member.Name == "Date" )
                {
                    return Expression.Call(_s_dbFunctions_TruncateTime_NullableOfDateTime, node.Expression);
                }
    
                if ( node.Expression.Type == typeof(DateTime) && node.Member.Name == "Date" )
                {
                    return Expression.Property(
                        Expression.Call(
                            _s_dbFunctions_TruncateTime_NullableOfDateTime, 
                            Expression.Convert(
                                node.Expression, 
                                typeof(DateTime?)
                            )
                        ),
                        _s_nullableOfDateTime_Value
                    );
                }
    
                return base.VisitMember(node);
            }
        }
    }
    

    Of course, the above implementation of QueryExpressionSpecializer can be generalized to allow plugging in any number of additional conversions, allowing members of custom types to be used in LINQ queries, even though they are not known to Entity Framework.

提交回复
热议问题