Skip to content

Instantly share code, notes, and snippets.

@RupertAvery
Created June 30, 2023 01:03
Show Gist options
  • Save RupertAvery/d17618958b8546f019ae760c3a6b782a to your computer and use it in GitHub Desktop.
Save RupertAvery/d17618958b8546f019ae760c3a6b782a to your computer and use it in GitHub Desktop.

DynamicMapping

DynamicMapping is library and a set of LINQ Extenstions that allow you to perform ordering and projection dynamically i.e. by string values determined at run time.

Example

Suppose you have a table with some columns, and you need to retrieve a fixed number of columns into a list. You would like the user to select the columns to be displayed.

It would be great to do something javascript-ish like this:

dataContext.Users.Select(u => new DisplayRow {
  Field1 = u[selectedColumn[0]],
  Field2 = u[selectedColumn[1]],
  Field3 = u[selectedColumn[2]]
})

First, create a MappingProvider. A MappingProvider encapsulates creation of set of key-value pairs that represent the mapping of a string to a property name on a given type.

var mappingProvider = new DefaultMappingProvider<User>();

The DefaultMappingProvider is a class that generates the same key for a corresponding property name.

Id, Id
FirstName, FirstName
MiddleName, MiddleName
LastName, LastName

We then call the GetPropertyMappings() method to return an IDictionary<string,string> containing keys to property name mappings.

var mappings = mappingProvider.GetPropertyMappings();

Now we can call AsDynamicQuery on any IQueryable, such as an EntityFramework DbSet, or an a call to NHibernate's ISession.Query<T>(). It returns an IDynamicQueryable that exposes these extension methods on top of IQueryable:

  • OrderByField
  • OrderByFields
  • SelectAs
  • SelectFields

Predefined mapping

var mapper = new DynamicMapper<User, Fields>(mappings);
mapper.Map(mappingOrder[0], x => x.Field1);
mapper.Map(mappingOrder[1], x => x.Field2);
mapper.Map(mappingOrder[2], x => x.Field3);

var result = users
	.AsDynamicQuery(mappings)
	.OrderByField(orderByField)
	.SelectAs(mapper)
	.ToList();

Inline Mapping

