1
\$\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);

    // key: "{sourceType.FullName}|{valueType.FullName}"  
    //  ie: "test.Employee|test.Employee"

    // activator: args => new valueType(args[0] as valueCtorParams[0].ParamType, ...) as object
    //        ie: args => new Employee(args[0] as String, args[1] as String) as Object 

    // parameterResolvers: [ ValueType(PropertyInfo of sourceTypeProp1, x => (x as sourceType).sourceTypeProp1) as Object), ... ]
    //                 ie: [ ( PropertyInfo of EmployeeFirstName , x => ((x as Employee).EmployeeFirstName) as Object) ),
    //                       ( PropertyInfo of EmployeeLastName , x => ((x as Employee).EmployeeLastName) as Object) ) ]
    ImmutableDictionary<string, (Activator activator, ParameterResolver[] parameterResolvers)> ActivationContextCache;

    // key: "{sourceType.FullName}|{WithProperty}"  
    //  ie: "test.Organization|DevelopmentDepartment.Manager"

    // value: x => WithProperty as Object  
    //    ie: x => x.DevelopmentDepartment.Manager as Object
    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}'.");
    }
  }



  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);
  }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.