动态选择

本文关键字:选择 动态 | 更新日期: 2023-09-27 18:13:19

假设我们有这样一个类:

    public  class Data
{
    public string Field1 { get; set; }
    public string Field2 { get; set; }
    public string Field3 { get; set; }
    public string Field4 { get; set; }
    public string Field5 { get; set; }
}

如何动态选择指定的列?像这样:

  var list = new List<Data>();
  var result= list.Select("Field1,Field2"); // How ?

这是唯一的解决方案吗=> Dynamic LINQ ?
所选字段在编译时是未知的。它们将在运行时指定

动态选择

您可以通过动态创建传递给Select:的lambda来实现这一点

Func<Data,Data> CreateNewStatement( string fields )
{
    // input parameter "o"
    var xParameter = Expression.Parameter( typeof( Data ), "o" );
    // new statement "new Data()"
    var xNew = Expression.New( typeof( Data ) );
    // create initializers
    var bindings = fields.Split( ',' ).Select( o => o.Trim() )
        .Select( o => {
            // property "Field1"
            var mi = typeof( Data ).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<Data,Data>>( xInit, xParameter );
    // compile to Func<Data, Data>
    return lambda.Compile();
}

那么你可以这样使用:

var result = list.Select( CreateNewStatement( "Field1, Field2" ) );

除了Nicholas Butler和Matt评论中的提示(使用T作为输入类的类型),我对Nicholas的回答进行了改进,动态生成实体属性,函数不需要发送field作为参数。

public static class Helpers
{
    public static Func<T, T> DynamicSelectGenerator<T>(string Fields = "")
    {
        string[] EntityFields;
        if (Fields == "")
            // get Properties of the T
            EntityFields = typeof(T).GetProperties().Select(propertyInfo => propertyInfo.Name).ToArray();
        else
            EntityFields = Fields.Split(',');
        // input parameter "o"
        var xParameter = Expression.Parameter(typeof(T), "o");
        // new statement "new Data()"
        var xNew = Expression.New(typeof(T));
        // create initializers
        var bindings = EntityFields.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();
    }
}

DynamicSelectGenerator方法获取类型为T的实体,这个方法有可选的输入参数Fields,如果你想从实体中选择特殊的字段作为字符串发送,如"Field1, Field2",如果你不发送任何东西给方法,它返回实体的所有字段,你可以使用这个方法如下:

 using (AppDbContext db = new AppDbContext())
            {
                //select "Field1, Field2" from entity
                var result = db.SampleEntity.Select(Helpers.DynamicSelectGenerator<SampleEntity>("Field1, Field2")).ToList();
                //select all field from entity
                var result1 = db.SampleEntity.Select(Helpers.DynamicSelectGenerator<SampleEntity>()).ToList();
            }

(假设您有一个名称为AppDbContextDbContext,并且上下文有一个名称为SampleEntity的实体)

您必须使用反射来获取和设置带有其名称的属性值。

  var result = new List<Data>();
  var data = new Data();
  var type = data.GetType();
  var fieldName = "Something";
  for (var i = 0; i < list.Count; i++)
  {
      foreach (var property in data.GetType().GetProperties())
      {
         if (property.Name == fieldName)
         {
            type.GetProperties().FirstOrDefault(n => n.Name == property.Name).SetValue(data, GetPropValue(list[i], property.Name), null);
            result.Add(data);
         }
      }
  }

这里是GetPropValue()方法

public static object GetPropValue(object src, string propName)
{
   return src.GetType().GetProperty(propName).GetValue(src, null);
}

使用反射和表达式构建可以做你说的。例子:

var list = new List<Data>();
//bulid a expression tree to create a paramter
ParameterExpression param = Expression.Parameter(typeof(Data), "d");
//bulid expression tree:data.Field1
Expression selector = Expression.Property(param,typeof(Data).GetProperty("Field1"));
Expression pred = Expression.Lambda(selector, param);
//bulid expression tree:Select(d=>d.Field1)
Expression expr = Expression.Call(typeof(Queryable), "Select",
    new Type[] { typeof(Data), typeof(string) },
    Expression.Constant(list.AsQueryable()), pred);
//create dynamic query
IQueryable<string> query = list.AsQueryable().Provider.CreateQuery<string>(expr);
var result=query.ToList();

我在下一行中编写方法,以便您可以利用Nicholas Butler和Ali使用嵌套字段。

您可以使用此方法动态创建传递到select的lambda,也适用于嵌套字段。您也可以使用IQueryable案例。

    /// <param name="Fields">
    /// Format1: "Field1"
    /// Format2: "Nested1.Field1"
    /// Format3: "Field1:Field1Alias"
    /// </param>
    public static Expression<Func<T, TSelect>> DynamicSelectGenerator<T, TSelect>(params string[] Fields)
    {
        string[] EntityFields = Fields;
        if (Fields == null || Fields.Length == 0)
            // get Properties of the T
            EntityFields = typeof(T).GetProperties().Select(propertyInfo => propertyInfo.Name).ToArray();
        // input parameter "x"
        var xParameter = Expression.Parameter(typeof(T), "x");
        // new statement "new Data()"
        var xNew = Expression.New(typeof(TSelect));
        // create initializers
        var bindings = EntityFields
            .Select(x =>
            {
                string[] xFieldAlias = x.Split(":");
                string field = xFieldAlias[0];
                string[] fieldSplit = field.Split(".");
                if (fieldSplit.Length > 1)
                {
                    // original value "x.Nested.Field1"
                    Expression exp = xParameter;
                    foreach (string item in fieldSplit)
                        exp = Expression.PropertyOrField(exp, item);
                    // property "Field1"
                    PropertyInfo member2 = null;
                    if (xFieldAlias.Length > 1)
                        member2 = typeof(TSelect).GetProperty(xFieldAlias[1]);
                    else
                        member2 = typeof(T).GetProperty(fieldSplit[fieldSplit.Length - 1]);
                    // set value "Field1 = x.Nested.Field1"
                    var res = Expression.Bind(member2, exp);
                    return res;
                }
                // property "Field1"
                var mi = typeof(T).GetProperty(field);
                PropertyInfo member;
                if (xFieldAlias.Length > 1)
                    member = typeof(TSelect).GetProperty(xFieldAlias[1]);
                else member = typeof(TSelect).GetProperty(field);
                // original value "x.Field1"
                var xOriginal = Expression.Property(xParameter, mi);
                // set value "Field1 = x.Field1"
                return Expression.Bind(member, xOriginal);
            }
        );
        // initialization "new Data { Field1 = x.Field1, Field2 = x.Field2 }"
        var xInit = Expression.MemberInit(xNew, bindings);
        // expression "x => new Data { Field1 = x.Field1, Field2 = x.Field2 }"
        var lambda = Expression.Lambda<Func<T, TSelect>>(xInit, xParameter);
        return lambda;
    }

用法:

var s = DynamicSelectGenerator<SalesTeam, SalesTeamSelect>(
            "Name:SalesTeamName",
            "Employee.FullName:SalesTeamExpert"
            );
var res = _context.SalesTeam.Select(s);
public class SalesTeam
{
    public string Name {get; set; }
    public Guid EmployeeId { get; set; }
    public Employee Employee { get; set; }
}
public class SalesTeamSelect
{
    public string SalesTeamName {get; set; }
    public string SalesTeamExpert {get; set; }
}

OP提到了动态Linq库,所以我想对它的用法做一个解释。

1。动态Linq内置Select

Dynamic Linq有一个内置的Select方法,可以这样使用:

var numbers = new List<int> { 1, 2, 3 };
var wrapped = numbers.Select(num => new { Value = num }).ToList();
// the "it" keyword functions as the lambda parameter,
// so essentialy it's like calling: numbers.Select(num => num)
var selectedNumbers = numbers.Select("it"); 
// the following is the equivalent of calling: wrapped.Select(num => num.Value)
var selectedValues = wrapped.Select("Value");
// the following is the equivalent of calling: numbers.Select(num => new { Value = num })
var selectedObjects = numbers.Select("new(it as Value)"); 
foreach (int num in selectedNumbers) Console.WriteLine(num);
foreach (int val in selectedValues) Console.WriteLine(val);
foreach (dynamic obj in selectedObjects) Console.WriteLine(obj.Value);

使用内置的Select:

有一些缺点

因为它是IQueryable(而不是IQueryable<T>)扩展方法,返回类型为IQueryable,所以不能使用常见的物化方法(如ToListFirstOrDefault)。这就是上面的例子使用foreach的原因——它只是实现结果的唯一方便的方法。

所以为了更方便,让我们支持这些方法。

2。在动态Linq中支持Select<T>(启用ToList和类似的)

要支持Select<T>,需要将其添加到Dynamic Linq文件中。这样做的简单步骤在这个答案和我的评论中都有解释。

这样做之后,它可以按以下方式使用:

var numbers = new List<int> { 1, 2, 3 };
var wrapped = numbers.Select(num => new { Value = num }).ToList();
// the following is the equivalent of calling: numbers.Select(num => num).ToList()
var selectedNumbers = numbers.Select<int>("it").ToList(); 
// the following is the equivalent of calling: wrapped.Select(num => num.Value).ToList()
var selectedValues = wrapped.Select<int>("Value").ToList();
// the following is the equivalent of calling: numbers.Select(num => new { Value = num }).ToList()
var selectedObjects = numbers.Select<object>("new(it as Value)").ToList(); 

可以说,这个实现引入了另一种缺点:由于必须显式地参数化Select<T>调用(例如,必须调用Select<int>),我们失去了库的动态特性。

然而,由于我们现在可以调用任何物化Linq方法,这种用法可能仍然非常有用。

我简化了Ali创建的神奇方法DynamicSelectGenerator(),并使这个扩展方法重载LINQ Select(),使其接受列分隔的参数,以简化使用并提高可读性:

public static IEnumerable<T> Select<T>(this IEnumerable<T> source, string parameters)
{
    return source.Select(DynamicSelectGenerator<T>(parameters));
}

所以不是:

var query = list.Select(Helpers.DynamicSelectGenerator<Data>("Field1,Field2")).ToList();
将:

var query = list.Select("Field1,Field2").ToList();

我使用的另一种方法是嵌套三元操作符:

string col = "Column3";
var query = table.Select(i => col == "Column1" ? i.Column1 :
                              col == "Column2" ? i.Column2 :
                              col == "Column3" ? i.Column3 :
                              col == "Column4" ? i.Column4 :
                              null);

三元运算符要求每个字段都是相同的类型,因此您需要在任何非字符串列上调用. tostring()。

为了同样的用途,我已经生成了我自己的类。

github: https://gist.github.com/mstrYoda/663789375b0df23e2662a53bebaf2c7c

为给定字符串生成动态选择lambda,并支持两层嵌套属性。

用法示例:

class Shipment {
   // other fields...
   public Address Sender;
   public Address Recipient;
}
class Address {
    public string AddressText;
    public string CityName;
    public string CityId;
}
// in the service method
var shipmentDtos = _context.Shipments.Where(s => request.ShipmentIdList.Contains(s.Id))
                .Select(new SelectLambdaBuilder<Shipment>().CreateNewStatement(request.Fields)) // request.Fields = "Sender.CityName,Sender.CityId"
                .ToList();

它按如下方式编译lambda:

s => new Shipment {
    Sender = new Address {
        CityId = s.Sender.CityId,
        CityName = s.Sender.CityName
    }
}

你也可以在这里找到我的问题和答案:c# -动态生成linq select嵌套属性

public class SelectLambdaBuilder<T>
{
// as a performence consideration I cached already computed type-properties
private static Dictionary<Type, PropertyInfo[]> _typePropertyInfoMappings = new Dictionary<Type, PropertyInfo[]>();
private readonly Type _typeOfBaseClass = typeof(T);
private Dictionary<string, List<string>> GetFieldMapping(string fields)
{
    var selectedFieldsMap = new Dictionary<string, List<string>>();
    foreach (var s in fields.Split(','))
    {
        var nestedFields = s.Split('.').Select(f => f.Trim()).ToArray();
        var nestedValue = nestedFields.Length > 1 ? nestedFields[1] : null;
        if (selectedFieldsMap.Keys.Any(key => key == nestedFields[0]))
        {
            selectedFieldsMap[nestedFields[0]].Add(nestedValue);
        }
        else
        {
            selectedFieldsMap.Add(nestedFields[0], new List<string> { nestedValue });
        }
    }
    return selectedFieldsMap;
}
public Func<T, T> CreateNewStatement(string fields)
{
    ParameterExpression xParameter = Expression.Parameter(_typeOfBaseClass, "s");
    NewExpression xNew = Expression.New(_typeOfBaseClass);
    var selectFields = GetFieldMapping(fields);
    var shpNestedPropertyBindings = new List<MemberAssignment>();
    foreach (var keyValuePair in selectFields)
    {
        PropertyInfo[] propertyInfos;
        if (!_typePropertyInfoMappings.TryGetValue(_typeOfBaseClass, out propertyInfos))
        {
            var properties = _typeOfBaseClass.GetProperties();
            propertyInfos = properties;
            _typePropertyInfoMappings.Add(_typeOfBaseClass, properties);
        }
        var propertyType = propertyInfos
            .FirstOrDefault(p => p.Name.ToLowerInvariant().Equals(keyValuePair.Key.ToLowerInvariant()))
            .PropertyType;
        if (propertyType.IsClass)
        {
            PropertyInfo objClassPropInfo = _typeOfBaseClass.GetProperty(keyValuePair.Key);
            MemberExpression objNestedMemberExpression = Expression.Property(xParameter, objClassPropInfo);
            NewExpression innerObjNew = Expression.New(propertyType);
            var nestedBindings = keyValuePair.Value.Select(v =>
            {
                PropertyInfo nestedObjPropInfo = propertyType.GetProperty(v);
                MemberExpression nestedOrigin2 = Expression.Property(objNestedMemberExpression, nestedObjPropInfo);
                var binding2 = Expression.Bind(nestedObjPropInfo, nestedOrigin2);
                return binding2;
            });
            MemberInitExpression nestedInit = Expression.MemberInit(innerObjNew, nestedBindings);
            shpNestedPropertyBindings.Add(Expression.Bind(objClassPropInfo, nestedInit));
        }
        else
        {
            Expression mbr = xParameter;
            mbr = Expression.PropertyOrField(mbr, keyValuePair.Key);
            PropertyInfo mi = _typeOfBaseClass.GetProperty( ((MemberExpression)mbr).Member.Name );
            var xOriginal = Expression.Property(xParameter, mi);
            shpNestedPropertyBindings.Add(Expression.Bind(mi, xOriginal));
        }
    }
    var xInit = Expression.MemberInit(xNew, shpNestedPropertyBindings);
    var lambda = Expression.Lambda<Func<T,T>>( xInit, xParameter );
    return lambda.Compile();
}

谢谢@morio。你对Expression<T,>>这正是我所需要的。

我不知道如何执行匿名投影,这似乎是最想要的。我说我想要Field1和Field2从数据,我得到的东西像:new { Field1 = o.Field1, Field2 = o.Field2 };
但我也有类似的需求,我想绘制x和y值,但直到运行时才知道它们是哪一个。因此,我不使用匿名对象,而是创建一个具有我想要的属性的对象。在这种情况下,是X和Y。下面是源类和目标类:

    public class Source
    {
        public int PropertyA { get; set; }
        public double PropertyB { get; set; }
        public double PropertyC { get; set; }
    }
    public class Target
    {
        public double X { get; set; }
        public double Y { get; set; }
    }

下面是在源和目标之间进行映射的代码。

public static class SelectBuilder
{
    /// <summary>
    /// Creates a Func that can be used in a Linq Select statement that will map from the source items to a new target type.
    /// Typical usage pattern is that you have an Entity that has many properties, but you want to dynamically set properties
    /// on a smaller target type, AND, you don't know the mapping at compile time.
    /// For example, you have an Entity that has a year and 10 properties. You want to have time (year) as the X axis, but
    /// the user can chose any of the 10 properties to plot on the y axis. This would allow you to map one of the entity 
    /// properties to the Y value dynamically.
    /// </summary>
    /// <typeparam name="TSource">Type of the source, for example, and Entity Framework entity.</typeparam>
    /// <typeparam name="TTarget">Type of the target, a projection of a smaller number of properties than the entity has.</typeparam>
    /// <param name="propertyMappings">A list of named tuples that map the sourceProperty to the targetProperty.</param>
    /// <returns>A func that can be used inside the Select. 
    /// So if 
    /// var select = SelectBuilder.GetSelectStatement<Source, Target>(propertyMappings), then
    /// you can perform the select, 
    /// var results = items.Select(select);</returns>
    public static Expression<Func<TSource, TTarget>> GetSelectStatement<TSource, TTarget>(IEnumerable<(string sourceProperty, string targetProperty)> propertyMappings)
    {
        // Get the source parameter, "source". This will allow the statement to be "X = source.SourceA".
        // It needs to be of the source type, and the name is what will be used in the Select lambda.
        var sourceParameter = Expression.Parameter(typeof(TSource), "source");
        // Now define the ability to create a new Target type.
        var newTarget = Expression.New(typeof(TTarget));
        // Now develop the bindings or member assignments for each property.
        var bindings = new List<MemberAssignment>();
        foreach (var propertyMapping in propertyMappings)
        {
            var sourceMemberInfo = typeof(TSource).GetProperty(propertyMapping.sourceProperty);
            var targetMemberInfo = typeof(TTarget).GetProperty(propertyMapping.targetProperty);
            // This allows getting the value. Source parameter will provide the "source" part and sourceMemberInfo the property name.
            // For example, "source.SourceA".
            var sourceValue = Expression.Property(sourceParameter, sourceMemberInfo);
            // Provide conversion in the event there is not a perfect match for the type.
            // For example, if SourceA is int and the target X is double?, we need to convert from int to double?
            var convertExpression = Expression.Convert(sourceValue, targetMemberInfo.PropertyType);
            // Put together the target assignment, "X = Convert(source.SourcA, double?)" (TODO: How does the convert actually happen?)
            var targetAssignment = Expression.Bind(targetMemberInfo, convertExpression);
            bindings.Add(targetAssignment);
        }
        var memberInit = Expression.MemberInit(newTarget, bindings);
        // Here if we map SourceA to X and SourceB to Y the lambda will be:
        // {source => new Target() {X = Convert(source.SourceA, Nullable`1), Y = Convert(source.SourceB, Nullable`1)}}
        var lambda = Expression.Lambda<Func<TSource, TTarget>>(memberInit, sourceParameter);
        return lambda;//.Compile();
    }
}

最后是一个可以工作的单元测试。

    [Fact(DisplayName = "GetSelectStatement works")]
    public void Test2()
    {
        // Arrange
        var source = new Source { PropertyA = 1, PropertyB = 2, PropertyC = 3 };
        var expectedX = Convert.ToDouble(source.PropertyA);
        var expectedY = Convert.ToDouble(source.PropertyB);
        var items = new List<Source> { source }.AsQueryable();
        // Let's map SourceA to X and SourceB to Y.
        var propertyMappings = new List<(string sourceProperty, string targetProperty)>
        {
            ("PropertyA", "X"), ("PropertyB", "Y")
            //(nameof(Source.PropertyA), nameof(Target.X)),
            //(nameof(Source.PropertyB), nameof(Target.Y))
        };
        // Act
        var select = SelectBuilder.GetSelectStatement<Source, Target>(propertyMappings);
        var actual = items.Select(select).First();
        // Assert
        actual.X.Should().Be(expectedX);
        actual.Y.Should().Be(expectedY);
    }

我已经编辑了我以前的答案,因为现在我知道如何从int转换到double。我还使单元测试更容易理解。

我希望这有助于别人。

使用ExpandoObject你可以建立一个动态对象或者从下面的例子中返回完整的对象。

public object CreateShappedObject(object obj, List<string> lstFields)
{
    if (!lstFields.Any())
    {
        return obj;
    }
    else
    {
        ExpandoObject objectToReturn = new ExpandoObject();
        foreach (var field in lstFields)
        {
            var fieldValue = obj.GetType()
                .GetProperty(field, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)
                .GetValue(obj, null);
            ((IDictionary<string, object>)objectToReturn).Add(field, fieldValue);
        }
        return objectToReturn;
    }
}

下面是如何在控制器中使用此功能的示例。

http://localhost: 12345/api/yourapi吗?字段= field1, field2

public IHttpActionResult Get(string fields = null)
{
    try
    {
        List<string> lstFields = new List<string>();
        if (fields != null)
        {
            lstFields = fields.ToLower().Split(',').ToList();
        }
   
        // Custom query
        var result = db.data.Select(i => CreateShappedObject(new Data()
        , lstFields)).ToList();
        return Ok(result);
    }
    catch(Exception)
    {
        return InternalServerError();
    }
}
var result = from g in list.AsEnumerable()
                select new {F1 = g.Field1,F2  = g.Field2};