Skip to content

Instantly share code, notes, and snippets.

@AlexBar
Created November 15, 2011 18:29
Show Gist options
  • Save AlexBar/1367880 to your computer and use it in GitHub Desktop.
Save AlexBar/1367880 to your computer and use it in GitHub Desktop.
Linq AutoProjection
public class Output
{
public string Simple { get; set; }
public string BasicCustom { get; set; }
public string WackyCustom { get; set; }
public string Ignored { get; set; }
}
public class Input
{
public virtual int Id { get; set; }
public virtual string Simple { get; set; }
public virtual string Custom { get; set; }
}
public interface IProjectionExpression<TSource>
{
/// <summary>
/// Project a elements of a query to a to a compatible model.
/// </summary>
/// <remarks>
/// Each property of <typeparamref name="TDest"/> is expected in <typeparamref name="TSource"/> with
/// the same name. The source property is assigned to the destination property, and hence must be of the same
/// type or implicitly castable.
/// </remarks>
/// <typeparam name="TDest"></typeparam>
/// <returns></returns>
IQueryable<TDest> To<TDest>();
/// <summary>
/// Project a elements of a query to a different shape, with custom mappings.
/// </summary>
/// <typeparam name="TDest"></typeparam>
/// <remarks>
/// Each property of <typeparamref name="TDest"/> is expected in <typeparamref name="TSource"/> with
/// the same name. The source property is assigned to the destination property, and hence must be of the same
/// type or implicitly castable.
/// <para/>
/// For destination properties that are not represented by name in the source, or that are not of the same type of that
/// cannot be case implicitly, use the <param name="customMapper"/> to assign a value to the property.
/// </remarks>
/// <returns></returns>
IQueryable<TDest> To<TDest>(Action<ProjectionMapperConfiguration<TSource, TDest>> customMapper);
}
public static class ProjectionExtensions
{
public static IProjectionExpression<TSource> Project<TSource>(this IQueryable<TSource> source)
{
return new ProjectionExpression<TSource>(source);
}
}
public class ProjectionExpression<TSource> : IProjectionExpression<TSource>
{
private readonly IQueryable<TSource> _source;
// ReSharper disable StaticFieldInGenericType
private static readonly ParameterExpression ParameterExpression = Expression.Parameter(typeof(TSource), "s");
// ReSharper restore StaticFieldInGenericType
public ProjectionExpression(IQueryable<TSource> source)
{
_source = source;
}
public IQueryable<TDest> To<TDest>()
{
return To<TDest>(mapper => { });
}
public IQueryable<TDest> To<TDest>(Action<ProjectionMapperConfiguration<TSource, TDest>> customMapper)
{
var customMappings = new List<ProjectionMapping>();
var ignoredProperties = new List<PropertyInfo>();
customMapper(new ProjectionMapperConfiguration<TSource, TDest>(customMappings, ignoredProperties));
var expr = BuildExpression<TDest>(customMappings, ignoredProperties);
return _source.Select(expr);
}
private Expression<Func<TSource, TDest>> BuildExpression<TDest>(IEnumerable<ProjectionMapping> customMaps, IEnumerable<PropertyInfo> ignoredProperties)
{
var sourceMembers = typeof(TSource).GetProperties();
var destinationMembers = typeof(TDest).GetProperties();
var bindings = destinationMembers
.Where(dest => !ignoredProperties.Contains(dest))
.Select(dest => BindCustom(dest, customMaps) ?? BindSimple(dest, sourceMembers))
.Where(binding => binding != null)
.ToArray();
var expression = Expression.Lambda<Func<TSource, TDest>>(
Expression.MemberInit(Expression.New(typeof (TDest)), bindings),
ParameterExpression);
return expression;
}
private static MemberBinding BindCustom(PropertyInfo dest, IEnumerable<ProjectionMapping> customMaps)
{
var expression = customMaps.Where(map => map.DestPropertyInfo == dest).Select(map => map.Transform).FirstOrDefault();
if (expression != null)
{
var rewriter = new ParameterRewriter(expression.Parameters[0], ParameterExpression);
var rewrittenExpression = rewriter.Visit(expression.Body);
return Expression.Bind(dest, rewrittenExpression);
}
return null;
}
private static MemberAssignment BindSimple(PropertyInfo dest, IEnumerable<PropertyInfo> sourceMembers)
{
var srcProperty = sourceMembers.FirstOrDefault(pi => pi.Name == dest.Name);
return srcProperty != null ? Expression.Bind(dest, Expression.Property(ParameterExpression, srcProperty)) : null;
}
}
public class ParameterRewriter : ExpressionVisitor
{
private readonly Expression _candidate;
private readonly Expression _replacement;
public ParameterRewriter(Expression candidate, Expression replacement)
{
_candidate = candidate;
_replacement = replacement;
}
public override Expression Visit(Expression node)
{
return node == _candidate ? _replacement : base.Visit(node);
}
}
public class ProjectionMapperConfiguration<TSource, TDest>
{
private readonly List<ProjectionMapping> _customMappings;
private readonly List<PropertyInfo> _ignoreProperties;
public ProjectionMapperConfiguration(List<ProjectionMapping> customMappings, List<PropertyInfo> ignoreProperties)
{
_customMappings = customMappings;
_ignoreProperties = ignoreProperties;
}
public ProjectionMapperConfiguration<TSource, TDest> ForMember<TProperty>(Expression<Func<TDest, TProperty>> targetProperty, Expression<Func<TSource, TProperty>> transform)
{
_customMappings.Add(new ProjectionMapping((PropertyInfo)ReflectionHelper.FindProperty(targetProperty), transform));
return this;
}
public ProjectionMapperConfiguration<TSource, TDest> Ignore<TProperty>(Expression<Func<TDest, TProperty>> property)
{
_ignoreProperties.Add((PropertyInfo)ReflectionHelper.FindProperty(property));
return this;
}
}
public class ProjectionMapping
{
public PropertyInfo DestPropertyInfo { get; private set; }
public LambdaExpression Transform { get; private set; }
public ProjectionMapping(PropertyInfo propertyInfo, LambdaExpression transform)
{
DestPropertyInfo = propertyInfo;
Transform = transform;
}
}
class Program
{
static void Main()
{
List<Input> session = new List<Input>();
session.Add(new Input()
{
Id = 1,
Custom = "custom111",
Simple = "simple111"
});
var output =
(from i in session.AsQueryable()
//.Query<Input>()
select i)
.Project().To<Output>(
mapper =>
mapper
.ForMember(o => o.BasicCustom, q => q.Custom)
.ForMember(o => o.WackyCustom, i => "modified " + i.Custom)
.Ignore(o => o.Ignored)
).Single();
foreach (var propertyInfo in output.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
Console.WriteLine("{0} = {1}", propertyInfo.Name, propertyInfo.GetValue(output, null));
}
Console.ReadKey();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment