How to map properties of an object to constructor arguments in C# for auto copy construction

1.5k views Asked by At

I have an immutable object. For example the simple case below.

class Person {
   public string Name {get;}
   public int Age {get;}
   public Person(string name, int age){
       Name = name;
       Age = age;
   } 
}

Now I would like to have a generic extension method such as

public static class ObjectExtensions {
  public void T With<T,P>(this T target, Expression<Func<T,P>> selector, P value){
      /* some implementatation */
  }
}

So that I can do

var person = new Person("brad", 12).With(p=>p.Age,55);
person.Age.Should().Be(55);

The With method should use reflection to match constructor argument names with properties on the existing object.

  • Performance is not an issue.
  • Runtime failure is ok if the constructor argument names do not match the property names. ( A roslyn analyser could solve that problem )
  • Using reflection to set properties using private setters after a shallow clone is also not desired. ( I have a solution currently that does this )

The intention is that no extra methods need be added to the immutable class other than a constructor with arguments with names that match properties.

An explicit With method with nullable arguments such as

class Person {
   public string Name {get;}
   public int Age {get;}
   public Person(string name, int age){
       Name = name;
       Age = age;
   }
   public Person With(string? name, int? age){
       return new Person(name ?? this.Name, age ?? this.Age);
   }  

}

though elegant is not the solution I am looking for for my use case.

1

There are 1 answers

0
Ash Burlaczenko On BEST ANSWER

Here's one example. This can probably be improve to cache the type information for speed but it's a baseline you can improve on. As I mentioned in the comment, this is assuming the constructor has a parameter named the same as the property, ignoring case. It also assumes there's only one constructor but you can update that as you please.

public static T With<T, P>(this T target, Expression<Func<T, P>> selector, P value)
{
    var expression = selector.Body as MemberExpression;

    if (expression == null)
    {
        throw new InvalidOperationException();
    }

    var name = expression.Member.Name;

    var constructor = typeof(T).GetConstructors().First();
    var args = GetParamaters(target, value, constructor, name);

    return (T)Activator.CreateInstance(typeof(T), args.ToArray());
}

private static IEnumerable<object> GetParamaters<T, P>(T target, P value, ConstructorInfo constructor, string name)
{
    foreach (var parameterInfo in constructor.GetParameters())
    {
        if (parameterInfo.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))
        {
            yield return value;
        }
        else
        {
            var property =
                typeof(T).GetProperties()
                    .First(x => x.Name.Equals(parameterInfo.Name, StringComparison.InvariantCultureIgnoreCase));
            yield return property.GetValue(target, null);
        }
    }
}