In software development, an immutable object is one that once created, can't change. Why would you want such an object? To answer that question, it may help to analyze the problems that result from mutating objects. Go back to the basics of what every application does: create, retrieve, update, and delete data (the CRUD operations). The core of any application manipulates objects. Whether or not an application works in a manner consistent with its specification is first answered by whether the data manipulation is correct. Any time code affects an object, you need to verify that the code works correctly.

What if objects existed that couldn't change? Based on the foregoing, it stands to reason that if an object can't change, code either won't exist that attempts to change the object or, if such code exists, it won't work.

If your application takes advantage of multi-threading, immutability should be a fundamental part of your architecture because immutable objects are inherently thread-safe and are immune from race-conditions. If your application uses data transport objects (DTOs), immutability is something you should consider. The question on the table is how you can efficiently enforce and work with immutable objects, given that C# doesn't offer native support for such objects. This article proposes an answer to that question.

The complete source code can be found on the following GitHub Repository: https://github.com/johnvpetersen/ImmutableClass.

The Case for Immutability

Consider an object, any object. It can be a customer, a product, or anything else that comes to mind. Consider an object's lifecycle. It's created, it lives for some period, and at some point, it ceases to exist. It's the middle of the lifecycle you care about, when it lives for some time period. What if, during some or all its life span, the object should never change. Given that C# doesn't natively support absolute immutability, how would you enforce the rule that an object, once created, shall never change.

If changes to an immutable object were attempted, one of two scenarios could exist. One, the compiler would catch such attempts and the application couldn't build. This is the best of tests because it presents the sharpest of sharp edges that prevent deployment. Two, assuming the complier couldn't detect all such attempts, runtime exceptions are thrown. Although less efficient than compiler detection, exceptions present another type of sharp edge that presents an affirmative data point that an error has occurred.

Too often in software today, silent failures occur. In other words, when an invalid operation happens, it's not properly handled in a try catch block, or an affirmative exception isn't thrown. For correct operation, software must follow rules. In this context, the rule is that objects shall not change, and there needs to be an efficient way to both enforce the rule and to provide feedback when there's an attempt to violate the rule. Exceptions are the sharp edges that provide that feedback.

Exceptions are the sharp edges that provide feedback.

If an object can change, it's an open-ended problem, as there are infinite ways an object can change. For the object that can change but isn't supposed to change, the application necessarily becomes more complex because safeguards must be embedded to ensure that the object, once created, doesn't change. In C#, there are some limited ways that you can achieve immutability.

In this article, I examine what C# gives you for free, the limitations of sticking to what C# gives you for free, and an approach that strikes a balance between the utility of immutability and the custom code required to address the limitations of what C# provides out of the box.

The Use Case for Immutability: DTOs

A DTO (Data Transport Object) is a classic case where immutability is a desired feature. DTOs have one purpose. That purpose is, as the name suggests, to transport data. Imagine that you have different application boundaries where data must traverse those boundaries. Often, data needs to be serialized, sent to a service in the receiving boundary, and then de-serialized whereby an object is hydrated (presumably, such an object would be immutable as well). In such a use case, you wish to guarantee that once the object has been created, under no circumstance will any characteristic of that object change during its lifetime. At the very least, you wish to make sure that once a property has been set and the object has been locked, no further changes are possible.

The Challenges with Implementing Immutability in C#

There are several challenges faced with implementing immutability in C#. To illustrate the challenges, Listing 1 shows the most direct and simplest way to achieve immutability. The code works, but there are two main problems. First, you must initialize your property values in the constructor. Second, because private setters are involved, you can't use a property initializer that allows you to ignore properties you don't wish to set.

Listing 1: The least scalable yet easiest way to achieve immutability: Constructor arguments with implicit private setters

