Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@leniency
Created August 1, 2011 22:31
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save leniency/1119157 to your computer and use it in GitHub Desktop.
Save leniency/1119157 to your computer and use it in GitHub Desktop.
Modifications to allow CamelCase.NestedProperties
/// <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