I wanted to update some properties of my UriString but since this type is immutable, it wouldn't work. Actually, I just wanted to update the Path
property but I don't want to have a constructor with several parameters like in the other question because this is too much work and sometime not desireable. Instead, I require the immutable type to have an immutable-update-construtor that takes the ImmutableUpdate
object that I use for this purpose.
To the user it looks like a dummy class without any useful properties:
public sealed class ImmutableUpdate : IEnumerable<(PropertyInfo Property, object Value)>
{
private readonly IEnumerable<(PropertyInfo Property, object Value)> _updates;
internal ImmutableUpdate(IEnumerable<(PropertyInfo Property, object Value)> updates)
{
_updates = updates;
}
public IEnumerator<(PropertyInfo Property, object Value)> GetEnumerator() => _updates.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _updates.GetEnumerator();
}
and it should, because it's only a carrier type for updates and an alias for the lengthy enumeration signature. These updates are created and used by the ImmutableUpdater
that binds values to properties via their backing-fields and allows the Bind
method to be called only from within the constructor of the object being updated. This should be a simple protection against mutating random instances. ImmutableUpdate
has also an internal
constructor and is sealed
which also prevents using it in a wrong way. (ImmutableUpdater
currently looks for properties that have no setter but extending it to understand some special attributes to customize this procees should be possible.)
public static class ImmutableUpdater
{
public static T With<T, TMember>(this T obj, Expression<Func<T, TMember>> memberSelector, TMember newValue)
{
if (!(memberSelector.Body is MemberExpression memberExpression))
{
throw new ArgumentException($"You must select a member. Affected expression '{memberSelector}'.");
}
if (!(memberExpression.Member is PropertyInfo selectedProperty))
{
throw new ArgumentException($"You must select a property. Affected expression '{memberSelector}'.");
}
if (selectedProperty.GetSetMethod() != null)
{
throw new ArgumentException($"You must select a readonly property. Affected expression '{memberSelector}'.");
}
if (GetBackingField<T>(selectedProperty.Name) == null)
{
throw new ArgumentException($"You must select a pure readonly property (not a computed one). Affected expression '{memberSelector}'.");
}
var immmutableUpdateCtor =
typeof(T)
.GetConstructor(new[] { typeof(ImmutableUpdate) });
var updates =
from property in obj.ImmutableProperties()
let getsUpdated = property.Name == selectedProperty.Name
select
(
property,
getsUpdated ? newValue : property.GetValue(obj)
);
return (T)Activator.CreateInstance(typeof(T), new ImmutableUpdate(updates));
}
public static void Bind<T>(this ImmutableUpdate update, T obj)
{
// todo - this could be cached
var isCalledByImmutableUpdateCtor = new StackFrame(1).GetMethod() == ImmutableUpdateConstructor(typeof(T));
if (!isCalledByImmutableUpdateCtor)
{
throw new InvalidOperationException($"You can call '{nameof(Bind)}' only from within an ImmutableUpdate constructor.");
}
foreach (var (property, value) in update)
{
GetBackingField<T>(property.Name)?.SetValue(obj, value);
}
}
private static FieldInfo GetBackingField<T>(string propertyName)
{
var backingFieldBindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;
var backingFieldName = $"<{propertyName}>k__BackingField";
return typeof(T).GetField(backingFieldName, backingFieldBindingFlags);
}
private static IEnumerable<PropertyInfo> ImmutableProperties<T>(this T obj)
{
return
typeof(T)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.GetSetMethod() is null);
}
private static ConstructorInfo ImmutableUpdateConstructor(Type type)
{
return type.GetConstructor(new[] { typeof(ImmutableUpdate) });
}
}
Example
Its usage is pretty simple, just use With
to set a new value.
var user = new User();
var updatedUser = user
.With(x => x.FirstName, "John")
.With(x => x.LastName, "Doe")
//.With(x => x.FullName, "Doe") // Boom!
.Dump();
user.Dump();
This is the type using Bind
inside its special constructor:
class User
{
public User() { }
public User(ImmutableUpdate update)
{
update.Bind(this);
}
public string FirstName { get; }
public string LastName { get; }
}
Is this solution any better than others, or worse? What do you say? I'm not really concerned about performance as this won't be used for any crazy scenarios (yet).
With
s that return a new object each time seem a little wasteful if the next one is discarding the previous. Perhaps it could return an "interim" container object that can accumulate theWith
expressions and a finalApply()
that executes them once and produces a singleUser
(in this case) object? \$\endgroup\$