Search code examples
c#linqlambdadynamicobject

How do I create and populate a dynamic object using a dynamically built lambda expression


I'm trying to create and populate a dynamic object from a dataset that is only known at run time. In the code below, I create my IEnumerable results from my dataset with some known fields (ID, Primary Data, DisplayOrder, IsActive) and one user-define field (Phone Number) which I won't know at design time, and therefore must be built dynamically. The code below works, but only because I've hard-coded the dynamic field Phone Number. How do I build the Lambda expression dynamically to handle those fields only known at runtime. I want the equivalent of

string fieldName = 'PhoneNumber = ';
string fieldSource = 'pd.tbList_DataText';
string expression = 'pd=>new { ID = pd.ID, PrimaryData=pd.PrimaryData';
expression += fieldName;
expression += FieldSource;
expression += '.Where(ld => ld.DataRowID == pd.ID && ld.ListColumnID == 1).Select(ld => ld.DataField).DefaultIfEmpty("").First()})';

var results = primaryData.Select(expression);

So how do I make that work? Thanks

         // Get base Data
         IEnumerable<tbList_Data> primaryData = await _tbList_DataRepository.GetAsync(ld => ld.ListID == listId && (ld.IsActive     == includeInactive ? ld.IsActive : true));

        // Get Final Results
        var results = primaryData.Select(pd => new {
            Id = pd.ID,
            PrimaryData = pd.PrimaryData,
            PhoneNumber = pd.tbList_DataText.Where(ld => ld.DataRowID == pd.ID && ld.ListColumnID == 1).Select(ld => ld.DataField).DefaultIfEmpty("").First()
        });

Solution

  • I see several options.

    1) Tuple

    var results = primaryData.Select(pd => new {
        Id = pd.ID,
        PrimaryData = pd.PrimaryData,
        Extra = Tuple.Create("PhoneNumber", pd.tbList_DataText.Where(ld => ld.DataRowID == pd.ID && ld.ListColumnID == 1).Select(ld => ld.DataField).DefaultIfEmpty("").First())
    });
    
    // How to access:
    foreach (var result in results)
    {
        Console.WriteLine(result.Id);
        Console.WriteLine(result.PrimaryData);
        Console.WriteLine(result.Extra.Item1);
        Console.WriteLine(result.Extra.Item2);
    }
    

    2) Dictionary

    // Using C# 6 notation
    var results = primaryData.Select(pd => new Dictionary<string, object>{
        ["Id"] = pd.ID,
        ["PrimaryData"] = pd.PrimaryData,
        ["PhoneNumber"] = pd.tbList_DataText.Where(ld => ld.DataRowID == pd.ID && ld.ListColumnID == 1).Select(ld => ld.DataField).DefaultIfEmpty("").First()
    });
    
    // Using C# 5 notation
    var results = primaryData.Select(pd => new Dictionary<string, object>{
        {"Id", pd.ID},
        {"PrimaryData", pd.PrimaryData},
        {"PhoneNumber", pd.tbList_DataText.Where(ld => ld.DataRowID == pd.ID && ld.ListColumnID == 1).Select(ld => ld.DataField).DefaultIfEmpty("").First()}
    });
    
    // How to access:
    foreach(var result in results)
    {
        Console.WriteLine(result["Id"]);
        Console.WriteLine(result["PrimaryData"]);
        Console.WriteLine(result["PhoneNumber"]);
    }
    

    3) Dynamic

    var results = primaryData.Select(pd => {
        dynamic result = new System.Dynamic.ExpandoObject();
        result.Id = pd.ID;
        result.PrimaryData = pd.PrimaryData;
    
        // Choose one of the following. Since you only "PhoneNumber" at runtime, probably the second one.
        result.PhoneNumber = pd.tbList_DataText.Where(ld => ld.DataRowID == pd.ID && ld.ListColumnID == 1).Select(ld => ld.DataField).DefaultIfEmpty("").First();
        ((IDictionary<string, object>)result).Add("PhoneNumber", pd.tbList_DataText.Where(ld => ld.DataRowID == pd.ID && ld.ListColumnID == 1).Select(ld => ld.DataField).DefaultIfEmpty("").First());
    
        return result;
    });
    
    // How to access:
    foreach(var result in results)
    {
        Console.WriteLine(result.Id);
        Console.WriteLine(result.PrimaryData);
    
        // Both work, independently how you created them
        Console.WriteLine(result.PhoneNumber);
        Console.WriteLine(((IDictionary<string, object>)result)["PhoneNumber"]);
    }
    

    EDIT: just realized from question that the field source should be dynamic as well. So, in the above code, replace any occurrence of pb.tbList_DataText by:

    ((IEnumerable<X>)pb.GetType().GetField("tbList_DataText").GetValue(pb))
    

    Where X should be the type of ld. But carefull! This cast can potentially fail.

    Also, if you want a property instead of a field, just use GetProperty instead of GetField.