Extension “With” for immutable types
$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);
}
}
c# extension-methods immutability expression-trees
$endgroup$
add a comment |
$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);
}
}
c# extension-methods immutability expression-trees
$endgroup$
add a comment |
$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);
}
}
c# extension-methods immutability expression-trees
$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
c# extension-methods immutability expression-trees
edited 32 mins ago
t3chb0t
34.5k748118
34.5k748118
asked 7 hours ago
kofifuskofifus
1013
1013
add a comment |
add a comment |
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
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
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.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
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