Projector for LINQ
Hi,
I would like to share simple projector system, that is I believe a great improvement for LINQ:
using AutoMapper;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace ANeT.SharedData.Projection
{
/// <summary>
/// Base class for any Projector. This design pattern is able to protect input IQueryable for single SQL generation
/// while for IEnumerable allowing to use single query definition by internally representing it as IQueryable.
/// Fill internal method is used to chaining extension modules to final collection (adjusting current collection with new joins, conditions, …).
/// </summary>
/// <typeparam name=”P”>Factory Type</typeparam>
/// <typeparam name=”E”>Entity Type</typeparam>
public abstract class Projector<P, E>
where P : Projector<P, E>, new()
{
bool providersChecked = false;
IQueryProvider? originalProvider = null;
IQueryable<E> collection = null!;
protected IQueryable<E> Collection => collection;
protected Projector()
{ } // Constructor should be hidden so that internal system is safe.
/// <summary>
/// Factory of this Projector. Accepting any input collection,
/// however for IQueryable it is maintaining a single SQL generation.
/// IEnumerable input is internally represented as IQueryable.
/// </summary>
/// <typeparam name=”C”></typeparam>
/// <param name=”collection”></param>
/// <returns>Instance P</returns>
public static P Create<C>(C collection)
where C : IEnumerable<E>
{
P instance = new P();
if (collection is IQueryable<E> queryable)
{
instance.originalProvider = queryable.Provider;
instance.collection = queryable;
}
else
{
instance.collection = collection.AsQueryable();
}
return instance;
}
/// <summary>
/// Chaining method to follow with next Projector,
/// which one must follow the same entity type.
/// </summary>
/// <typeparam name=”C”>Factory Type</typeparam>
/// <returns>Instance C</returns>
public C Chain<C>()
where C : Projector<C, E>, new()
{
C instance = new C();
instance.originalProvider = this.originalProvider;
instance.collection = this.collection;
return instance;
}
/// <summary>
/// This method should be used at begin of any collection extension.
/// It is enforcing Provider rule on any join collection. (Protecting final collection from cross-context mixup.)
/// </summary>
/// <param name=”collections”>Checked collections</param>
/// <exception cref=”InvalidOperationException”></exception>
protected void CheckProviders(params object[] collections)
{
if (originalProvider == null)
{
return;
}
foreach (object collection in collections)
{
if (collection is IQueryable queryable && queryable.Provider != originalProvider)
{
throw new InvalidOperationException(“Chain of querying was broken.”);
}
}
providersChecked = true;
}
/// <summary>
/// Fill current Projector IQueryable with new altered version.
/// For Projector collection that was IQueryable at the input, it enforces Provider compatibility.
/// (Which is requiring programmers to call CheckProviders beforehand.)
/// </summary>
/// <typeparam name=”C”>Collection Type</typeparam>
/// <param name=”collection”>New collection</param>
/// <exception cref=”InvalidOperationException”></exception>
protected void Fill<C>(C collection)
where C : IEnumerable<E>
{
if (originalProvider != null &&
(!providersChecked || collection is not IQueryable<E> queryable || queryable.Provider != originalProvider))
{
throw new InvalidOperationException(“Chain of querying was broken.”);
}
this.collection = (collection as IQueryable<E>) ?? collection.AsQueryable();
this.providersChecked = false;
}
/// <summary>
/// Základní přístup k projekci. Ponechává IQueryable tak, jak bylo vytvořeno.
/// </summary>
/// <returns></returns>
public IQueryable<E> Project()
=> collection;
/// <summary>
/// Projection of current Projector IQueryable to a new collection of re-typed entities.
/// (If unable to re-type, it will copy entities by property name and property type matching.)
/// </summary>
/// <typeparam name=”ME”>Entity Type</typeparam>
/// <returns>Collection ME</returns>
public IQueryable<ME> ProjectTo<ME>()
where ME : new()
{
object result = collection;
if (typeof(ME) != typeof(E))
{
if (CanCast<ME>())
{
result = collection.Cast<ME>();
}
else
{
Expression<Func<E, ME>> selectExpression = GetSelectExpression<ME>();
result = collection.Select(selectExpression);
}
}
return (IQueryable<ME>)result;
}
/// <summary>
/// Projection of current Projector IQueryable to re-typed collection (C) of re-typed entities.
/// (If unable to re-type, it will copy entities by property name and property type matching.)
/// </summary>
/// <typeparam name=”C”>Collection Type</typeparam>
/// <typeparam name=”ME”>Entity Type</typeparam>
/// <returns>Collection C</returns>
public C ProjectTo<C, ME>()
where ME : new()
where C : IEnumerable<ME>
{
object result = collection;
if (typeof(C) == typeof(IEnumerable))
{
if (typeof(ME) == typeof(E))
{
result = collection.AsEnumerable();
}
else
{
if (CanCast<ME>())
{
result = collection.Cast<ME>().AsEnumerable();
}
else
{
Expression<Func<E, ME>> selectExpression = GetSelectExpression<ME>();
result = collection.Select(selectExpression).AsEnumerable();
}
}
}
else if (typeof(ME) != typeof(E))
{
if (CanCast<ME>())
{
result = collection.Cast<ME>();
}
else
{
Expression<Func<E, ME>> selectExpression = GetSelectExpression<ME>();
result = collection.Select(selectExpression);
}
}
return (C)result;
}
/// <summary>
/// Projection of current Projector IQueryable to a new collection of re-typed entities.
/// (If unable to re-type, it will copy entities using AutoMapper.)
/// </summary>
/// <typeparam name=”ME”>Entity Type</typeparam>
/// <returns>Collection ME</returns>
public IQueryable<ME> ProjectTo<ME>(IMapper mapper)
where ME : new()
{
object result = collection;
if (typeof(ME) != typeof(E))
{
if (CanCast<ME>())
{
result = collection.Cast<ME>();
}
else
{
result = collection.Select(e => mapper.Map<ME>(e));
}
}
return (IQueryable<ME>)result;
}
/// <summary>
/// Projection of current Projector IQueryable to re-typed collection (C) of re-typed entities.
/// (If unable to re-type, it will copy entities using AutoMapper.)
/// </summary>
/// <typeparam name=”C”>Collection Type</typeparam>
/// <typeparam name=”ME”>Entity Type</typeparam>
/// <returns>Collection C</returns>
public C ProjectTo<C, ME>(IMapper mapper)
where ME : new()
where C : IEnumerable<ME>
{
object result = collection;
if (typeof(C) == typeof(IEnumerable))
{
if (typeof(ME) == typeof(E))
{
result = collection.AsEnumerable();
}
else
{
if (CanCast<ME>())
{
result = collection.Cast<ME>().AsEnumerable();
}
else
{
result = collection.Select(e => mapper.Map<ME>(e)).AsEnumerable();
}
}
}
else if (typeof(ME) != typeof(E))
{
if (CanCast<ME>())
{
result = collection.Cast<ME>();
}
else
{
result = collection.Select(e => mapper.Map<ME>(e));
}
}
return (C)result;
}
private bool CanCast<ME>()
=> typeof(E).IsAssignableFrom(typeof(ME));
/// <summary>
/// Inner method for Select Linq creation. (Solving property tests and their conversions.)
/// </summary>
/// <typeparam name=”ME”>Entity Type</typeparam>
/// <returns>Lambda Expression Linq Select</returns>
/// <exception cref=”InvalidOperationException”></exception>
private Expression<Func<E, ME>> GetSelectExpression<ME>()
{
Type
sourceType = typeof(E),
targetType = typeof(ME);
PropertyInfo[] sourceProperties = sourceType.GetProperties();
Dictionary<string, PropertyInfo> targetProperties = targetType.GetProperties()
.ToDictionary(key => key.Name, val => val);
ParameterExpression parameter = Expression.Parameter(sourceType, “entity”);
List<MemberBinding> bindings = new List<MemberBinding>();
foreach (PropertyInfo sourceProperty in sourceProperties)
{
if (!targetProperties.ContainsKey(sourceProperty.Name))
{
continue;
}
PropertyInfo? targetProperty = targetProperties[sourceProperty.Name];
Type sourcePropertyType = Nullable.GetUnderlyingType(sourceProperty.PropertyType) ?? sourceProperty.PropertyType;
if (!targetProperty.PropertyType.IsAssignableFrom(sourcePropertyType))
{
throw new InvalidOperationException($”Incompatible property types for property {sourceProperty.Name}”);
}
MemberExpression sourceValue = Expression.Property(parameter, sourceProperty);
Expression convertedSourceValue;
if (sourcePropertyType != sourceProperty.PropertyType) // Is Nullable.
{
convertedSourceValue = Expression.Condition(
Expression.Equal(sourceValue, Expression.Constant(null)), // Test null.
Expression.Default(targetProperty.PropertyType), // Default value.
Expression.Convert(Expression.Property(sourceValue, “Value”), targetProperty.PropertyType));
}
else
{
convertedSourceValue = sourceValue;
}
MemberAssignment binding = Expression.Bind(targetProperty, convertedSourceValue);
bindings.Add(binding);
}
MemberInitExpression initializer = Expression.MemberInit(Expression.New(targetType), bindings);
return Expression.Lambda<Func<E, ME>>(initializer, parameter);
}
}
}
Which has a simple FluentAPI with chaining usage:
public class Small
{
public string IXS_REF { get; set; } = null!;
public DateTime LAST_TIME { get; set; }
}
Small ddd = PersonProjector.Create(new List<IPOREF> { new IPOREF { IXS_REF = “AAA”, LAST_TIME = null } })
.FilterWhatever(joinOne, inList)
.Chain<SecondProjector>()
.ProjectTo<Small>()
.First();
It allows you to define single chain of modular queries and apply it to any collection type. For IQueryable it is protecting input Provider, while for IEnumerable allowing to use all IQueryable methods … Final projection can lead to any ancestor allowing to chain another wave of projections …
Hi,I would like to share simple projector system, that is I believe a great improvement for LINQ:using AutoMapper;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace ANeT.SharedData.Projection
{
/// <summary>
/// Base class for any Projector. This design pattern is able to protect input IQueryable for single SQL generation
/// while for IEnumerable allowing to use single query definition by internally representing it as IQueryable.
/// Fill internal method is used to chaining extension modules to final collection (adjusting current collection with new joins, conditions, …).
/// </summary>
/// <typeparam name=”P”>Factory Type</typeparam>
/// <typeparam name=”E”>Entity Type</typeparam>
public abstract class Projector<P, E>
where P : Projector<P, E>, new()
{
bool providersChecked = false;
IQueryProvider? originalProvider = null;
IQueryable<E> collection = null!;
protected IQueryable<E> Collection => collection;
protected Projector()
{ } // Constructor should be hidden so that internal system is safe.
/// <summary>
/// Factory of this Projector. Accepting any input collection,
/// however for IQueryable it is maintaining a single SQL generation.
/// IEnumerable input is internally represented as IQueryable.
/// </summary>
/// <typeparam name=”C”></typeparam>
/// <param name=”collection”></param>
/// <returns>Instance P</returns>
public static P Create<C>(C collection)
where C : IEnumerable<E>
{
P instance = new P();
if (collection is IQueryable<E> queryable)
{
instance.originalProvider = queryable.Provider;
instance.collection = queryable;
}
else
{
instance.collection = collection.AsQueryable();
}
return instance;
}
/// <summary>
/// Chaining method to follow with next Projector,
/// which one must follow the same entity type.
/// </summary>
/// <typeparam name=”C”>Factory Type</typeparam>
/// <returns>Instance C</returns>
public C Chain<C>()
where C : Projector<C, E>, new()
{
C instance = new C();
instance.originalProvider = this.originalProvider;
instance.collection = this.collection;
return instance;
}
/// <summary>
/// This method should be used at begin of any collection extension.
/// It is enforcing Provider rule on any join collection. (Protecting final collection from cross-context mixup.)
/// </summary>
/// <param name=”collections”>Checked collections</param>
/// <exception cref=”InvalidOperationException”></exception>
protected void CheckProviders(params object[] collections)
{
if (originalProvider == null)
{
return;
}
foreach (object collection in collections)
{
if (collection is IQueryable queryable && queryable.Provider != originalProvider)
{
throw new InvalidOperationException(“Chain of querying was broken.”);
}
}
providersChecked = true;
}
/// <summary>
/// Fill current Projector IQueryable with new altered version.
/// For Projector collection that was IQueryable at the input, it enforces Provider compatibility.
/// (Which is requiring programmers to call CheckProviders beforehand.)
/// </summary>
/// <typeparam name=”C”>Collection Type</typeparam>
/// <param name=”collection”>New collection</param>
/// <exception cref=”InvalidOperationException”></exception>
protected void Fill<C>(C collection)
where C : IEnumerable<E>
{
if (originalProvider != null &&
(!providersChecked || collection is not IQueryable<E> queryable || queryable.Provider != originalProvider))
{
throw new InvalidOperationException(“Chain of querying was broken.”);
}
this.collection = (collection as IQueryable<E>) ?? collection.AsQueryable();
this.providersChecked = false;
}
/// <summary>
/// Základní přístup k projekci. Ponechává IQueryable tak, jak bylo vytvořeno.
/// </summary>
/// <returns></returns>
public IQueryable<E> Project()
=> collection;
/// <summary>
/// Projection of current Projector IQueryable to a new collection of re-typed entities.
/// (If unable to re-type, it will copy entities by property name and property type matching.)
/// </summary>
/// <typeparam name=”ME”>Entity Type</typeparam>
/// <returns>Collection ME</returns>
public IQueryable<ME> ProjectTo<ME>()
where ME : new()
{
object result = collection;
if (typeof(ME) != typeof(E))
{
if (CanCast<ME>())
{
result = collection.Cast<ME>();
}
else
{
Expression<Func<E, ME>> selectExpression = GetSelectExpression<ME>();
result = collection.Select(selectExpression);
}
}
return (IQueryable<ME>)result;
}
/// <summary>
/// Projection of current Projector IQueryable to re-typed collection (C) of re-typed entities.
/// (If unable to re-type, it will copy entities by property name and property type matching.)
/// </summary>
/// <typeparam name=”C”>Collection Type</typeparam>
/// <typeparam name=”ME”>Entity Type</typeparam>
/// <returns>Collection C</returns>
public C ProjectTo<C, ME>()
where ME : new()
where C : IEnumerable<ME>
{
object result = collection;
if (typeof(C) == typeof(IEnumerable))
{
if (typeof(ME) == typeof(E))
{
result = collection.AsEnumerable();
}
else
{
if (CanCast<ME>())
{
result = collection.Cast<ME>().AsEnumerable();
}
else
{
Expression<Func<E, ME>> selectExpression = GetSelectExpression<ME>();
result = collection.Select(selectExpression).AsEnumerable();
}
}
}
else if (typeof(ME) != typeof(E))
{
if (CanCast<ME>())
{
result = collection.Cast<ME>();
}
else
{
Expression<Func<E, ME>> selectExpression = GetSelectExpression<ME>();
result = collection.Select(selectExpression);
}
}
return (C)result;
}
/// <summary>
/// Projection of current Projector IQueryable to a new collection of re-typed entities.
/// (If unable to re-type, it will copy entities using AutoMapper.)
/// </summary>
/// <typeparam name=”ME”>Entity Type</typeparam>
/// <returns>Collection ME</returns>
public IQueryable<ME> ProjectTo<ME>(IMapper mapper)
where ME : new()
{
object result = collection;
if (typeof(ME) != typeof(E))
{
if (CanCast<ME>())
{
result = collection.Cast<ME>();
}
else
{
result = collection.Select(e => mapper.Map<ME>(e));
}
}
return (IQueryable<ME>)result;
}
/// <summary>
/// Projection of current Projector IQueryable to re-typed collection (C) of re-typed entities.
/// (If unable to re-type, it will copy entities using AutoMapper.)
/// </summary>
/// <typeparam name=”C”>Collection Type</typeparam>
/// <typeparam name=”ME”>Entity Type</typeparam>
/// <returns>Collection C</returns>
public C ProjectTo<C, ME>(IMapper mapper)
where ME : new()
where C : IEnumerable<ME>
{
object result = collection;
if (typeof(C) == typeof(IEnumerable))
{
if (typeof(ME) == typeof(E))
{
result = collection.AsEnumerable();
}
else
{
if (CanCast<ME>())
{
result = collection.Cast<ME>().AsEnumerable();
}
else
{
result = collection.Select(e => mapper.Map<ME>(e)).AsEnumerable();
}
}
}
else if (typeof(ME) != typeof(E))
{
if (CanCast<ME>())
{
result = collection.Cast<ME>();
}
else
{
result = collection.Select(e => mapper.Map<ME>(e));
}
}
return (C)result;
}
private bool CanCast<ME>()
=> typeof(E).IsAssignableFrom(typeof(ME));
/// <summary>
/// Inner method for Select Linq creation. (Solving property tests and their conversions.)
/// </summary>
/// <typeparam name=”ME”>Entity Type</typeparam>
/// <returns>Lambda Expression Linq Select</returns>
/// <exception cref=”InvalidOperationException”></exception>
private Expression<Func<E, ME>> GetSelectExpression<ME>()
{
Type
sourceType = typeof(E),
targetType = typeof(ME);
PropertyInfo[] sourceProperties = sourceType.GetProperties();
Dictionary<string, PropertyInfo> targetProperties = targetType.GetProperties()
.ToDictionary(key => key.Name, val => val);
ParameterExpression parameter = Expression.Parameter(sourceType, “entity”);
List<MemberBinding> bindings = new List<MemberBinding>();
foreach (PropertyInfo sourceProperty in sourceProperties)
{
if (!targetProperties.ContainsKey(sourceProperty.Name))
{
continue;
}
PropertyInfo? targetProperty = targetProperties[sourceProperty.Name];
Type sourcePropertyType = Nullable.GetUnderlyingType(sourceProperty.PropertyType) ?? sourceProperty.PropertyType;
if (!targetProperty.PropertyType.IsAssignableFrom(sourcePropertyType))
{
throw new InvalidOperationException($”Incompatible property types for property {sourceProperty.Name}”);
}
MemberExpression sourceValue = Expression.Property(parameter, sourceProperty);
Expression convertedSourceValue;
if (sourcePropertyType != sourceProperty.PropertyType) // Is Nullable.
{
convertedSourceValue = Expression.Condition(
Expression.Equal(sourceValue, Expression.Constant(null)), // Test null.
Expression.Default(targetProperty.PropertyType), // Default value.
Expression.Convert(Expression.Property(sourceValue, “Value”), targetProperty.PropertyType));
}
else
{
convertedSourceValue = sourceValue;
}
MemberAssignment binding = Expression.Bind(targetProperty, convertedSourceValue);
bindings.Add(binding);
}
MemberInitExpression initializer = Expression.MemberInit(Expression.New(targetType), bindings);
return Expression.Lambda<Func<E, ME>>(initializer, parameter);
}
}
} Which has a simple FluentAPI with chaining usage: public class Small
{
public string IXS_REF { get; set; } = null!;
public DateTime LAST_TIME { get; set; }
}
Small ddd = PersonProjector.Create(new List<IPOREF> { new IPOREF { IXS_REF = “AAA”, LAST_TIME = null } })
.FilterWhatever(joinOne, inList)
.Chain<SecondProjector>()
.ProjectTo<Small>()
.First(); It allows you to define single chain of modular queries and apply it to any collection type. For IQueryable it is protecting input Provider, while for IEnumerable allowing to use all IQueryable methods … Final projection can lead to any ancestor allowing to chain another wave of projections … Read More