Created
November 15, 2011 18:29
-
-
Save AlexBar/1367880 to your computer and use it in GitHub Desktop.
Linq AutoProjection
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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