Open sandboxFocusImprove this doc

Metalama.Patterns.Immutability

Immutability is a widely recognized concept in software programming. An immutable type is a type whose instances can't be modified after creation. Designs that use immutable types are typically easier to understand than those that rely heavily on mutating objects. Examples of immutable types in C# include intrinsic types like int, float, or string, enums, delegates, most system value types like DateTime, and collections from the System.Collections.Immutable namespace.

The Metalama.Patterns.Immutability package provides the [Immutable] aspect and the ConfigureImmutability fabric method to mark and enforce immutability.

Benefits

The Metalama.Patterns.Immutability package provides the following benefits:

  • Exposes immutability to other aspects: Provides immutability information for code analysis by other aspects, such as Observable (see Metalama.Patterns.Observability).
  • Documents design intent: Declares immutability explicitly in code, eliminating the need to infer it from implementation details.
  • Enforces immutability: Reports warnings when a type marked as immutable contains mutable fields.

Kinds of immutability

The Metalama.Patterns.Immutability package recognizes two kinds of immutability, represented by the ImmutabilityKind type:

  • Shallow immutability: All instance fields are read-only and no automatic property has a setter.
  • Deep immutability: All instance fields and automatic properties are of a deeply immutable type, applied recursively.

Deep immutability ensures that all objects reachable by recursively evaluating fields or properties are immutable. This provides guarantees for code analyses, such as those performed by the Metalama.Patterns.Observability package.

System-supported types

The Metalama.Patterns.Immutability package contains rules that define the following types as deeply immutable:

  • Intrinsic types like bool, byte, int, or string
  • Structs from the System namespace
  • Delegates and enums
  • Immutable collections from the System.Collections.Immutable namespace, when all type parameters are themselves deeply immutable

Additionally, the following types are implicitly classified as shallowly immutable:

  • Read-only structs
  • Immutable collections from the System.Collections.Immutable namespace, when any type parameter isn't deeply immutable
Warning

The Metalama.Patterns.Immutability package doesn't attempt to infer the immutability of types by analyzing their fields. It relies purely on the rules defined above and the types manually marked as immutable.

Marking types as immutable in source code

Mark a type as immutable by applying the [Immutable] aspect. By default, the [Immutable] attribute represents shallow immutability. To represent deep immutability, supply the ImmutabilityKind.Deep argument.

The [Immutable] aspect reports warnings when fields aren't read-only or when automatic properties have a setter. Resolve the warning or suppress it using #pragma warning disable.

The [Immutable] aspect is automatically inherited by derived types.

To add this aspect in bulk, use a fabric method and AddAspectIfEligible. For details, see Adding many aspects simultaneously.

Example: Shallow immutability with warning

The following example shows a class marked as immutable that contains a mutable property. A warning is reported on this property.

1using Metalama.Patterns.Immutability;
2
3namespace Metalama.Documentation.SampleCode.Immutability.Warning;
4
5[Immutable]
6public class Person
7{
    Warning LAMA5021: The 'Person.FirstName' property must not have a setter because of the [Immutable] aspect.

8    public required string FirstName { get; set; }
9
10    public required string LastName { get; init; }
11}

Marking types you don't own

To assign an ImmutabilityKind to types where you can't add the [Immutable] aspect, use the ConfigureImmutability fabric extension method. Pass either an ImmutabilityKind if the type always has the same immutability, or an IImmutabilityClassifier to determine the ImmutabilityKind dynamically. This mechanism is useful for generic types when their immutability depends on the immutability of type arguments.

Example: Marking System.Uri as immutable

The following example marks the Uri class as deeply immutable. As a result, you can use a Uri property in the deeply immutable type Person without generating a warning.

1using Metalama.Framework.Fabrics;
2using Metalama.Patterns.Immutability;
3using Metalama.Patterns.Immutability.Configuration;
4using System;
5
6namespace Metalama.Documentation.SampleCode.Immutability.Fabric;
7
8internal class Fabric : ProjectFabric
9{
10    public override void AmendProject( IProjectAmender amender )
11    {
12        amender.SelectReflectionType( typeof(Uri) ).ConfigureImmutability( ImmutabilityKind.Deep );
13    }
14}
1using Metalama.Patterns.Immutability;
2using System;
3
4namespace Metalama.Documentation.SampleCode.Immutability.Fabric;
5
6[Immutable( ImmutabilityKind.Deep )]
7public class Person
8{
9    public required string FirstName { get; init; }
10
11    public required string LastName { get; init; }
12
13    public Uri? HomePage { get; init; }
14}