3
\$\begingroup\$

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).

\$\endgroup\$
  • 1
    \$\begingroup\$ I was literally just contemplating writing one of these... I was wondering how best to specify properties, and had completely forgotten about Expressions, so thanks ;) \$\endgroup\$ – VisualMelon Dec 20 '18 at 15:34
  • 1
    \$\begingroup\$ At first blush, the chained Withs 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 the With expressions and a final Apply() that executes them once and produces a single User (in this case) object? \$\endgroup\$ – Jesse C. Slicer Dec 20 '18 at 17:16
2
\$\begingroup\$

A couple of small things here. My bigger thought is in the comment on the original question.

One:

Empty collections are nice if nulls show their ugly faces:

internal ImmutableUpdate(IEnumerable<(PropertyInfo Property, object Value)> updates)
{
    _updates = updates ?? Enumerable.Empty<(PropertyInfo Property, object Value)>();
}

Two:

    var immmutableUpdateCtor =
        typeof(T)
            .GetConstructor(new[] { typeof(ImmutableUpdate) });

is never used. I'd rewrite that little block as:

var immmutableUpdateCtor = ImmutableUpdateConstructor(typeof(T));

if (immutableUpdateCtor == null)
{
    return obj;
}

var updates =
    from property in obj.ImmutableProperties()
    let getsUpdated = property.Name == selectedProperty.Name
    select
    (
        property,
        getsUpdated ? newValue : property.GetValue(obj)
    );

return (T)immutableUpdateCtor.Invoke(new object[] { new ImmutableUpdate(updates) });

I'll addend more if I think of anything.

Three:

The caching of constructor information as was commented in the code:

    private static readonly ConcurrentDictionary<Type, ConstructorInfo> _ImmutableConstructors =
        new ConcurrentDictionary<Type, ConstructorInfo>();

    private static ConstructorInfo ImmutableUpdateConstructor(Type type)
    {
        if (!_ImmutableConstructors.TryGetValue(type, out var constructor))
        {
            constructor = type.GetConstructor(new[] { typeof(ImmutableUpdate) });
            _ImmutableConstructors.TryAdd(type, constructor);
        }

        return constructor;
    }

Four:

Here are the builder pieces:

In ImmutableUpdater class:

public static UpdateBuilder<T> With<T, TMember>(this T obj, Expression<Func<T, TMember>> memberSelector, TMember newValue)
{
    ConstructorInfo immutableUpdateCtor = ImmutableUpdateConstructor(typeof(T));

    return new UpdateBuilder<T>(obj, immutableUpdateCtor).With(memberSelector, newValue);
}

public static UpdateBuilder<T> With<T, TMember>(this UpdateBuilder<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 (selectedProperty.Name.GetBackingField<T>() == null)
    {
        throw new ArgumentException(
            $"You must select a pure readonly property (not a computed one). Affected expression '{memberSelector}'.");
    }

    var updates =
        from property in obj.ImmutableProperties()
        where property.Name == selectedProperty.Name
        select
            (
                property, (object)newValue
            );

    return obj.Add(updates);
}

private static IEnumerable<PropertyInfo> ImmutableProperties<T>(this UpdateBuilder<T> obj)
{
    return
        typeof(T)
            .GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .Where(propertyInfo => propertyInfo.GetSetMethod() is null);
}

and finally, the UpdateBuilder<T> class:

using System.Linq;
using System.Reflection;

using IEnumerablePropertyValue = System.Collections.Generic.IEnumerable<(System.Reflection.PropertyInfo Property, object Value)>;
using PropertyValueList = System.Collections.Generic.List<(System.Reflection.PropertyInfo Property, object Value)>;

public sealed class UpdateBuilder<T>
{
    private readonly PropertyValueList _updates = new PropertyValueList();

    private readonly ConstructorInfo _immutableUpdateCtor;

    public UpdateBuilder(T obj, ConstructorInfo immutableUpdateCtor)
    {
        this.Object = obj;
        this._immutableUpdateCtor = immutableUpdateCtor;
    }

    public T Object { get; }

    public UpdateBuilder<T> Add(IEnumerablePropertyValue updates)
    {
        foreach (var update in updates ?? Enumerable.Empty<(PropertyInfo Property, object Value)>())
        {
            this._updates.Add(update);
        }

        return this;
    }

    public static implicit operator T(UpdateBuilder<T> updateBuilder)
    {
        if (updateBuilder == null)
        {
            return default(T);
        }

        if (updateBuilder._immutableUpdateCtor == null)
        {
            return updateBuilder.Object;
        }

        return (T)updateBuilder._immutableUpdateCtor.Invoke(new object[] { new ImmutableUpdate(updateBuilder._updates) });
    }
\$\endgroup\$
  • 1
    \$\begingroup\$ Ooops, it stayed there after moving it do the helper ;-] \$\endgroup\$ – t3chb0t Dec 20 '18 at 17:35
  • 1
    \$\begingroup\$ You must like it ;-) I see you've added a builder. I'll definitely borrow it. \$\endgroup\$ – t3chb0t Dec 20 '18 at 20:21

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.