var result = users
    .AsDynamicQuery(mappings)
    .SelectAs<User, Fields>(
        mapper =>
        {
            mapper.Map(mappingOrder[0], x => x.Field1);
            mapper.Map(mappingOrder[1], x => x.Field2);
            mapper.Map(mappingOrder[2], x => x.Field3);
        })
    .ToList();

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace MappingUtil.Common
{
/// <summary>
/// Implements a MappingProvider
/// </summary>
/// <typeparam name="T">The type that will be used to create the mapping</typeparam>
public class DefaultMappingProvider<T> : IMappingProvider
{
private string _identifier;
private static IEnumerable<PropertyInfo> _typeProperties;
protected Dictionary<string, string> _propertyMappings;
private static IEnumerable<PropertyInfo> GetProperties()
{
if (_typeProperties == null)
{
_typeProperties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public).ToList();
}
return _typeProperties;
}
public void SetIdentifierColumn<TIdent>(Expression<Func<T, TIdent>> identifierFieldExpression)
{
_identifier = ((MemberExpression)identifierFieldExpression.Body).Member.Name;
}
/// <summary>
/// Creates an instance of the default mapping provider
/// </summary>
public DefaultMappingProvider()
{
var properties = GetProperties();
_propertyMappings = properties.Select(x => new { Key = x.Name, Value = x.Name }).ToDictionary(x => x.Key, x => x.Value);
}
/// <summary>
/// Returns a dictionary containing the mapping of property names to column names where the column name is equal to the property name
/// enclosed with square brackets
/// </summary>
/// <returns></returns>
public IDictionary<string, string> GetPropertyMappings()
{
return _propertyMappings;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
namespace DynamicMapping
{
public static class DynamicLINQExtensions
{
/// <summary>
/// Creates a DynamicQueryable&lt;T&gt; from the query using the supplied column mappings
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
/// <param name="columnMappings"></param>
/// <returns></returns>
public static IDynamicQueryable<T> AsDynamicQuery<T>(this IQueryable<T> query, IDictionary<string, string> columnMappings)
{
return new DynamicQueryable<T>(query, columnMappings);
}
/// <summary>
/// Projects each element of a sequence into a new form. Specify the mappings using the mapAction parameter.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="query"></param>
/// <param name="mapAction"></param>
/// <returns></returns>
public static IDynamicQueryable<TResult> SelectAs<T, TResult>(this IDynamicQueryable<T> query, Action<DynamicMapper<T, TResult>> mapAction)
{
var mappings = query.Mappings;
var mapper = new DynamicMapper<T, TResult>(mappings);
mapAction(mapper);
var squery = query.Select(mapper.GetMappingTransform());
return new DynamicQueryable<TResult>(squery, mappings);
}
/// <summary>
/// Projects each element of a sequence into a new form, using the specified DynamicMapper
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="query"></param>
/// <param name="dynamicMapper"></param>
/// <returns></returns>
public static IQueryable<TResult> SelectAs<T, TResult>(this IDynamicQueryable<T> query, DynamicMapper<T, TResult> dynamicMapper)
{
return query.Select(dynamicMapper.GetMappingTransform());
}
/// <summary>
/// Returns an instance of each element in a sequence with only the properties mapped by the specified column names populated
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
/// <param name="columnNames"></param>
/// <returns></returns>
public static IDynamicQueryable<T> SelectFields<T>(this IDynamicQueryable<T> query, IEnumerable<string> columnNames)
{
var mappings = query.Mappings;
var propertyNames = columnNames.Select(x => mappings[x]);
var newquery = query.Select(CreateDynamicNewExpression<T>(propertyNames));
return new DynamicQueryable<T>(newquery, mappings);
}
/// <summary>
/// Sorts elements of a sequence in ascending order according a specified column name
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
/// <param name="columnName"></param>
/// <returns></returns>
public static IDynamicQueryable<T> OrderByField<T>(this IDynamicQueryable<T> query, string columnName)
{
var mappings = query.Mappings;
var propertyName = mappings[columnName];
var newquery = query.OrderBy(CreateDynamicPropertyExpression<T, object>(propertyName));
return new DynamicQueryable<T>(newquery, mappings);
}
/// <summary>
/// Consecutively sorts elements of a sequence in ascending order according to a list of column names in the order that they appear
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
/// <param name="columnNanes"></param>
/// <returns></returns>
public static IDynamicQueryable<T> OrderByFields<T>(this IDynamicQueryable<T> query, IEnumerable<string> columnNanes)
{
var mappings = query.Mappings;
var propertyNames = columnNanes.Select(x => mappings[x]);
foreach (var propertyName in propertyNames)
{
var newquery = query.OrderBy(CreateDynamicPropertyExpression<T, object>(propertyName));
query = new DynamicQueryable<T>(newquery, mappings);
}
return query;
}
/// <summary>
/// Creates an expression that returns the property specified by the propertyName parameter
/// </summary>
/// <typeparam name="T">The containing type of the property</typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="propertyName"></param>
/// <returns></returns>
private static Expression<Func<T, TResult>> CreateDynamicPropertyExpression<T, TResult>(string propertyName)
{
var parameter = Expression.Parameter(typeof(T));
var body = Expression.Property(parameter, propertyName);
return Expression.Lambda<Func<T, TResult>>(body, false, parameter);
}
/// <summary>
/// Creates a New object expression with only the members specified by the proprtyNames parameter initalized
/// </summary>
/// <typeparam name="T">The type of object to create</typeparam>
/// <param name="propertyNames">A list of member names on the type</param>
/// <returns></returns>
private static Expression<Func<T, T>> CreateDynamicNewExpression<T>(IEnumerable<string> propertyNames)
{
var body = Expression.New(typeof(T).GetConstructor(new Type[] { }));
var parameter = Expression.Parameter(typeof(T));
var bindings = new List<MemberBinding>();
foreach (var propertyName in propertyNames)
{
bindings.Add(Expression.Bind(typeof(T).GetProperty(propertyName), Expression.Property(parameter, propertyName)));
}
var memberinit = Expression.MemberInit(body, bindings);
// returns a delegate x => new T() { Field1 = x.Field1, Field2 = x.Field2 ... }
return Expression.Lambda<Func<T, T>>(memberinit, false, parameter);
}
}
}
namespace DynamicMapping
{
/// <summary>
/// A class that stores the mapping from a source property to a destination property
/// </summary>
public class DynamicMap
{
public string SourceProperty { get; set; }
public string DestinationProperty { get; set; }
public DynamicMap(string sourceProperty, string destinationProperty)
{
SourceProperty = sourceProperty;
DestinationProperty = destinationProperty;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace DynamicMapping
{
public class DynamicMapper<TSource, TDestination>
{
private readonly List<DynamicMap> mappings = new List<DynamicMap>();
private readonly IDictionary<string, string> _sourceMappings;
private readonly IDictionary<string, string> _destinationMappings;
/// <summary>
/// Creates a DynamicMapper without any mapper providers
/// </summary>
public DynamicMapper()
{
}
/// <summary>
/// Creates a DynamicMapper where properties on the source will be mapped using the mapping provider
/// </summary>
public DynamicMapper(IDictionary<string, string> sourceMappings)
{
_sourceMappings = sourceMappings;
}
/// <summary>
/// Creates a DynamicMapper where properties on the source and destination will be mapped using the mapping providers
/// </summary>
public DynamicMapper(IDictionary<string, string> sourceMappings, IDictionary<string, string> destinationMappings)
{
_sourceMappings = sourceMappings;
_destinationMappings = destinationMappings;
}
/// <summary>
/// Maps a property on the source type that is known at run time to a property on the destination type that is known at compile time
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="sourceProperty"></param>
/// <param name="destinationProperty"></param>
/// <returns></returns>
public DynamicMapper<TSource, TDestination> Map<TResult>(string sourceProperty, Expression<Func<TDestination, TResult>> destinationProperty)
{
var member = ((MemberExpression)destinationProperty.Body).Member;
mappings.Add(new DynamicMap(sourceProperty, member.Name));
return this;
}
/// <summary>
/// Maps a property on the source type that is known at compile time to a property on the destination type that is known at run time
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="sourceProperty"></param>
/// <param name="destinationProperty"></param>
/// <returns></returns>
public DynamicMapper<TSource, TDestination> Map<TResult>(Expression<Func<TSource, TResult>> sourceProperty, string destinationProperty)
{
var member = ((MemberExpression)sourceProperty.Body).Member;
mappings.Add(new DynamicMap(member.Name, destinationProperty));
return this;
}
/// <summary>
/// Maps a property on the source type that is known at run time to a property on the destination type that is known at run time
/// </summary>
/// <param name="sourceProperty"></param>
/// <param name="destinationProperty"></param>
/// <returns></returns>
public DynamicMapper<TSource, TDestination> Map(string sourceProperty, string destinationProperty)
{
mappings.Add(new DynamicMap(sourceProperty, destinationProperty));
return this;
}
/// <summary>
/// Returns a mapping expression that can be used as the parameter of a LINQ Select function
/// </summary>
/// <returns></returns>
public Expression<Func<TSource, TDestination>> GetMappingTransform()
{
var body = Expression.New(typeof(TDestination).GetConstructor(new Type[] { }));
var parameter = Expression.Parameter(typeof(TSource));
var bindings = new List<MemberBinding>();
foreach (var mapping in mappings)
{
string sourceMappedField = mapping.SourceProperty;
if (_sourceMappings != null)
{
sourceMappedField = _sourceMappings[sourceMappedField];
}
string destinationMappedField = mapping.DestinationProperty;
if (_destinationMappings != null)
{
destinationMappedField = _destinationMappings[sourceMappedField];
}
var destProperty = typeof(TDestination).GetProperty(destinationMappedField);
var srcProperty = typeof(TSource).GetProperty(sourceMappedField);
Expression srcPropertyExpr = Expression.Property(parameter, srcProperty);
if (destProperty.PropertyType == typeof(string) && srcProperty.PropertyType != typeof(string))
{
var toStringMethod = srcProperty.PropertyType.GetMethod("ToString", new Type[] { });
srcPropertyExpr = Expression.Call(srcPropertyExpr, toStringMethod);
}
bindings.Add(Expression.Bind(destProperty, srcPropertyExpr));
}
var memberinit = Expression.MemberInit(body, bindings);
return Expression.Lambda<Func<TSource, TDestination>>(memberinit, false, parameter);
}
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
namespace DynamicMapping
{
/// <summary>
/// Associates a set of mappings to an IQueryable
/// </summary>
/// <typeparam name="T"></typeparam>
public class DynamicQueryable<T> : IDynamicQueryable<T>
{
private readonly IQueryable<T> _query;
/// <summary>
/// A set of Key-Value pairs that map columns to properties
/// </summary>
public IDictionary<string, string> Mappings { get; }
/// <summary>
/// Creates a DynamicQueryable&lt;T&gt; from the query using the supplied column mappings
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
/// <param name="columnMappings"></param>
/// <returns></returns>
public DynamicQueryable(IQueryable<T> query, IDictionary<string, string> columnMappings)
{
_query = query;
// Re-create the dictionary with a case insensitive comparison, since column names don't need to match case to be mapped to a property
Mappings = new Dictionary<string, string>(columnMappings, new StringOrdinalIgnoreCase());
}
public IEnumerator<T> GetEnumerator()
{
return _query.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public Expression Expression { get { return _query.Expression; } }
public Type ElementType { get { return _query.ElementType; } }
public IQueryProvider Provider { get { return _query.Provider; } }
}
}
using System.Collections.Generic;
using System.Linq;
namespace DynamicMapping
{
public interface IDynamicQueryable<out T> : IQueryable<T>
{
IDictionary<string, string> Mappings { get; }
}
}
using System;
using System.Collections.Generic;
namespace DynamicMapping
{
public class StringOrdinalIgnoreCase : IEqualityComparer<string>
{
public bool Equals(string x, string y)
{
return StringComparer.OrdinalIgnoreCase.Equals(x, y);
}
public int GetHashCode(string obj)
{
return StringComparer.OrdinalIgnoreCase.GetHashCode(obj);
}
}
}
@PrimeTimeTran
Copy link

Thanks for sharing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment