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"))));
}