Skip to content

Instantly share code, notes, and snippets.

@eliottrobson
Created March 10, 2021 11:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eliottrobson/ec9668d037b81fa6ecc0bd5dfeb89f67 to your computer and use it in GitHub Desktop.
Save eliottrobson/ec9668d037b81fa6ecc0bd5dfeb89f67 to your computer and use it in GitHub Desktop.
Specification Expression Replacer
public interface ISpecification<T> where T : class
{
Expression<Func<T, bool>> Predicate { get; }
bool IsSatisfiedBy(T entity);
}
public class ReplaceWithExpressionAttribute : Attribute
{
public string Method { get; set; }
public string Property { get; set; }
}
public abstract class SpecificationBuilder<T> : ISpecification<T> where T : class
{
public abstract Expression<Func<T, bool>> Predicate { get; }
[ReplaceWithExpression(Property = nameof(Predicate))]
public bool IsSatisfiedBy(T entity)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
if (Predicate == null)
throw new InvalidSpecificationException("Predicate cannot be null");
var predicate = Predicate.Compile();
return predicate(entity);
}
}
/// <summary>
/// Replaces nested specifications with their predicates and rebinds the parameters.
/// <example>
/// For example:
/// <code>
/// var predicate = new SpecificationExpressionReplacer().Replace(specification.Predicate);
/// queryable = queryable.Where(predicate);
/// </code>
/// will replace any nested specifications in the <c>specification.Predicate</c> with correctly bound expressions.
/// </example>
/// </summary>
public class SpecificationExpressionReplacer : ExpressionVisitor
{
private readonly Stack<Dictionary<ParameterExpression, Expression>> _parameters =
new Stack<Dictionary<ParameterExpression, Expression>>();
public Expression<Func<TSource, TDest>> Replace<TSource, TDest>(
Expression<Func<TSource, TDest>> predicate)
{
return Visit(predicate) as Expression<Func<TSource, TDest>>;
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
// Resolve class instance from method call
var member = node.Object as MemberExpression;
var constant = member?.Expression as ConstantExpression;
var anonymousClassInstance = constant?.Value;
var anonymousField = member?.Member as FieldInfo;
var classInstance = anonymousField?.GetValue(anonymousClassInstance);
if (classInstance == null)
return base.VisitMethodCall(node);
// Resolve method implementation
var classMethod = classInstance.GetType().GetMethod(node.Method.Name,
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
if (classMethod == null)
return base.VisitMethodCall(node);
// Parse if attributes are present
var replaceAttributes =
Attribute.GetCustomAttributes(classMethod, typeof(ReplaceWithExpressionAttribute), true);
if (!(replaceAttributes.FirstOrDefault() is ReplaceWithExpressionAttribute replaceAttribute))
return base.VisitMethodCall(node);
object replaceItem = null;
if (!string.IsNullOrEmpty(replaceAttribute.Property))
{
var property = node.Method.DeclaringType?.GetProperty(replaceAttribute.Property,
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
if (property != null) replaceItem = property.GetValue(classInstance);
}
else if (!string.IsNullOrEmpty(replaceAttribute.Method))
{
var method = node.Method.DeclaringType?.GetMethod(replaceAttribute.Method,
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
if (method != null) replaceItem = method.Invoke(classInstance, null);
}
if (replaceItem is LambdaExpression replaceExpression)
{
_parameters.Push(new Dictionary<ParameterExpression, Expression>());
RegisterParameters(node.Arguments.ToArray(), replaceExpression);
var result = Visit(replaceExpression.Body);
_parameters.Pop();
return result;
}
return base.VisitMethodCall(node);
}
protected override Expression VisitParameter(ParameterExpression node)
{
if (_parameters.TryPeek(out var parms))
if (parms.TryGetValue(node, out var replacement))
return Visit(replacement);
return base.VisitParameter(node);
}
private void RegisterParameters(IReadOnlyList<Expression> parameters, LambdaExpression expression)
{
if (parameters.Count != expression.Parameters.Count)
throw new ArgumentException(
$"The parameter values count ({parameters.Count}) does not match the expression parameter count ({expression.Parameters.Count})");
for (var i = 0; i < parameters.Count; i++)
if (_parameters.TryPeek(out var parms))
parms.Add(expression.Parameters[i], parameters[i]);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment