Created
August 1, 2011 22:31
-
-
Save leniency/1119157 to your computer and use it in GitHub Desktop.
Modifications to allow CamelCase.NestedProperties
This file contains hidden or 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
/// <summary> | |
/// Provides projection mapping from an IQueryable sourceto a target type. | |
/// | |
/// This allows from strongly-typed mapping and querying only necessary fields from the database. | |
/// It takes the place of Domain -> ViewModel mapping as it allows the ViewModel to stay as | |
/// IQueryable. AutoMapper works on in-memory objects and will pull all full records to perform | |
/// a collection mapping. Use AutoMapper for Input -> Domain scenarios, but not DAL. | |
/// | |
/// Reference: http://devtrends.co.uk/blog/stop-using-automapper-in-your-data-access-code | |
/// </summary> | |
public static class IQueryableExtensions | |
{ | |
public static ProjectionExpression<TSource> Project<TSource>(this IQueryable<TSource> source) | |
{ | |
return new ProjectionExpression<TSource>(source); | |
} | |
} | |
public class ProjectionExpression<TSource> | |
{ | |
private static readonly Dictionary<string, Expression> ExpressionCache = new Dictionary<string, Expression>(); | |
private readonly IQueryable<TSource> _source; | |
public ProjectionExpression(IQueryable<TSource> source) | |
{ | |
_source = source; | |
} | |
/// <summary> | |
/// Specifies the target type | |
/// </summary> | |
/// <typeparam name="TDest"></typeparam> | |
/// <returns></returns> | |
public IQueryable<TDest> To<TDest>() | |
{ | |
var queryExpression = GetCachedExpression<TDest>() ?? BuildExpression<TDest>(); | |
return _source.Select(queryExpression); | |
} | |
private static Expression<Func<TSource, TDest>> GetCachedExpression<TDest>() | |
{ | |
var key = GetCacheKey<TDest>(); | |
return ExpressionCache.ContainsKey(key) ? ExpressionCache[key] as Expression<Func<TSource, TDest>> : null; | |
} | |
private static Expression<Func<TSource, TDest>> BuildExpression<TDest>() | |
{ | |
var sourceProperties = typeof(TSource).GetProperties(); | |
var destinationProperties = typeof(TDest).GetProperties().Where(dest => dest.CanWrite); | |
var parameterExpression = Expression.Parameter(typeof(TSource), "src"); | |
var bindings = destinationProperties | |
.Select(destinationProperty => BuildBinding(parameterExpression, destinationProperty, sourceProperties)) | |
.Where(binding => binding != null); | |
var expression = Expression.Lambda<Func<TSource, TDest>>(Expression.MemberInit(Expression.New(typeof(TDest)), bindings), parameterExpression); | |
var key = GetCacheKey<TDest>(); | |
ExpressionCache.Add(key, expression); | |
return expression; | |
} | |
private static MemberAssignment BuildBinding(Expression parameterExpression, MemberInfo destinationProperty, IEnumerable<PropertyInfo> sourceProperties) | |
{ | |
var sections = SplitCamelCase(destinationProperty.Name); | |
return ResolveProperty( | |
parameterExpression, | |
destinationProperty, | |
sections[0], | |
1, | |
sourceProperties, | |
sections); | |
} | |
/// <summary> | |
/// <para> | |
/// Attempt to find the first property that matches with a depth-first recursive search. | |
/// This will yield a path along the shortest property names. | |
/// </para> | |
/// <para> | |
/// If sections = {"Bar", "Two"} it will match Foo.Bar, and then the child | |
/// Bar.Two. Failing that, it will continue to Foo.BarTwo | |
/// </para> | |
/// </summary> | |
/// <param name="parameterExpression"></param> | |
/// <param name="destinationProperty"></param> | |
/// <param name="currentName">The current property name being looked for</param> | |
/// <param name="currentIndex">The current index in the sections collection</param> | |
/// <param name="properties">Collection of properties on the source object</param> | |
/// <param name="sections">collection of name sections, split by camel case</param> | |
/// <returns></returns> | |
private static MemberAssignment ResolveProperty( | |
Expression parameterExpression, | |
MemberInfo destinationProperty, | |
string currentName, | |
int currentIndex, | |
IEnumerable<PropertyInfo> properties, | |
string[] sections) | |
{ | |
// Check if any of the current properties match in name | |
var property = properties.FirstOrDefault(src => src.Name == currentName); | |
// If we're at the end of the sections, attempt to bind, or nothing found | |
if (currentIndex == sections.Length) | |
{ | |
return (property != null) ? | |
Expression.Bind(destinationProperty, Expression.Property(parameterExpression, property)) : | |
null; | |
} | |
// The property exists and there are still sections left - look into the child | |
if (property != null) | |
{ | |
// We found a property with currentName, so move to the next section | |
// Examine the remaining sections on child properties | |
var result = ResolveProperty( | |
Expression.Property(parameterExpression, property), | |
destinationProperty, | |
sections[currentIndex], | |
currentIndex + 1, // proceed to the next section index | |
property.PropertyType.GetProperties(), | |
sections); | |
// If we found a property on the child, return it. Otherwise continue searching | |
if (result != null) | |
return result; | |
} | |
// currentName doesn't exist, so add the next section and keep searching | |
return ResolveProperty( | |
parameterExpression, | |
destinationProperty, | |
currentName + sections[currentIndex], | |
currentIndex + 1, // proceed to the next section index | |
properties, | |
sections); | |
} | |
private static string GetCacheKey<TDest>() | |
{ | |
return string.Concat(typeof(TSource).FullName, typeof(TDest).FullName); | |
} | |
private static string[] SplitCamelCase(string input) | |
{ | |
return Regex.Replace(input, "([A-Z])", " $1", RegexOptions.Compiled).Trim().Split(' '); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment