Created
March 10, 2021 11:40
-
-
Save eliottrobson/ec9668d037b81fa6ecc0bd5dfeb89f67 to your computer and use it in GitHub Desktop.
Specification Expression Replacer
This file contains 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
public interface ISpecification<T> where T : class | |
{ | |
Expression<Func<T, bool>> Predicate { get; } | |
bool IsSatisfiedBy(T entity); | |
} |
This file contains 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
public class ReplaceWithExpressionAttribute : Attribute | |
{ | |
public string Method { get; set; } | |
public string Property { get; set; } | |
} |
This file contains 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
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); | |
} | |
} |
This file contains 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> | |
/// 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