Record should be immutable
C#9 record is meant for value-based semantic: value-based equality and immutability, explained here: https://blog.ndepend.com/c9-records-immutable-classes/
A property setter not declared init still allows mutable state, which disrupts the value-based semantics: mutating a record used as a hash table key can corrupt the table, and equality results become unexpected — bugs difficult to spot and fix.
A property setter not declared init still allows mutable state, which disrupts the value-based semantics: mutating a record used as a hash table key can corrupt the table, and equality results become unexpected — bugs difficult to spot and fix.
How to fix
Make the record immutable: transform mutable property setters (culpritSetters) into init property initializers, and refactor their callers (methodsCallingCulpritSetters) to stop changing record states.
Estimated Debt: 8 minutes plus 3 minutes per mutable setter and per method calling one.
Estimated Debt: 8 minutes plus 3 minutes per mutable setter and per method calling one.
CQLinq source
This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.
// <Name>Record should be immutable</Name>
// <Id>ND1913:RecordClassesShouldBeImmutable</Id>
warnif count > 0
from t in Application.Types
where t.IsRecord &&
// Don't match record struct because when declared
// with primary constructor it is not immutable
t.IsClass &&
!t.IsImmutable
let culpritSetters = t.InstanceMethods.Where(
m => m.IsPropertySetter && !m.IsPropertyInit)
let methodsCallingCulpritSetters = culpritSetters.SelectMany(
m => m.MethodsCallingMe.Where(mc => mc.ParentType != t) )
select new { t,
culpritSetters,
methodsCallingCulpritSetters ,
Debt = (8 + 3*methodsCallingCulpritSetters.Count()
+ 3*methodsCallingCulpritSetters.Count())
.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The *record {0}* is mutable because of the *{1}*.
// It is recommended to refrain from including mutable states
// in a record as it disrupts the *value-based semantics*.
//</Expl>
//<Desc>
// C#9 **record** is meant for **value-based semantic**: **value-based
// equality** and **immutability**, explained here:
// https://blog.ndepend.com/c9-records-immutable-classes/
//
// A property setter not declared *init* still allows mutable state, which
// disrupts the value-based semantics: mutating a record used as a hash
// table key can corrupt the table, and equality results become unexpected —
// bugs difficult to spot and fix.
//</Desc>
//<HowToFix>
// Make the record immutable: transform mutable property setters
// (*culpritSetters*) into *init* property initializers, and refactor their
// callers (*methodsCallingCulpritSetters*) to stop changing record states.
//
// Estimated Debt: 8 minutes plus 3 minutes per mutable setter and per
// method calling one.
//</HowToFix>
Open in the interactive Rules Explorer · All default rules
Immutability: all 15 items
- Fields should be marked as ReadOnly when possible ND1900
- Avoid non-readonly static fields ND1901
- Avoid static fields with a mutable field type ND1902
- Structures should be immutable ND1903
- Immutable struct should be declared as readonly ND1914
- Property Getters should be pure ND1904
- A field must not be assigned from outside its parent hierarchy types ND1905
- Don't assign a field from many methods ND1906
- Do not declare read only fields with mutable reference types ND1907
- Public read only array fields can be modified ND1908
- Types tagged with ImmutableAttribute must be immutable ND1909
- Types immutable should be tagged with ImmutableAttribute ND1910
- Methods tagged with PureAttribute must be pure ND1911
- Pure methods should be tagged with PureAttribute ND1912
- Record should be immutable ND1913