Pass multiple properties of a class into a function and return values of a custom attribute

Viewed 735

I am working on a requirement in which I need to map between two systems. Because this mapping is rather extensive I am using a domain model with some custom attributes to do the mapping for data retrieval and crud operations.

So my domain classes look somthing like this:

[OhtherSystemObjectName("otherSystemAccountName")]
public class Account : SomeBaseClass
{
    [OhtherSystemAttributeName("otherSystsemAccountNameColumn")]
    public string Name { get; set; }

    [OhtherSystemAttributeName("otherSystsemAccountNumberColumn")]
    public string AccountNumber { get; set; }

    [OhtherSystemAttributeName("otherSystsemAddressColumn")]
    public string Address { get; set; }

    [OhtherSystemAttributeName("otherSystsemPostalCodeColumn")]
    public string PostalCode { get; set; }

}

I use a custom attribute to specify the column names of the other system so I do not need to bother about these when data needs to be retrieved or modified. Now I would like to create a data retrieval method in which I can specify one or more columns using expressions. The method look somthings like this:

public string[] GetColumnSet<T>(Expression<Func<T, object[]>> attributes) where T : SomeBaseClass
{
    
    return ((NewArrayExpression)attributes.Body).Expressions.Select(a =>
    {

        switch (a.NodeType)
        {
            case ExpressionType.MemberAccess:
                return GetAttributeName((MemberExpression)a);
            case ExpressionType.Convert:
                return GetAttributeName((MemberExpression)((UnaryExpression)a).Operand);

        }

        return null;

    }).Where(a => !string.IsNullOrEmpty(a)).ToArray();  
}

private static string GetAttributeName(MemberExpression memberExpression)
{
    var attribute = memberExpression.Member.GetCustomAttribute<OhtherSystemAttributeName>();
    return attribute?.AttributeName;
}

I use this method in my mapping layer to create a query to the other system and I need to specify the column names as an array of string. This call to this method looks something like this:

var colums = GetColumnSet<Account>(a => new object[] {a.Name, a.AccountNumber});

No I would like it to be more like this:

var colums = GetColumnSet<Account>(a => {a.Name, a.AccountNumber});

Without the new object[]{} but Expression<Func<T, param object[]>> does not compile.

Is it possible to rewrite the input expression to accomplish a syntax like this?

2 Answers

You can't use the exact syntax that you're after, but you could use an anonymous object:

var colums = GetColumnSet<Account>(a => new { a.Name, a.AccountNumber });

You could implement the method like this:

public string[] GetColumnSet<T>(Expression<Func<T, object>> selector) where T : SomeBaseClass
{
    return ((NewExpression)selector.Body)
        .Type
        .GetProperties()
        .Select(prop => typeof(T)
            .GetProperty(prop.Name)?
            .GetCustomAttribute<OtherSystemAttributeName>()?
            .AttributeName)
        .Where(name => !string.IsNullOrEmpty(name))
        .ToArray();
}

Any property names that are specified in your anonymous object that don't exist in your T object, or properties that are not decorated with a OtherSystemAttributeName attribute are simply ignored, which seems to be the behaviour your after.

You can also use the following Dude() function:

public static void Dude(params object[] theListOfArguments) {}

Then the call to GetColumnSet would look only slightly different and in my opinion better than new {...}

var colums = GetColumnSet<Account>(a => Dude(a.Name, a.AccountNumber));

It also requires two minor changes to the GetColumnSet function.

  • Func<T,object[]> to Action<T> parameter type change and
  • the expression that retrieves the list of fields from the action parameters. It is the first code line in the function.

The rest of the code is the same.

public static string[] GetColumnSet<T>(Expression<Action<T>> attributes) where T : SomeBaseClass
{
    return ((NewArrayExpression)((MethodCallExpression)attributes.Body).Arguments.First()).Expressions.Select(a =>
    {
        switch (a.NodeType)
        {
            case ExpressionType.MemberAccess:
                return GetAttributeName((MemberExpression)a);
            case ExpressionType.Convert:
                return GetAttributeName((MemberExpression)((UnaryExpression)a).Operand);
        }
        return null;
    }).Where(a => !string.IsNullOrEmpty(a)).ToArray();
}
Related