public class SimpleImmutableClass
{
    public SimpleImmutableClass(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
    //Private setters are implicit
    public string LastName { get; }
    public string FirstName { get; }
}

Other challenges include more complex types like lists, dictionaries, classes, and whether they are part of the base framework or custom. In other words, property types that are not bools, strings, ints, etc. Listing 2 illustrates a simple immutable class and a list of string properties.

Listing 2: Immutability via private setters only extends to the property definition itself, not the underlying properties and methods of class the property represents

public class SimpleImmutableClass
{
    public SimpleImmutableClass(
       string firstName, string lastName, List<string> items
    )
    {
        FirstName = firstName;
        LastName = lastName;
        Items = items;
    }
    //Private setters are implicit
    public string LastName { get; }
    public string FirstName { get; }
    public List<string> Items { get; }
}

var sut = new SimpleImmutableClass(
    "John",
    "Petesen",
    new List<string>()
);
sut.Items.Add("BS");
sut.Items.Add("MBA");
sut.Items.Add("JD");

The fact that you can change a property's characteristics when the setter is private might come as a surprise. The setter is about the property itself, in relation to its host class. It has nothing to do with the property's underlying object. Primitive types (strings, ints, floats, doubles, bools, etc.) don't have this problem. Primitive types, when in the context of a collection, array, dictionary, etc., despite the private setter, can be side-effected.

What About Structs - They're Immutable, Right?

No. That's the simple answer to that question. It's a common misconception that structs in C# are inherently immutable. Structs should be immutable. Structs are a great choice when you have small amounts of related data. Struct properties can be primitive or complex. You can use an object initializer like a class. Structs are more lightweight in terms of memory than a class, and that's their chief advantage. I must admit, I don't recall ever having implemented structs in an application notwithstanding the fact that in some cases, a struct would have been a better technical choice. Listing 3 illustrates a simple mutable struct example.

Listing 3: Out of the box, despite what many assume, structs are mutable, like classes.

public struct Person
{
    public string FirstName;
    public string LastName;
    public List<string> Items;
}


var sut = new Person() {
    FirstName = "John",
    LastName = "Petersen"
};

sut.Items = new List<string>();
sut.FirstName = "john";
sut.Items.Add("Structs are not immutable!");

The difference between software and enterprise software has to do with how it handles situations when things go wrong and specifically, how it reports such situations.

Implementing Immutability in C# (Without Too Much Pain!)

Some expectations: First, let's accept that not everything in C# can be made immutable. There are thousands of classes in the framework. Not everything needs to be immutable. Not every use case calls for immutability. Second, there will be the need for some custom code. Full immutability doesn't come straight out of the box. Therefore, you'll have to implement some code. The good news is that it's not a lot of code to worry about! In this case, you'll define an abstract class upon which your concrete classes will be based. For those concrete classes, there are only a few rules to obey.

Based on the foregoing, you want to leverage initializers. Therefore, your property setters need to be public. That means that you'll need to use a device to make properties with public setters immutable. In other words, you'll need to leverage custom code!

Step 1: Leverage JSON.NET and System.Immutable.Collections

Figure 1 illustrates two Nuget Packages that are required for this solution: NewtonSoft.Json (JSON.Net) and System.Immutable.Collections.

Figure 1: The two Nuget Packages required for immutability.
Figure 1: The two Nuget Packages required for immutability.

JSON.NET is required for easy serialization and de-serialization of objects. Even though immutable objects don't change, there is a requirement to make quick edits for the purpose of creating a new object instance. JSON.NET is a means by which you can quickly create and manage immutable objects.

System.Collections.Immutable is a Nuget Package managed by Microsoft that has immutable versions of Lists, Dictionaries, Arrays, Hashes, and Queues. These are the kinds of objects that illustrate the problem of private setters with collection and array properties. To re-iterate the problem, the private setter doesn't prevent you from adding or manipulating items in the underlying collection.

In this context, the rule is that objects shall not change and there needs to be an efficient way to both enforce the rule and provide feedback when there's an attempt to violate the rule.

Step 2: Create Custom Exceptions

The difference between software and “enterprise software” has nothing to do with the software's core function. The difference has to do with how it handles situations when things go wrong and specifically, how it reports such situations. In this case, you need to trap two invalid situations. The first is when an attempt is made to alter an immutable property. The second is an attempt to add a property to an immutable class instance that doesn't support immutability. Listing 4 illustrates ImmutableObjectEditAttempt exception class.

Listing 4: The ImmutableObjectEditAttempt exception is thrown whenever an attempt is made to change an immutable property after the property has been set.


public class ImmutableObjectEditException : Exception
    {
        public ImmutableObjectEditException() : base("An immutable object cannot be changed after it has been created.")
    {

    }
}

Listing 5 illustrates the InvalidDataType Exception Class.

Listing 5: The InvalidDataTypeException exception is thrown when an attempt is made to define a property with a type not contained in the ValidImmutableClassTypes HashSet.


public class InvalidDataTypeException : Exception
{
        public static ImmutableHashSet<string>
    ValidImmutableClassTypes =
        ImmutableHashSet.Create<string>(
        "Boolean",
        "Byte",
        "SByte",
        "Char",
        "Decimal",
        "Double",
        "Single",
        "Int32",
        "UInt32",
        "Int64",
        "UInt64",
        "Int16",
        "UInt16",
        "String",
        "ImmutableArray",
        "ImmutableDictionary",
        "ImmutableList",
        "ImmutableHashSet",
        "ImmutableSortedDictionary",
        "ImmutableSortedSet",
        "ImmutableStack",
        "ImmutableQueue"
    );

