Search code examples
c#lambdalinq-expressions

IndexExpression to MemberBinding


I have an issue which I need some help with. There is an object say TestObject

public class TestObject
{
      public int Value { get; set; }
      public string StringValue {get;set;}
} 

This object is dynamically being written to a csv file with the "Value" & "StringValue" being the header so a sample list of this type initialized below

    var testObjects = new List<TestObject> { new TestObject() { Value = 1, StringValue = "A" },
        new TestObject() { Value = 2, StringValue = "B" }, new TestObject() { Value = 3, StringValue = "C" } };

would be represented as a table

╔═══════╦═════════════╗
║ Value ║ StringValue ║
╠═══════╬═════════════╣
║     1 ║ A           ║
║     2 ║ B           ║
║     3 ║ C           ║
╚═══════╩═════════════╝    

I am dynamically creating a list of member bindings for this object, which in this instance is 2 bindings; 1 for the "Value" property and 1 for the "StringValue" property. Lets call the list of these bindings where testObjectBindings is a List of MemberBindings.

I ultimately want to recreate the list of testobjects that have been written so the pseudo code would be

var recreatedList = new List<TestObject>();
for (int i=0; i <3; i++)
{
   //create a new test object and use
   //the binding information
   var newObject = new TestObject()
   {binding[0], binding[1]};
}

From what I understand about expressions the way it would work is as follows

    var indexer = testObjectBindings.GetType()
        .GetDefaultMembers()
        .OfType<PropertyInfo>()
        .First();

//where 0 is the memberbinding at index 0 of the generated bindings

IndexExpression propertyExpression = Expression.Property(Expression.Constant(testObjectBindings), indexer, 0);

I want to use a Expression.MemberInit to construct the test object and assign the binding like below

 MemberInitExpression body =  Expression.MemberInit(Expression.New(typeof(TestObject)), propertyExpression[0] { });

That does not work since the IndexExpression is not a MemberBinding. How do I cast or represent the returned property as a MemberBinding so I can use the MemberInit call?

Thanks for reading


Solution

  • I'm assuming the following because your question is quite unclear to me:

    • You want to convert a CSV file into a list of objects.
    • The first line of a CSV file contains a header, where the names in the header map to a property names for a target objects.
    • The objects are created using a default constructor, after which you set the properties one by one (the MemberInit expression type represents C# syntax sugar, which has very little added value when dynamically constructing code).

    I've added more details at the end to explain why MemberBinding does not help in this situation.

    If this is the case, we can boil our main problem down to creating the following method dynamically:

    TestObject FromCsvLine(string[] csvFields) {
        var result = new TestObject();
        result.Value = (int)Convert.ChangeType(csvFields[0], typeof(int));
        result.StringValue = (string)Convert.ChangeType(csvFields[1], typeof(string));
        return result;
    }
    

    Note the Convert.ChangeType call, its structure remains the same regardless of property types, we only have to supply a different parameter, making it an easy case to construct dynamically. Also note that the signature of this function can be described using Func<string[], TestObject>.

    The data we need to create this method is:

    • The target type to deserialize to
    • A list of column names

    Therefore, we get the following method signature:

    Func<string[], T> CreateCsvDeserializer<T>(string[] columnNames)
          where T : new()
    

    The new() constraint enforces at compile time that the type T will have a constructor with 0 parameters.

    The implementation:

    private static Func<string[], T> CreateCsvDeserializer<T>(string[] columnNames)
        where T : new()
    {
        var resultVariable = Expression.Variable(typeof (T), "result");
        var csvFieldsParameter = Expression.Parameter(typeof (string[]), "csvFields");
        var constructorCall = Expression.Assign(resultVariable, Expression.New(typeof (T)));
    
        //will contain all code lines that implement the method
        var codeLines = new List<Expression> {constructorCall};
    
        for (int i = 0; i < columnNames.Length; i++)
        {
            string columnName = columnNames[i];
            PropertyInfo property = typeof (T).GetProperty(columnName);
            if (property == null || !property.CanWrite || !property.GetSetMethod().IsPublic)
            {
                //cannot write to property
                throw new Exception();
            }
    
            //Convert.ChangeType(object, Type)
            var convertChangeTypeMethod = typeof (Convert).GetMethod("ChangeType",
                new[] {typeof (object), typeof (Type)});
    
            //csvFields[i]
            var getColumn = Expression.ArrayIndex(csvFieldsParameter, Expression.Constant(i));
    
            //Convert.ChangeType(csvFields[i], [propertyType])
            var conversion = Expression.Call(convertChangeTypeMethod, getColumn,
                Expression.Constant(property.PropertyType));
    
            //([propertyType])Convert.ChangeType(csvFields[i], [propertyType])
            var cast = Expression.Convert(conversion, property.PropertyType);
    
            //result.[property]
            var propertyExpression = Expression.Property(resultVariable, property);
    
            //result.[property] = ([propertyType])Convert.ChangeType(csvFields[i], [propertyType])
            codeLines.Add(Expression.Assign(propertyExpression, cast));
        }
    
        //create a line that returns the resultVariable
        codeLines.Add(resultVariable);
    
        //now, we have a list of code lines, it's time to build our function
        Type returnType = typeof (T);
        var variablesUsed = new[] {resultVariable};
        var codeBlock = Expression.Block(returnType, variablesUsed, codeLines);
        var parameterList = new[] {csvFieldsParameter};
        return Expression.Lambda<Func<string[], T>>(codeBlock, parameterList).Compile();
    } 
    

    As can be seen in the debugger, the generated code lines when called using CreateCsvDeserializer<TestObject>(new [] { "Value", "StringValue" }); matches (apart from some syntax differences) the code we set out to build in the sample above.

    Debugger output

    After the method has been created, we just need to feed it the CSV lines and build the list, which is relatively easy:

    private static List<T> BuildFromCsvFile<T>(string path, string separator = ",")
        where T : new()
    {
        string[] separators = {separator};
        var lines = File.ReadAllLines(path);
        var deserializer = CreateCsvDeserializer<T>(lines[0].Split(separators, StringSplitOptions.RemoveEmptyEntries));
        return
            lines.Skip(1)
                .Select(s => s.Split(separators, StringSplitOptions.RemoveEmptyEntries))
                .Select(deserializer)
                .ToList();
    } 
    

    And then construct the list by calling, for example BuildFromCsvFile<TestObject>("Data.csv");

    Problems not addressed by this solution I can think of off the top of my head:

    • Localization of data, different date/number formats. The Convert.ChangeType accepts an IFormatProvider allowing culture to be specified. Property attributes could then specify how to convert the CSV data to the appropriate type.
    • Complex types / custom type construction. (e.g. Person.Address.Street)
    • Decent error handling

    Why MemberBinding is not the way to go

    The System.Linq.Expressions namespace allows us to essentialy build code generators. The MemberBinding class is a base class for the MemberAssignment, MemberListBinding and MemberMemberBinding classes. Each of these expressions represent operations that could also be expressed using property or field setters. Using your original example of TestObject:

    var obj = new TestObject { 
           Value = 1 //MemberBinding expression
    };
    

    This is equivalent to:

    var obj = new TestObject();
    obj.Value = 1;
    

    As we have seen, writing a code generator is much more complex than writing the code we generate, and we want to keep it as simple as possible. Using different kinds of syntax usually will not make it less complex.