###T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.
- Generated Equals, GetHashCode and ToString now properly support collections as long as they implement IEnumerator. This means that arrays, List, Set, Dictionary, plus its Immutable variants are properly handled.
- ImmutableClassOptions.EnableXXX/DisableXXX have been renamed to ImmutableClassOptions.IncludeXXX/ExcludeXXX
- preConstructorParam code comment has been changed to the PreConstructorParam attribute
Creating proper immutable objects in C# requires a lot boilerplate code. The aim of this project is to reduce this to a minimum by means of automatic code generation via T4 templates. For instance, given the following class:
[ImmutableClass(Options = ImmutableClassOptions.IncludeOperatorEquals)]
class Person {
private const int AgeDefaultValue = 18;
public string FirstName { get; }
public string LastName { get; }
public int Age { get; }
[ComputedProperty]
public string FullName {
get {
return FirstName + " " + LastName;
}
}
}
It will automatically generate for you in a separate partial class file the following:
- A constructor such as
public Person(string firstName, string lastName, int age = 18)
that will initialize the values. - Working implementations for
Equals(object other)
andEquals(Person other)
. - Working implementations for
operator==
andoperator!=
- A working implementation of
GetHashCode()
. - A better
ToString()
with output such as"Person { FirstName=John, LastName=Doe, Age=21 }"
- A
Person With(...)
method that can be used to generate a new immutable clone with 0 or more properties changed (e.g.var janeDoe = johnDoe.With(firstName: "Jane", age: 20)
Just install the T4Immutable nuget package.
Just use the [ImmutableClass]
attribute over the class. The class will be auto-checked to meet the following constraints before code generation takes place:
- Any properties NOT marked as
ComputedProperty
will need to be either auto properites or have a non-public setter. - It cannot have any custom constructors since one will be auto-generated, however please check the "Constructor overrides" section below to see ways to overcome this limitation.
- Any default values (see "How to specify property default values?") will be checked to have the same type than the properties.
- It cannot be static.
- It cannot have any extra partials besides the generated one (this support is still TODO).
- It cannot have a base class (probably to be lifted in a future update if anybody can show a proper use case), it can however have any interfaces.
Besides those checks it is your responsibility to make the immutable object behave correctly. For example you should use ImmutableList instead of List and so on. This project is just made to reduce the boilerplate after all, not ensure correctness.
How are collection (Array, List, Set, Dictionary... plues their Immutable versions) based properties handled?
They just work as long as they inherit from IEnumerable
(as all of the basic ones do). The generated Equals() will check they are equivalent by checking their contents, as well as the generated GetHashCode(). Nested collections are not a problem as well.
You sure can, just add to the ImmutableClass attribute something like this:
[ImmutableClass(Options =
ImmutableClassOptions.ExcludeEquals |
ImmutableClassOptions.ExcludeGetHashCode |
ImmutableClassOptions.IncludeOperatorEquals |
ImmutableClassOptions.ExcludeToString |
ImmutableClassOptions.ExcludeWith)]
The names should be pretty self explanatory. Note that even if you exclude for example the Equals implementation you can still use them internally by invoking the private bool ImmutableEquals(...)
implementation. This is done in case you might want to write your own Equals(...)
yet still use the generated one as a base.
Take care in not using "using Foo = ImmutableClassOptions" to save some typing, it won't work.
Yes. Do something like this:
[ImmutableClass(ConstructorAccessLevel = ConstructorAccessLevel.Private)]
If you need to do extra initialization / validation on the generated constructor just define a private void PostConstructor()
method and do your work there. It will be invoked after all assignations are done inside the generated constructor.
Alternatively it is of course also possible to do validation inside the properties private/protected setters. E.g:
private int _Age;
public int Age {
get { return _Age; }
set {
if (value < 18) throw new Exception("You are too young!");
_Age = value;
}
}
Yes, use the following when defining a property:
[PreConstructorParam("[JetBrains.Annotations.NotNull]")]
public string FirstName { get; }
Bear in mind that if you use it to specify attributes they must have the full name (including namespace) or else there would be compilation errors. Also bear in mind the string has to be constant, this is, it shouldn't depend on other const values.
How do I enforce automatic null checking for the constructor parameters? What about for the properties?
If you use this:
[PreNotNullCheck, PostNotNullCheck]
public string FirstName { get; }
The constructor will be this:
public Person(string firstName) {
// pre not null check
if (firstName == null) throw new ArgumentNullException(nameof(firstName));
// assignations + PostConstructor() if needed
// post not null check
if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName));
}
Having said this, if you use JetBrains Annotations for null checking, you can also do this:
[JetBrains.Annotations.NotNull, ConstructorParamNotNull]
public string FirstName { get; }
And the constructor will be this:
public Person([JetBrains.Annotations.NotNull] string firstName) {
// pre not null check is implied by ConstructorParamNotNull
if (firstName == null) throw new ArgumentNullException(nameof(firstName));
// assignations + PostConstructor() if needed
// post not null check implied by JetBrains.Annotations.NotNull on the property
if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName));
}
If you need to do alternate constructors (for example having a Point<T>(T x, T y) Immutable class and you want to generate a point from a distance and an angle) then you can do something like:
public static Point<T> FromAngleAndDistance(T distance, double angle) {
// your code here
return new Point(x, y);
}
Just change the order of the properties.
If you want a property to have a given default value on the auto-generated constructor there are two ways. Say that you have a property named int Age, and you want it to have the default value of 18:
- Way 1:
(private/protected/public/whatever) const int AgeDefaultValue = 18;
- Way 2:
(private/protected/public/whatever) static readonly int AgeDefaultValue = 18;
If you wonder why there are two alternatives it is because sometimes it is possible to add stuff such as new Foo()
as a default parameter for constructors and that expression works as a readonly but does not work as a const.
Please note that default values, like in a constructor, should not have gaps. This is, if you have int x, int y then you should have a default value for y or for x and y. If you want a default value for x then move it to the end.
It sure does!
What if I want to make the class smarter though not strictly immutable, like caching a point distance after it has been requested the first time?
This is more about reducing boilerplate than ensuring immutability, so you can. E.g.:
[ImmutableClass]
class Point {
public double X { get; }
public double Y { get; }
private double _Distance;
[ComputedProperty]
public double Distance {
get {
if (_Distance == null) _Distance = Math.Sqrt(X*X + Y*Y);
return _Distance.Value;
}
}
}
However if you do stuff like this, then since internally it has become mutable-ish you will need to use a lock or some other method if you want it to work properly when the object is used concurrently. A probably better solution would be to initialize the _Distance
member inside the PostConstructor()
. It all depends on your use case.
There are plugins out there that auto-run T4 templates once code changes, but if you don't want/need one then just use ¨Build - Transform All T4 Templates¨.
Absolutely, since the generated files are .cs files Intellisense will pick the syntax without problems.
Why would you need one when you can set all parameters at once by using the constructor or change as many parameters as you want at once with a single With(...)
invocation? That being said, please let me know if you think otherwise.
Please do!
Here you go (excluding some redundant attributes):
using System;
partial class Person : IEquatable<Person> {
[T4Immutable.GeneratedCode, System.CodeDom.Compiler.GeneratedCode("T4Immutable", "1.1.1"), System.Diagnostics.DebuggerNonUserCode]
public Person(string firstName, string lastName, int age = 18) {
this.FirstName = firstName;
this.LastName = lastName;
this.Age = age;
_ImmutableHashCode = T4Immutable.Helpers.GetHashCodeFor(this.FirstName, this.LastName, this.Age);
}
private bool ImmutableEquals(Person obj) {
if (ReferenceEquals(this, obj)) return true;
if (ReferenceEquals(obj, null)) return false;
return T4Immutable.Helpers.AreEqual(this.FirstName, obj.FirstName) && T4Immutable.Helpers.AreEqual(this.LastName, obj.LastName) && T4Immutable.Helpers.AreEqual(this.Age, obj.Age);
}
public override bool Equals(object obj) {
return ImmutableEquals(obj as Person);
}
public bool Equals(Person obj) {
return ImmutableEquals(obj);
}
public static bool operator ==(Person a, Person b) {
return T4Immutable.Helpers.AreEqual(a, b);
}
public static bool operator !=(Person a, Person b) {
return !T4Immutable.Helpers.AreEqual(a, b);
}
private readonly int _ImmutableHashCode;
private int ImmutableGetHashCode() {
return _ImmutableHashCode;
}
public override int GetHashCode() {
return ImmutableGetHashCode();
}
private string ImmutableToString() {
return T4Immutable.Helpers.ToStringFor(nameof(Person), new System.Tuple<string, object>(nameof(this.FirstName), this.FirstName), new System.Tuple<string, object>(nameof(this.LastName), this.LastName), new System.Tuple<string, object>(nameof(this.Age), this.Age));
}
public override string ToString() {
return ImmutableToString();
}
private Person ImmutableWith(T4Immutable.WithParam<string> firstName = default(T4Immutable.WithParam<string>), T4Immutable.WithParam<string> lastName = default(T4Immutable.WithParam<string>), T4Immutable.WithParam<int> age = default(T4Immutable.WithParam<int>)) {
return new Person(
!firstName.HasValue ? this.FirstName : firstName.Value,
!lastName.HasValue ? this.LastName : lastName.Value,
!age.HasValue ? this.Age : age.Value
);
}
public Person With(T4Immutable.WithParam<string> firstName = default(T4Immutable.WithParam<string>), T4Immutable.WithParam<string> lastName = default(T4Immutable.WithParam<string>), T4Immutable.WithParam<int> age = default(T4Immutable.WithParam<int>)) {
return ImmutableWith(firstName, lastName, age);
}
}