Extension “With” for immutable types












0












$begingroup$


(My code is basically a rewrite of https://github.com/ababik/Remute so much of the credit goes there)



The idea is to use lambda expression to provide a general With method for immutable objects.



With With you can do:



using With;

public class Employee {
public string EmployeeFirstName { get; }
public string EmployeeLastName { get; }

public Employee(string employeeFirstName, string employeeLastName) {
EmployeeFirstName = employeeFirstName;
EmployeeLastName = employeeLastName;
}
}

public class Department {
public string DepartmentTitle { get; }
public Employee Manager { get; }

public Department() { /* .. */ }
public Department(int manager, string title) { /* .. */ }

public Department(string departmentTitle, Employee manager) {
DepartmentTitle = departmentTitle;
Manager = manager;
}
}

public class Organization {
public string OrganizationName { get; }
public Department DevelopmentDepartment { get; }

public Organization(string organizationName, Department developmentDepartment) {
OrganizationName = organizationName;
DevelopmentDepartment = developmentDepartment;
}
}

var expected = new Organization("Organization", new Department("Development Department", new Employee("John", "Doe")));
var actual = expected.With(x => x.DevelopmentDepartment.Manager.EmployeeFirstName, "Foo");

Console.WriteLine(expected.DevelopmentDepartment.Manager.EmployeeFirstName); // "Doe"
Console.WriteLine(actual.DevelopmentDepartment.Manager.EmployeeFirstName); // "Foo"
Console.WriteLine(Object.ReferenceEquals(expected, actual)); // false
Console.WriteLine(Object.ReferenceEquals(expected.DevelopmentDepartment, actual.DevelopmentDepartment)); // false
Console.WriteLine(Object.ReferenceEquals(expected.DevelopmentDepartment.Manager, actual.DevelopmentDepartment.Manager)); // false
Console.WriteLine(Object.ReferenceEquals(expected.OrganizationName, actual.OrganizationName)); // true
Console.WriteLine(Object.ReferenceEquals(expected.DevelopmentDepartment.DepartmentTitle, expected.DevelopmentDepartment.DepartmentTitle)); // true

var actual1 = expected.With(x => x.DevelopmentDepartment.Manager.EmployeeFirstName, "Foo");
Console.WriteLine(Object.ReferenceEquals(actual, actual1)); // false


With expects any classes in the modified 'path' to have a constructor accepting all properties (with a case insensitive match on property name) and will use that constructor only when necessary to create new objects. There is a lot of caching going on to try an give maximum performance.



The code is quite dense but I still hope to get some inputs on improving it or bug reports !



namespace With {

using ParameterResolver = ValueTuple<PropertyInfo, Func<object, object>>;

internal class WithInternal {
delegate object Activator(params object args);
delegate object ResolveInstanceDelegate<T>(T source);

ImmutableDictionary<string, (Activator activator, ParameterResolver parameterResolvers)> ActivationContextCache;
ImmutableDictionary<string, Delegate> ResolveInstanceExpressionCache;

WithInternal() {
ActivationContextCache = ImmutableDictionary<string, (Activator Activator, ParameterResolver ParameterResolvers)>.Empty;
ResolveInstanceExpressionCache = ImmutableDictionary<string, Delegate>.Empty;
}
public readonly static WithInternal Default = new WithInternal();

// Constructs immutable object from any other object.
public TInstance With<TInstance>(object source) {
if (source is null) throw new ArgumentNullException(nameof(source));
return (TInstance)ResolveActivator(source.GetType(), typeof(TInstance), null, source, null);
}

// Contructs immutable object from existing one with changed property specified by lambda expression.
public TInstance With<TInstance, TValue>(TInstance source, Expression<Func<TInstance, TValue>> expression, object target) {
if (source is null) throw new ArgumentNullException(nameof(source));
if (expression is null) throw new ArgumentNullException(nameof(expression));

var sourceParameterExpression = expression.Parameters.Single();
var instanceExpression = expression.Body;

while (instanceExpression != sourceParameterExpression) {
if (!(instanceExpression is MemberExpression memberExpression) || !(memberExpression.Member is PropertyInfo property))
throw new NotSupportedException($"Unable to process expression. Expression: '{instanceExpression}'.");

instanceExpression = memberExpression.Expression;

// create unique cache key, calc same key for x=>x.p and y=>y.p
string key;
try {
var exprStr = instanceExpression.ToString();
key = typeof(TInstance).FullName + '|' + exprStr.Remove(0, exprStr.IndexOf(Type.Delimiter) + 1);
} catch (Exception ex) {
throw new Exception($"Unable to parse expression '{instanceExpression}'.", ex);
}

ResolveInstanceDelegate<TInstance> compiledExpression;

if (ResolveInstanceExpressionCache.TryGetValue(key, out var resolveInstanceDelegate)) {
compiledExpression = (ResolveInstanceDelegate<TInstance>)resolveInstanceDelegate;
} else {
var instanceConvertExpression = Expression.Convert(instanceExpression, typeof(object));
var lambdaExpression = Expression.Lambda<ResolveInstanceDelegate<TInstance>>(instanceConvertExpression, sourceParameterExpression);
compiledExpression = lambdaExpression.Compile();
ResolveInstanceExpressionCache = ResolveInstanceExpressionCache.SetItem(key, compiledExpression);
}

var type = property.DeclaringType;
var instance = compiledExpression.Invoke(source);
target = ResolveActivator(type, type, property, instance, target);
}

return (TInstance)target;
}

object ResolveActivator(Type sourceType, Type valueType, PropertyInfo property, object instance, object target) {
var (activator, parameterResolvers) = GetActivator(sourceType, valueType);

// resolve activator arguments
var arguments = new object[parameterResolvers.Length];
var match = false;

for (var i = 0; i < parameterResolvers.Length; i++) {
var (resolverProperty, resolver) = parameterResolvers[i];
arguments[i] = resolverProperty == property ? target : resolver.Invoke(instance);
if (resolverProperty == property) match = true;
}

if (!match) throw new Exception($"Unable to construct object of type '{property.DeclaringType.Name}'. There is no constructor parameter matching property '{property.Name}'.");
return activator.Invoke(arguments);
}

(Activator activator, ParameterResolver parameterResolvers) GetActivator(Type sourceType, Type valueType) {
var key = sourceType.FullName + '|' + valueType.FullName;
if (ActivationContextCache.TryGetValue(key, out var res)) return res;

foreach (var constructor in valueType.GetTypeInfo().DeclaredConstructors) {
var parameters = constructor.GetParameters();

// Get ParameterResolvers
var parameterResolvers = new ParameterResolver[parameters.Length];
{
var properties = sourceType.GetTypeInfo().DeclaredProperties.ToArray();
if (parameters.Length != properties.Length) continue;

var i=0;
foreach (var parameter in parameters) {
var property = properties.Where(x => string.Equals(x.Name, parameter.Name, StringComparison.OrdinalIgnoreCase)).SingleOrDefault();
if (property is null || property.PropertyType != parameter.ParameterType) break;

var parameterExpression = Expression.Parameter(typeof(object));
var parameterConvertExpression = Expression.Convert(parameterExpression, sourceType);
var propertyExpression = Expression.Property(parameterConvertExpression, property);
var propertyConvertExpression = Expression.Convert(propertyExpression, typeof(object));
var lambdaExpression = Expression.Lambda<Func<object, object>>(propertyConvertExpression, parameterExpression);
var compiledExpression = lambdaExpression.Compile();

parameterResolvers[i++] = (property, compiledExpression);
}
if (i < parameters.Length) continue; // this ctor is no good
}

// get target activator
Activator activator;
{
var parameterExpression = Expression.Parameter(typeof(object));
var argumentExpressions = new Expression[parameters.Length];

for (var i = 0; i < parameters.Length; i++) {
var arrayExpression = Expression.ArrayIndex(parameterExpression, Expression.Constant(i));
var arrayConvertExpression = Expression.Convert(arrayExpression, parameters[i].ParameterType);
argumentExpressions[i] = arrayConvertExpression;
}

var constructorExpression = Expression.New(constructor, argumentExpressions);
var constructorConvertExpression = Expression.Convert(constructorExpression, typeof(object));
var activatorLambdaExpression = Expression.Lambda<Activator>(constructorConvertExpression, parameterExpression);
activator = activatorLambdaExpression.Compile();
}

res = (activator, parameterResolvers);
ActivationContextCache = ActivationContextCache.SetItem(key, res);
return res;
}

throw new Exception($"Unable to find appropriate Constructor. Type '{sourceType.Name}'.");
}
}

// -------------------- ExtensionMethods.cs

public static class ExtensionMethods {
/// <summary>
/// Contructs immutable object from existing one with changed property specified by lambda expression.
/// </summary>
/// <typeparam name="TInstance">Immutable object type.</typeparam>
/// <typeparam name="TValue">Value to set type.</typeparam>
/// <param name="instance">Original immutable object.</param>
/// <param name="expression">Navigation property specifying what to change.</param>
/// <param name="value">Value to set in the resulting object.</param>
/// <returns></returns>
public static TInstance With<TInstance, TValue>(this TInstance instance, Expression<Func<TInstance, TValue>> expression, TValue value) =>
WithInternal.Default.With(instance, expression, value);


/// <summary>
/// Constructs immutable object from any other object.
/// Helpful cloning immutable object or converting POCO, DTO, anonymous type, dynamic ect.
/// </summary>
/// <typeparam name="TInstance">Immutable object type.</typeparam>
/// <param name="source">Original object.</param>
/// <returns>Configuration to use. Default if not specified.</returns>
public static TInstance With<TInstance>(this object source) =>
WithInternal.Default.With<TInstance>(source);
}
}









share|improve this question











$endgroup$

















    0












    $begingroup$


    (My code is basically a rewrite of https://github.com/ababik/Remute so much of the credit goes there)



    The idea is to use lambda expression to provide a general With method for immutable objects.



    With With you can do:



    using With;

    public class Employee {
    public string EmployeeFirstName { get; }
    public string EmployeeLastName { get; }

    public Employee(string employeeFirstName, string employeeLastName) {
    EmployeeFirstName = employeeFirstName;
    EmployeeLastName = employeeLastName;
    }
    }

    public class Department {
    public string DepartmentTitle { get; }
    public Employee Manager { get; }

    public Department() { /* .. */ }
    public Department(int manager, string title) { /* .. */ }

    public Department(string departmentTitle, Employee manager) {
    DepartmentTitle = departmentTitle;
    Manager = manager;
    }
    }

    public class Organization {
    public string OrganizationName { get; }
    public Department DevelopmentDepartment { get; }

    public Organization(string organizationName, Department developmentDepartment) {
    OrganizationName = organizationName;
    DevelopmentDepartment = developmentDepartment;
    }
    }

    var expected = new Organization("Organization", new Department("Development Department", new Employee("John", "Doe")));
    var actual = expected.With(x => x.DevelopmentDepartment.Manager.EmployeeFirstName, "Foo");

    Console.WriteLine(expected.DevelopmentDepartment.Manager.EmployeeFirstName); // "Doe"
    Console.WriteLine(actual.DevelopmentDepartment.Manager.EmployeeFirstName); // "Foo"
    Console.WriteLine(Object.ReferenceEquals(expected, actual)); // false
    Console.WriteLine(Object.ReferenceEquals(expected.DevelopmentDepartment, actual.DevelopmentDepartment)); // false
    Console.WriteLine(Object.ReferenceEquals(expected.DevelopmentDepartment.Manager, actual.DevelopmentDepartment.Manager)); // false
    Console.WriteLine(Object.ReferenceEquals(expected.OrganizationName, actual.OrganizationName)); // true
    Console.WriteLine(Object.ReferenceEquals(expected.DevelopmentDepartment.DepartmentTitle, expected.DevelopmentDepartment.DepartmentTitle)); // true

    var actual1 = expected.With(x => x.DevelopmentDepartment.Manager.EmployeeFirstName, "Foo");
    Console.WriteLine(Object.ReferenceEquals(actual, actual1)); // false


    With expects any classes in the modified 'path' to have a constructor accepting all properties (with a case insensitive match on property name) and will use that constructor only when necessary to create new objects. There is a lot of caching going on to try an give maximum performance.



    The code is quite dense but I still hope to get some inputs on improving it or bug reports !



    namespace With {

    using ParameterResolver = ValueTuple<PropertyInfo, Func<object, object>>;

    internal class WithInternal {
    delegate object Activator(params object args);
    delegate object ResolveInstanceDelegate<T>(T source);

    ImmutableDictionary<string, (Activator activator, ParameterResolver parameterResolvers)> ActivationContextCache;
    ImmutableDictionary<string, Delegate> ResolveInstanceExpressionCache;

    WithInternal() {
    ActivationContextCache = ImmutableDictionary<string, (Activator Activator, ParameterResolver ParameterResolvers)>.Empty;
    ResolveInstanceExpressionCache = ImmutableDictionary<string, Delegate>.Empty;
    }
    public readonly static WithInternal Default = new WithInternal();

    // Constructs immutable object from any other object.
    public TInstance With<TInstance>(object source) {
    if (source is null) throw new ArgumentNullException(nameof(source));
    return (TInstance)ResolveActivator(source.GetType(), typeof(TInstance), null, source, null);
    }

    // Contructs immutable object from existing one with changed property specified by lambda expression.
    public TInstance With<TInstance, TValue>(TInstance source, Expression<Func<TInstance, TValue>> expression, object target) {
    if (source is null) throw new ArgumentNullException(nameof(source));
    if (expression is null) throw new ArgumentNullException(nameof(expression));

    var sourceParameterExpression = expression.Parameters.Single();
    var instanceExpression = expression.Body;

    while (instanceExpression != sourceParameterExpression) {
    if (!(instanceExpression is MemberExpression memberExpression) || !(memberExpression.Member is PropertyInfo property))
    throw new NotSupportedException($"Unable to process expression. Expression: '{instanceExpression}'.");

    instanceExpression = memberExpression.Expression;

    // create unique cache key, calc same key for x=>x.p and y=>y.p
    string key;
    try {
    var exprStr = instanceExpression.ToString();
    key = typeof(TInstance).FullName + '|' + exprStr.Remove(0, exprStr.IndexOf(Type.Delimiter) + 1);
    } catch (Exception ex) {
    throw new Exception($"Unable to parse expression '{instanceExpression}'.", ex);
    }

    ResolveInstanceDelegate<TInstance> compiledExpression;

    if (ResolveInstanceExpressionCache.TryGetValue(key, out var resolveInstanceDelegate)) {
    compiledExpression = (ResolveInstanceDelegate<TInstance>)resolveInstanceDelegate;
    } else {
    var instanceConvertExpression = Expression.Convert(instanceExpression, typeof(object));
    var lambdaExpression = Expression.Lambda<ResolveInstanceDelegate<TInstance>>(instanceConvertExpression, sourceParameterExpression);
    compiledExpression = lambdaExpression.Compile();
    ResolveInstanceExpressionCache = ResolveInstanceExpressionCache.SetItem(key, compiledExpression);
    }

    var type = property.DeclaringType;
    var instance = compiledExpression.Invoke(source);
    target = ResolveActivator(type, type, property, instance, target);
    }

    return (TInstance)target;
    }

    object ResolveActivator(Type sourceType, Type valueType, PropertyInfo property, object instance, object target) {
    var (activator, parameterResolvers) = GetActivator(sourceType, valueType);

    // resolve activator arguments
    var arguments = new object[parameterResolvers.Length];
    var match = false;

    for (var i = 0; i < parameterResolvers.Length; i++) {
    var (resolverProperty, resolver) = parameterResolvers[i];
    arguments[i] = resolverProperty == property ? target : resolver.Invoke(instance);
    if (resolverProperty == property) match = true;
    }

    if (!match) throw new Exception($"Unable to construct object of type '{property.DeclaringType.Name}'. There is no constructor parameter matching property '{property.Name}'.");
    return activator.Invoke(arguments);
    }

    (Activator activator, ParameterResolver parameterResolvers) GetActivator(Type sourceType, Type valueType) {
    var key = sourceType.FullName + '|' + valueType.FullName;
    if (ActivationContextCache.TryGetValue(key, out var res)) return res;

    foreach (var constructor in valueType.GetTypeInfo().DeclaredConstructors) {
    var parameters = constructor.GetParameters();

    // Get ParameterResolvers
    var parameterResolvers = new ParameterResolver[parameters.Length];
    {
    var properties = sourceType.GetTypeInfo().DeclaredProperties.ToArray();
    if (parameters.Length != properties.Length) continue;

    var i=0;
    foreach (var parameter in parameters) {
    var property = properties.Where(x => string.Equals(x.Name, parameter.Name, StringComparison.OrdinalIgnoreCase)).SingleOrDefault();
    if (property is null || property.PropertyType != parameter.ParameterType) break;

    var parameterExpression = Expression.Parameter(typeof(object));
    var parameterConvertExpression = Expression.Convert(parameterExpression, sourceType);
    var propertyExpression = Expression.Property(parameterConvertExpression, property);
    var propertyConvertExpression = Expression.Convert(propertyExpression, typeof(object));
    var lambdaExpression = Expression.Lambda<Func<object, object>>(propertyConvertExpression, parameterExpression);
    var compiledExpression = lambdaExpression.Compile();

    parameterResolvers[i++] = (property, compiledExpression);
    }
    if (i < parameters.Length) continue; // this ctor is no good
    }

    // get target activator
    Activator activator;
    {
    var parameterExpression = Expression.Parameter(typeof(object));
    var argumentExpressions = new Expression[parameters.Length];

    for (var i = 0; i < parameters.Length; i++) {
    var arrayExpression = Expression.ArrayIndex(parameterExpression, Expression.Constant(i));
    var arrayConvertExpression = Expression.Convert(arrayExpression, parameters[i].ParameterType);
    argumentExpressions[i] = arrayConvertExpression;
    }

    var constructorExpression = Expression.New(constructor, argumentExpressions);
    var constructorConvertExpression = Expression.Convert(constructorExpression, typeof(object));
    var activatorLambdaExpression = Expression.Lambda<Activator>(constructorConvertExpression, parameterExpression);
    activator = activatorLambdaExpression.Compile();
    }

    res = (activator, parameterResolvers);
    ActivationContextCache = ActivationContextCache.SetItem(key, res);
    return res;
    }

    throw new Exception($"Unable to find appropriate Constructor. Type '{sourceType.Name}'.");
    }
    }

    // -------------------- ExtensionMethods.cs

    public static class ExtensionMethods {
    /// <summary>
    /// Contructs immutable object from existing one with changed property specified by lambda expression.
    /// </summary>
    /// <typeparam name="TInstance">Immutable object type.</typeparam>
    /// <typeparam name="TValue">Value to set type.</typeparam>
    /// <param name="instance">Original immutable object.</param>
    /// <param name="expression">Navigation property specifying what to change.</param>
    /// <param name="value">Value to set in the resulting object.</param>
    /// <returns></returns>
    public static TInstance With<TInstance, TValue>(this TInstance instance, Expression<Func<TInstance, TValue>> expression, TValue value) =>
    WithInternal.Default.With(instance, expression, value);


    /// <summary>
    /// Constructs immutable object from any other object.
    /// Helpful cloning immutable object or converting POCO, DTO, anonymous type, dynamic ect.
    /// </summary>
    /// <typeparam name="TInstance">Immutable object type.</typeparam>
    /// <param name="source">Original object.</param>
    /// <returns>Configuration to use. Default if not specified.</returns>
    public static TInstance With<TInstance>(this object source) =>
    WithInternal.Default.With<TInstance>(source);
    }
    }









    share|improve this question











    $endgroup$















      0












      0








      0





      $begingroup$


      (My code is basically a rewrite of https://github.com/ababik/Remute so much of the credit goes there)



      The idea is to use lambda expression to provide a general With method for immutable objects.



      With With you can do:



      using With;

      public class Employee {
      public string EmployeeFirstName { get; }
      public string EmployeeLastName { get; }

      public Employee(string employeeFirstName, string employeeLastName) {
      EmployeeFirstName = employeeFirstName;
      EmployeeLastName = employeeLastName;
      }
      }

      public class Department {
      public string DepartmentTitle { get; }
      public Employee Manager { get; }

      public Department() { /* .. */ }
      public Department(int manager, string title) { /* .. */ }

      public Department(string departmentTitle, Employee manager) {
      DepartmentTitle = departmentTitle;
      Manager = manager;
      }
      }

      public class Organization {
      public string OrganizationName { get; }
      public Department DevelopmentDepartment { get; }

      public Organization(string organizationName, Department developmentDepartment) {
      OrganizationName = organizationName;
      DevelopmentDepartment = developmentDepartment;
      }
      }

      var expected = new Organization("Organization", new Department("Development Department", new Employee("John", "Doe")));
      var actual = expected.With(x => x.DevelopmentDepartment.Manager.EmployeeFirstName, "Foo");

      Console.WriteLine(expected.DevelopmentDepartment.Manager.EmployeeFirstName); // "Doe"
      Console.WriteLine(actual.DevelopmentDepartment.Manager.EmployeeFirstName); // "Foo"
      Console.WriteLine(Object.ReferenceEquals(expected, actual)); // false
      Console.WriteLine(Object.ReferenceEquals(expected.DevelopmentDepartment, actual.DevelopmentDepartment)); // false
      Console.WriteLine(Object.ReferenceEquals(expected.DevelopmentDepartment.Manager, actual.DevelopmentDepartment.Manager)); // false
      Console.WriteLine(Object.ReferenceEquals(expected.OrganizationName, actual.OrganizationName)); // true
      Console.WriteLine(Object.ReferenceEquals(expected.DevelopmentDepartment.DepartmentTitle, expected.DevelopmentDepartment.DepartmentTitle)); // true

      var actual1 = expected.With(x => x.DevelopmentDepartment.Manager.EmployeeFirstName, "Foo");
      Console.WriteLine(Object.ReferenceEquals(actual, actual1)); // false


      With expects any classes in the modified 'path' to have a constructor accepting all properties (with a case insensitive match on property name) and will use that constructor only when necessary to create new objects. There is a lot of caching going on to try an give maximum performance.



      The code is quite dense but I still hope to get some inputs on improving it or bug reports !



      namespace With {

      using ParameterResolver = ValueTuple<PropertyInfo, Func<object, object>>;

      internal class WithInternal {
      delegate object Activator(params object args);
      delegate object ResolveInstanceDelegate<T>(T source);

      ImmutableDictionary<string, (Activator activator, ParameterResolver parameterResolvers)> ActivationContextCache;
      ImmutableDictionary<string, Delegate> ResolveInstanceExpressionCache;

      WithInternal() {
      ActivationContextCache = ImmutableDictionary<string, (Activator Activator, ParameterResolver ParameterResolvers)>.Empty;
      ResolveInstanceExpressionCache = ImmutableDictionary<string, Delegate>.Empty;
      }
      public readonly static WithInternal Default = new WithInternal();

      // Constructs immutable object from any other object.
      public TInstance With<TInstance>(object source) {
      if (source is null) throw new ArgumentNullException(nameof(source));
      return (TInstance)ResolveActivator(source.GetType(), typeof(TInstance), null, source, null);
      }

      // Contructs immutable object from existing one with changed property specified by lambda expression.
      public TInstance With<TInstance, TValue>(TInstance source, Expression<Func<TInstance, TValue>> expression, object target) {
      if (source is null) throw new ArgumentNullException(nameof(source));
      if (expression is null) throw new ArgumentNullException(nameof(expression));

      var sourceParameterExpression = expression.Parameters.Single();
      var instanceExpression = expression.Body;

      while (instanceExpression != sourceParameterExpression) {
      if (!(instanceExpression is MemberExpression memberExpression) || !(memberExpression.Member is PropertyInfo property))
      throw new NotSupportedException($"Unable to process expression. Expression: '{instanceExpression}'.");

      instanceExpression = memberExpression.Expression;

      // create unique cache key, calc same key for x=>x.p and y=>y.p
      string key;
      try {
      var exprStr = instanceExpression.ToString();
      key = typeof(TInstance).FullName + '|' + exprStr.Remove(0, exprStr.IndexOf(Type.Delimiter) + 1);
      } catch (Exception ex) {
      throw new Exception($"Unable to parse expression '{instanceExpression}'.", ex);
      }

      ResolveInstanceDelegate<TInstance> compiledExpression;

      if (ResolveInstanceExpressionCache.TryGetValue(key, out var resolveInstanceDelegate)) {
      compiledExpression = (ResolveInstanceDelegate<TInstance>)resolveInstanceDelegate;
      } else {
      var instanceConvertExpression = Expression.Convert(instanceExpression, typeof(object));
      var lambdaExpression = Expression.Lambda<ResolveInstanceDelegate<TInstance>>(instanceConvertExpression, sourceParameterExpression);
      compiledExpression = lambdaExpression.Compile();
      ResolveInstanceExpressionCache = ResolveInstanceExpressionCache.SetItem(key, compiledExpression);
      }

      var type = property.DeclaringType;
      var instance = compiledExpression.Invoke(source);
      target = ResolveActivator(type, type, property, instance, target);
      }

      return (TInstance)target;
      }

      object ResolveActivator(Type sourceType, Type valueType, PropertyInfo property, object instance, object target) {
      var (activator, parameterResolvers) = GetActivator(sourceType, valueType);

      // resolve activator arguments
      var arguments = new object[parameterResolvers.Length];
      var match = false;

      for (var i = 0; i < parameterResolvers.Length; i++) {
      var (resolverProperty, resolver) = parameterResolvers[i];
      arguments[i] = resolverProperty == property ? target : resolver.Invoke(instance);
      if (resolverProperty == property) match = true;
      }

      if (!match) throw new Exception($"Unable to construct object of type '{property.DeclaringType.Name}'. There is no constructor parameter matching property '{property.Name}'.");
      return activator.Invoke(arguments);
      }

      (Activator activator, ParameterResolver parameterResolvers) GetActivator(Type sourceType, Type valueType) {
      var key = sourceType.FullName + '|' + valueType.FullName;
      if (ActivationContextCache.TryGetValue(key, out var res)) return res;

      foreach (var constructor in valueType.GetTypeInfo().DeclaredConstructors) {
      var parameters = constructor.GetParameters();

      // Get ParameterResolvers
      var parameterResolvers = new ParameterResolver[parameters.Length];
      {
      var properties = sourceType.GetTypeInfo().DeclaredProperties.ToArray();
      if (parameters.Length != properties.Length) continue;

      var i=0;
      foreach (var parameter in parameters) {
      var property = properties.Where(x => string.Equals(x.Name, parameter.Name, StringComparison.OrdinalIgnoreCase)).SingleOrDefault();
      if (property is null || property.PropertyType != parameter.ParameterType) break;

      var parameterExpression = Expression.Parameter(typeof(object));
      var parameterConvertExpression = Expression.Convert(parameterExpression, sourceType);
      var propertyExpression = Expression.Property(parameterConvertExpression, property);
      var propertyConvertExpression = Expression.Convert(propertyExpression, typeof(object));
      var lambdaExpression = Expression.Lambda<Func<object, object>>(propertyConvertExpression, parameterExpression);
      var compiledExpression = lambdaExpression.Compile();

      parameterResolvers[i++] = (property, compiledExpression);
      }
      if (i < parameters.Length) continue; // this ctor is no good
      }

      // get target activator
      Activator activator;
      {
      var parameterExpression = Expression.Parameter(typeof(object));
      var argumentExpressions = new Expression[parameters.Length];

      for (var i = 0; i < parameters.Length; i++) {
      var arrayExpression = Expression.ArrayIndex(parameterExpression, Expression.Constant(i));
      var arrayConvertExpression = Expression.Convert(arrayExpression, parameters[i].ParameterType);
      argumentExpressions[i] = arrayConvertExpression;
      }

      var constructorExpression = Expression.New(constructor, argumentExpressions);
      var constructorConvertExpression = Expression.Convert(constructorExpression, typeof(object));
      var activatorLambdaExpression = Expression.Lambda<Activator>(constructorConvertExpression, parameterExpression);
      activator = activatorLambdaExpression.Compile();
      }

      res = (activator, parameterResolvers);
      ActivationContextCache = ActivationContextCache.SetItem(key, res);
      return res;
      }

      throw new Exception($"Unable to find appropriate Constructor. Type '{sourceType.Name}'.");
      }
      }

      // -------------------- ExtensionMethods.cs

      public static class ExtensionMethods {
      /// <summary>
      /// Contructs immutable object from existing one with changed property specified by lambda expression.
      /// </summary>
      /// <typeparam name="TInstance">Immutable object type.</typeparam>
      /// <typeparam name="TValue">Value to set type.</typeparam>
      /// <param name="instance">Original immutable object.</param>
      /// <param name="expression">Navigation property specifying what to change.</param>
      /// <param name="value">Value to set in the resulting object.</param>
      /// <returns></returns>
      public static TInstance With<TInstance, TValue>(this TInstance instance, Expression<Func<TInstance, TValue>> expression, TValue value) =>
      WithInternal.Default.With(instance, expression, value);


      /// <summary>
      /// Constructs immutable object from any other object.
      /// Helpful cloning immutable object or converting POCO, DTO, anonymous type, dynamic ect.
      /// </summary>
      /// <typeparam name="TInstance">Immutable object type.</typeparam>
      /// <param name="source">Original object.</param>
      /// <returns>Configuration to use. Default if not specified.</returns>
      public static TInstance With<TInstance>(this object source) =>
      WithInternal.Default.With<TInstance>(source);
      }
      }









      share|improve this question











      $endgroup$




      (My code is basically a rewrite of https://github.com/ababik/Remute so much of the credit goes there)



      The idea is to use lambda expression to provide a general With method for immutable objects.



      With With you can do:



      using With;

      public class Employee {
      public string EmployeeFirstName { get; }
      public string EmployeeLastName { get; }

      public Employee(string employeeFirstName, string employeeLastName) {
      EmployeeFirstName = employeeFirstName;
      EmployeeLastName = employeeLastName;
      }
      }

      public class Department {
      public string DepartmentTitle { get; }
      public Employee Manager { get; }

      public Department() { /* .. */ }
      public Department(int manager, string title) { /* .. */ }

      public Department(string departmentTitle, Employee manager) {
      DepartmentTitle = departmentTitle;
      Manager = manager;
      }
      }

      public class Organization {
      public string OrganizationName { get; }
      public Department DevelopmentDepartment { get; }

      public Organization(string organizationName, Department developmentDepartment) {
      OrganizationName = organizationName;
      DevelopmentDepartment = developmentDepartment;
      }
      }

      var expected = new Organization("Organization", new Department("Development Department", new Employee("John", "Doe")));
      var actual = expected.With(x => x.DevelopmentDepartment.Manager.EmployeeFirstName, "Foo");

      Console.WriteLine(expected.DevelopmentDepartment.Manager.EmployeeFirstName); // "Doe"
      Console.WriteLine(actual.DevelopmentDepartment.Manager.EmployeeFirstName); // "Foo"
      Console.WriteLine(Object.ReferenceEquals(expected, actual)); // false
      Console.WriteLine(Object.ReferenceEquals(expected.DevelopmentDepartment, actual.DevelopmentDepartment)); // false
      Console.WriteLine(Object.ReferenceEquals(expected.DevelopmentDepartment.Manager, actual.DevelopmentDepartment.Manager)); // false
      Console.WriteLine(Object.ReferenceEquals(expected.OrganizationName, actual.OrganizationName)); // true
      Console.WriteLine(Object.ReferenceEquals(expected.DevelopmentDepartment.DepartmentTitle, expected.DevelopmentDepartment.DepartmentTitle)); // true

      var actual1 = expected.With(x => x.DevelopmentDepartment.Manager.EmployeeFirstName, "Foo");
      Console.WriteLine(Object.ReferenceEquals(actual, actual1)); // false


      With expects any classes in the modified 'path' to have a constructor accepting all properties (with a case insensitive match on property name) and will use that constructor only when necessary to create new objects. There is a lot of caching going on to try an give maximum performance.



      The code is quite dense but I still hope to get some inputs on improving it or bug reports !



      namespace With {

      using ParameterResolver = ValueTuple<PropertyInfo, Func<object, object>>;

      internal class WithInternal {
      delegate object Activator(params object args);
      delegate object ResolveInstanceDelegate<T>(T source);

      ImmutableDictionary<string, (Activator activator, ParameterResolver parameterResolvers)> ActivationContextCache;
      ImmutableDictionary<string, Delegate> ResolveInstanceExpressionCache;

      WithInternal() {
      ActivationContextCache = ImmutableDictionary<string, (Activator Activator, ParameterResolver ParameterResolvers)>.Empty;
      ResolveInstanceExpressionCache = ImmutableDictionary<string, Delegate>.Empty;
      }
      public readonly static WithInternal Default = new WithInternal();

      // Constructs immutable object from any other object.
      public TInstance With<TInstance>(object source) {
      if (source is null) throw new ArgumentNullException(nameof(source));
      return (TInstance)ResolveActivator(source.GetType(), typeof(TInstance), null, source, null);
      }

      // Contructs immutable object from existing one with changed property specified by lambda expression.
      public TInstance With<TInstance, TValue>(TInstance source, Expression<Func<TInstance, TValue>> expression, object target) {
      if (source is null) throw new ArgumentNullException(nameof(source));
      if (expression is null) throw new ArgumentNullException(nameof(expression));

      var sourceParameterExpression = expression.Parameters.Single();
      var instanceExpression = expression.Body;

      while (instanceExpression != sourceParameterExpression) {
      if (!(instanceExpression is MemberExpression memberExpression) || !(memberExpression.Member is PropertyInfo property))
      throw new NotSupportedException($"Unable to process expression. Expression: '{instanceExpression}'.");

      instanceExpression = memberExpression.Expression;

      // create unique cache key, calc same key for x=>x.p and y=>y.p
      string key;
      try {
      var exprStr = instanceExpression.ToString();
      key = typeof(TInstance).FullName + '|' + exprStr.Remove(0, exprStr.IndexOf(Type.Delimiter) + 1);
      } catch (Exception ex) {
      throw new Exception($"Unable to parse expression '{instanceExpression}'.", ex);
      }

      ResolveInstanceDelegate<TInstance> compiledExpression;

      if (ResolveInstanceExpressionCache.TryGetValue(key, out var resolveInstanceDelegate)) {
      compiledExpression = (ResolveInstanceDelegate<TInstance>)resolveInstanceDelegate;
      } else {
      var instanceConvertExpression = Expression.Convert(instanceExpression, typeof(object));
      var lambdaExpression = Expression.Lambda<ResolveInstanceDelegate<TInstance>>(instanceConvertExpression, sourceParameterExpression);
      compiledExpression = lambdaExpression.Compile();
      ResolveInstanceExpressionCache = ResolveInstanceExpressionCache.SetItem(key, compiledExpression);
      }

      var type = property.DeclaringType;
      var instance = compiledExpression.Invoke(source);
      target = ResolveActivator(type, type, property, instance, target);
      }

      return (TInstance)target;
      }

      object ResolveActivator(Type sourceType, Type valueType, PropertyInfo property, object instance, object target) {
      var (activator, parameterResolvers) = GetActivator(sourceType, valueType);

      // resolve activator arguments
      var arguments = new object[parameterResolvers.Length];
      var match = false;

      for (var i = 0; i < parameterResolvers.Length; i++) {
      var (resolverProperty, resolver) = parameterResolvers[i];
      arguments[i] = resolverProperty == property ? target : resolver.Invoke(instance);
      if (resolverProperty == property) match = true;
      }

      if (!match) throw new Exception($"Unable to construct object of type '{property.DeclaringType.Name}'. There is no constructor parameter matching property '{property.Name}'.");
      return activator.Invoke(arguments);
      }

      (Activator activator, ParameterResolver parameterResolvers) GetActivator(Type sourceType, Type valueType) {
      var key = sourceType.FullName + '|' + valueType.FullName;
      if (ActivationContextCache.TryGetValue(key, out var res)) return res;

      foreach (var constructor in valueType.GetTypeInfo().DeclaredConstructors) {
      var parameters = constructor.GetParameters();

      // Get ParameterResolvers
      var parameterResolvers = new ParameterResolver[parameters.Length];
      {
      var properties = sourceType.GetTypeInfo().DeclaredProperties.ToArray();
      if (parameters.Length != properties.Length) continue;

      var i=0;
      foreach (var parameter in parameters) {
      var property = properties.Where(x => string.Equals(x.Name, parameter.Name, StringComparison.OrdinalIgnoreCase)).SingleOrDefault();
      if (property is null || property.PropertyType != parameter.ParameterType) break;

      var parameterExpression = Expression.Parameter(typeof(object));
      var parameterConvertExpression = Expression.Convert(parameterExpression, sourceType);
      var propertyExpression = Expression.Property(parameterConvertExpression, property);
      var propertyConvertExpression = Expression.Convert(propertyExpression, typeof(object));
      var lambdaExpression = Expression.Lambda<Func<object, object>>(propertyConvertExpression, parameterExpression);
      var compiledExpression = lambdaExpression.Compile();

      parameterResolvers[i++] = (property, compiledExpression);
      }
      if (i < parameters.Length) continue; // this ctor is no good
      }

      // get target activator
      Activator activator;
      {
      var parameterExpression = Expression.Parameter(typeof(object));
      var argumentExpressions = new Expression[parameters.Length];

      for (var i = 0; i < parameters.Length; i++) {
      var arrayExpression = Expression.ArrayIndex(parameterExpression, Expression.Constant(i));
      var arrayConvertExpression = Expression.Convert(arrayExpression, parameters[i].ParameterType);
      argumentExpressions[i] = arrayConvertExpression;
      }

      var constructorExpression = Expression.New(constructor, argumentExpressions);
      var constructorConvertExpression = Expression.Convert(constructorExpression, typeof(object));
      var activatorLambdaExpression = Expression.Lambda<Activator>(constructorConvertExpression, parameterExpression);
      activator = activatorLambdaExpression.Compile();
      }

      res = (activator, parameterResolvers);
      ActivationContextCache = ActivationContextCache.SetItem(key, res);
      return res;
      }

      throw new Exception($"Unable to find appropriate Constructor. Type '{sourceType.Name}'.");
      }
      }

      // -------------------- ExtensionMethods.cs

      public static class ExtensionMethods {
      /// <summary>
      /// Contructs immutable object from existing one with changed property specified by lambda expression.
      /// </summary>
      /// <typeparam name="TInstance">Immutable object type.</typeparam>
      /// <typeparam name="TValue">Value to set type.</typeparam>
      /// <param name="instance">Original immutable object.</param>
      /// <param name="expression">Navigation property specifying what to change.</param>
      /// <param name="value">Value to set in the resulting object.</param>
      /// <returns></returns>
      public static TInstance With<TInstance, TValue>(this TInstance instance, Expression<Func<TInstance, TValue>> expression, TValue value) =>
      WithInternal.Default.With(instance, expression, value);


      /// <summary>
      /// Constructs immutable object from any other object.
      /// Helpful cloning immutable object or converting POCO, DTO, anonymous type, dynamic ect.
      /// </summary>
      /// <typeparam name="TInstance">Immutable object type.</typeparam>
      /// <param name="source">Original object.</param>
      /// <returns>Configuration to use. Default if not specified.</returns>
      public static TInstance With<TInstance>(this object source) =>
      WithInternal.Default.With<TInstance>(source);
      }
      }






      c# extension-methods immutability expression-trees






      share|improve this question















      share|improve this question













      share|improve this question




      share|improve this question








      edited 32 mins ago









      t3chb0t

      34.5k748118




      34.5k748118










      asked 7 hours ago









      kofifuskofifus

      1013




      1013






















          0






          active

          oldest

          votes











          Your Answer





          StackExchange.ifUsing("editor", function () {
          return StackExchange.using("mathjaxEditing", function () {
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          });
          });
          }, "mathjax-editing");

          StackExchange.ifUsing("editor", function () {
          StackExchange.using("externalEditor", function () {
          StackExchange.using("snippets", function () {
          StackExchange.snippets.init();
          });
          });
          }, "code-snippets");

          StackExchange.ready(function() {
          var channelOptions = {
          tags: "".split(" "),
          id: "196"
          };
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function() {
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled) {
          StackExchange.using("snippets", function() {
          createEditor();
          });
          }
          else {
          createEditor();
          }
          });

          function createEditor() {
          StackExchange.prepareEditor({
          heartbeatType: 'answer',
          autoActivateHeartbeat: false,
          convertImagesToLinks: false,
          noModals: true,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          imageUploader: {
          brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
          contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
          allowUrls: true
          },
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          });


          }
          });














          draft saved

          draft discarded


















          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f213481%2fextension-with-for-immutable-types%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown

























          0






          active

          oldest

          votes








          0






          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes
















          draft saved

          draft discarded




















































          Thanks for contributing an answer to Code Review Stack Exchange!


          • Please be sure to answer the question. Provide details and share your research!

          But avoid



          • Asking for help, clarification, or responding to other answers.

          • Making statements based on opinion; back them up with references or personal experience.


          Use MathJax to format equations. MathJax reference.


          To learn more, see our tips on writing great answers.




          draft saved


          draft discarded














          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f213481%2fextension-with-for-immutable-types%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown





















































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown

































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown







          Popular posts from this blog

          How to make a Squid Proxy server?

          Is this a new Fibonacci Identity?

          Touch on Surface Book