Property and Field Validation
Overview
Section titled “Overview”When a public set is sneakily added to a domain class, immutability breaks. Even if a setter is added to Product.Price, compilation succeeds and existing tests pass since they only read values. The problem only surfaces in production when someone calls product.Price = -100. In this chapter, you will learn how to verify architecture rules on class properties and fields — enforcing rules like public setter prohibition, instance field prohibition, and primitive-type-only constraints through tests.
“Immutability is not sufficient with just getter-only properties and private constructors. It must be continuously verified through architecture tests.”
Learning Objectives
Section titled “Learning Objectives”Core Learning Goals
Section titled “Core Learning Goals”-
Required property existence verification
- Guarantee core domain model properties with
RequireProperty(name) - Immediately detect if a property is accidentally removed or renamed
- Guarantee core domain model properties with
-
Immutability enforcement
- Prohibit public setters in domain classes with
RequireNoPublicSetters() - Restrict instance field usage with
RequireNoInstanceFields()(backing fields automatically excluded)
- Prohibit public setters in domain classes with
-
Property type restriction
- Allow only primitive types with
RequireOnlyPrimitiveProperties() - Extension options to specify additional allowed types
- Allow only primitive types with
What You Will Verify Through Practice
Section titled “What You Will Verify Through Practice”- Product: Verify
Name,Priceproperty existence and public setter prohibition - OrderLine: Verify instance field prohibition and primitive type properties
- ProductViewModel: Example excluded from domain rule scope
Domain Code
Section titled “Domain Code”Product / OrderLine Classes
Section titled “Product / OrderLine Classes”Domain classes that ensure immutability through getter-only properties and private constructors.
public sealed class Product{ public string Name { get; } public decimal Price { get; } public int Quantity { get; }
private Product(string name, decimal price, int quantity) { Name = name; Price = price; Quantity = quantity; }
public static Product Create(string name, decimal price, int quantity) => new(name, price, quantity);}ProductViewModel Class
Section titled “ProductViewModel Class”Unlike domain classes, ViewModels have public setters. Since this class resides in the ViewModels namespace rather than the Domains namespace, it is not subject to domain rules.
public class ProductViewModel{ public string Name { get; set; } = string.Empty; public decimal Price { get; set; } public int Quantity { get; set; }}Test Code
Section titled “Test Code”Required Property Existence Verification
Section titled “Required Property Existence Verification”Verify that specific properties must exist using RequireProperty.
[Fact]public void Product_ShouldHave_NameAndPriceProperties(){ ArchRuleDefinition .Classes() .That() .ResideInNamespace("PropertyAndFieldValidation.Domains") .And() .HaveNameEndingWith("Product") .ValidateAllClasses(Architecture, @class => @class .RequireProperty("Name") .RequireProperty("Price"), verbose: true) .ThrowIfAnyFailures("Product Property Rule");}Public Setter Prohibition
Section titled “Public Setter Prohibition”Enforce immutability by prohibiting public setters in domain classes.
[Fact]public void DomainClasses_ShouldNotHave_PublicSetters(){ ArchRuleDefinition .Classes() .That() .ResideInNamespace("PropertyAndFieldValidation.Domains") .ValidateAllClasses(Architecture, @class => @class .RequireNoPublicSetters(), verbose: true) .ThrowIfAnyFailures("No Public Setter Rule");}Instance Field Prohibition
Section titled “Instance Field Prohibition”Prohibit instance field usage in domain classes. Backing fields automatically generated by the compiler are excluded.
[Fact]public void DomainClasses_ShouldNotHave_InstanceFields(){ ArchRuleDefinition .Classes() .That() .ResideInNamespace("PropertyAndFieldValidation.Domains") .ValidateAllClasses(Architecture, @class => @class .RequireNoInstanceFields(), verbose: true) .ThrowIfAnyFailures("No Instance Field Rule");}Primitive Types Only
Section titled “Primitive Types Only”Verify that domain class properties use only primitive types (string, int, decimal, double, etc.).
[Fact]public void DomainClasses_ShouldHave_OnlyPrimitiveProperties(){ ArchRuleDefinition .Classes() .That() .ResideInNamespace("PropertyAndFieldValidation.Domains") .ValidateAllClasses(Architecture, @class => @class .RequireOnlyPrimitiveProperties(), verbose: true) .ThrowIfAnyFailures("Primitive Property Rule");}Summary at a Glance
Section titled “Summary at a Glance”The following table summarizes the property/field verification APIs and the design principles they protect.
Property/Field Verification API Summary
Section titled “Property/Field Verification API Summary”| API | Design Principle Protected | Meaning When Violated |
|---|---|---|
RequireProperty(name) | Domain model completeness | Core property is missing |
RequireNoPublicSetters() | Immutability | State can be changed externally |
RequireNoInstanceFields() | Encapsulation | State exposed through fields instead of properties |
RequireOnlyPrimitiveProperties(additionalAllowed) | Type safety | Complex types penetrating the domain |
Domain vs Non-Domain Rule Application
Section titled “Domain vs Non-Domain Rule Application”| Aspect | Domain Classes (Domains NS) | ViewModel (ViewModels NS) |
|---|---|---|
| Public Setter | Prohibited | Allowed |
| Instance Fields | Prohibited | No restriction |
| Property Types | Primitive types only | No restriction |
| Application Method | Automatically separated by namespace filter | Outside rule scope |
Q1: Does RequireNoPublicSetters also prohibit init setters?
Section titled “Q1: Does RequireNoPublicSetters also prohibit init setters?”A: init setters can only set values during object initialization, so they do not harm immutability. RequireNoPublicSetters() verifies only public set and allows init setters. This is compatible with C# record types and the required init pattern.
Q2: How does RequireNoInstanceFields automatically exclude backing fields?
Section titled “Q2: How does RequireNoInstanceFields automatically exclude backing fields?”A: The C# compiler automatically generates backing fields with names like <Name>k__BackingField for auto-properties (public string Name { get; }). RequireNoInstanceFields() detects these compiler-generated fields by name pattern and excludes them from verification targets. Only instance fields explicitly declared by developers are reported as violations.
Q3: How are additional allowed types specified in RequireOnlyPrimitiveProperties?
Section titled “Q3: How are additional allowed types specified in RequireOnlyPrimitiveProperties?”A: Pass the full names of additional allowed types as strings like RequireOnlyPrimitiveProperties("System.DateTime", "System.Guid"). The specified types are allowed in addition to the default primitive types (string, int, decimal, double, bool, etc.).
Q4: Why are domain rules not applied to ViewModels?
Section titled “Q4: Why are domain rules not applied to ViewModels?”A: ViewModels are data transfer objects for UI binding and require public set for two-way binding. Applying domain class immutability rules to ViewModels would break compatibility with UI frameworks. Using the ResideInNamespace("...Domains") filter to target only the domain namespace naturally achieves this separation.
Q5: Can multiple verifications be combined in a single test?
Section titled “Q5: Can multiple verifications be combined in a single test?”A: Yes, you can chain them like @class.RequireProperty("Name").RequireNoPublicSetters().RequireNoInstanceFields(). However, it is better to separate tests when their verification purposes differ, because when a test fails, you can immediately identify which rule was violated.
With property and field verification, you can now continuously ensure domain class immutability. The next Part covers advanced techniques including RequireImmutable() with 6-dimension immutability verification, nested classes, interface verification, and custom rule composition.