简体   繁体   中英

string expression to c# function delegate

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?

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"});

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#? .

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:

在此输入图像描述

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"

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM