4

I want to convert the following string into function delegate.

[Id]-[Description]

C# class:

public class Foo
{
    public string Id {get;set;}
    public string Description {get;set;}
}

Result function delegate:

Func<Foo, string> GetExpression = delegate()
{
    return x => string.Format("{0}-{1}", x.Id, x.Description);
};

I think compiled lambda or expression parser would be a way here, but not sure about the best way much. Any inputs?

18
  • 1
    Why don't you overwrite the ToString() method? Commented May 17, 2019 at 10:23
  • 1
    @asd since OP needs a delegate which can be executed from other places, by passing relevant Input Commented May 17, 2019 at 10:25
  • Is there a reason you want to use a delegate and not just make a generic function that will parse the string for you? Commented May 17, 2019 at 10:25
  • 2
    @WiktorZychla I think OP wants the Func to be created on the fly based on the example string given in the first line Commented May 17, 2019 at 10:27
  • 1
    But why do you want a function delegate? Writing a simple function to do this (with a bunch of string.Replace for example) is very easy. Is there a reason you need a function? Commented May 17, 2019 at 10:29

3 Answers 3

3

It's possible as: to construct Linq Expression then compile it. Compiled expression is an ordinary delegate, with no performance drawbacks.

An example of implementation if type of argument(Foo) is known at compile time:

class ParserCompiler
{
    private static (string format, IReadOnlyCollection<string> propertyNames) Parse(string text)
    {
        var regex = new Regex(@"(.*?)\[(.+?)\](.*)");

        var formatTemplate = new StringBuilder();
        var propertyNames = new List<string>();
        var restOfText = text;
        Match match;
        while ((match = regex.Match(restOfText)).Success)
        {
            formatTemplate.Append(match.Groups[1].Value);
            formatTemplate.Append("{");
            formatTemplate.Append(propertyNames.Count);
            formatTemplate.Append("}");

            propertyNames.Add(match.Groups[2].Value);

            restOfText = match.Groups[3].Value;
        }

        formatTemplate.Append(restOfText);

        return (formatTemplate.ToString(), propertyNames);
    }

    public static Func<T, string> GetExpression<T>(string text) //"[Id]-[Description]"
    {
        var parsed = Parse(text); //"{0}-{1}  Id, Description"

        var argumentExpression = Expression.Parameter(typeof(T));

        var properties = typeof(T)
            .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetField)
            .ToDictionary(keySelector: propInfo => propInfo.Name);

        var formatParamsArrayExpr = Expression.NewArrayInit(
            typeof(object), 
            parsed.propertyNames.Select(propName => Expression.Property(argumentExpression, properties[propName])));

        var formatStaticMethod = typeof(string).GetMethod("Format", BindingFlags.Static | BindingFlags.Public, null,new[] { typeof(string), typeof(object[]) }, null);
        var formatExpr = Expression.Call(
            formatStaticMethod,
            Expression.Constant(parsed.format, typeof(string)),
            formatParamsArrayExpr);

        var resultExpr = Expression.Lambda<Func<T, string>>(
            formatExpr,
            argumentExpression); // Expression<Func<Foo, string>> a = (Foo x) => string.Format("{0}-{1}", x.Id, x.Description);

        return resultExpr.Compile();
    }
}

And usage:

        var func = ParserCompiler.GetExpression<Foo>("[Id]-[Description]");
        var formattedString = func(new Foo {Id = "id1", Description = "desc1"});
Sign up to request clarification or add additional context in comments.

1 Comment

Ah, yes, it just queries each property as many times as it is mentioned in the format string... the one I was going to post calls each property once ;)
2

An almost identical answer was posted while I was testing this, but, as the below code has an advantage of calling each property mentioned in the formatting string at most once, I'm posting it anyway:

public static Func<Foo, string> GetExpression(string query_string)
{
    (string format_string, List<string> prop_names) = QueryStringToFormatString(query_string);

    var lambda_parameter = Expression.Parameter(typeof(Foo));

    Expression[] formatting_params = prop_names.Select(
        p => Expression.MakeMemberAccess(lambda_parameter, typeof(Foo).GetProperty(p))
     ).ToArray();

    var formatMethod = typeof(string).GetMethod("Format", new[] { typeof(string), typeof(object[]) });

    var format_call = Expression.Call(formatMethod, Expression.Constant(format_string), Expression.NewArrayInit(typeof(object), formatting_params));

    var lambda = Expression.Lambda(format_call, lambda_parameter) as Expression<Func<Foo, string>>;
    return lambda.Compile();
}

// A *very* primitive parser, improve as needed
private static (string format_string, List<string> ordered_prop_names) QueryStringToFormatString(string query_string)
{
    List<string> prop_names = new List<string>();

    string format_string = Regex.Replace(query_string, @"\[.+?\]", m => {
        string prop_name = m.Value.Substring(1, m.Value.Length - 2);

        var known_pos = prop_names.IndexOf(prop_name);

        if (known_pos < 0)
        {
            prop_names.Add(prop_name);
            known_pos = prop_names.Count - 1;
        }

        return $"{{{known_pos}}}";
    });

    return (format_string, prop_names);
}

The inspiration comes from Generate lambda Expression By Clause using string.format in C#?.

Comments

1

A simple step by step version to create an Expression tree based on simple use case, can help in creating any kind of Expression tree

What we want to Achieve: (coding in linqpad, Dump is a print call)

Expression<Func<Foo,string>> expression = (f) => string.Format($"{f.Id}- 
{f.Description}"); 

var foo = new Foo{Id = "1",Description="Test"};

var func  = expression.Compile();

func(foo).Dump(); // Result "1-Test"

expression.Dump();

Following is the Expression generated:

enter image description here

Step by Step process to Create an Expression Tree

On Reviewing the Expression Tree, following points can be understood:

  1. We create a Func delegate of type typeof(Func<Foo,String>)
  2. Outer Node Type for Expression is Lambda Type
  3. Just needs one parameter Expression of typeof(Foo)
  4. In Arguments it needs, MethodInfo of string.Format
  5. In arguments to Format method, it needs following Expressions
  6. a.) Constant Expression - {0}-{1}
  7. b.) MemberExpression for Id field
  8. c.) MemberExpression for Description field
  9. Viola and we are done

Using the Steps above following is the simple code to create Expression:

// Create a ParameterExpression
var parameterExpression = Expression.Parameter(typeof(Foo),"f");

// Create a Constant Expression
var formatConstant  = Expression.Constant("{0}-{1}");

// Id MemberExpression
var idMemberAccess = Expression.MakeMemberAccess(parameterExpression, typeof(Foo).GetProperty("Id"));

// Description MemberExpression         
var descriptionMemberAccess = Expression.MakeMemberAccess(parameterExpression, typeof(Foo).GetProperty("Description"));

// String.Format (MethodCallExpression)
var formatMethod = Expression.Call(typeof(string),"Format",null,formatConstant,idMemberAccess,descriptionMemberAccess);

// Create Lambda Expression
var lambda = Expression.Lambda<Func<Foo,string>>(formatMethod,parameterExpression);

// Create Func delegate via Compilation
var func = lambda.Compile();

// Execute Delegate 
func(foo).Dump(); // Result "1-Test"

2 Comments

Thanks. This is very useful to understand how it works and helps creating complex ones easily.
Yes though you need a Expression Tree Visualizer as it goes about solving the issue in reverse direction, Linqpad is good and there are others too like Expression Tree Visualizer, but a very good way to construct from Scratch, Happy Programming

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.