DbContext get IQueryable for scalar system functions (Count, Any, Sum, Max)

久未见 提交于 2019-12-13 02:23:57

问题


I have DBContext with DbSet called Assignments. It's not a problem to create queryable for enumerable expressions and concatenated them, however I don't see the way to get IQueryable with deferred execution for functions like Count, Any, Max, Sum.

Basically I want to have some IQueryable extension so I can execute it like this:

IQueryable<int> query = 
          myDbContext.SelectValue((ctx)=>ctx.Assignments.Where(...).Count())
.UnionAll(myDbContext.SelectValue((ctx)=>ctx.Assignments.Where(...).Count()));

and get the following SQL (query.ToString()):

SELECT 
[UnionAll1].[C1] AS [C1]
FROM  (SELECT 
    [GroupBy1].[A1] AS [C1]
    FROM ( SELECT 
        COUNT([Extent1].[UserId]) AS [A1]
        FROM [dbo].[Assignments] AS [Extent1]
                WHERE ...
    )  AS [GroupBy1]
UNION ALL
    SELECT 
    [GroupBy2].[A1] AS [C1]
    FROM ( SELECT 
        COUNT([Extent2].[UserId]) AS [A1]
        FROM [dbo].[Assignments] AS [Extent2]
                WHERE ...
    )  AS [GroupBy2]) AS [UnionAll1]

IMPORTANT: As you see I need to be able to use it in sub queries, with unions and joins, having ONE SQL REQUEST GENERATED at the end. I cannot use RAW SQL and I cannot use string names for entities, that's why I don't see ObjectContextAdapter.ObjectContext.CreateQuery working for me.

Here you can find a way to achieve it using ObjectContext, but I cannot use this approach for my case, because it throws error:

Unable to create a constant value of type 'Assignment'. Only primitive types or enumeration types are supported in this context.


回答1:


The same approach as in my answer to that other question works here too. Here is a self-contained test program using EF5:

using System;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace ScratchProject
{
    public class A
    {
        public int Id { get; set; }

        public string TextA { get; set; }
    }

    public class B
    {
        public int Id { get; set; }

        public string TextB { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<A> As { get; set; }

        public DbSet<B> Bs { get; set; }

        protected IQueryProvider QueryProvider
        {
            get
            {
                IQueryable queryable = As;
                return queryable.Provider;
            }
        }

        public IQueryable<TResult> CreateScalarQuery<TResult>(Expression<Func<TResult>> expression)
        {
            return QueryProvider.CreateQuery<TResult>(
                Expression.Call(
                    method: GetMethodInfo(() => Queryable.Select<int, TResult>(null, (Expression<Func<int, TResult>>)null)),
                    arg0: Expression.Call(
                        method: GetMethodInfo(() => Queryable.AsQueryable<int>(null)),
                        arg0: Expression.NewArrayInit(typeof(int), Expression.Constant(1))),
                    arg1: Expression.Lambda(body: expression.Body, parameters: new[] { Expression.Parameter(typeof(int)) })));
        }

        static MethodInfo GetMethodInfo(Expression<Action> expression)
        {
            return ((MethodCallExpression)expression.Body).Method;
        }
    }

    static class Program
    {
        static void Main()
        {
            using (var context = new MyContext())
            {
                Console.WriteLine(context.CreateScalarQuery(() => context.As.Count(a => a.TextA != "A"))
                    .Concat(context.CreateScalarQuery(() => context.Bs.Count(b => b.TextB != "B"))));
            }
        }
    }
}

Output:

SELECT
[UnionAll1].[C1] AS [C1]
FROM  (SELECT
        [GroupBy1].[A1] AS [C1]
        FROM ( SELECT
                COUNT(1) AS [A1]
                FROM [dbo].[A] AS [Extent1]
                WHERE N'A' <> [Extent1].[TextA]
        )  AS [GroupBy1]
UNION ALL
        SELECT
        [GroupBy2].[A1] AS [C1]
        FROM ( SELECT
                COUNT(1) AS [A1]
                FROM [dbo].[B] AS [Extent2]
                WHERE N'B' <> [Extent2].[TextB]
        )  AS [GroupBy2]) AS [UnionAll1]

And yes, actually executing the query works as expected too.

Update:

As requested, here is what you can add to get it working for Expression<Func<MyContext, TResult>> expression) as well:

public IQueryable<TResult> CreateScalarQuery<TResult>(Expression<Func<MyContext, TResult>> expression)
{
    var parameterReplacer = new ParameterReplacer(expression.Parameters[0], Expression.Property(Expression.Constant(new Tuple<MyContext>(this)), "Item1"));
    return CreateScalarQuery(Expression.Lambda<Func<TResult>>(parameterReplacer.Visit(expression.Body)));
}

class ParameterReplacer : ExpressionVisitor
{
    readonly ParameterExpression parameter;
    readonly Expression replacement;

    public ParameterReplacer(ParameterExpression parameter, Expression replacement)
    {
        this.parameter = parameter;
        this.replacement = replacement;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        if (node == parameter)
            return replacement;

        return base.VisitParameter(node);
    }
}

This works even if called from inside the current context:

// member of MyContext
public void Test1()
{
    Console.WriteLine(this.CreateScalarQuery(ctx => ctx.As.Count(a => a.TextA != "A"))
        .Concat(this.CreateScalarQuery(ctx => ctx.Bs.Count(b => b.TextB != "B"))));
}

The parameter replacement stores the context in a Tuple<MyContext> instead of MyContext directly, because EF does not know how to handle Expression.Constant(this). That's something that the C# compiler will never produce anyway, so EF does not need to know how to handle it. Getting a context as a member of a class is something that the C# compiler does produce, so EF has been made to know how to handle that.

However, the simpler version of CreateScalarQuery can be made to work too, if you save this in a local variable:

// member of MyContext
public void Test2()
{
    var context = this;
    Console.WriteLine(this.CreateScalarQuery(() => context.As.Count(a => a.TextA != "A"))
        .Concat(this.CreateScalarQuery(() => context.Bs.Count(b => b.TextB != "B"))));
}


来源:https://stackoverflow.com/questions/19385346/dbcontext-get-iqueryable-for-scalar-system-functions-count-any-sum-max

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!