1

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.

Community
  • 1
  • 1
Philipp Munin
  • 5,610
  • 7
  • 37
  • 60
  • Using the `CreateScalarQuery` in my answer to that other question, I've taken a random nonsensical query `context.CreateScalarQuery(() => context.Countries.Where(c => c.Code.StartsWith("N"))).Concat(context.CreateScalarQuery(() => context.Regions.Where(c => c.Code.StartsWith("N"))))` and confirmed that this generates a single SQL query, which gives two results. The "Unable to create a constant value of type 'Assignment'." suggests you've got a reference to one specific `Assignment` coming from a variable somewhere in the `...` that you excluded from the question. Can you include more details? –  Oct 15 '13 at 19:57
  • Are you perhaps doing `myDbContext.SelectValue((ctx)=>ctx.Assignments.Where(a => a.Level == someOtherAssignment.Level).Count())`? If so, yes, I can see how that would fail. If you're doing something like that, the query parameter needs to be `someOtherAssignmentLevel`, but for that to work, you need to put that in a variable, not access it as a property of an object. First `var someOtherAssignmentLevel = someOtherAssignment.Level;`, then `myDbContext.SelectValue((ctx)=>ctx.Assignments.Where(a => a.Level == someOtherAssignmentLevel).Count())`. –  Oct 15 '13 at 19:59
  • @hvd, thanks for reply, but no even simplest expression throws that exception: context.CreateScalarQuery(()=>ctx.Assignments.Count()) - try it with any entity and you will get exception if you work with DbContext (Assignments - is DbSet). – Philipp Munin Oct 15 '13 at 20:21
  • That is the first thing I tried, and what does work for me. ...How did you modify `CreateScalarQuery` to work with `DbContext`? Since `QueryProvider` is no longer accessible directly, it needed a little bit of updating: one way you can access it (the way I did it) is by checking the `Provider` property of the simplest `IQueryable` you can find: `public IQueryProvider QueryProvider { get { IQueryable assignments = this.Assignments; return assignments.Provider; } }` Did you make sure to get an appropriate query provider? –  Oct 15 '13 at 20:39
  • Yeah, I tried different things. Keep in mind, that Assignment is not Entity of ObjectContext, it is DbSet entity of DbContext and "ctx" is DbContext. If it's not too much to ask - can you please provide your answer with a code that matches the question's situation? – Philipp Munin Oct 15 '13 at 20:45
  • Yes, I know, I am using `DbContext` and `DbSet`. I don't have a simple scratch project readily available (I added it into a larger project I could easily experiment in), but I'll probably be able to get one some time tomorrow. –  Oct 15 '13 at 20:51
  • Not sure maybe one of us missed some detail. For simplicity you can even remove Where clause in your code - it fails for me the same way. I'd really appreciate your code sample with DbContext and methods implementation – Philipp Munin Oct 15 '13 at 20:56

1 Answers1

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"))));
}
  • Thanks! Guess what was the problem? :) I built my expressions as methods of partial class MyDbContext - so my Linq expressions were like (()=>this.Assignments.Count()) instead of (()=>ctx.Assignments.Count()) - and THIS was the reason of that exception! – Philipp Munin Oct 16 '13 at 13:28
  • can you please add to your code of CreateScalarQuery overload accepting Expression> instead of just Expression> and overload accepting Expression,TResult>>? Those would be superhelpful overloads worthy to be a part of Entityframework.dll. I tried to make it by myself, but can't manage those dynamic lambda building – Philipp Munin Oct 16 '13 at 13:34
  • I finally found a way to do it! I published blog post about it: http://blog.pmunin.com/2013/10/EF5-Extensions-ScalarQuery.html I'm going to update your Answer with a link to my post with a code prepared to copy-paste – Philipp Munin Oct 16 '13 at 18:01
  • @PhilippMunin It's good that you got this working, but I'm not sure I like the way you're doing it, as it relies on implementation details that may change in future versions of EF, and as expression rewriters may hinder query precompilation. If I can see a more straightforward way (and if I can test that it works), I'll update my answer. –  Oct 16 '13 at 18:38
  • Please post it here if you find any better solution. I appreciate your help with this subject! – Philipp Munin Oct 16 '13 at 23:16
  • @PhilippMunin Answer updated, it is actually very similar to what you had already, just slightly simplified, as my criticism of your expression rewriting was based on a misreading of your code. Since the rewriting is completely done by the time EF sees it, the problem I saw isn't a problem at all. I do still get the provider from a query rather than reflecting to access internal properties, which should help make sure it continues to work even as EF gets updated. –  Oct 17 '13 at 07:51
  • thanks for update. Yes you take QueryProvider without accessing internals, but sacrificing universalization of code as an extension that can be just copy-pasted to any project, b/c you take QueryProvider from As member, which is specific to MyDbContext - so the code have to be adapted to each particular context as partial class. Anyways you help is greatly appreciated. I am surprised to realize the level of EF's immaturity, dealing with issues like this. The code that we have here should be a part of Entity Framework. – Philipp Munin Oct 17 '13 at 17:28
  • @PhilippMunin That's a fair point, though I suppose it should be possible to dynamically figure out which entity types are part of the context, pick the first one, and dynamically call `context.Set()`. I agree, it would be nice if this were part of EF already, but for me personally, this doesn't bother me nearly as much as some other things that don't work, that could easily be made to work, but require changes to EF internals. –  Oct 17 '13 at 18:01