Skip to content

Instantly share code, notes, and snippets.

@quexy
Last active August 29, 2015 13:56
Show Gist options
  • Save quexy/9291267 to your computer and use it in GitHub Desktop.
Save quexy/9291267 to your computer and use it in GitHub Desktop.
A smarter replacement for SpecFlow.Assist's CreateInstance<T>
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
namespace TechTalk.SpecFlow.ObjectConversion
{
public static class ObjectConverterExtensions
{
/// <summary>
/// Creates a conversion object for the given set of properties
/// </summary>
/// <typeparam name="TEntity">the type of the object to create</typeparam>
public static IObjectConverter<TEntity> AsObjectConverter<TEntity>(this IDictionary<string, string> objectData)
{
return new ObjectConverter<TEntity>(new[] { objectData });
}
/// <summary>
/// Creates a conversion object for the given set of properties
/// </summary>
/// <typeparam name="TEntity">the type of the object to create</typeparam>
public static IObjectConverter<TEntity> AsObjectConverter<TEntity>(this IEnumerable<IDictionary<string, string>> objectDataCollection)
{
return new ObjectConverter<TEntity>(objectDataCollection);
}
/// <summary>
/// Creates the objects from the given <paramref name="objectDataCollection"/> argument
/// </summary>
public static IEnumerable<TEntity> CreateObjects<TEntity>(this IObjectConverter<TEntity> objectConverter, IEnumerable<IDictionary<string, string>> objectDataCollection)
{
return objectDataCollection.Select(objectConverter.CreateObject);
}
/// <summary>
/// Specifies the aliases in the import data for the given property of the entity. There can be more than one alias for any property, but not vice versa.
/// </summary>
public static IConfiguredObjectConverter<TEntity> WithPropertyAlias<TEntity, TProperty>(this IConfiguredObjectConverter<TEntity> objectConverter, Expression<Func<TEntity, TProperty>> selector, params string[] aliases)
{
foreach (var alias in aliases)
objectConverter.WithPropertyAlias(selector, alias);
return objectConverter;
}
/// <summary>
/// Shorthand to specify required fields that the import data is verified to contain in a type safe way
/// </summary>
public static IConfiguredObjectConverter<TEntity> WithRequiredField<TEntity, TProperty>(this IConfiguredObjectConverter<TEntity> objectConverter, params Expression<Func<TEntity, TProperty>>[] propertySelectors)
{
return objectConverter.WithRequiredField(propertySelectors.Select(ps => ps.GetPropertyName()).ToArray());
}
/// <summary>
/// Specifies the default value to use for a property if the import data wouldn't specify otherwise
/// </summary>
public static IConfiguredObjectConverter<TEntity> WithDefaultValue<TEntity, TProperty>(this IConfiguredObjectConverter<TEntity> objectConverter, Expression<Func<TEntity, TProperty>> selector, TProperty defaultValue)
{
return objectConverter.WithDefaultValue(selector, () => defaultValue);
}
/// <summary>
/// Returns a converter function for the given type. This method is the default converter provider.
/// </summary>
public static Func<string, object> GetDefaultConverter(Type type)
{
if (type == typeof(string))
return s => s;
if (type.IsGenericType && type.Name == "Nullable`1")
return val =>
{
if (string.IsNullOrEmpty(val)) return null;
else return GetDefaultConverter(type.GetGenericArguments()[0])(val);
};
if (type.IsEnum)
return value => Enum.Parse(type, value);
return value => TypeDescriptor.GetConverter(type).ConvertFromString(value);
}
}
public static class ExpressionExtensions
{
/// <summary>
/// Returns the property name from a property selector expression
/// </summary>
public static string GetPropertyName<TEntity, TValue>(this Expression<Func<TEntity, TValue>> property)
{
var exp = (LambdaExpression)property;
if (exp.Body.NodeType == ExpressionType.Parameter)
return "item";
var mExp = (exp.Body.NodeType == ExpressionType.MemberAccess) ?
(MemberExpression)exp.Body :
(MemberExpression)((UnaryExpression)exp.Body).Operand;
return mExp.Member.Name;
}
}
public interface IObjectConverter<TEntity>
{
/// <summary>
/// Allows customizing the object conversion
/// </summary>
IConfiguredObjectConverter<TEntity> WithConfiguration();
/// <summary>
/// Creates the objects from the argument provided at initialization
/// </summary>
IEnumerable<TEntity> CreateObjects();
/// <summary>
/// Creates the objects from the given <param name="collectionData"/> argument
/// </summary>
IEnumerable<TEntity> CreateObjects(IEnumerable<IDictionary<string, string>> collectionData);
/// <summary>
/// Creates the object from the given <paramref name="objectData"/> argument
/// </summary>
TEntity CreateObject(IDictionary<string, string> objectData);
}
public interface IConfiguredObjectConverter<TEntity> : IObjectConverter<TEntity>
{
/// <summary>
/// Defines the object factory to use when creating a new entity
/// </summary>
IConfiguredObjectConverter<TEntity> WithObjectFactory(Func<TEntity> factory);
/// <summary>
/// Specifies that the import data should contain the specified fields
/// </summary>
IConfiguredObjectConverter<TEntity> WithRequiredField(params string[] fieldNames);
/// <summary>
/// Specifies the fields of the import data the algorithm should skip
/// </summary>
IConfiguredObjectConverter<TEntity> WithSkippedField(params string[] fieldNames);
/// <summary>
/// Specifies an alias in the import data for the given property of the entity. There can be more than one alias for any property, but not vice versa.
/// </summary>
IConfiguredObjectConverter<TEntity> WithPropertyAlias<TProperty>(Expression<Func<TEntity, TProperty>> selector, string alias);
/// <summary>
/// Specifies the converter to use for the given import data field (converter priority: table field, object property, target value, default)
/// </summary>
IConfiguredObjectConverter<TEntity> WithFieldValueConverter<TProperty>(string name, Func<string, TProperty> converter);
/// <summary>
/// Specifies the converter to use for the given property of the entity (converter priority: table field, object property, target value, default)
/// </summary>
IConfiguredObjectConverter<TEntity> WithPropertyValueConverter<TProperty>(Expression<Func<TEntity, TProperty>> selector, Func<string, TProperty> converter);
/// <summary>
/// Specifies a conversion method to use when converting to the given value (converter priority: table field, object property, target value, default)
/// </summary>
IConfiguredObjectConverter<TEntity> WithValueConverter<TValue>(Func<string, TValue> converter);
/// <summary>
/// Specifies the provider method to use to obtain the default value converter for any given type (converter priority: table field, object property, target value, default)
/// </summary>
IConfiguredObjectConverter<TEntity> WithDefaultConverter(Func<Type, Func<string, object>> converterProvider);
/// <summary>
/// Specifies the default value to use for a property if the import data wouldn't specify otherwise
/// </summary>
IConfiguredObjectConverter<TEntity> WithDefaultValue<TProperty>(Expression<Func<TEntity, TProperty>> selector, Func<TProperty> valueProvider);
}
public static class ObjectConverter
{
/// <summary>
/// Creates an object converter for the given type
/// </summary>
public static IObjectConverter<TEntity> For<TEntity>()
{
return new ObjectConverter<TEntity>(null);
}
}
internal sealed class ObjectConverter<TEntity> : IConfiguredObjectConverter<TEntity>
{
private readonly IEnumerable<IDictionary<string, string>> objectDataCollection;
public ObjectConverter(IEnumerable<IDictionary<string, string>> objectDataCollection)
{
this.objectDataCollection = objectDataCollection;
}
public IConfiguredObjectConverter<TEntity> WithConfiguration()
{
return this;
}
public IEnumerable<TEntity> CreateObjects()
{
return CreateObjects(objectDataCollection);
}
public IEnumerable<TEntity> CreateObjects(IEnumerable<IDictionary<string, string>> collectionData)
{
if (collectionData == null) return Enumerable.Empty<TEntity>();
return collectionData.Select(CreateObject).ToArray();
}
public TEntity CreateObject(IDictionary<string, string> objectData)
{
var missingFields = requiredFields.Except(objectData.Keys).ToArray();
if (missingFields.Length > 0)
new InvalidOperationException("Required field(s) missing: " + string.Join(", ", missingFields));
var entity = objectFactory();
foreach (var name in objectData.Keys.Except(skippedFields))
{
string propertyName;
if (!propertyAliases.TryGetValue(name, out propertyName))
propertyName = name;
var property = typeof(TEntity).GetProperty(propertyName);
if (property == null)
throw new InvalidOperationException(string.Format("Invalid property name '{0}'", propertyName));
Func<string, object> converter = null;
if (converter == null) fieldValueConverters.TryGetValue(name, out converter);
if (converter == null) propertyValueConverters.TryGetValue(propertyName, out converter);
if (converter == null) valueConverters.TryGetValue(property.PropertyType, out converter);
if (converter == null) converter = defaultConverterProvider(property.PropertyType);
property.SetValue(entity, converter(objectData[name]), null);
}
foreach (var entry in defaultValueProviders)
{
var fieldNames = propertyAliases.Where(pa => pa.Value == entry.Key).Select(pa => pa.Key).Concat(new[] { entry.Key });
if (!objectData.Keys.Any(k => fieldNames.Contains(k)))
{
var property = typeof(TEntity).GetProperty(entry.Key);
property.SetValue(entity, entry.Value(), null);
}
}
return entity;
}
private readonly List<string> requiredFields = new List<string>();
public IConfiguredObjectConverter<TEntity> WithRequiredField(params string[] fieldNames)
{
requiredFields.AddRange(fieldNames);
return this;
}
private Func<TEntity> objectFactory = () => Activator.CreateInstance<TEntity>();
public IConfiguredObjectConverter<TEntity> WithObjectFactory(Func<TEntity> factory)
{
objectFactory = factory;
return this;
}
private readonly List<string> skippedFields = new List<string>();
public IConfiguredObjectConverter<TEntity> WithSkippedField(params string[] fieldNames)
{
skippedFields.AddRange(fieldNames);
return this;
}
private readonly Dictionary<string, string> propertyAliases = new Dictionary<string, string>();
public IConfiguredObjectConverter<TEntity> WithPropertyAlias<TProperty>(Expression<Func<TEntity, TProperty>> selector, string alias)
{
propertyAliases.Add(alias, selector.GetPropertyName());
return this;
}
private readonly Dictionary<string, Func<string, object>> propertyValueConverters = new Dictionary<string, Func<string, object>>();
public IConfiguredObjectConverter<TEntity> WithPropertyValueConverter<TProperty>(Expression<Func<TEntity, TProperty>> selector, Func<string, TProperty> converter)
{
propertyValueConverters.Add(selector.GetPropertyName(), v => converter(v));
return this;
}
private readonly Dictionary<string, Func<string, object>> fieldValueConverters = new Dictionary<string, Func<string, object>>();
public IConfiguredObjectConverter<TEntity> WithFieldValueConverter<TProperty>(string name, Func<string, TProperty> converter)
{
fieldValueConverters.Add(name, v => converter(v));
return this;
}
private readonly Dictionary<Type, Func<string, object>> valueConverters = new Dictionary<Type, Func<string, object>>();
public IConfiguredObjectConverter<TEntity> WithValueConverter<TValue>(Func<string, TValue> converter)
{
valueConverters.Add(typeof(TValue), v => converter(v));
return this;
}
private Func<Type, Func<string, object>> defaultConverterProvider = ObjectConverterExtensions.GetDefaultConverter;
public IConfiguredObjectConverter<TEntity> WithDefaultConverter(Func<Type, Func<string, object>> converterProvider)
{
defaultConverterProvider = converterProvider;
return this;
}
private readonly Dictionary<string, Func<object>> defaultValueProviders = new Dictionary<string, Func<object>>();
public IConfiguredObjectConverter<TEntity> WithDefaultValue<TProperty>(Expression<Func<TEntity, TProperty>> selector, Func<TProperty> valueProvider)
{
defaultValueProviders.Add(selector.GetPropertyName(), () => valueProvider());
return this;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment