如何创建LINQ表达式树以选择匿名类型

48

我想使用表达式树动态生成以下选择语句:

var v = from c in Countries
        where c.City == "London"
        select new {c.Name, c.Population};

我已经想出如何生成

var v = from c in Countries
        where c.City == "London"
        select new {c.Name};

但我似乎找不到一个构造函数/重载,可以让我在选择 lambda 中指定多个属性。

9个回答

74

如上所述,可以通过Reflection Emit和我下面提供的辅助类来实现。下面的代码还在持续改进中,因此接受它的价值是有限的...“在我的电脑上可以用”。SelectDynamic方法应该被放在一个静态扩展方法类中。

预期的是,由于类型直到运行时才创建,因此您不会得到任何Intellisense。适用于后期绑定数据控件。

public static IQueryable SelectDynamic(this IQueryable source, IEnumerable<string> fieldNames)
{
    Dictionary<string, PropertyInfo> sourceProperties = fieldNames.ToDictionary(name => name, name => source.ElementType.GetProperty(name));
    Type dynamicType = LinqRuntimeTypeBuilder.GetDynamicType(sourceProperties.Values);

    ParameterExpression sourceItem = Expression.Parameter(source.ElementType, "t");
    IEnumerable<MemberBinding> bindings = dynamicType.GetFields().Select(p => Expression.Bind(p, Expression.Property(sourceItem, sourceProperties[p.Name]))).OfType<MemberBinding>();

    Expression selector = Expression.Lambda(Expression.MemberInit(
        Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)), bindings), sourceItem);

    return source.Provider.CreateQuery(Expression.Call(typeof(Queryable), "Select", new Type[] { source.ElementType, dynamicType },
                 Expression.Constant(source), selector));
}



public static class LinqRuntimeTypeBuilder
{
    private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
    private static AssemblyName assemblyName = new AssemblyName() { Name = "DynamicLinqTypes" };
    private static ModuleBuilder moduleBuilder = null;
    private static Dictionary<string, Type> builtTypes = new Dictionary<string, Type>();

    static LinqRuntimeTypeBuilder()
    {
        moduleBuilder = Thread.GetDomain().DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run).DefineDynamicModule(assemblyName.Name);
    }

    private static string GetTypeKey(Dictionary<string, Type> fields)
    {
        //TODO: optimize the type caching -- if fields are simply reordered, that doesn't mean that they're actually different types, so this needs to be smarter
        string key = string.Empty;
        foreach (var field in fields)
            key += field.Key + ";" + field.Value.Name + ";";

        return key;
    }

    public static Type GetDynamicType(Dictionary<string, Type> fields)
    {
        if (null == fields)
            throw new ArgumentNullException("fields");
        if (0 == fields.Count)
            throw new ArgumentOutOfRangeException("fields", "fields must have at least 1 field definition");

        try
        {
            Monitor.Enter(builtTypes);
            string className = GetTypeKey(fields);

            if (builtTypes.ContainsKey(className))
                return builtTypes[className];

            TypeBuilder typeBuilder = moduleBuilder.DefineType(className, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Serializable);

            foreach (var field in fields)                    
                typeBuilder.DefineField(field.Key, field.Value, FieldAttributes.Public);

            builtTypes[className] = typeBuilder.CreateType();

            return builtTypes[className];
        }
        catch (Exception ex)
        {
            log.Error(ex);
        }
        finally
        {
            Monitor.Exit(builtTypes);
        }

        return null;
    }


    private static string GetTypeKey(IEnumerable<PropertyInfo> fields)
    {
        return GetTypeKey(fields.ToDictionary(f => f.Name, f => f.PropertyType));
    }

    public static Type GetDynamicType(IEnumerable<PropertyInfo> fields)
    {
        return GetDynamicType(fields.ToDictionary(f => f.Name, f => f.PropertyType));
    }
}

好的,但是“无法序列化接口System.Linq.IQueryable”。 - Davut Gürbüz
2
你可以在你的//TODO中加入OrderBy来进行优化,然后就完成了。 - Akash Kava
@Ethan J. Brown,您能告诉我如何修改您的代码,如果源是IEnumerable而不是IQueryable吗?谢谢! - Lei Yang
3
我一直在使用类似的东西,但是遇到了“无法创建常量类型值”的错误。我通过将最后一行中的Expression.Constant(source)替换为source.Expression来解决这个问题。希望这对某些人有所帮助 :) - Connell
@Ethan J. Brown,非常感谢您的回答。我该如何在嵌套类型中使用此代码?例如 x=> new { a=1, b = new { ba=1, bb=2} }; - mesut
显示剩余2条评论

10
被接受的答案非常有用,但我需要更接近真正匿名类型的东西。
一个真正的匿名类型具有只读属性、用于填充所有值的构造函数、用于比较每个属性的值的Equals/GetHashCode实现,以及包括每个属性的名称/值的ToString实现。(有关匿名类型的完整描述,请参见https://msdn.microsoft.com/en-us/library/bb397696.aspx。)
基于匿名类的定义,我在github上放了一个生成动态匿名类型的类,网址是https://github.com/dotlattice/LatticeUtils/blob/master/LatticeUtils/AnonymousTypeUtils.cs。该项目还包含一些单元测试,以确保假匿名类型的行为像真实类型一样。
以下是如何使用它的一个非常基本的示例:
AnonymousTypeUtils.CreateObject(new Dictionary<string, object>
{
    { "a", 1 },
    { "b", 2 }
});

另外,需要注意的是:当使用动态匿名类型与Entity Framework一起使用时,必须使用设置了"members"参数的构造函数。例如:
Expression.New(
    constructor: anonymousType.GetConstructors().Single(), 
    arguments: propertyExpressions,
    members: anonymousType.GetProperties().Cast<MemberInfo>().ToArray()
); 

如果您使用的Expression.New版本不包括“members”参数,则Entity Framework将无法将其识别为匿名类型的构造函数。因此,我认为真正的匿名类型的构造函数表达式将包括该“members”信息。

6

也许有些晚了,但可能会对某些人有所帮助。

你可以通过在实体中调用 DynamicSelectGenerator 来生成动态选择器。

public static Func<T, T> DynamicSelectGenerator<T>()
            {
                // get Properties of the T
                var fields = typeof(T).GetProperties().Select(propertyInfo => propertyInfo.Name).ToArray();

            // input parameter "o"
            var xParameter = Expression.Parameter(typeof(T), "o");

            // new statement "new Data()"
            var xNew = Expression.New(typeof(T));

            // create initializers
            var bindings = fields.Select(o => o.Trim())
                .Select(o =>
                {

                    // property "Field1"
                    var mi = typeof(T).GetProperty(o);

                    // original value "o.Field1"
                    var xOriginal = Expression.Property(xParameter, mi);

                    // set value "Field1 = o.Field1"
                    return Expression.Bind(mi, xOriginal);
                }
            );

            // initialization "new Data { Field1 = o.Field1, Field2 = o.Field2 }"
            var xInit = Expression.MemberInit(xNew, bindings);

            // expression "o => new Data { Field1 = o.Field1, Field2 = o.Field2 }"
            var lambda = Expression.Lambda<Func<T, T>>(xInit, xParameter);

            // compile to Func<Data, Data>
            return lambda.Compile();
        }

并使用以下代码:

var result = dbContextInstancs.EntityClass.Select(DynamicSelectGenerator<EntityClass>());

Expression.New(typeof(T))如果T是实体映射类型之一,则无法正常工作。 - Mabakay

2
你可以在这里使用IQueryable-Extensions,它是“Ethan J. Brown”所描述的解决方案的实现: https://github.com/thiscode/DynamicSelectExtensions 该扩展动态构建一个匿名类型。
然后你可以这样做:
var YourDynamicListOfFields = new List<string>(

    "field1",
    "field2",
    [...]

)
var query = query.SelectPartially(YourDynamicListOfFields);

2
我不相信你能够实现这个。尽管当你执行select new { c.Name, c.Population }时,看起来好像你并没有创建一个类,但实际上是创建了一个类。如果你查看Reflector或原始IL的编译输出,你就会看到这一点。
你将会得到一个类,它看起来像这样:
[CompilerGenerated]
private class <>c__Class {
  public string Name { get; set; }
  public int Population { get; set; }
}

(好的,我稍微整理了一下,因为属性实际上只是一个get_Name()set_Name(name)方法集)

你想要做的是正确的动态类创建,这是在.NET 4.0之前不可用的(即使在那之后,我也不确定它是否能够实现你想要的)。

你最好的解决方案是定义不同的匿名类,然后有一些逻辑检查来确定要创建哪一个,并且可以使用对象System.Linq.Expressions.NewExpression来创建它。

但是,如果你对基础的LINQ提供程序非常熟悉的话,理论上可能会做到。如果你正在编写自己的LINQ提供程序,你可以检测当前解析的表达式是否为Select,然后确定CompilerGenerated类,反射其构造函数并创建。

这绝对不是一项简单的任务,但这就是LINQ to SQL、LINQ to XML等都是如何实现的。


很好的总结。可惜没有办法生成一个表达式来生成新类型。虽然,我可以想象这会带来很多麻烦。 :) - Inferis
我将检查System.Linq.Dynamic中的扩展功能是如何工作的,我猜想如果这个类可以做到,肯定有一种方法可以实现。 - Tom Deloford

