Latest commit

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

T4Immutable

###T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.

NuGet package

v1.1.2 release notes

  • 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.

v1.1.0 release notes

  • ImmutableClassOptions.EnableXXX/DisableXXX have been renamed to ImmutableClassOptions.IncludeXXX/ExcludeXXX
  • preConstructorParam code comment has been changed to the PreConstructorParam attribute

Why use this?

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)]classPerson{privateconstintAgeDefaultValue=18;publicstringFirstName{get;}publicstringLastName{get;}publicintAge{get;}[ComputedProperty]publicstringFullName{get{returnFirstName+" "+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) and Equals(Person other).
  • Working implementations for operator== and operator!=
  • 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)

How do I start?

Just install the T4Immutable nuget package.

What's needed to make an immutable class?

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.

I don't want X. Can I control what gets generated?

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.

Can I control the access level (public/private/...) of the constructor?

Yes. Do something like this:

[ImmutableClass(ConstructorAccessLevel = ConstructorAccessLevel.Private)]

Constructor post-initalization / validation

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:

privateint_Age;publicintAge{get{return_Age;}set{if(value<18)thrownewException("You are too young!");_Age=value;}}

Can I add extra attributes to each constructor parameter?

Yes, use the following when defining a property:

[PreConstructorParam("[JetBrains.Annotations.NotNull]")]publicstringFirstName{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]publicstringFirstName{get;}

The constructor will be this:

publicPerson(string firstName){// pre not null checkif(firstName==null)thrownewArgumentNullException(nameof(firstName));// assignations + PostConstructor() if needed// post not null checkif(this.FirstName==null)thrownewNullReferenceException(nameof(this.FirstName));}

Having said this, if you use JetBrains Annotations for null checking, you can also do this:

[JetBrains.Annotations.NotNull,ConstructorParamNotNull]publicstringFirstName{get;}

And the constructor will be this:

publicPerson([JetBrains.Annotations.NotNull]string firstName){// pre not null check is implied by ConstructorParamNotNullif(firstName==null)thrownewArgumentNullException(nameof(firstName));// assignations + PostConstructor() if needed// post not null check implied by JetBrains.Annotations.NotNull on the propertyif(this.FirstName==null)thrownewNullReferenceException(nameof(this.FirstName));}

Constructor overrides

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:

publicstaticPoint<T>FromAngleAndDistance(Tdistance,doubleangle){// your code herereturnnewPoint(x,y);}

How do I change the order of the arguments in the generated constructor?

Just change the order of the properties.

How to specify property default values?

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.

Does it work with generic classes? Custom methods? Nested classes?

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]classPoint{publicdoubleX{get;}publicdoubleY{get;}privatedouble_Distance;[ComputedProperty]publicdoubleDistance{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.

How do I rebuild the auto-generated files once I make a change in my code?

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¨.

Does Intellisense and all that stuff work after using this?

Absolutely, since the generated files are .cs files Intellisense will pick the syntax without problems.

Why doesn't it generate a builder?

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.

Can I suggest new features or whatever?

Please do!

Can I see the extra code generated for the very first example?

Here you go (excluding some redundant attributes):

usingSystem;partialclassPerson:IEquatable<Person>{[T4Immutable.GeneratedCode,System.CodeDom.Compiler.GeneratedCode("T4Immutable","1.1.1"),System.Diagnostics.DebuggerNonUserCode]publicPerson(stringfirstName,stringlastName,intage=18){this.FirstName=firstName;this.LastName=lastName;this.Age=age;_ImmutableHashCode=T4Immutable.Helpers.GetHashCodeFor(this.FirstName,this.LastName,this.Age);}privateboolImmutableEquals(Personobj){if(ReferenceEquals(this,obj))returntrue;if(ReferenceEquals(obj,null))returnfalse;returnT4Immutable.Helpers.AreEqual(this.FirstName,obj.FirstName)&&T4Immutable.Helpers.AreEqual(this.LastName,obj.LastName)&&T4Immutable.Helpers.AreEqual(this.Age,obj.Age);}publicoverrideboolEquals(objectobj){returnImmutableEquals(objasPerson);}publicboolEquals(Personobj){returnImmutableEquals(obj);}publicstaticbooloperator==(Persona,Personb){returnT4Immutable.Helpers.AreEqual(a,b);}publicstaticbooloperator!=(Persona,Personb){return!T4Immutable.Helpers.AreEqual(a,b);}privatereadonlyint_ImmutableHashCode;privateintImmutableGetHashCode(){return_ImmutableHashCode;}publicoverrideintGetHashCode(){returnImmutableGetHashCode();}privatestringImmutableToString(){returnT4Immutable.Helpers.ToStringFor(nameof(Person),newSystem.Tuple<string,object>(nameof(this.FirstName),this.FirstName),newSystem.Tuple<string,object>(nameof(this.LastName),this.LastName),newSystem.Tuple<string,object>(nameof(this.Age),this.Age));}publicoverridestringToString(){returnImmutableToString();}privatePersonImmutableWith(T4Immutable.WithParam<string>firstName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<string>lastName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<int>age=default(T4Immutable.WithParam<int>)){returnnewPerson(!firstName.HasValue?this.FirstName:firstName.Value,!lastName.HasValue?this.LastName:lastName.Value,!age.HasValue?this.Age:age.Value);}publicPersonWith(T4Immutable.WithParam<string>firstName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<string>lastName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<int>age=default(T4Immutable.WithParam<int>)){returnImmutableWith(firstName,lastName,age);}}

About

T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

T4Immutable

###T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.

NuGet package

v1.1.2 release notes

  • 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.

v1.1.0 release notes

  • ImmutableClassOptions.EnableXXX/DisableXXX have been renamed to ImmutableClassOptions.IncludeXXX/ExcludeXXX
  • preConstructorParam code comment has been changed to the PreConstructorParam attribute

Why use this?

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)]classPerson{privateconstintAgeDefaultValue=18;publicstringFirstName{get;}publicstringLastName{get;}publicintAge{get;}[ComputedProperty]publicstringFullName{get{returnFirstName+" "+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) and Equals(Person other).
  • Working implementations for operator== and operator!=
  • 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)

How do I start?

Just install the T4Immutable nuget package.

What's needed to make an immutable class?

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.

I don't want X. Can I control what gets generated?

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.

Can I control the access level (public/private/...) of the constructor?

Yes. Do something like this:

[ImmutableClass(ConstructorAccessLevel = ConstructorAccessLevel.Private)]

Constructor post-initalization / validation

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:

privateint_Age;publicintAge{get{return_Age;}set{if(value<18)thrownewException("You are too young!");_Age=value;}}

Can I add extra attributes to each constructor parameter?

Yes, use the following when defining a property:

[PreConstructorParam("[JetBrains.Annotations.NotNull]")]publicstringFirstName{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]publicstringFirstName{get;}

The constructor will be this:

publicPerson(string firstName){// pre not null checkif(firstName==null)thrownewArgumentNullException(nameof(firstName));// assignations + PostConstructor() if needed// post not null checkif(this.FirstName==null)thrownewNullReferenceException(nameof(this.FirstName));}

Having said this, if you use JetBrains Annotations for null checking, you can also do this:

[JetBrains.Annotations.NotNull,ConstructorParamNotNull]publicstringFirstName{get;}

And the constructor will be this:

publicPerson([JetBrains.Annotations.NotNull]string firstName){// pre not null check is implied by ConstructorParamNotNullif(firstName==null)thrownewArgumentNullException(nameof(firstName));// assignations + PostConstructor() if needed// post not null check implied by JetBrains.Annotations.NotNull on the propertyif(this.FirstName==null)thrownewNullReferenceException(nameof(this.FirstName));}

Constructor overrides

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:

publicstaticPoint<T>FromAngleAndDistance(Tdistance,doubleangle){// your code herereturnnewPoint(x,y);}

How do I change the order of the arguments in the generated constructor?

Just change the order of the properties.

How to specify property default values?

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.

Does it work with generic classes? Custom methods? Nested classes?

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]classPoint{publicdoubleX{get;}publicdoubleY{get;}privatedouble_Distance;[ComputedProperty]publicdoubleDistance{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.

How do I rebuild the auto-generated files once I make a change in my code?

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¨.

Does Intellisense and all that stuff work after using this?

Absolutely, since the generated files are .cs files Intellisense will pick the syntax without problems.

Why doesn't it generate a builder?

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.

Can I suggest new features or whatever?

Please do!

Can I see the extra code generated for the very first example?

Here you go (excluding some redundant attributes):

usingSystem;partialclassPerson:IEquatable<Person>{[T4Immutable.GeneratedCode,System.CodeDom.Compiler.GeneratedCode("T4Immutable","1.1.1"),System.Diagnostics.DebuggerNonUserCode]publicPerson(stringfirstName,stringlastName,intage=18){this.FirstName=firstName;this.LastName=lastName;this.Age=age;_ImmutableHashCode=T4Immutable.Helpers.GetHashCodeFor(this.FirstName,this.LastName,this.Age);}privateboolImmutableEquals(Personobj){if(ReferenceEquals(this,obj))returntrue;if(ReferenceEquals(obj,null))returnfalse;returnT4Immutable.Helpers.AreEqual(this.FirstName,obj.FirstName)&&T4Immutable.Helpers.AreEqual(this.LastName,obj.LastName)&&T4Immutable.Helpers.AreEqual(this.Age,obj.Age);}publicoverrideboolEquals(objectobj){returnImmutableEquals(objasPerson);}publicboolEquals(Personobj){returnImmutableEquals(obj);}publicstaticbooloperator==(Persona,Personb){returnT4Immutable.Helpers.AreEqual(a,b);}publicstaticbooloperator!=(Persona,Personb){return!T4Immutable.Helpers.AreEqual(a,b);}privatereadonlyint_ImmutableHashCode;privateintImmutableGetHashCode(){return_ImmutableHashCode;}publicoverrideintGetHashCode(){returnImmutableGetHashCode();}privatestringImmutableToString(){returnT4Immutable.Helpers.ToStringFor(nameof(Person),newSystem.Tuple<string,object>(nameof(this.FirstName),this.FirstName),newSystem.Tuple<string,object>(nameof(this.LastName),this.LastName),newSystem.Tuple<string,object>(nameof(this.Age),this.Age));}publicoverridestringToString(){returnImmutableToString();}privatePersonImmutableWith(T4Immutable.WithParam<string>firstName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<string>lastName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<int>age=default(T4Immutable.WithParam<int>)){returnnewPerson(!firstName.HasValue?this.FirstName:firstName.Value,!lastName.HasValue?this.LastName:lastName.Value,!age.HasValue?this.Age:age.Value);}publicPersonWith(T4Immutable.WithParam<string>firstName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<string>lastName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<int>age=default(T4Immutable.WithParam<int>)){returnImmutableWith(firstName,lastName,age);}}

About

T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

T4Immutable

###T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.

NuGet package

v1.1.2 release notes

  • 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.

v1.1.0 release notes

  • ImmutableClassOptions.EnableXXX/DisableXXX have been renamed to ImmutableClassOptions.IncludeXXX/ExcludeXXX
  • preConstructorParam code comment has been changed to the PreConstructorParam attribute

Why use this?

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)]classPerson{privateconstintAgeDefaultValue=18;publicstringFirstName{get;}publicstringLastName{get;}publicintAge{get;}[ComputedProperty]publicstringFullName{get{returnFirstName+" "+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) and Equals(Person other).
  • Working implementations for operator== and operator!=
  • 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)

How do I start?

Just install the T4Immutable nuget package.

What's needed to make an immutable class?

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.

I don't want X. Can I control what gets generated?

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.

Can I control the access level (public/private/...) of the constructor?

Yes. Do something like this:

[ImmutableClass(ConstructorAccessLevel = ConstructorAccessLevel.Private)]

Constructor post-initalization / validation

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:

privateint_Age;publicintAge{get{return_Age;}set{if(value<18)thrownewException("You are too young!");_Age=value;}}

Can I add extra attributes to each constructor parameter?

Yes, use the following when defining a property:

[PreConstructorParam("[JetBrains.Annotations.NotNull]")]publicstringFirstName{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]publicstringFirstName{get;}

The constructor will be this:

publicPerson(string firstName){// pre not null checkif(firstName==null)thrownewArgumentNullException(nameof(firstName));// assignations + PostConstructor() if needed// post not null checkif(this.FirstName==null)thrownewNullReferenceException(nameof(this.FirstName));}

Having said this, if you use JetBrains Annotations for null checking, you can also do this:

[JetBrains.Annotations.NotNull,ConstructorParamNotNull]publicstringFirstName{get;}

And the constructor will be this:

publicPerson([JetBrains.Annotations.NotNull]string firstName){// pre not null check is implied by ConstructorParamNotNullif(firstName==null)thrownewArgumentNullException(nameof(firstName));// assignations + PostConstructor() if needed// post not null check implied by JetBrains.Annotations.NotNull on the propertyif(this.FirstName==null)thrownewNullReferenceException(nameof(this.FirstName));}

Constructor overrides

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:

publicstaticPoint<T>FromAngleAndDistance(Tdistance,doubleangle){// your code herereturnnewPoint(x,y);}

How do I change the order of the arguments in the generated constructor?

Just change the order of the properties.

How to specify property default values?

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.

Does it work with generic classes? Custom methods? Nested classes?

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]classPoint{publicdoubleX{get;}publicdoubleY{get;}privatedouble_Distance;[ComputedProperty]publicdoubleDistance{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.

How do I rebuild the auto-generated files once I make a change in my code?

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¨.

Does Intellisense and all that stuff work after using this?

Absolutely, since the generated files are .cs files Intellisense will pick the syntax without problems.

Why doesn't it generate a builder?

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.

Can I suggest new features or whatever?

Please do!

Can I see the extra code generated for the very first example?

Here you go (excluding some redundant attributes):

usingSystem;partialclassPerson:IEquatable<Person>{[T4Immutable.GeneratedCode,System.CodeDom.Compiler.GeneratedCode("T4Immutable","1.1.1"),System.Diagnostics.DebuggerNonUserCode]publicPerson(stringfirstName,stringlastName,intage=18){this.FirstName=firstName;this.LastName=lastName;this.Age=age;_ImmutableHashCode=T4Immutable.Helpers.GetHashCodeFor(this.FirstName,this.LastName,this.Age);}privateboolImmutableEquals(Personobj){if(ReferenceEquals(this,obj))returntrue;if(ReferenceEquals(obj,null))returnfalse;returnT4Immutable.Helpers.AreEqual(this.FirstName,obj.FirstName)&&T4Immutable.Helpers.AreEqual(this.LastName,obj.LastName)&&T4Immutable.Helpers.AreEqual(this.Age,obj.Age);}publicoverrideboolEquals(objectobj){returnImmutableEquals(objasPerson);}publicboolEquals(Personobj){returnImmutableEquals(obj);}publicstaticbooloperator==(Persona,Personb){returnT4Immutable.Helpers.AreEqual(a,b);}publicstaticbooloperator!=(Persona,Personb){return!T4Immutable.Helpers.AreEqual(a,b);}privatereadonlyint_ImmutableHashCode;privateintImmutableGetHashCode(){return_ImmutableHashCode;}publicoverrideintGetHashCode(){returnImmutableGetHashCode();}privatestringImmutableToString(){returnT4Immutable.Helpers.ToStringFor(nameof(Person),newSystem.Tuple<string,object>(nameof(this.FirstName),this.FirstName),newSystem.Tuple<string,object>(nameof(this.LastName),this.LastName),newSystem.Tuple<string,object>(nameof(this.Age),this.Age));}publicoverridestringToString(){returnImmutableToString();}privatePersonImmutableWith(T4Immutable.WithParam<string>firstName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<string>lastName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<int>age=default(T4Immutable.WithParam<int>)){returnnewPerson(!firstName.HasValue?this.FirstName:firstName.Value,!lastName.HasValue?this.LastName:lastName.Value,!age.HasValue?this.Age:age.Value);}publicPersonWith(T4Immutable.WithParam<string>firstName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<string>lastName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<int>age=default(T4Immutable.WithParam<int>)){returnImmutableWith(firstName,lastName,age);}}

About

T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

T4Immutable

###T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.

NuGet package

v1.1.2 release notes

  • 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.

v1.1.0 release notes

  • ImmutableClassOptions.EnableXXX/DisableXXX have been renamed to ImmutableClassOptions.IncludeXXX/ExcludeXXX
  • preConstructorParam code comment has been changed to the PreConstructorParam attribute

Why use this?

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)]classPerson{privateconstintAgeDefaultValue=18;publicstringFirstName{get;}publicstringLastName{get;}publicintAge{get;}[ComputedProperty]publicstringFullName{get{returnFirstName+" "+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) and Equals(Person other).
  • Working implementations for operator== and operator!=
  • 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)

How do I start?

Just install the T4Immutable nuget package.

What's needed to make an immutable class?

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.

I don't want X. Can I control what gets generated?

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.

Can I control the access level (public/private/...) of the constructor?

Yes. Do something like this:

[ImmutableClass(ConstructorAccessLevel = ConstructorAccessLevel.Private)]

Constructor post-initalization / validation

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:

privateint_Age;publicintAge{get{return_Age;}set{if(value<18)thrownewException("You are too young!");_Age=value;}}

Can I add extra attributes to each constructor parameter?

Yes, use the following when defining a property:

[PreConstructorParam("[JetBrains.Annotations.NotNull]")]publicstringFirstName{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]publicstringFirstName{get;}

The constructor will be this:

publicPerson(string firstName){// pre not null checkif(firstName==null)thrownewArgumentNullException(nameof(firstName));// assignations + PostConstructor() if needed// post not null checkif(this.FirstName==null)thrownewNullReferenceException(nameof(this.FirstName));}

Having said this, if you use JetBrains Annotations for null checking, you can also do this:

[JetBrains.Annotations.NotNull,ConstructorParamNotNull]publicstringFirstName{get;}

And the constructor will be this:

publicPerson([JetBrains.Annotations.NotNull]string firstName){// pre not null check is implied by ConstructorParamNotNullif(firstName==null)thrownewArgumentNullException(nameof(firstName));// assignations + PostConstructor() if needed// post not null check implied by JetBrains.Annotations.NotNull on the propertyif(this.FirstName==null)thrownewNullReferenceException(nameof(this.FirstName));}

Constructor overrides

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:

publicstaticPoint<T>FromAngleAndDistance(Tdistance,doubleangle){// your code herereturnnewPoint(x,y);}

How do I change the order of the arguments in the generated constructor?

Just change the order of the properties.

How to specify property default values?

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.

Does it work with generic classes? Custom methods? Nested classes?

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]classPoint{publicdoubleX{get;}publicdoubleY{get;}privatedouble_Distance;[ComputedProperty]publicdoubleDistance{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.

How do I rebuild the auto-generated files once I make a change in my code?

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¨.

Does Intellisense and all that stuff work after using this?

Absolutely, since the generated files are .cs files Intellisense will pick the syntax without problems.

Why doesn't it generate a builder?

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.

Can I suggest new features or whatever?

Please do!

Can I see the extra code generated for the very first example?

Here you go (excluding some redundant attributes):

usingSystem;partialclassPerson:IEquatable<Person>{[T4Immutable.GeneratedCode,System.CodeDom.Compiler.GeneratedCode("T4Immutable","1.1.1"),System.Diagnostics.DebuggerNonUserCode]publicPerson(stringfirstName,stringlastName,intage=18){this.FirstName=firstName;this.LastName=lastName;this.Age=age;_ImmutableHashCode=T4Immutable.Helpers.GetHashCodeFor(this.FirstName,this.LastName,this.Age);}privateboolImmutableEquals(Personobj){if(ReferenceEquals(this,obj))returntrue;if(ReferenceEquals(obj,null))returnfalse;returnT4Immutable.Helpers.AreEqual(this.FirstName,obj.FirstName)&&T4Immutable.Helpers.AreEqual(this.LastName,obj.LastName)&&T4Immutable.Helpers.AreEqual(this.Age,obj.Age);}publicoverrideboolEquals(objectobj){returnImmutableEquals(objasPerson);}publicboolEquals(Personobj){returnImmutableEquals(obj);}publicstaticbooloperator==(Persona,Personb){returnT4Immutable.Helpers.AreEqual(a,b);}publicstaticbooloperator!=(Persona,Personb){return!T4Immutable.Helpers.AreEqual(a,b);}privatereadonlyint_ImmutableHashCode;privateintImmutableGetHashCode(){return_ImmutableHashCode;}publicoverrideintGetHashCode(){returnImmutableGetHashCode();}privatestringImmutableToString(){returnT4Immutable.Helpers.ToStringFor(nameof(Person),newSystem.Tuple<string,object>(nameof(this.FirstName),this.FirstName),newSystem.Tuple<string,object>(nameof(this.LastName),this.LastName),newSystem.Tuple<string,object>(nameof(this.Age),this.Age));}publicoverridestringToString(){returnImmutableToString();}privatePersonImmutableWith(T4Immutable.WithParam<string>firstName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<string>lastName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<int>age=default(T4Immutable.WithParam<int>)){returnnewPerson(!firstName.HasValue?this.FirstName:firstName.Value,!lastName.HasValue?this.LastName:lastName.Value,!age.HasValue?this.Age:age.Value);}publicPersonWith(T4Immutable.WithParam<string>firstName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<string>lastName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<int>age=default(T4Immutable.WithParam<int>)){returnImmutableWith(firstName,lastName,age);}}

About

T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

T4Immutable

###T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.

NuGet package

v1.1.2 release notes

  • 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.

v1.1.0 release notes

  • ImmutableClassOptions.EnableXXX/DisableXXX have been renamed to ImmutableClassOptions.IncludeXXX/ExcludeXXX
  • preConstructorParam code comment has been changed to the PreConstructorParam attribute

Why use this?

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)]classPerson{privateconstintAgeDefaultValue=18;publicstringFirstName{get;}publicstringLastName{get;}publicintAge{get;}[ComputedProperty]publicstringFullName{get{returnFirstName+" "+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) and Equals(Person other).
  • Working implementations for operator== and operator!=
  • 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)

How do I start?

Just install the T4Immutable nuget package.

What's needed to make an immutable class?

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.

I don't want X. Can I control what gets generated?

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.

Can I control the access level (public/private/...) of the constructor?

Yes. Do something like this:

[ImmutableClass(ConstructorAccessLevel = ConstructorAccessLevel.Private)]

Constructor post-initalization / validation

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:

privateint_Age;publicintAge{get{return_Age;}set{if(value<18)thrownewException("You are too young!");_Age=value;}}

Can I add extra attributes to each constructor parameter?

Yes, use the following when defining a property:

[PreConstructorParam("[JetBrains.Annotations.NotNull]")]publicstringFirstName{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]publicstringFirstName{get;}

The constructor will be this:

publicPerson(string firstName){// pre not null checkif(firstName==null)thrownewArgumentNullException(nameof(firstName));// assignations + PostConstructor() if needed// post not null checkif(this.FirstName==null)thrownewNullReferenceException(nameof(this.FirstName));}

Having said this, if you use JetBrains Annotations for null checking, you can also do this:

[JetBrains.Annotations.NotNull,ConstructorParamNotNull]publicstringFirstName{get;}

And the constructor will be this:

publicPerson([JetBrains.Annotations.NotNull]string firstName){// pre not null check is implied by ConstructorParamNotNullif(firstName==null)thrownewArgumentNullException(nameof(firstName));// assignations + PostConstructor() if needed// post not null check implied by JetBrains.Annotations.NotNull on the propertyif(this.FirstName==null)thrownewNullReferenceException(nameof(this.FirstName));}

Constructor overrides

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:

publicstaticPoint<T>FromAngleAndDistance(Tdistance,doubleangle){// your code herereturnnewPoint(x,y);}

How do I change the order of the arguments in the generated constructor?

Just change the order of the properties.

How to specify property default values?

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.

Does it work with generic classes? Custom methods? Nested classes?

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]classPoint{publicdoubleX{get;}publicdoubleY{get;}privatedouble_Distance;[ComputedProperty]publicdoubleDistance{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.

How do I rebuild the auto-generated files once I make a change in my code?

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¨.

Does Intellisense and all that stuff work after using this?

Absolutely, since the generated files are .cs files Intellisense will pick the syntax without problems.

Why doesn't it generate a builder?

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.

Can I suggest new features or whatever?

Please do!

Can I see the extra code generated for the very first example?

Here you go (excluding some redundant attributes):

usingSystem;partialclassPerson:IEquatable<Person>{[T4Immutable.GeneratedCode,System.CodeDom.Compiler.GeneratedCode("T4Immutable","1.1.1"),System.Diagnostics.DebuggerNonUserCode]publicPerson(stringfirstName,stringlastName,intage=18){this.FirstName=firstName;this.LastName=lastName;this.Age=age;_ImmutableHashCode=T4Immutable.Helpers.GetHashCodeFor(this.FirstName,this.LastName,this.Age);}privateboolImmutableEquals(Personobj){if(ReferenceEquals(this,obj))returntrue;if(ReferenceEquals(obj,null))returnfalse;returnT4Immutable.Helpers.AreEqual(this.FirstName,obj.FirstName)&&T4Immutable.Helpers.AreEqual(this.LastName,obj.LastName)&&T4Immutable.Helpers.AreEqual(this.Age,obj.Age);}publicoverrideboolEquals(objectobj){returnImmutableEquals(objasPerson);}publicboolEquals(Personobj){returnImmutableEquals(obj);}publicstaticbooloperator==(Persona,Personb){returnT4Immutable.Helpers.AreEqual(a,b);}publicstaticbooloperator!=(Persona,Personb){return!T4Immutable.Helpers.AreEqual(a,b);}privatereadonlyint_ImmutableHashCode;privateintImmutableGetHashCode(){return_ImmutableHashCode;}publicoverrideintGetHashCode(){returnImmutableGetHashCode();}privatestringImmutableToString(){returnT4Immutable.Helpers.ToStringFor(nameof(Person),newSystem.Tuple<string,object>(nameof(this.FirstName),this.FirstName),newSystem.Tuple<string,object>(nameof(this.LastName),this.LastName),newSystem.Tuple<string,object>(nameof(this.Age),this.Age));}publicoverridestringToString(){returnImmutableToString();}privatePersonImmutableWith(T4Immutable.WithParam<string>firstName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<string>lastName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<int>age=default(T4Immutable.WithParam<int>)){returnnewPerson(!firstName.HasValue?this.FirstName:firstName.Value,!lastName.HasValue?this.LastName:lastName.Value,!age.HasValue?this.Age:age.Value);}publicPersonWith(T4Immutable.WithParam<string>firstName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<string>lastName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<int>age=default(T4Immutable.WithParam<int>)){returnImmutableWith(firstName,lastName,age);}}

About

T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

T4Immutable

###T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.

NuGet package

v1.1.2 release notes

  • 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.

v1.1.0 release notes

  • ImmutableClassOptions.EnableXXX/DisableXXX have been renamed to ImmutableClassOptions.IncludeXXX/ExcludeXXX
  • preConstructorParam code comment has been changed to the PreConstructorParam attribute

Why use this?

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)]classPerson{privateconstintAgeDefaultValue=18;publicstringFirstName{get;}publicstringLastName{get;}publicintAge{get;}[ComputedProperty]publicstringFullName{get{returnFirstName+" "+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) and Equals(Person other).
  • Working implementations for operator== and operator!=
  • 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)

How do I start?

Just install the T4Immutable nuget package.

What's needed to make an immutable class?

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.

I don't want X. Can I control what gets generated?

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.

Can I control the access level (public/private/...) of the constructor?

Yes. Do something like this:

[ImmutableClass(ConstructorAccessLevel = ConstructorAccessLevel.Private)]

Constructor post-initalization / validation

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:

privateint_Age;publicintAge{get{return_Age;}set{if(value<18)thrownewException("You are too young!");_Age=value;}}

Can I add extra attributes to each constructor parameter?

Yes, use the following when defining a property:

[PreConstructorParam("[JetBrains.Annotations.NotNull]")]publicstringFirstName{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]publicstringFirstName{get;}

The constructor will be this:

publicPerson(string firstName){// pre not null checkif(firstName==null)thrownewArgumentNullException(nameof(firstName));// assignations + PostConstructor() if needed// post not null checkif(this.FirstName==null)thrownewNullReferenceException(nameof(this.FirstName));}

Having said this, if you use JetBrains Annotations for null checking, you can also do this:

[JetBrains.Annotations.NotNull,ConstructorParamNotNull]publicstringFirstName{get;}

And the constructor will be this:

publicPerson([JetBrains.Annotations.NotNull]string firstName){// pre not null check is implied by ConstructorParamNotNullif(firstName==null)thrownewArgumentNullException(nameof(firstName));// assignations + PostConstructor() if needed// post not null check implied by JetBrains.Annotations.NotNull on the propertyif(this.FirstName==null)thrownewNullReferenceException(nameof(this.FirstName));}

Constructor overrides

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:

publicstaticPoint<T>FromAngleAndDistance(Tdistance,doubleangle){// your code herereturnnewPoint(x,y);}

How do I change the order of the arguments in the generated constructor?

Just change the order of the properties.

How to specify property default values?

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.

Does it work with generic classes? Custom methods? Nested classes?

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]classPoint{publicdoubleX{get;}publicdoubleY{get;}privatedouble_Distance;[ComputedProperty]publicdoubleDistance{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.

How do I rebuild the auto-generated files once I make a change in my code?

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¨.

Does Intellisense and all that stuff work after using this?

Absolutely, since the generated files are .cs files Intellisense will pick the syntax without problems.

Why doesn't it generate a builder?

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.

Can I suggest new features or whatever?

Please do!

Can I see the extra code generated for the very first example?

Here you go (excluding some redundant attributes):

usingSystem;partialclassPerson:IEquatable<Person>{[T4Immutable.GeneratedCode,System.CodeDom.Compiler.GeneratedCode("T4Immutable","1.1.1"),System.Diagnostics.DebuggerNonUserCode]publicPerson(stringfirstName,stringlastName,intage=18){this.FirstName=firstName;this.LastName=lastName;this.Age=age;_ImmutableHashCode=T4Immutable.Helpers.GetHashCodeFor(this.FirstName,this.LastName,this.Age);}privateboolImmutableEquals(Personobj){if(ReferenceEquals(this,obj))returntrue;if(ReferenceEquals(obj,null))returnfalse;returnT4Immutable.Helpers.AreEqual(this.FirstName,obj.FirstName)&&T4Immutable.Helpers.AreEqual(this.LastName,obj.LastName)&&T4Immutable.Helpers.AreEqual(this.Age,obj.Age);}publicoverrideboolEquals(objectobj){returnImmutableEquals(objasPerson);}publicboolEquals(Personobj){returnImmutableEquals(obj);}publicstaticbooloperator==(Persona,Personb){returnT4Immutable.Helpers.AreEqual(a,b);}publicstaticbooloperator!=(Persona,Personb){return!T4Immutable.Helpers.AreEqual(a,b);}privatereadonlyint_ImmutableHashCode;privateintImmutableGetHashCode(){return_ImmutableHashCode;}publicoverrideintGetHashCode(){returnImmutableGetHashCode();}privatestringImmutableToString(){returnT4Immutable.Helpers.ToStringFor(nameof(Person),newSystem.Tuple<string,object>(nameof(this.FirstName),this.FirstName),newSystem.Tuple<string,object>(nameof(this.LastName),this.LastName),newSystem.Tuple<string,object>(nameof(this.Age),this.Age));}publicoverridestringToString(){returnImmutableToString();}privatePersonImmutableWith(T4Immutable.WithParam<string>firstName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<string>lastName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<int>age=default(T4Immutable.WithParam<int>)){returnnewPerson(!firstName.HasValue?this.FirstName:firstName.Value,!lastName.HasValue?this.LastName:lastName.Value,!age.HasValue?this.Age:age.Value);}publicPersonWith(T4Immutable.WithParam<string>firstName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<string>lastName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<int>age=default(T4Immutable.WithParam<int>)){returnImmutableWith(firstName,lastName,age);}}

About

T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

T4Immutable

###T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.

NuGet package

v1.1.2 release notes

  • 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.

v1.1.0 release notes

  • ImmutableClassOptions.EnableXXX/DisableXXX have been renamed to ImmutableClassOptions.IncludeXXX/ExcludeXXX
  • preConstructorParam code comment has been changed to the PreConstructorParam attribute

Why use this?

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)]classPerson{privateconstintAgeDefaultValue=18;publicstringFirstName{get;}publicstringLastName{get;}publicintAge{get;}[ComputedProperty]publicstringFullName{get{returnFirstName+" "+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) and Equals(Person other).
  • Working implementations for operator== and operator!=
  • 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)

How do I start?

Just install the T4Immutable nuget package.

What's needed to make an immutable class?

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.

I don't want X. Can I control what gets generated?

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.

Can I control the access level (public/private/...) of the constructor?

Yes. Do something like this:

[ImmutableClass(ConstructorAccessLevel = ConstructorAccessLevel.Private)]

Constructor post-initalization / validation

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:

privateint_Age;publicintAge{get{return_Age;}set{if(value<18)thrownewException("You are too young!");_Age=value;}}

Can I add extra attributes to each constructor parameter?

Yes, use the following when defining a property:

[PreConstructorParam("[JetBrains.Annotations.NotNull]")]publicstringFirstName{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]publicstringFirstName{get;}

The constructor will be this:

publicPerson(string firstName){// pre not null checkif(firstName==null)thrownewArgumentNullException(nameof(firstName));// assignations + PostConstructor() if needed// post not null checkif(this.FirstName==null)thrownewNullReferenceException(nameof(this.FirstName));}

Having said this, if you use JetBrains Annotations for null checking, you can also do this:

[JetBrains.Annotations.NotNull,ConstructorParamNotNull]publicstringFirstName{get;}

And the constructor will be this:

publicPerson([JetBrains.Annotations.NotNull]string firstName){// pre not null check is implied by ConstructorParamNotNullif(firstName==null)thrownewArgumentNullException(nameof(firstName));// assignations + PostConstructor() if needed// post not null check implied by JetBrains.Annotations.NotNull on the propertyif(this.FirstName==null)thrownewNullReferenceException(nameof(this.FirstName));}

Constructor overrides

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:

publicstaticPoint<T>FromAngleAndDistance(Tdistance,doubleangle){// your code herereturnnewPoint(x,y);}

How do I change the order of the arguments in the generated constructor?

Just change the order of the properties.

How to specify property default values?

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.

Does it work with generic classes? Custom methods? Nested classes?

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]classPoint{publicdoubleX{get;}publicdoubleY{get;}privatedouble_Distance;[ComputedProperty]publicdoubleDistance{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.

How do I rebuild the auto-generated files once I make a change in my code?

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¨.

Does Intellisense and all that stuff work after using this?

Absolutely, since the generated files are .cs files Intellisense will pick the syntax without problems.

Why doesn't it generate a builder?

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.

Can I suggest new features or whatever?

Please do!

Can I see the extra code generated for the very first example?

Here you go (excluding some redundant attributes):

usingSystem;partialclassPerson:IEquatable<Person>{[T4Immutable.GeneratedCode,System.CodeDom.Compiler.GeneratedCode("T4Immutable","1.1.1"),System.Diagnostics.DebuggerNonUserCode]publicPerson(stringfirstName,stringlastName,intage=18){this.FirstName=firstName;this.LastName=lastName;this.Age=age;_ImmutableHashCode=T4Immutable.Helpers.GetHashCodeFor(this.FirstName,this.LastName,this.Age);}privateboolImmutableEquals(Personobj){if(ReferenceEquals(this,obj))returntrue;if(ReferenceEquals(obj,null))returnfalse;returnT4Immutable.Helpers.AreEqual(this.FirstName,obj.FirstName)&&T4Immutable.Helpers.AreEqual(this.LastName,obj.LastName)&&T4Immutable.Helpers.AreEqual(this.Age,obj.Age);}publicoverrideboolEquals(objectobj){returnImmutableEquals(objasPerson);}publicboolEquals(Personobj){returnImmutableEquals(obj);}publicstaticbooloperator==(Persona,Personb){returnT4Immutable.Helpers.AreEqual(a,b);}publicstaticbooloperator!=(Persona,Personb){return!T4Immutable.Helpers.AreEqual(a,b);}privatereadonlyint_ImmutableHashCode;privateintImmutableGetHashCode(){return_ImmutableHashCode;}publicoverrideintGetHashCode(){returnImmutableGetHashCode();}privatestringImmutableToString(){returnT4Immutable.Helpers.ToStringFor(nameof(Person),newSystem.Tuple<string,object>(nameof(this.FirstName),this.FirstName),newSystem.Tuple<string,object>(nameof(this.LastName),this.LastName),newSystem.Tuple<string,object>(nameof(this.Age),this.Age));}publicoverridestringToString(){returnImmutableToString();}privatePersonImmutableWith(T4Immutable.WithParam<string>firstName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<string>lastName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<int>age=default(T4Immutable.WithParam<int>)){returnnewPerson(!firstName.HasValue?this.FirstName:firstName.Value,!lastName.HasValue?this.LastName:lastName.Value,!age.HasValue?this.Age:age.Value);}publicPersonWith(T4Immutable.WithParam<string>firstName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<string>lastName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<int>age=default(T4Immutable.WithParam<int>)){returnImmutableWith(firstName,lastName,age);}}

About

T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

T4Immutable

###T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.

NuGet package

v1.1.2 release notes

  • 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.

v1.1.0 release notes

  • ImmutableClassOptions.EnableXXX/DisableXXX have been renamed to ImmutableClassOptions.IncludeXXX/ExcludeXXX
  • preConstructorParam code comment has been changed to the PreConstructorParam attribute

Why use this?

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)]classPerson{privateconstintAgeDefaultValue=18;publicstringFirstName{get;}publicstringLastName{get;}publicintAge{get;}[ComputedProperty]publicstringFullName{get{returnFirstName+" "+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) and Equals(Person other).
  • Working implementations for operator== and operator!=
  • 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)

How do I start?

Just install the T4Immutable nuget package.

What's needed to make an immutable class?

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.

I don't want X. Can I control what gets generated?

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.

Can I control the access level (public/private/...) of the constructor?

Yes. Do something like this:

[ImmutableClass(ConstructorAccessLevel = ConstructorAccessLevel.Private)]

Constructor post-initalization / validation

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:

privateint_Age;publicintAge{get{return_Age;}set{if(value<18)thrownewException("You are too young!");_Age=value;}}

Can I add extra attributes to each constructor parameter?

Yes, use the following when defining a property:

[PreConstructorParam("[JetBrains.Annotations.NotNull]")]publicstringFirstName{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]publicstringFirstName{get;}

The constructor will be this:

publicPerson(string firstName){// pre not null checkif(firstName==null)thrownewArgumentNullException(nameof(firstName));// assignations + PostConstructor() if needed// post not null checkif(this.FirstName==null)thrownewNullReferenceException(nameof(this.FirstName));}

Having said this, if you use JetBrains Annotations for null checking, you can also do this:

[JetBrains.Annotations.NotNull,ConstructorParamNotNull]publicstringFirstName{get;}

And the constructor will be this:

publicPerson([JetBrains.Annotations.NotNull]string firstName){// pre not null check is implied by ConstructorParamNotNullif(firstName==null)thrownewArgumentNullException(nameof(firstName));// assignations + PostConstructor() if needed// post not null check implied by JetBrains.Annotations.NotNull on the propertyif(this.FirstName==null)thrownewNullReferenceException(nameof(this.FirstName));}

Constructor overrides

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:

publicstaticPoint<T>FromAngleAndDistance(Tdistance,doubleangle){// your code herereturnnewPoint(x,y);}

How do I change the order of the arguments in the generated constructor?

Just change the order of the properties.

How to specify property default values?

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.

Does it work with generic classes? Custom methods? Nested classes?

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]classPoint{publicdoubleX{get;}publicdoubleY{get;}privatedouble_Distance;[ComputedProperty]publicdoubleDistance{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.

How do I rebuild the auto-generated files once I make a change in my code?

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¨.

Does Intellisense and all that stuff work after using this?

Absolutely, since the generated files are .cs files Intellisense will pick the syntax without problems.

Why doesn't it generate a builder?

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.

Can I suggest new features or whatever?

Please do!

Can I see the extra code generated for the very first example?

Here you go (excluding some redundant attributes):

usingSystem;partialclassPerson:IEquatable<Person>{[T4Immutable.GeneratedCode,System.CodeDom.Compiler.GeneratedCode("T4Immutable","1.1.1"),System.Diagnostics.DebuggerNonUserCode]publicPerson(stringfirstName,stringlastName,intage=18){this.FirstName=firstName;this.LastName=lastName;this.Age=age;_ImmutableHashCode=T4Immutable.Helpers.GetHashCodeFor(this.FirstName,this.LastName,this.Age);}privateboolImmutableEquals(Personobj){if(ReferenceEquals(this,obj))returntrue;if(ReferenceEquals(obj,null))returnfalse;returnT4Immutable.Helpers.AreEqual(this.FirstName,obj.FirstName)&&T4Immutable.Helpers.AreEqual(this.LastName,obj.LastName)&&T4Immutable.Helpers.AreEqual(this.Age,obj.Age);}publicoverrideboolEquals(objectobj){returnImmutableEquals(objasPerson);}publicboolEquals(Personobj){returnImmutableEquals(obj);}publicstaticbooloperator==(Persona,Personb){returnT4Immutable.Helpers.AreEqual(a,b);}publicstaticbooloperator!=(Persona,Personb){return!T4Immutable.Helpers.AreEqual(a,b);}privatereadonlyint_ImmutableHashCode;privateintImmutableGetHashCode(){return_ImmutableHashCode;}publicoverrideintGetHashCode(){returnImmutableGetHashCode();}privatestringImmutableToString(){returnT4Immutable.Helpers.ToStringFor(nameof(Person),newSystem.Tuple<string,object>(nameof(this.FirstName),this.FirstName),newSystem.Tuple<string,object>(nameof(this.LastName),this.LastName),newSystem.Tuple<string,object>(nameof(this.Age),this.Age));}publicoverridestringToString(){returnImmutableToString();}privatePersonImmutableWith(T4Immutable.WithParam<string>firstName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<string>lastName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<int>age=default(T4Immutable.WithParam<int>)){returnnewPerson(!firstName.HasValue?this.FirstName:firstName.Value,!lastName.HasValue?this.LastName:lastName.Value,!age.HasValue?this.Age:age.Value);}publicPersonWith(T4Immutable.WithParam<string>firstName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<string>lastName=default(T4Immutable.WithParam<string>),T4Immutable.WithParam<int>age=default(T4Immutable.WithParam<int>)){returnImmutableWith(firstName,lastName,age);}}

About

T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages