Search code examples
c#lambdaextension-methods

How to decide with lambda expression which property should be handled?


Context: we have the following code example:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
}

public static class PersonExtension
{
    public static void TrimFirstName(Person person)
    {
        Console.WriteLine($"Start Trim {nameof(person.FirstName)}");
        person.FirstName = person.FirstName.Remove(32);
        Console.WriteLine($"End Trim {nameof(person.FirstName)}");
    }
    
    public static void TrimLastName(Person person)
    {
        Console.WriteLine($"Start Trim {nameof(person.LastName)}");
        person.LastName = person.LastName.Remove(32);
        Console.WriteLine($"End Trim {nameof(person.LastName)}");
    }
}

public class Program
{
    public static void Main()
    {
        var person = new Person { FirstName = "Foo", LastName = "Bar" };
        
        PersonExtension.TrimFirstName(person);
        PersonExtension.TrimLastName(person);
    }
}

Target: we want to refactor the code and transform the functions TrimFirstName and TrimLastName into a single function to make it more dry.

We are currently stuck at a lambda expression to try to solve the problem:

public static class PersonExtension
{   
    public static void TrimName(this Person person, Func<Person, string> action)
    {
        // Console.WriteLine($"Start Trim {nameof(person.LastName)}"); // ??
        person.LastName = action(person).Remove(32); // how to assign to LastName/FirstName ???
        // Console.WriteLine($"End Trim {nameof(person.LastName)}"); // ??

        // more code here that uses person.<propertyName>
    }
}

public class Program
{
    public static void Main()
    {
        var person = new Person { FirstName = "Foo", LastName = "Bar" };
        
        person.TrimName(x => x.FirstName);
    }
}

Question: how can we use a lambda expression to decide which property should be trimmed?


Solution

  • If you just want to get the property's value and also get its name, you can take an expression tree (similar to my answer here):

    public static void TrimName(this Person person, Expression<Func<Person, string>> propertySelector)
    {
        if (propertySelector.Body is MemberExpression memberAccess) {
            var getter = propertySelector.Compile();
            var propertyValue = getter(person);
            var propertyName = memberAccess.Member.Name;
        } else {
            // lambda is not a member access
        }
    }
    

    If you also want to set the property however, I can't think of anything better than using reflection to call PropertyInfo.SetValue:

    public static void TrimName(this Person person, Expression<Func<Person, string>> propertySelector)
    {
        if (propertySelector.Body is MemberExpression memberAccess &&
            memberAccess.Member is PropertyInfo propertyInfo &&
            propertyInfo.CanWrite) {
            var getter = propertySelector.Compile();
    
            Console.WriteLine($"Start Trim {memberAccess.Member.Name}");
            propertyInfo.SetValue(person, getter(person).Remove(32));
            Console.WriteLine($"End Trim {memberAccess.Member.Name}");
        } else {
            // lambda is not a settable property
        }
    }
    

    The other option is to pass in another lambda that is dedicated to setting the property, turning the usage into:

    person.TrimName(x => x.LastName, (x, name) => x.LastName = name);