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
I'm assuming the following because your question is quite unclear to me:
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:
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.
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:
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.Person.Address.Street
)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.