- Add a reference to the
Uno.CodeGen
Nuget package in your project. - Create a new POCO
class with the
[GeneratedImmutable]
attribute[GeneratedImmutable] // Uno.GeneratedImmutableAttribute public partial class MyEntity { // Please take note all properties are "get-only" public string A { get; } = "a"; public string B { get; } = "b"; }
- Compile (this is important to generate the partial portion of the class).
- Use it in your code
MyEntity entity1 = MyEntity.Default; // A="a", B="b" MyEntity entity2 = entity1.WithA("c"); // A="c", B="b" MyEntity entity3 = new MyEntity.Builder(entity2) { B="123" }; // A="c", B="123" MyEntity entity4 = MyEntity.Default .WithA("value for A") // Intermediate fluent value is a builder, .WithB("value for B"); // so there's no memory impact doing this.
-
The class needs to be partial - because generated code will augment it.
-
Restrictions:
- No default constructor allowed
(will with Newtownsoft's JSON.NET,
or with new .NET Core 3.0 API System.Text.Json
when detected, it will generate custom JSON Converter. You can disable
this globally by setting this attribute:
[assembly: ImmutableGenerationOptions(GenerateNewtownsoftJsonNetConverters=false, GenerateSystemTextJsonConverters=false)]
- No property setters allowed (even
private
ones): properties should be read only, even for the class itself. - No fields allowed (except static fields, or
readonly
fields of a recognized immutable type). - No write indexers read-only indexers are ok and sometime useful for complex types.
- Static members are ok, as they can only manipulate immutable stuff. Same apply for extensions.
- Nested classes not supported, the class must be directly in its namespace for the generator to happen.
- No default constructor allowed
(will with Newtownsoft's JSON.NET,
or with new .NET Core 3.0 API System.Text.Json
when detected, it will generate custom JSON Converter. You can disable
this globally by setting this attribute:
-
Property Initializers will become default values in the builder. ex:
public partial class UserPreferences { // Each time a new version of the builder is created // the initializer ("DateTimeOffset.Now" here) will be // applied first. public DateTimeOffSet LastChange { get; } = DateTimeOffset.Now; }
Warning: don't use this for generating an id like a guid, because time you'll update the entity you'll get a new id.
-
Properties with implementation will be ignored (also called computed property).
Example:
/// <summary> /// Gets the fullname of the contact. /// </summary> public string FullName => LastName + ", " + FirstName;
-
Generated builder are implicitly convertible to/from the entity
-
Collections must be IReadOnlyCollection, IReadonlyDictionary or an immutable type from System.Collection.Immutable namespace: ImmutableList, ImmutableDictionary, ImmutableArray, ImmutableSet... The type of the collection must be immutable too.
-
-
A static
.Default
readonly property will contain a default instance with properties to their default initial values.It can be used as starting point to create a new instance, example:
Invoice invoice = Invoice.Default .WithId(invoiceId) .WithCustomer(customer) .WithItems(items);
// No default constructor allowed on [Immutable] classes
var x = new MyEntity() ; // Won't compile
// Method 1: Creating a builder from its constructor
MyEntity.Builder b = new MyEntity.Builder();
// Method 2: Creating a builder using implicit cast
MyEntity myEntity = [...];
MyEntity.Builder b = myEntity;
// Method 3: Creating a builder using .WithXXX() method
MyEntity myEntity = [...];
MyEntity.Builder b = myEntity.WithName("new name");
// Method 4: You can also create a builder from a previous version
MyEntity.Builder b = new MyEntity.Builder(previousEntity);
// To get the Immutable entity...
// Method 1 : Use implicit conversion (MyEntity.Builder => MyEntity)
MyEntity e = b;
// Method 2 : Use the .ToImmutable() method
MyEntity e = b.ToImmutable();
// Method 3 : Use the generated constructor with builder as parameter
MyEntity e = new MyEntity(b);
All set+set properties will also generate a With<propertyName>()
method.
The method will take a parameter of the type of the corresponding property.
This method is present on both the class and the builder and always
returns a builder.
Usage:
public partial class MyEntity
{
public string A { get; } = string.Empty;
public string B { get; } = null;
}
[...]
// Create a first immutable instance
var v1 = new MyEntity.Builder { A="a", B="b" };
// Create a new immutable instance
var v2 = v1
.WithB("b2")
.ToImmutable();
// Same as previous but with the usage of implicit conversion
MyEntity v2bis = v1.WithB("b2");
You can also use a lambda to project a property value. This is very useful for object hierarchy:
public partial class MyEntity
{
public MySubEntity A { get; } = null;
}
public partial class MySubEntity
{
public string X { get; } = null;
}
var original = MyEntity.Default;
var modified = original.WithA(a => a.WithX("!!!"); // won't generate null-ref exception!
Let's say we write this...
[Immutable]
public partial class MyRootEntity
{
public string A { get; }
public MySubEntity B { get; }
}
[Immutable]
public partial class MySubEntity
{
public string C { get; }
public string D { get; }
public ImmutableList<string> E { get; }
}
This will generate something like this:
[Immutable]
public partial class MyRootEntity
{
public partial class Builder
{
public string A { get; set; }
public MySubEntity B { get; set; }
}
}
[Immutable]
public partial class MySubEntity
{
public partial class Builder
{
public string C { get; set; }
public string D { get; set; }
}
}
Important:
- Complex properties **MUST** be immutable entities. A complex property is when it's a defined type, not a CLR primitive.
- Indexers are not supported.
- Events Properties are not supported.
The generator will automatically detect the presence of the package Uno.Core
to generate helper methods for working with Option<T>
.
When Uno.Core
is detected, the following code will be generated additionally:
- A new static
T.None
- An automatic conversion from
Option<T>
toT.Builder
. - An automatic conversion from
T.Builder
toOption<T>
(producingOption.None
when the instance isdefault(T)
) - Extensions methods for .WithXXX() on
Option<T>
.
The generated code will produce the following effect:
using Uno;
// Without `Option` generated code:
Option<MyEntity> original = Option.Some(MyEntity.Default);
Option<MyEntity> modified = new MyEntity.Builder(original.SomeOrDefault()
.WithA("new-a")
.ToImmutable();
// With generated code:
Option<MyEntity> original = Option.Some(MyEntity.Default);
Option<MyEntity> modified = original.WithA("new-a");
// You can also do that:
Option<MyEntity> x = MyEntity.None.WithA("new-a");
// **************************************************************
// IMPORTANT: Calling .WithXXX() methods on a `None` will convert
// it to `Default`. So MyEntity.None.WithA() produce the exact
// same result as MyEntity.Default.WithA().
// **************************************************************
For more information on the
Uno.Core
package:
- On Github: https://github.com/nventive/Uno.Core
- On Nuget: https://www.nuget.org/packages/Uno.Core/
What if I need to use it with Newtownsoft's JSON.NET or with System.Text.Json?
You simply need to deserialize the builder instead of the class itself. The implicit casting will automatically convert it to the right type.
Example:
MyEntity e1 = Newtonsoft.Json.JsonConvert.DeserializeObject<MyEntity.Builder>(json);
MyEntity e2 = System.Text.Json.JsonSerializer.Deserialize<MyEntity.Builder>(json);
For most application the compiled code won't be significant. Assets in projects are usually a lot bigger than that.
If you are using a linker tool (Mono Linker / DotNetCore Linker), those unused methods will be removed from compiled result.
We think the cost of this unused code is cheaper than the potential bugs when writing and maintaining this code manually.
- ImmutableAttribute:
The
[Immutable]
is used by other parts of Uno (some are not published as opened source) to identify an entity has been immutable. - ImmutableBuilderAttribute:
The
[ImmutableBuilder]
is used to indicate which class to use to build the target immutable type. The builder is expected to implement theIImmutableBuilder<TImmutable>
interface.
If you want, you can manually create immutable classes and use those attributes in your code: the code generators will use it as if it was generated.
Yes! That's the major aspect of immutable entities. Once an instance of an immutable class is created, it's impossible to change it. (ok, it's possible by using reflection, but why would you do that?)
But the builders are not thread safe. That means updating the same property of the same instance concurrently (from many threads) will produce unexpected result.
Yes. You can continue to update the builder even after calling
.ToImmutable()
. The created instance won't be updated.
This attribute is used to indicate a pure method. It means a method having no side effect.
Since calling a pure method without using the result is a waste of resources, some IDE tools like ReSharper(TM) will give you a visual warning when you're not using the result.
Not supported yet. Open an issue if you need this.
No. The type must be a reference type (class
).
All attributes are replicated, except those defined in Uno.Immutables
and
Uno.Equality
. If you need to remove other attributes, you just need
to use the [ImmutableAttributeCopyIgnore(<regex>)]
attribute.
For a finer control, you can put it at assembly level, on a type or even on a property itself.
Exactly, arrays are not immutable. But they have one big advantages over other
declarations: they are great looking. Declaring a property as A[]
is
more concise than declaring IReadonlyCollection<T>
.
You should not use arrays for immutable types, but if you really prefer the concise declaration of arrays, you can allow arrays to be treated as immutables by setting this attribute on your assembly:
[assembly: Uno.ImmutableGenerationOptions(TreatArrayAsImmutable = true)]
Yes they are by default. If you want to chagne this behavior, use the global
[ImmutableGenerationOptions]
attribute. Example:
[assembly: Uno.ImmutableGenerationOptions(GenerateEqualityByDefault = true)]
You can also override this default by specifying per-type:
[GeneratedImmutable(GenerateEquality = false)]
public class MyImmutable
{
}
GOOD TO KNOW: Both
[GeneratedImmutable]
and[GeneratedEquality]
attributes are inheritable. It means > any inherited class will be generated too, even if they are defined in another assembly (Assuming theUno.CodeGen
package is used, obviously). So, disabling equality generation ([GeneratedImmutable(GenerateEquality = false)]
) won't have any effect in inherited class if the generation is active on the base class.
I'm getting this error:
#error: 'ImmutableGenerator: Property MyClass.SomeField (BaseClass) is not immutable. It cannot be used in an immutable entity.'
To fix this, put this attribute on your assembly:
[assembly: Uno.TreatAsImmutable(typeof(BaseClass))]