Beachwalker
Beachwalker

Reputation: 7915

Change property of immutable type

I have stored immutable types in a temporary CQRS read store (query/read side, in fact implemented by a simple List with abstraction access layer, I don't want to use a full blown document database at this point). These read stores contains items like the following:

public class SomeItem
{
    private readonly string name;
    private readonly string description;

    public SomeItem(string name, string description)
    {
        this.name = name;
        this.description = description;
    }

    public string Name
    {
        get { return this.name; }
    }

    public string Description
    {
        get { return this.description; }
    }
}

Now I want to change the Name and in a 2nd Command the Description. These changes should keep the current state, which means for the example above:

// initial state
var someItem = new SomeItem("name", "description");

// update name -> newName
someItem = new SomeItem("newName", someItem.Description);

// update description -> newDescription
someItem = new SomeItem(someItem.Name, "newDescription");

This does look error prone to me if you have several properties... you have to manage keeping the current state. I could add something like Clone() to every type but I think/hope there is something better out there that performs well and is easy to use, I don't want to write much repetive code (lazy programmer). Any suggestions how to improve the code above? The SomeItem class needs to stay immutable (transported through several different threads).

Upvotes: 4

Views: 3499

Answers (5)

Simple solution

I also thought about this question. Record's are not suitable for my purposes, since it is necessary to interact with EF Core.

I suggest a simple and low-cost way:

  • add a copy constructor to the class;
  • Make properties that change during cloning available for initialization;
  • clone an object with a change through the copy constructor with an initialization list:
var a = new SomeItem("name", "abracadabra");
var b = new SomeItem(a) {Description="descr"};

Simple code

var a = new SomeItem("name", "abracadabra");
var b = new SomeItem(a) {Description="descr"};

public class SomeItem
{
    private string name;
    private string description;

    public SomeItem(string name, string description)
    {
        Name = name;
        Description = description;
    }

    public SomeItem(SomeItem another): this(another.Name, another.Description)
    {
    }

    public string Name
    {
        get => name;
        init => name = value;
    }

    public string Description
    {
        get => description;
        init => description = value;
    }
}

Extended solution

If the final type is not known at compile time, then this approach is easy to extend. Let's say there is a class "ValueObject" whose derived types we need to clone.

Note: I apologize for the incorrect translation in some places. English version obtained using google.translate

Additional code

using System.Linq.Expressions;
using Light.GuardClauses;
using JetBrains.Annotations;
using static DotNext.Linq.Expressions.ExpressionBuilder;

using ValueObject = Company.Domain....;


/// <summary>
/// The plagiarizer creates a copy of the object with a change in its individual properties using an initializer
/// </summary>
/// <remarks> The foreign object must define a copy constructor, and mutable members must support initialization </remarks>
public struct Plagiarist {
    /// <summary>
    /// Object to be copied
    /// </summary>
    private readonly object _alienObject;

    /// <summary>
    /// Type <see cref="_alienObject" />
    /// </summary>
    private Type _type => _alienObject.GetType();

    /// <summary>
    /// Object parsing Expression
    /// </summary>
    private readonly ParsingInitializationExpression _parser = new();

    public Plagiarist(object alienObject) {
        _alienObject = alienObject.MustNotBeNullReference();
        if (!CopyConstructorIs())
            throw new ArgumentException($"Type {_type.FullName} must implement a copy constructor");
    }

    /// <summary>
    /// Does the object we want to plagiarize have a copy constructor?
    /// </summary>
    /// <returns>True - there is a copy constructor, otherwise - false</returns>
    [Pure]
    private bool CopyConstructorIs() {
        return _type.GetConstructor(new[] { _type }) is not null;
    }

    /// <summary>
    /// Returns a copy of a foreign object with a change in its individual properties using an initializer
    /// </summary>
    /// <param name="initializer">
    /// <see cref="Expression" /> create an object with initialization of those fields,
    /// which need to be changed:
    /// <code>() => new T() {Member1 = "Changed value1", Member2 = "Changed value2"}</code>
    /// or <see cref="Expression" /> create an anonymous type with initialization of those fields
    /// that need to be changed:
    /// <code>() => new {Member1 = "Changed value1", Member2 = "Changed value2"}</code>
    /// </param>
    /// <returns></returns>
    [Pure]
    public object Plagiarize(Expression<Func<object>> initializer) {
        var (newValues, constructParam) = _parser.ParseInitialization(initializer);
        var constrCopies = _type.New(_alienObject.Const().Convert(_type));

        Expression plagiarist = (newValues.Count, constructParam.Count) switch {
            (> 0, _) => Expression.MemberInit(constrCopies, newValues.Values),
            (0, > 0) => Expression.MemberInit(constrCopies, ConstructorInInitializationList(constructParam).Values),
            _ => constrCopies
        };

        var plagiarize = Expression.Lambda<Func<object>>(plagiarist).Compile();

        return plagiarize();
    }

    [Pure]
    public Dictionary<string, MemberAssignment> ConstructorInInitializationList(
        Dictionary<string, Expression> constructorParameters) {
        Dictionary<string, MemberAssignment> initializer = new();
        const BindingFlags flagReflections = BindingFlags.Default | BindingFlags.Instance | BindingFlags.Public;
        var allProperties = _type.GetProperties(flagReflections);
        var allFields = _type.GetFields(flagReflections);

        foreach (var memberName in constructorParameters.Keys) {
            var property = allProperties.FirstOrDefault(s => s.Name ==memberName);
            var field = allFields.FirstOrDefault(s => s.Name == memberName);
            (MemberInfo member, Type memberType) = (property, field) switch {
                ({ }, _) => (property, property.PropertyType),
                (null, { }) => ((MemberInfo)field, field.FieldType),
                _ => throw new ArgumentException($"{_type.FullName} does not contain member {memberName}")
            };

            initializer[memberName] = Expression.Bind(member, constructorParameters[memberName].Convert(memberType));
        }

        return initializer;
    }
    
    /// <summary>
    /// Template "Visitor" for traversing the expression tree in order to highlight
    /// initialization expression and constructor
    /// </summary>
    private class ParsingInitializationExpression : ExpressionVisitor {
        private Dictionary<string, MemberAssignment>? _initializer;
        private Dictionary<string, Expression>? _initializerAnonym;

        /// <summary>
        /// Parses the expression tree and returns the initializer and constructor parameters
        /// </summary>
        /// <param name="initializer"><see cref="Expression" /> to parse</param>
        /// <returns> tuple of initializer and constructor</returns>
        public ParsedInitialization ParseInitialization(Expression initializer) {
            _initializer = new Dictionary<string, MemberAssignment>();
            _initializerAnonym = new Dictionary<string, Expression>();
            Visit(initializer);
            return new ParsedInitialization(_initializer, _initializerAnonym);
        }

        protected override MemberAssignment VisitMemberAssignment(MemberAssignment node) {
            _initializer![node.Member.Name] = node;
            return base.VisitMemberAssignment(node);
        }

        protected override Expression VisitNew(NewExpression node) {
            foreach (var (member, value) in node.Members?.Zip(node.Arguments) ??
                                             Array.Empty<(MemberInfo First, Expression Second)>())
                _initializerAnonym![member.Name] = value;

            return base.VisitNew(node);
        }

        /// <summary>
        /// Type to return values from method <see cref="ParseInitialization" />
        /// </summary>
        /// <param name="Initializer"></param>
        /// <param name="ConstructorParameters"></param>
        public record struct ParsedInitialization(Dictionary<string, MemberAssignment> Initializer,
            Dictionary<string, Expression> ConstructorParameters);
    }
}

public static class ValueObjectPlagiarizer{
    /// <summary>
    /// Creates a copy of the object with a change in its individual properties using an initializer
    /// </summary>
    /// <param name="alien">Object to be plagiarized</param>
    /// <param name="initializer">
    /// <see cref="Expression" /> creating an object of type <typeparamref name="T" />
    /// with initialization of those fields that need to be changed:
    /// <code>ob.Plagiarize(() => new T() {Member1 = "Changed value1", Member2 = "Changed value2"})</code>
    /// or <see cref="Expression" /> create an anonymous type with initialization of those fields that need to be changed:
    /// <code>ob.Plagiarize(() => new {Member1 = "Changed value1", Member2 = "Changed value2"})</code>
    /// </param>
    /// <returns>plagiarism of the object</returns>
    public static object Plagiarize<T>(this ValueObject alien, Expression<Func<T>> initializer)
        where T : class {
        var bodyReduced = initializer.Convert<object>();
        var initializerReduced = Expression.Lambda<Func<object>>(bodyReduced, initializer.Parameters);

        return new Plagiarist(alien).Plagiarize(initializerReduced);
    }
} 

Usages

If SomeItem is a descendant of ValueObject

ValueObject a = new SomeItem("name", "abracadabra");

// via type constructor
var b = (SomeItem)a.Plagiarize(()=>new SomeItem(null){Description="descr"});
// anonymous type 
var c = (SomeItem)a.Plagiarize(()=>new{Description="descr"});

b.Description.Should().Be("descr"); //true
c.Description.Should().Be("descr"); //true

Upvotes: 0

Esset
Esset

Reputation: 1056

With C#9 we got the with operator for this purpose.

   public record Car
    {
        public string Brand { get; init; }   
        public string Color { get; init; }    
    }
    var car = new Car{ Brand = "BMW", Color = "Red" }; 
    var anotherCar = car with { Brand = "Tesla"};

With-expressions When working with immutable data, a common pattern is to create new values from existing ones to represent a new state. For instance, if our person were to change their last name we would represent it as a new object that’s a copy of the old one, except with a different last name. This technique is often referred to as non-destructive mutation. Instead of representing the person over time, the record represents the person’s state at a given time. To help with this style of programming, records allow for a new kind of expression; the with-expression:

News in C#9

NOTE With operator is only supported by records.

Records At the core of classic object-oriented programming is the idea that an object has strong identity and encapsulates mutable state that evolves over time. C# has always worked great for that, But sometimes you want pretty much the exact opposite, and here C#’s defaults have tended to get in the way, making things very laborious.

Recods in C#9

Upvotes: 6

Heinzi
Heinzi

Reputation: 172280

What you are looking for is commonly called the with operator:

// returns a new immutable object with just the single property changed
someItem = { someItem with Name = "newName" };

Unfortunately, unlike F#, C# does not have such an operator (yet?).

Other C# developers are missing this feature as well, which is why someone wrote a Fody extension to do exactly that:

Here's another approach, which implements an UpdateWith method manually but requires an Option<T> helper class. Luaan's answer describes this approach in more detail:

Upvotes: 3

Luaan
Luaan

Reputation: 63742

Sadly, there's no simple way in C#. F# has the with keyword, and you could have a look at lenses, but it's all somewhat tedious in C#. The best I can give you is something like this:

class SomeItem
{
  private readonly string name;
  private readonly string description;

  public SomeItem(string name, string description)
  {
    this.name = name;
    this.description = description;
  }

  public SomeItem With
    (
      Option<string> name = null,
      Option<string> description = null
    )
  {
    return new SomeItem
      (
        name.GetValueOrDefault(this.name), 
        description.GetValueOrDefault(this.description)
      );
  }
}

This allows you to do the updates like

var newItem = oldItem.With(name: "My name!");

I've used this approach with extension methods and T4s to great effect, but even when you write the code manually, it's reasonably reliable - if you add a new field, you must add it to the With as well, so it works quite well.

There's a few more approaches if you are willing to tolerate runtime code generation and reducing type safety, but that's kind of going against the grain IMO.

Upvotes: 6

user3292642
user3292642

Reputation: 761

If what you want to do is (as you commented) update the name of an existing object, a readonly property might be bad design. Otherwise if its really a new object you want to create, you might want your class to implement some interface with a 'Dispose' method.

Upvotes: -2

Related Questions