Apply a dynamically created lambda to an object instance

c# expression expression-trees lambda

Question

I have some code that builds a lambda dynamically from strings. For instance, my filter class looks like this:

public class Criteria {
    public string Property { get; set; }
    public string Operator { get; set; }
    public string Value { get; set; }
}

I can also make a lambda-like function.x => x.Name == "Foo" beginning with an occurrence of this criteria

Criteria c = new Criteria() {
    Property = "Name",
    Operator = "equal",
    Value = "Foo"
}

Suppose you had a class like that

public class Receipt {
    public string Name { get; set; }
    public int Amount { get; set; }
    [other props omitted]
    public ICollection<ReceiptDetail> Details { get; set; }
}

Ideally, I'd want to

  1. Using the lambda, examine any object (I know that the lambda should be created with a ParameterExpression of the Receipt class)
  2. obtaining the lambda's boolean result, such as if the name is equivalent to Foo.
  3. Apply the same reasoning to collections' Count() methods, such as when creating a lambda that verifies receipts. Details. Count()

Can this be done?

EDIT: I'm explaining my requirements a little bit further in response to the comments. This code will allow me the opportunity to fulfill a demand I have, which is that the application should act somewhat differently if a rule is supplied for my object. Although this is a standard requirement, I want to develop a code that will allow me to expand it when new rules are implemented. I really only have 5 different rule kinds.

  • Check to see whether the input arrives on a certain day of the week.
  • Check to see whether the input falls inside a certain time frame.
  • Check to see whether a value is less than, equal to, or greater than the input's field "X."
  • Check to see whether the input's field "Y" has a value.
  • Check to see whether the input's collection-based field "Z" contains a count that is less than, equal to, or greater than a number.

With code similar to that in P. Brian Mackey response, I was able to dynamically build a lambda expression for the first four points that I could then apply to the object itself using the Specification pattern.

The last point has to be done nearly identically, with the exception that the left portion of the phrase was a method call rather than a Property (more precisely, the left part of the statement was theICollection<T>.Count() method)

1
2
5/23/2017 12:12:56 PM

Accepted Answer

To get you going, I'll give you this. There is a great deal of space for development. particularly the obnoxious factory This demo does not serve as an example of best practices or a factory design; rather, it demonstrates how to utilize Expressions to solve issues. Please feel free to ask for clarification if anything is unclear.

Usage

    [Test]
    public void ApplySameLogicToCollectionsCount()
    {
        var receipt = new Receipt();
        var details = new ReceiptDetail();
        var details2 = new ReceiptDetail();
        receipt.Details.Add(details);
        receipt.Details.Add(details2);
        var result = LambdaGeneratorFactory<ICollection<ReceiptDetail>>.Run(detailsCount);
        Assert.IsTrue(result(receipt.Details));
    }

Factory

 public static class LambdaGeneratorFactory<T>
    {
        //This is an ugly implementation of a Factory pattern.
        //You should improve this possibly with interfaces, maybe abstract factory.  I'd start with an ICriteria.
        public static Predicate<T> Run(Criteria criteria)
        {
            if (typeof(T) == typeof (Receipt))
            {
                return CreateLambda(criteria);
            }
            else if (typeof (T) == typeof (ICollection<ReceiptDetail>))
            {
                return CreateLambdaWithCount(criteria);
            }

            return null;
        }
        private static Predicate<T> CreateLambda(Criteria criteria)
        {
            ParameterExpression pe = Expression.Parameter(typeof(T), "i");

            Expression left = Expression.Property(pe, typeof(T).GetProperty(criteria.Property));
            Expression right = Expression.Constant(criteria.Value);

            Expression predicateBody = Expression.Equal(left, right);

            var predicate = Expression.Lambda<Predicate<T>>(predicateBody, new ParameterExpression[] { pe }).Compile();

            return predicate;
        }

        private static Predicate<T> CreateLambdaWithCount(Criteria criteria)
        {
            ParameterExpression pe = Expression.Parameter(typeof(T), "i");

            Expression count = Expression.Property(pe, typeof(T).GetProperty("Count"));
            Expression left = Expression.Call(count, typeof(Object).GetMethod("ToString"));
            Expression right = Expression.Constant(criteria.Value);

            Expression predicateBody = Expression.Equal(left, right);

            var predicate = Expression.Lambda<Predicate<T>>(predicateBody, new ParameterExpression[] { pe }).Compile();

            return predicate;
        }
    }

Criteria

    private Criteria detailsCount = new Criteria()
    {
        Property = "Details",
        Operator = "equal",
        Value = "2"
    };

Change toICriteria in addition to being cleaner. better factory, no need forToString Software to an interface

Having said that, this code has an odd feeling to it. What use does it serve to create functions from strings? This seems to be moving toward creating C# from a grammar. I'm not sure how well it will scale. Consider lex/yacc first for implementations that are not simple. More information on how to achieve this is provided in the program pragmatist "Implementing a micro language."

3
3/14/2015 4:19:10 AM

Popular Answer

I find your question to be really interesting, and I want to know what is needed. I made a demo, and I'm curious how it differs from what you're attempting to do. There is a functional version here as well, https://dotnetfiddle.net/AEBZ1w.

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

public class Program
{
    public static void Main()
    {
        Criteria c = new Criteria() { 
            Property = "Name", 
            Operator = "==", 
            Value = "Foo" };

        var queryable = (new List<Receipt>() { 
            new Receipt { Name = "Foo", Amount = 1 },
            new Receipt { Name = "Foo", Amount = 2 }, 
            new Receipt { Name = "Bar" }  
        }).AsQueryable();

        var parameter = Expression.Parameter(typeof(Receipt), "x");
        var property = Expression.Property(parameter, typeof(Receipt).GetProperty(c.Property));
        var constant = Expression.Constant(c.Value);
        var operation = Expression.Equal(property, constant);
        var expression = Expression.Call(
            typeof(Queryable),
            "Where",
            new Type[] { queryable.ElementType },
            queryable.Expression, 
            Expression.Lambda<Func<Receipt, bool>>(operation, new ParameterExpression[] { parameter })
        );

        Console.WriteLine("Linq Expression: {0} \n", expression.ToString());
        Console.WriteLine("Results: \n");

        var results = queryable.Provider.CreateQuery<Receipt>(expression);
        foreach(var r in results)
        {
            Console.WriteLine("{0}:{1}", r.Name, r.Amount);
        }
    }
}

public class Criteria
{
    public string Property, Operator, Value;
}

public class ReceiptDetail
{
    public string ItemName;
}

public class Receipt
{
    public string Name { get; set; }
    public int Amount;
    public ICollection<ReceiptDetail> Details;
}

References



Related Questions





Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow