'c# Get name of variable(s) during object deconstruction

Given this code:

var (c, d) = new Test();

This it possible to get the variables names from the Deconstruct method?

public class Test
{
    public void Deconstruct(out string value1, out string value2)
    {
        // is there a way to know the parameters are 
        // being mapped to "c" and "d" respectively
        // inside this method?
    }
}

The idea to refactor the follow code to reduce the amount of repetition:

var parsed = Regex
    .Match("123: qwe: qweasd", @"(?<id>\d+?): (?<level>\d+?): (?<message>\d+?):")
    .Apply(m => !m.Success ? null : new 
    {
        // notice the names are repeated on both side
        ID = m.Group["id"].Value,
        Level = m.Group["level"].Value,
        Message = m.Group["message"].Value,
    });

What I'm trying to solve with Test class:

var (id, level, message) = Regex
    .Match("123: qwe: qweasd", @"(?<id>\d+?): (?<level>\w+?): (?<message>\w+?):")
    .Groups
    .AsDeconstructable(); // 


Solution 1:[1]

In C# 10, you can use the new attribute CallerArgumentExpression combined with out variable declaration in the call to get close to what you want:

First, a string extension method:

public static class StringExt {
    public static string Past(this string s, string separator) {
        var starterPos = s.IndexOf(separator);
        return starterPos == -1 ? s : s.Substring(starterPos + separator.Length);
    }
}

Now a sample ToVariables extension method for GroupCollection for arity 3 (obviously it is easy to create other arities):

public static class GroupCollectionExt {
    public static void ToVariables(this GroupCollection gc, out string v1, out string v2, out string v3,
                                   [CallerArgumentExpression("v1")] string name1 = "", [CallerArgumentExpression("v2")] string name2 = "", [CallerArgumentExpression("v3")] string name3 = "") {
        v1 = gc.TryGetValue(name1.Past(" "), out var g1) ? g1.Value : default;
        v2 = gc.TryGetValue(name2.Past(" "), out var g2) ? g2.Value : default;
        v3 = gc.TryGetValue(name3.Past(" "), out var g3) ? g3.Value : default;
    }
}

And now you can declare variables and assign them to group values from a Regex match with the only repetition in the pattern:

Regex
    .Match("123: qwe: qweasd:", @"(?<id>\d+?): (?<level>\w+?): (?<message>\w+?):")
    .Groups
    .ToVariables(out var id, out var level, out var message);

Note: An obvious extension would be to create ToVariables for Dictionary<string,T>. Another extension might be to fall back to positional groups when there are no named groups.

Solution 2:[2]

I really don't think this is a good idea, and Reflection can be terribly slow, but here we go.

First, we need some extensions to make dealing with properties and fields a little cleaner:

public static class HelperExtensions {
    // ***
    // *** Type Extensions
    // ***
    public static List<MemberInfo> GetPropertiesOrFields(this Type t, BindingFlags bf = BindingFlags.Public | BindingFlags.Instance) =>
        t.GetMembers(bf).Where(mi => mi.MemberType == MemberTypes.Field | mi.MemberType == MemberTypes.Property).ToList();

    // ***
    // *** MemberInfo Extensions
    // ***
    public static void SetValue<T>(this MemberInfo member, object destObject, T value) {
        switch (member) {
            case FieldInfo mfi:
                mfi.SetValue(destObject, value);
                break;
            case PropertyInfo mpi:
                mpi.SetValue(destObject, value);
                break;
            default:
                throw new ArgumentException("MemberInfo must be of type FieldInfo or PropertyInfo", nameof(member));
        }
    }

    public static TOut Apply<TIn, TOut>(this TIn m, Func<TIn, TOut> applyFn) => applyFn(m);
}

Then, we need to create a class to represent the desired result:

public class ParsedMessage {
    public string ID;
    public string Level;
    public string Message;
}

Now, we write an extension to map Group named values to properties or fields in an object:

public static class MatchExt {
    public static T MakeObjectFromGroups<T>(this Match m) where T : new() {
        var members = typeof(T).GetPropertiesOrFields().ToDictionary(pf => pf.Name.ToLower());
        var ans = new T();
        foreach (Group g in m.Groups) {
            if (members.TryGetValue(g.Name.ToLower(), out var mi))
                mi.SetValue(ans, g.Value);
        }

        return ans;
    }

    public static string[] MakeArrayFromGroupValues(this Match m) {
        var ans = new string[m.Groups.Count-1];
        for (int j1 = 1; j1 < m.Groups.Count; ++j1)
            ans[j1-1] = m.Groups[j1].Value;

        return ans;
    }
}

Finally, we can use our new extension:

var parsed = Regex
    .Match("123: qwe: qweasd", @"(?<id>\d+?): (?<level>\w+?): (?<message>\w+?)")
    .Apply(m => m.Success ? m.MakeObjectFromGroups<ParsedMessage>() : null);

Note: it is possible to create anonymous types on the fly at runtime, but they are rarely useful. Since you don't know anywhere else in your code what the properties are, you must do everything through Reflection, and unless you are using the objects in a Reflection heavy environment like ASP.Net, you might as well use a Dictionary, or if you must, a DynamicObject (though, again, without knowing the field names that isn't too practical).

I added an additional extension to map Groups to a string[]. Since names for ValueTuple fields are only usable at compile time, creating an array and using indexes is just as good as creating a ValueTuple and using Item1, etc.

Finally, an attempt to work with an anonymous object. By passing in a template for the anonymous object, you can create a new anonymous object from the capture group values that have matching names.

Using a method extension for type inference:

public static class ToAnonymousExt {
    public static T ToAnonymous<T>(this T patternV, Match m) {
        var it = typeof(T).GetPropertiesOrFields();
        var cd = m.Groups.Cast<Group>().ToDictionary(g => g.Name, g => g.Value);
        return (T)Activator.CreateInstance(typeof(T), Enumerable.Range(0, it.Count).Select(n => cd[it[n].Name]).ToArray());
    }
}

Now you can pass in an anonymous type as a template and get a filled in anonymous object back. Note that only the fields in the anonymous type that match capture group names will be filled in, and no runtime error handling is done.

var parsed3 = Regex.Match("123: qwe: qweasd", @"(?<id>\d+?): (?<level>\w+?): (?<message>\w+?)")
                   .Apply(m => m.Success ? new { message = "", id = "", level = "" }.ToAnonymous(m) : null);

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2