Since you already knew the headers as used in your for
loop, then a delegate would reduce the number of loops needed to do the repeated task of creating the POCO.
string[] headers = parser.ReadFields();
Func<string[], MassUploadUser> factory = (string[] fields) =>
new MassUploadUser {
email = fields[headers.IndexOf("email")],
firstName = fields[headers.IndexOf("First Name")],
lastName = fields[headers.IndexOf("Last Name")],
role = fields[headers.IndexOf("Role")]
};
//...
Which in itself is rather simple for this example. But for more complex data, can become really complicated really fast.
I am very partial to building expressions and delegates to handle generic repeated tasks that lend themselves well to LINQ. This scenario would fall into that category.
Building upon the already provided suggestions, which provide some good suggestions, a lambda expression can be built using reflection and naming conventions to simplify the creation of your objects from the CSV file.
The following utilities were constructed to satisfy that. Hopefully the code and accompanying documentation speaks for itself.
public static class ExpressionManager {
/// <summary>
/// Builds an expression that creates a new object and initializes properties from a string array
/// </summary>
public static Expression<Func<string[], T>> BuildFactoryExpressionFor<T>(string[] headers) where T : new() {
var type = typeof(T);
var properties = type.GetCachedProperties();
var columns = MapColumnIndexes(headers);
//Desired delegate
//Func<string[], T> factory = (string[] fields) => new T() { X = fields[0], Y = fields[1] };
// (string[] fields) => ...
var parameter = Expression.Parameter(headers.GetType(), "fields");
// new T()
var newOfType = Expression.New(type);
// { PropertyA = fields[0], PropertyB = (int)fields[1] }
var memberBindings = getMemberBindings(columns, properties, parameter);
// new T() { PropertyA = fields[0], PropertyB = (int)fields[1] };
var body = Expression.MemberInit(newOfType, memberBindings);
// (string[] fields) => new T() { PropertyA = fields[0], PropertyB = (int)fields[1] };
var lambda = Expression.Lambda<Func<string[], T>>(body, parameter);
return lambda;
}
/// <summary>
/// Get the bindings used to populate the provided properties
/// </summary>
private static IEnumerable<MemberAssignment> getMemberBindings(IDictionary<string, int> columns, PropertyInfo[] properties, ParameterExpression parameter) {
using (var e = columns.Keys.GetEnumerator()) {
while (e.MoveNext()) {
var headerName = e.Current;
var propertyName = headerName.Replace(" ", "");//<-- simple naming convention
var propertyInfo = properties.FirstOrDefault(_ => string.Equals(_.Name, propertyName, StringComparison.InvariantCultureIgnoreCase));
if (propertyInfo != null) {
var setMthd = propertyInfo.GetSetMethod(true);
if (propertyInfo.CanWrite && setMthd != null && setMthd.IsPublic) {
var propertyType = propertyInfo.PropertyType;
// index
var headerIndex = Expression.Constant(columns[headerName]);
// fields[index]
Expression value = Expression.ArrayAccess(parameter, headerIndex);
if (propertyType != typeof(string)) {
// (int)Coerce(fields[index], typeof(int))
value = Expression.Convert(Expression.Call(getConverter(), value, Expression.Constant(propertyType)), propertyType);
}
// Property = value
var setter = Expression.Bind(propertyInfo, value);
yield return setter;
}
}
}
}
}
static MethodInfo coerce;
static MethodInfo getConverter() {
if (coerce == null) {
var flags = BindingFlags.Static | BindingFlags.NonPublic;
coerce = typeof(ExpressionManager).GetMethod("CoerceValue", flags);
}
return coerce;
}
static object CoerceValue(object value, Type conversionType) {
if (value == null || (value is string && string.IsNullOrWhiteSpace(value as string))) {
return conversionType.GetDefaultValueForType();
}
//TODO: room for improvement here for other types. consider automapper.
try {
return Convert.ChangeType(value, conversionType,
System.Globalization.CultureInfo.InvariantCulture);
} catch { }
if (isNullable(conversionType)) {
try {
var underlyingType = Nullable.GetUnderlyingType(conversionType);
return Convert.ChangeType(value, underlyingType,
System.Globalization.CultureInfo.InvariantCulture);
} catch { }
}
return conversionType.GetDefaultValueForType();
}
static bool isNullable(Type conversionType) {
return conversionType.IsGenericType &&
conversionType.GetGenericTypeDefinition().IsAssignableFrom(typeof(Nullable<>));
}
static Dictionary<Type, object> defaultValueTypes = new Dictionary<Type, object>();
/// <summary>
/// Gets the default value for a type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>The default value.</returns>
static object GetDefaultValueForType(this Type type) {
if (!type.IsValueType) return null;
object defaultValue;
if (defaultValueTypes.TryGetValue(type, out defaultValue)) return defaultValue;
defaultValue = type.CreateInstance();
defaultValueTypes[type] = defaultValue;
return defaultValue;
}
public static IDictionary<string, int> MapColumnIndexes(this string[] headers) {
return headers
.Select((header, index) => new { header, index })
.ToDictionary(o => o.header, o => o.index);
}
private static readonly IDictionary<Type, PropertyInfo[]> propertyCache = new Dictionary<Type, PropertyInfo[]>();
/// <summary>
/// Returns all the public properties of the current <seealso cref="System.Type"/>.
/// </summary>
/// <param name="type">The type to get the properties from</param>
/// <returns></returns>
public static PropertyInfo[] GetCachedProperties(this Type type) {
PropertyInfo[] properties = new PropertyInfo[0];
if (!propertyCache.TryGetValue(type, out properties)) {
lock (propertyCache) {
if (!propertyCache.TryGetValue(type, out properties)) {
var flags = BindingFlags.Public | BindingFlags.Instance;
properties = type.GetProperties(flags);
propertyCache[type] = properties;
}
}
}
return properties;
}
}
My initial issue was with allowing for a more flexible factory delegate that would try to convert the provided string values when mapping to non-string properties.
//...
if (propertyType != typeof(string)) {
// (int)Coerce(fields[index], typeof(int))
value = Expression.Convert(Expression.Call(getConverter(), value, Expression.Constant(propertyType)), propertyType);
}
//...
This would allow a property like
public DateTime BirthDate { get; set; }
to map to a CSV column like
"2018-03-21"
and a proper DateTime
would be passed to the mapped property.
For now, the simplified CoerceValue
method should be able to handle simple type conversions between value types and also nullables. There is room for improvements here as a library like Automapper could come in handy.
Using a simple naming convention like removing spaces from CSV header names to compare against target property names, simplifies a one to one mapping of CSV header names to property names. Again this could be improved to use metadata from attributes if so desired as suggested in another answer.
for example
"First Name" => firstName
"Last Name" => lastName
...etc
While the provided utilities may look like a lot under the hood, it will allow for a more simplified method when refactored
using (var parser = new TextFieldParser(Program.file)) {
parser.SetDelimiters(",");
string[] headers = parser.ReadFields();
//delegate to build desired objects
var factory = ExpressionManager.BuildFactoryExpressionFor<MassUploadUser>(headers).Compile();
//Need this to access Password as it is not included in POCO
var passwordColumn = headers.IndexOf("Password");
while (!parser.EndOfData){
string[] fields = parser.ReadFields();
MassUploadUser massUploadUser = factory(fields);
string password = fields[passwordColumn];
// After the for loop, I have some additional logic
// to serialize the newly-created object to JSON
// and then POST it to a RESTful API
DoPostToWebService(massUploadUser, password);
// At this point, we created the new user on the server, so no need
// to keep the current instance of massUploadUser around
}
}
switch
is unneessary of course. \$\endgroup\$