    public InvalidDataTypeException(
        ImmutableHashSet<string> invalidProperties) : base(
        $"Properties of an instance of " +
        "ImmutableClass may only " +
        "contain the following types: Boolean, Byte, " +
        "SByte, Char, Decimal, Double, Single, " +
        "Int32, UInt32, Int64, " +
        "UInt64, Int16, UInt16, String, ImmutableArray, " +
        "ImmutableDictionary, ImmutableList, ImmutableQueue, " +
        "ImmutableSortedSet, ImmutableStack or ImmutableClass. " +

        $"Invalid property types: " +
        $" {string.Join(",", invalidProperties.ToArray())}")
    {
        Data.Add("InvalidPropertyTypes",
            invalidProperties.ToArray());
    }
}

Step 3: Create an Immutable Abstract Class

In this section, over the next few figures, the ImmutableClass Abstract Class will be discussed. Listing 6 illustrates the Constructor.

Listing 6: The ImmutableClass Constructor ensures that only valid data types are present in the object's properties. If an invalid type exists, an exception is thrown, thus preventing the object from being created.

public abstract class ImmutableClass
{
    private bool _lock;

    protected ImmutableClass()
    {
        var properties = GetType()
           .GetProperties()
           .Where(x => x.PropertyType.BaseType.Name != "ImmutableClass")
           .Select(x =>
               x.PropertyType.Name.Substring(0,
              (x.PropertyType.Name.Contains("`")
                  ? x.PropertyType.Name.IndexOf("`", StringComparison.Ordinal)
                  : x.PropertyType.Name.Length)))
            .ToImmutableHashSet();

        var invalidProperties = properties
            .Except(InvalidDataTypeException
            .ValidImmutableClassTypes);

        if (invalidProperties.Count > 0)

            throw new InvalidDataTypeException(invalidProperties);
    }

When an immutable object instance is created, the private _lock field is set to true. One of the requirements is to keep the property initializer feature available. In your immutable class instances, if you want to use the constructor to initialize your properties, you're free to do that. To use initializers, it means that the property setters must be public. While I'm on the topic of property setters, Listing 7 illustrates the common property setter code. If the object instance is locked, the ImmutableOjbectEditException Exception (previously illustrated in Listing 4) is thrown.

Listing 7: With a common setter, you can be assured that for every property, an exception will be thrown if an attempt is made to change the property once the property has been set.

protected void Setter<T>(string name, T value, ref T variable)
{
    if (_lock)
        throw new ImmutableObjectEditException();
    variable = value;
}

As you may have gathered, if you create an instance of a class based on ImmutableClass, for any property that wasn't set upon creation, it must mean that you didn't intend to set the property. Listing 8 illustrates the generic static Create method to create an immutable object instance.

Listing 8: The Create() accepts a json string argument to create a locked ImmutableClass Instance.

public static T Create<T>() where T : ImmutableClass
        {
            ImmutableClass retVal = Activator.CreateInstance<T>();
            retVal._lock = true;
            return (T)retVal;
        }

In the next section, examples will be provided to illustrate how to work with ImmutableClass. The Create<T>() method automatically locks the returned instance.

Step 4: Implementing an ImmutableClass

Listing 9 illustrates two approaches to implementing an ImmutableClass instance. In this illustration, there's a Person Class instance of the abstract ImmutableClass Class. In the first approach, the immutable object is hydrated via a JSON string. How you choose to create the JSON is up to you. If the field names are consistent, JSON.NET via its deserialization feature will correctly hydrate the object. The other approach is to create the Person Class directly, outside the Create Method. Note: When you create an instance directly, the class IS NOT immutable because the _lock field is not set to true.

Listing 9: An immutable instance can be created either with a JSON string or a non-immutable instance of the immutable class.

public class Person : ImmutableClass
    {
        private string _firstName;
        public string FirstName
        {
            get => _firstName;
            set => Setter(
                MethodBase
                    .GetCurrentMethod()
                    .Name
                    .Substring(4),
                value,
                ref _firstName);
        }
        private string _lastName;