1
你可以使用参数类来代替使用匿名类型。在你的例子中,你可以创建一个像这样的参数类:
public struct ParamClass {
    public string Name { get; set; };
    public int Population { get; set; };
}

...并将其放入您的选择器中,如下所示:

var v = from c in Countries
        where c.City == "London"
        select new ParamClass {c.Name, c.Population};

你得到的是类型为IQueryable<ParamClass>的东西。


1

这个可以编译,但我不知道它是否有效...

myEnumerable.Select((p) => { return new { Name = p.Name, Description = p.Description }; });

假设p就是你正在转换的对象,而select语句返回的是匿名类型,使用lambda函数声明。

编辑:我也不知道如何动态生成这个。但至少它展示了如何使用select lambda返回一个具有多个值的匿名类型。

编辑2:

你还必须记住,c#编译器实际上会生成匿名类型的静态类。因此,在编译时后,匿名类型确实有一个类型。所以如果你在运行时生成这些查询(我认为你是在这样做),你可能需要使用各种反射方法构造一个类型(我相信你可以用它们来即时创建类型),将创建的类型加载到执行上下文中,并在生成的输出中使用它们。


1

我认为大部分问题已经得到了解答 - 正如Slace所说,您需要一些从Select方法返回的类。一旦您拥有了这个类,就可以使用System.Linq.Expressions.NewExpression方法来创建表达式。

如果您真的想要这样做,您也可以在运行时生成类。这需要更多的工作,因为它不能使用LINQ表达式树来完成,但是它是可能的。您可以使用System.Reflection.Emit命名空间来完成这个任务 - 我只是进行了一个快速搜索,这里有一篇文章解释了这个过程:


0
你可以使用动态表达式 API 来动态构建你的选择语句,就像这样:
 Select("new(<property1>,<property2>,...)");

你需要从Visual Studio的LINQ和语言示例中获取Dynamics.cs文件才能使其工作,这两个文件都在this page底部链接。你也可以在相同的URL上看到一个展示该功能的工作示例。

我相信这只适用于LINQ to SQL,而不是其他的LINQ提供程序。 - Aaron Powell
我相信这个框架只能与IQueryable一起使用,而不能与IEnumerable一起使用。 - Adrian Grigore
我尝试了你的代码,但是它出现了错误。如何在使用DataContext的Entity Framework中实现上述代码? - Thulasiram

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接