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, orstring - Structs from the
Systemnamespace - Delegates and enums
- Immutable collections from the
System.Collections.Immutablenamespace, 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.Immutablenamespace, 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}