问题
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