        public string LastName
        {
            get => _lastName;
            set => Setter(
                MethodBase
                    .GetCurrentMethod()
                    .Name
                    .Substring(4),
                value,
                ref _lastName);
        }
    }


var immutablePerson = ImmutableClass.Create<Person>
                (
                "{\"FirstName\":\"John\"," +
                "\"LastName\":\"Petersen\"}"
                );
    //Or

    immutablePerson = ImmutableClass.Create
                 (
                 new Person()
                 { FirstName = "John",
                 LastName = "Petersen"}
                 );

    //Next line will throw an exception
    immutablePerson.FirstName = "john";

}

Figure 2 illustrates the thrown exception that occurs when an attempt is made to edit a property of an immutable class instance.

Figure 2: If an attempt is made to edit an immutable class instance property, the ImmutableObjectEditException Exception is thrown.
Figure 2: If an attempt is made to edit an immutable class instance property, the ImmutableObjectEditException Exception is thrown.

Properties of a class based on ImmutableClass may be of the following types:

  • Boolean
  • Byte
  • SByte
  • Char
  • Decimal
  • Double
  • Single
  • Int32
  • UInt32
  • Int64
  • UInt64
  • Int16
  • UInt16
  • String
  • ImmutableArray
  • ImmutableDictionary
  • ImmutableList
  • ImmutableHashSet
  • ImmutableSortedDictionary
  • ImmutableQueue
  • ImmutableSortedSet
  • ImmutableStack
  • ImmutableClass

The first 14 are straight out-of-the-box primitive .NET types. The next six, ImmutableArray, ImmutableDictionary, ImmutableList, ImmutableHashSet, ImmutableSortedDictionary and ImmutableQueue, are implemented via the System.Collections.Immutable Nuget Package. The Nuget Package also makes available extension methods to their base .NET types that makes it easy to cast to its immutable counterpart. Finally, ImmutableClass based classes may contain other ImmutableClass based classes. More information on the System.Collections.Immutable Namespace can be found here: https://docs.microsoft.com/en-us/dotnet/api/system.collections.immutable?view=netcore-2.2.

Listing 10 illustrates a slightly more complicated version of the Person Class.

Listing 10: An immutable class can contain properties based on other immutable class instances.

public class Person : ImmutableClass
    {
        private string _firstName;
        public string FirstName
        {
            get => _firstName;
            set => Setter(
                MethodBase
                   .GetCurrentMethod()
                   .Name
                   .Substring(4),
                value,
                ref _firstName);
        }
        private string _lastName;

        public string LastName
        {
            get => _lastName;
            set => Setter(
                MethodBase
                    .GetCurrentMethod()
                    .Name
                    .Substring(4),
                value,
                ref _lastName);
        }

        private ImmutableArray<string> _schools;

        public ImmutableArray<string> Schools
        {
            get => _schools;
            set => Setter(
                MethodBase
                    .GetCurrentMethod()
                    .Name
                    .Substring(4),
                value,
                ref _schools);
        }
    }

The ImmutableArray property resolves the problem with collection and array-based properties. System.Collections.Immutable contains immutable counterparts to all of the base collection types in .NET. If you're using .NET Core or Standard, System.Collections.Immutable supports those platforms as well.

Conclusion

If you search the Web for how to achieve immutability in C#, you'll find a common theme. The approaches tend to stress using constructor parameters for initialization, making backing variable read-only and removing setters (leaving only getters) for public properties. If your class only has primitive types, this approach will work. If your class has many primitive-typed properties, the approach will work, but it's less feasible. Using this approach, you don't get to use property initializers. Because DTOs are a common use case for immutability and because DTOs often have to be serialized and de-serialized, the lack of setters is almost a non-starter.

To get around these limitations, developers often turn to code to solve the problem. Although some code is necessary to fill the gaps of what doesn't come into the box, the more code added to the mix, the more problems that tend to be created. To ensure that the objects you need to be immutable are immutable, be sure to leverage unit tests. The approach presented here emphasized simplicity and, as much as possible, colors within the lines with as little code as possible. By ensuring that necessary objects are immutable, you can be assured that those objects won't be susceptible to side-effects from downstream code. The result is simpler applications that require less testing.