ardalis / Specification

Base class with tests for adding specifications to a DDD model

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Expression Evaluation

Jimmy-Ahmed opened this issue · comments

Hi,

Is there a way to evaluate/convert the IEnumerable<WhereExpressionInfo<T>> WhereExpressions to Expression<Func<T, bool>>

Thanks,

Hi @Jimmy-Ahmed,

Here you can get the expressions:

IEnumerable<Expression<Func<Customer, bool>>> expressions = spec.WhereExpressions.Select(x => x.Filter);

Not sure what are your requirements, but if you're trying to apply a given specification against in-memory data, that is already available out of the box. Here is a sample

var spec = new CustomerSpec();
var customers = new List<Customer>();
var evaluatedCustomers = spec.Evaluate(customers);

Thanks for your quick reply. I am asking if I can evaluate/convert to Expression<Func<T, bool>> meaning reducing all the IEnumerable<WhereExpressionInfo<T>> WhereExpressions to a single Expression

The reason I want to do this I wanna use the Specification against AzureTableStorage but their API does not support IQuerable they only accept Expression<Func<T, bool>> So I want to convert IEnumerable<WhereExpressionInfo<T>> WhereExpressions to Expression<Func<T, bool>> and pass it as a parameter to their API

Hey @Jimmy-Ahmed,

Yea, it can be done. It's not directly related to this library, you just need to merge the expressions.
Here is an extension for you

public static class SpecExtensions
{
    public static Expression<Func<T, bool>>? MergeWhereExpressions<T>(this ISpecification<T> spec)
    {
        Expression? result = null;
        var parameter = Expression.Parameter(typeof(T), "x");

        foreach (var expression in spec.WhereExpressions.Select(x => x.Filter))
        {
            var expr = ParameterReplacerVisitor.Replace(expression, expression.Parameters[0], parameter) as LambdaExpression;
            _ = expr ?? throw new InvalidExpressionException();

            result = result is null
                ? expr.Body
                : Expression.AndAlso(result, expr.Body);
        }

        return result is null
            ? null // If the input is empty collection, what do you want to return? null?
            : Expression.Lambda<Func<T, bool>>(result, parameter);
    }
}
public class ParameterReplacerVisitor : ExpressionVisitor
{
    private readonly Expression _newExpression;
    private readonly ParameterExpression _oldParameter;

    private ParameterReplacerVisitor(ParameterExpression oldParameter, Expression newExpression)
    {
        _oldParameter = oldParameter;
        _newExpression = newExpression;
    }

    public static Expression Replace(Expression expression, ParameterExpression oldParameter, Expression newExpression)
      => new ParameterReplacerVisitor(oldParameter, newExpression).Visit(expression);

    protected override Expression VisitParameter(ParameterExpression p)
      => p == _oldParameter ? _newExpression : p;
}

And the usage

var spec = new CustomerSpec();
var expr = spec.MergeWhereExpressions();

Thanks a lot! This works like a charm :) I have a final question is there a way to convert/evaluate Expression<Func<T, TResult>> Selector to a list of strings meaning if you have a selector to get Prop1, prop2, prop3 for example and you called something like
Selector.ToStringSelector() this would return a list of strings containing "Prop1", "Prop2", "Prop3"
Thanks in advance...

I haven't tested it but you might try this code (made with help from chatgpt):

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

public static class ExpressionExtensions
{
    public static List<string> ToStringSelector<T, TResult>(
        this Expression<Func<T, TResult>> expression)
    {
        var result = new List<string>();
        if (expression.Body is NewExpression newExpression)
        {
            foreach (var argument in newExpression.Arguments)
            {
                ExtractProperties(argument, result);
            }
        }
        else
        {
            ExtractProperties(expression.Body, result);
        }

        return result;
    }

    private static void ExtractProperties(Expression expression, List<string> result)
    {
        switch (expression)
        {
            case MemberExpression memberExpression:
                var props = new List<string>();
                while (!(memberExpression.Expression is ParameterExpression))
                {
                    props.Add(memberExpression.Member.Name);
                    memberExpression = memberExpression.Expression as MemberExpression;
                }
                props.Add(memberExpression.Member.Name);
                props.Reverse();
                result.Add(string.Join(".", props));
                break;
            case MethodCallExpression methodCallExpression:
                if (IsIndexerProperty(methodCallExpression))
                {
                    var argument = methodCallExpression.Arguments[0];
                    if (argument is ConstantExpression constantExpression)
                    {
                        var propName = $"{methodCallExpression.Object.Type.Name}[{constantExpression.Value}]";
                        result.Add(propName);
                    }
                }
                break;
            default:
                throw new NotSupportedException("Expression type not supported");
        }
    }

    private static bool IsIndexerProperty(MethodCallExpression expression)
    {
        return expression.Method.Name == "get_Item" &&
               expression.Method.DeclaringType != null &&
               typeof(System.Collections.IEnumerable).IsAssignableFrom(expression.Method.DeclaringType);
    }
}

Use it like this:

public class Person
{
    public string Name { get; set; }
    public Address Address { get; set; }
    public List<int> Scores { get; set; }
}

public class Address
{
    public string City { get; set; }
}

public static void Main(string[] args)
{
    Expression<Func<Person, object>> expression = p => new { p.Name, p.Address.City, Score = p.Scores[0] };
    var result = expression.ToStringSelector();
    foreach (var prop in result)
    {
        Console.WriteLine(prop);
    }
}

Let me know if it actually works :)

It failed on the edge cases like Score = p.Scores[0] however it was a good start. Thanks for sharing :)

Hey, @Jimmy-Ahmed.
I assume Steve's suggestion gave you some hints on how to move forward. I'm closing this issue. If you have further questions regarding the library, let us know.