Skip to content

Comments

Add stateless type support for ad-hoc discriminated unions #17

Merged
PawelGerr merged 2 commits intomasterfrom
features/stateless-type-members
Feb 3, 2026
Merged

Add stateless type support for ad-hoc discriminated unions #17
PawelGerr merged 2 commits intomasterfrom
features/stateless-type-members

Conversation

@PawelGerr
Copy link
Owner

Summary

Adds the ability to mark type members of ad-hoc discriminated unions as "stateless" for memory optimization. Stateless types carry no instance data—only the discriminator
index is stored in the union, while member accessors return default(T).

What's New

API Changes

  • Added T1IsStateless, T2IsStateless, T3IsStateless, T4IsStateless, T5IsStateless properties to both:
    • Generic UnionAttribute<T1, T2, ...>
    • Non-generic AdHocUnionAttribute(...)
  • When a member is marked stateless:
    • No backing field is allocated for that member
    • IsT properties work normally (discriminator-based)
    • AsT accessors return default(T)
    • For reference types, automatically sets TXIsNullableReferenceType = true

Example Usage

// API response with stateless error states                                                                                                                               
[Union<SuccessResponse, NotFound, Unauthorized>(                                                                                                                          
   T1Name = "Success",                                                                                                                                                    
   T2Name = "NotFound", T2IsStateless = true,                                                                                                                             
   T3Name = "Unauthorized", T3IsStateless = true)]                                                                                                                        
public partial class ApiResponse;                                                                                                                                         
                                                                                                                                                                          
public sealed class SuccessResponse                                                                                                                                       
{                                                                                                                                                                         
   public required string Data { get; init; }                                                                                                                             
}                                                                                                                                                                         
                                                                                                                                                                          
// Stateless types - prefer structs to avoid null-handling                                                                                                                
public readonly record struct NotFound;                                                                                                                                   
public readonly record struct Unauthorized;                                                                                                                               
                                                                                                                                                                          
// Usage                                                                                                                                                                  
ApiResponse response = new NotFound(); // Only stores discriminator, not NotFound instance                                                                                
response.IsNotFound; // true                                                                                                                                              
response.AsNotFound; // returns default(NotFound)                                                                                                                         
                                                                                                                                                                          
Benefits                                                                                                                                                                  
                                                                                                                                                                          
- Memory efficiency: Unions with stateless members (e.g., sentinel/marker types) only store the discriminator without allocating backing fields                           
- Type safety: Full pattern matching support (Switch/Map) with stateless types                                                                                            
- Clean API: Ideal for representing states like "NotFound", "Unauthorized", "Empty", etc.                                                                                 
                                                                                                                                                                          
Best Practice                                                                                                                                                             
                                                                                                                                                                          
Use struct types for stateless members to avoid null-handling complexity, since default(struct) is a valid value while default(class) is null.                            
                                                                                                                                                                          
Changes                                                                                                                                                                   
                                                                                                                                                                          
- Source generator updates for ad-hoc unions to handle stateless members                                                                                                  
- Comprehensive test coverage (15+ new snapshot tests)                                                                                                                    
- Demo code showing practical API response scenarios                                                                                                                      
- Documentation updates in CLAUDE.md and CLAUDE-ATTRIBUTES.md                                                                                                             
                                                                                                                                                                          
Testing                                                                                                                                                                   
                                                                                                                                                                          
All existing tests pass. Added extensive new tests covering:                                                                                                              
- Equality comparison with stateless members                                                                                                                              
- Pattern matching (Switch/Map) with stateless types                                                                                                                      
- Implicit conversions                                                                                                                                                    
- Hash code generation                                                                                                                                                    
- Various combinations of class/struct stateless types    

Copilot AI review requested due to automatic review settings January 30, 2026 07:29
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for marking type members of ad-hoc discriminated unions as "stateless" to optimize memory usage. Stateless types store only the discriminator index without allocating backing fields, with member accessors returning default(T).

Changes:

  • Added T1IsStateless through T5IsStateless properties to both generic and non-generic union attributes
  • Modified source generator to skip backing field allocation for stateless members and return default(T) from accessors
  • Added comprehensive test coverage (15+ new snapshot tests) for equality, pattern matching, hash codes, and conversions

Reviewed changes

Copilot reviewed 56 out of 56 changed files in this pull request and generated no comments.

Show a summary per file
File Description
AdHocUnionAttribute.cs Added TXIsStateless properties with auto-nullable reference type handling
AdHocUnionCodeGenerator.cs Modified code generation to handle stateless members in Switch/Map/Equals/GetHashCode/ToString
AdHocUnionSettings.cs Updated to read stateless settings from attributes
AdHocUnionMemberTypeSetting.cs Added IsStateless property to member type settings
AttributeDataExtensions.cs Added FindTxIsStateless helper method
Test files (Switch.cs, Map.cs, etc.) Added comprehensive tests for stateless type behavior
Sample files Added demo showing practical API response scenarios with stateless types
Documentation Updated CLAUDE.md and CLAUDE-ATTRIBUTES.md with stateless type information

@github-actions
Copy link

github-actions bot commented Jan 30, 2026

tests results

    29 files  ±  0      29 suites  ±0   38m 19s ⏱️ +23s
 6 335 tests + 88   6 335 ✅ + 88  0 💤 ±0  0 ❌ ±0 
20 173 runs  +264  20 173 ✅ +264  0 💤 ±0  0 ❌ ±0 

Results for commit 9d23026. ± Comparison against base commit 3400675.

This pull request removes 1 and adds 89 tests. Note that renamed tests count towards both.
Thinktecture.Runtime.Tests.AttributeDataExtensionsTests.FindTxName ‑ Should_return_null_for_invalid_index
Thinktecture.Runtime.Tests.AdHocUnionTests.AsValue ‑ Should_return_default_for_duplicate_markers_in_struct_union
Thinktecture.Runtime.Tests.AdHocUnionTests.AsValue ‑ Should_return_default_for_duplicate_value_struct_markers_T2_and_T3
Thinktecture.Runtime.Tests.AdHocUnionTests.AsValue ‑ Should_return_default_for_duplicate_value_struct_stateless
Thinktecture.Runtime.Tests.AdHocUnionTests.AsValue ‑ Should_return_default_for_multiple_stateless_types
Thinktecture.Runtime.Tests.AdHocUnionTests.AsValue ‑ Should_return_default_for_nullable_struct_marker
Thinktecture.Runtime.Tests.AdHocUnionTests.AsValue ‑ Should_return_default_for_stateless_type_T1
Thinktecture.Runtime.Tests.AdHocUnionTests.AsValue ‑ Should_return_default_for_stateless_type_T2
Thinktecture.Runtime.Tests.AdHocUnionTests.AsValue ‑ Should_return_default_for_stateless_type_in_struct_union
Thinktecture.Runtime.Tests.AdHocUnionTests.AsValue ‑ Should_return_null_for_multiple_reference_type_stateless
Thinktecture.Runtime.Tests.AdHocUnionTests.AsValue ‑ Should_return_null_for_multiple_reference_type_stateless_1
…

♻️ This comment has been updated with latest results.

@PawelGerr PawelGerr force-pushed the features/stateless-type-members branch from e3002b1 to 3b29a97 Compare January 30, 2026 08:52
@PawelGerr PawelGerr force-pushed the features/stateless-type-members branch from 3b29a97 to 9d23026 Compare January 30, 2026 16:35
@PawelGerr PawelGerr merged commit 9d23026 into master Feb 3, 2026
3 checks passed
@PawelGerr PawelGerr deleted the features/stateless-type-members branch February 3, 2026 17:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant