简体   繁体   中英

IQueryable LINQ cast error for group by using dynamic expression

I'm using C#, .Net 4.5, MVC, entity framework 5.0 with code first. I've encountered error on using one of the examples from the devexpress. The problem lies on getting a list of groupby value and aggregate (count) queries.

The error is Unable to cast the type 'System.Int32' to type 'System.Object'. LINQ to Entities only supports casing EDM primitive or enumeration types .

The entity/table is

public class Test
{
    [Key]
    public int ID { get; set; }
    public string Name { get; set; }
    public DateTime SubmitDate { get; set; }
    public int TotalValue { get; set; }
}

Test code on getting the grouping information

public void GetGroupInfo() 
{
    GetGroupInfo(Context.Tests, "TotalValue");
    GetGroupInfo(Context.Tests, "Name");
}

public static void GetGroupInfo(this IQueryable query, string fieldName)
{
    CriteriaToExpressionConverter converter = new CriteriaToExpressionConverter();

    var rowType = query.ElementType;
    query = query.MakeGroupBy(converter, new OperandProperty(fieldName));
    query = query.MakeOrderBy(converter, new ServerModeOrderDescriptor(new OperandProperty("Key"), false));

    /*
      i think the problem is from here
    */
    query = ApplyExpression(query, rowType, "Key", "Count");

    // ignore the GridViewGroupInfo, just a class to store the value
    var list = new List<GridViewGroupInfo>();
    foreach (var item in query)
    {
        var obj = (object[])item;
        list.Add(new GridViewGroupInfo() {KeyValue=obj[0], DataRowCount =(int)obj[1]});
    }
}

static IQueryable ApplyExpression(IQueryable query, Type rowType, params string[] names)   
{
    var parameter = Expression.Parameter(query.ElementType, string.Empty);
    var expressions = names.Select(n => query.GetExpression(n, rowType, parameter));
    var arrayExpressions = Expression.NewArrayInit(
        typeof(object),
        expressions.Select(expr=>Expression.Convert(expr,typeof(object))).ToArray()
    );
    var lambda = Expression.Lambda(arrayExpressions, parameter);

    var expression = Expression.Call(
        typeof(Queryable),
        "Select",
        new Type[] { query.ElementType, lambda.Body.Type },
        query.Expression,
        Expression.Quote(lambda)
    );
    return query.Provider.CreateQuery(expression);
}

static Expression GetExpression(this IQueryable query, string commandName, Type rowType,    ParameterExpression parameter)
{
    switch (commandName)
    {
        case "Key":
            return Expression.Property(parameter, "Key");
        case "Count":
            return Expression.Call(typeof(Enumerable), "Count", new Type[] { rowType }, parameter);
    }
    return null;
}

It gives me error regardless the grouping is on the "Name" (string) type or the "TotalValue" (int) type. Anyone can help? Appreciate if anyone tells me why, what and how since I'm still learning about this entire .net, mvc & linq.

I recon what you're trying to do is something like this:

Context.Tests.GroupBy(t => t.TotalValue).Select(g => new { Key = g.Key, Count = g.Count() });
Context.Tests.GroupBy(t => t.Name).Select(g => new { Key = g.Key, Count = g.Count() });

but using manually created Expressions .

What you're actually creating and where the problem lies is the last select:

 var arrayExpressions = Expression.NewArrayInit(
    typeof(object),
    expressions.Select(expr=>Expression.Convert(expr,typeof(object))).ToArray()
);

Will give you the equivalent of:

Select(g => new object[] { (object)g.Key, (object)g.Count() });

And indeed, trying to execute a query like this will result in LINQ to Entities (and Entity Framework too, for that matter) complaining it cannot do the cast to object .

What it can handle is casting to string . So:

Select(g => new string[] { g.Key.ToString(), g.Count().ToString() });

Is almost OK, but now there is a problem with the array initializer: "The array type 'System.String[]' cannot be initialized in a query result. Consider using 'System.Collections.Generic.List`1[System.String]' instead." That's easy:

Select(g => new List<string> { g.Key.ToString(), g.Count().ToString() });

And now that can be translated into SQL (at least by Entity Framework, but i suppose Linq to SQL can handle it too). So, you should replace arrayExpressions with this:

var arrayExpressions = Expression.ListInit(Expression.New(typeof(List<string>)),
                expressions.Select(expr => Expression.Call(expr, "ToString", null)).ToArray()
            );

And now it works.

All in all, this is a rather complicated way to build Linq queries, it's hard to debug and even harder to read. Consider using generic type IQueryable and writing lambdas - that's what Linq was designed for in the first place. If you have or want to stick with manual Expressions, i would recommend writing a Linq query first (maybe for a more specific case), analyzing the Expression it generates and then trying to create this Expression manually.

Also, compare the SQL query that this expression generates with a simple linq query that should do the same job - the first one will be way more complicated.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM