Skip to content

Commit 3b29a97

Browse files
committed
type members of ad hoc union can be flagged as "stateless"
1 parent 0b89f31 commit 3b29a97

File tree

57 files changed

+7614
-78
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+7614
-78
lines changed

.claude/CLAUDE-ATTRIBUTES.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,11 @@ Ad-hoc unions for 2-5 types (generic syntax).
179179

180180
**Properties** (per generic parameter):
181181

182-
| Property | Type | Default | Description |
183-
|---------------------------------------------------------------|-----------|-----------|--------------------------------------------------------------------------|
184-
| `T1Name`, `T2Name`, ... | `string?` | Type name | Override member name for T1, T2, etc. |
185-
| `T1IsNullableReferenceType`, `T2IsNullableReferenceType`, ... | `bool` | `false` | Mark T1, T2, etc. as nullable reference type (no effect for value types) |
182+
| Property | Type | Default | Description |
183+
|---------------------------------------------------------------|-----------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
184+
| `T1Name`, `T2Name`, ... | `string?` | Type name | Override member name for T1, T2, etc. |
185+
| `T1IsNullableReferenceType`, `T2IsNullableReferenceType`, ... | `bool` | `false` | Mark T1, T2, etc. as nullable reference type (no effect for value types). Automatically set to `true` for reference types when `TXIsStateless = true` |
186+
| `T1IsStateless`, `T2IsStateless`, ... | `bool` | `false` | Mark T1, T2, etc. as a stateless type that carries no instance data. Reduces memory by storing only discriminator index. Accessors return `default(T)`. Automatically sets `TXIsNullableReferenceType = true` for reference types. **Recommended: Use struct types for stateless members to avoid null-handling complexity.** |
186187

187188
**Inherits all properties from `UnionAttributeBase`** (see above).
188189

@@ -202,12 +203,13 @@ AdHocUnionAttribute(Type t1, Type t2, Type? t3 = null, Type? t4 = null, Type? t5
202203

203204
**Properties**:
204205

205-
| Property | Type | Default | Description |
206-
|--------------------------------------------------------------------------------------------|-----------|------------|--------------------------------------------------------------------------|
207-
| `T1`, `T2` | `Type` | (required) | Required member types |
208-
| `T3`, `T4`, `T5` | `Type?` | `null` | Optional member types |
209-
| `T1Name`, `T2Name`, ..., `T5Name` | `string?` | Type name | Override member name for T1, T2, etc. |
210-
| `T1IsNullableReferenceType`, `T2IsNullableReferenceType`, ..., `T5IsNullableReferenceType` | `bool` | `false` | Mark T1, T2, etc. as nullable reference type (no effect for value types) |
206+
| Property | Type | Default | Description |
207+
|--------------------------------------------------------------------------------------------|-----------|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
208+
| `T1`, `T2` | `Type` | (required) | Required member types |
209+
| `T3`, `T4`, `T5` | `Type?` | `null` | Optional member types |
210+
| `T1Name`, `T2Name`, ..., `T5Name` | `string?` | Type name | Override member name for T1, T2, etc. |
211+
| `T1IsNullableReferenceType`, `T2IsNullableReferenceType`, ..., `T5IsNullableReferenceType` | `bool` | `false` | Mark T1, T2, etc. as nullable reference type (no effect for value types). Automatically set to `true` for reference types when `TXIsStateless = true` |
212+
| `T1IsStateless`, `T2IsStateless`, ..., `T5IsStateless` | `bool` | `false` | Mark T1, T2, etc. as a stateless type that carries no instance data. Reduces memory by storing only discriminator index. Accessors return `default(T)`. Automatically sets `TXIsNullableReferenceType = true` for reference types. **Recommended: Use struct types for stateless members to avoid null-handling complexity.** |
211213

212214
**Inherits all properties from `UnionAttributeBase`** (see above).
213215

.claude/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ This is a .NET library providing **Smart Enums**, **Value Objects**, and **Discr
151151

152152
- **Ad-hoc `[Union<T1, T2>]` or `[AdHocUnion(typeof(T1), typeof(T2))]`**: Simple 2-5 type combinations
153153
- Implicit conversion operators, IsT1/AsT1 properties, Switch/Map
154+
- Stateless types (`TXIsStateless = true`): Memory-efficient members that store only discriminator, not instance data (prefer structs to avoid null-handling)
154155
- **Regular `[Union]`**: Inheritance-based unions with derived types
155156
- Static factory methods, Switch/Map over all derived types
156157

docs

Submodule docs updated from 8e82408 to b83366b
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
namespace Thinktecture.Unions;
2+
3+
/// <summary>
4+
/// API response union demonstrating stateless types for memory optimization.
5+
/// NotFound and Unauthorized are stateless types that carry no instance data.
6+
/// </summary>
7+
[Union<SuccessResponse, NotFound, Unauthorized>(
8+
T1Name = "Success",
9+
T2Name = "NotFound", T2IsStateless = true,
10+
T3Name = "Unauthorized", T3IsStateless = true)]
11+
public partial class ApiResponseWithStateless;
12+
13+
public sealed class SuccessResponse
14+
{
15+
public required string Data { get; init; }
16+
}
17+
18+
/// <summary>
19+
/// Stateless type for "not found" state.
20+
/// No backing field is allocated - only the discriminator index is stored.
21+
/// Using a struct avoids null-handling complexity since default(NotFound) is a valid value.
22+
/// </summary>
23+
public readonly record struct NotFound;
24+
25+
/// <summary>
26+
/// Stateless type for "unauthorized" state.
27+
/// No backing field is allocated - only the discriminator index is stored.
28+
/// Using a struct avoids null-handling complexity since default(Unauthorized) is a valid value.
29+
/// </summary>
30+
public readonly record struct Unauthorized;

samples/Basic.Samples/Unions/DiscriminatedUnionsDemos.cs

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public static void Demo(ILogger logger)
1515
DemoForJurisdiction(logger);
1616
DemoForPartiallyKnownDate(logger);
1717
DemoForNestedUnionsAndSwitchMapOverloads(logger);
18+
DemoForStatelessTypes(logger);
1819
}
1920

2021
private static void DemoForAdHocUnions(ILogger logger)
@@ -336,8 +337,8 @@ void HandleFailure(ApiResponse.Failure failure)
336337
// With simple parameter naming, nested types use only their own name
337338
apiResponseSimple.Switch(
338339
success: success => logger.Information("[Switch] Success"),
339-
notFound: notFound => logger.Information("[Switch] Not Found"), // Simple name
340-
unauthorized: unauthorized => logger.Information("[Switch] Unauthorized") // Simple name
340+
notFound: notFound => logger.Information("[Switch] Not Found"), // Simple name
341+
unauthorized: unauthorized => logger.Information("[Switch] Unauthorized") // Simple name
341342
);
342343

343344
// Non-exhaustive overload (stopped at Failure level)
@@ -353,4 +354,89 @@ void HandleFailureSimple(ApiResponseWithSimpleParameterNames.Failure failure)
353354
);
354355
}
355356
}
357+
358+
private static void DemoForStatelessTypes(ILogger logger)
359+
{
360+
logger.Information("""
361+
362+
363+
==== Demo for Stateless Types (Memory Optimization) ====
364+
365+
""");
366+
367+
// Creating different API responses
368+
ApiResponseWithStateless successResponse = new SuccessResponse { Data = "Hello, World!" };
369+
ApiResponseWithStateless notFoundResponse = new NotFound();
370+
ApiResponseWithStateless unauthorizedResponse = new Unauthorized();
371+
372+
logger.Information("--- Type Checking ---");
373+
logger.Information("Success response - IsSuccess: {IsSuccess}, IsNotFound: {IsNotFound}, IsUnauthorized: {IsUnauthorized}",
374+
successResponse.IsSuccess, successResponse.IsNotFound, successResponse.IsUnauthorized);
375+
logger.Information("NotFound response - IsSuccess: {IsSuccess}, IsNotFound: {IsNotFound}, IsUnauthorized: {IsUnauthorized}",
376+
notFoundResponse.IsSuccess, notFoundResponse.IsNotFound, notFoundResponse.IsUnauthorized);
377+
logger.Information("Unauthorized response - IsSuccess: {IsSuccess}, IsNotFound: {IsNotFound}, IsUnauthorized: {IsUnauthorized}",
378+
unauthorizedResponse.IsSuccess, unauthorizedResponse.IsNotFound, unauthorizedResponse.IsUnauthorized);
379+
380+
logger.Information("--- Accessing Values ---");
381+
// For stateless types, accessors return default(T)
382+
var notFoundValue = notFoundResponse.AsNotFound;
383+
logger.Information("NotFound accessor returns: {Value} (default struct)", notFoundValue);
384+
385+
var unauthorizedValue = unauthorizedResponse.AsUnauthorized;
386+
logger.Information("Unauthorized accessor returns: {Value} (default struct)", unauthorizedValue);
387+
388+
// Regular member still returns the actual instance
389+
var successValue = successResponse.AsSuccess;
390+
logger.Information("Success accessor returns: {Data}", successValue.Data);
391+
392+
logger.Information("--- Switch Pattern Matching ---");
393+
successResponse.Switch(
394+
success: s => logger.Information("[Switch] Success with data: {Data}", s.Data),
395+
notFound: _ => logger.Information("[Switch] Not Found"),
396+
unauthorized: _ => logger.Information("[Switch] Unauthorized")
397+
);
398+
399+
notFoundResponse.Switch(
400+
success: s => logger.Information("[Switch] Success with data: {Data}", s.Data),
401+
notFound: _ => logger.Information("[Switch] Not Found"),
402+
unauthorized: _ => logger.Information("[Switch] Unauthorized")
403+
);
404+
405+
unauthorizedResponse.Switch(
406+
success: s => logger.Information("[Switch] Success with data: {Data}", s.Data),
407+
notFound: _ => logger.Information("[Switch] Not Found"),
408+
unauthorized: _ => logger.Information("[Switch] Unauthorized")
409+
);
410+
411+
logger.Information("--- Map Pattern Matching ---");
412+
var statusCode = successResponse.Map(
413+
success: 200,
414+
notFound: 404,
415+
unauthorized: 401
416+
);
417+
logger.Information("Success response maps to status code: {StatusCode}", statusCode);
418+
419+
statusCode = notFoundResponse.Map(
420+
success: 200,
421+
notFound: 404,
422+
unauthorized: 401
423+
);
424+
logger.Information("NotFound response maps to status code: {StatusCode}", statusCode);
425+
426+
statusCode = unauthorizedResponse.Map(
427+
success: 200,
428+
notFound: 404,
429+
unauthorized: 401
430+
);
431+
logger.Information("Unauthorized response maps to status code: {StatusCode}", statusCode);
432+
433+
logger.Information("--- Equality Comparison ---");
434+
var anotherNotFound = new NotFound();
435+
logger.Information("notFoundResponse == anotherNotFound: {IsEqual}", notFoundResponse == anotherNotFound);
436+
437+
var anotherUnauthorized = new Unauthorized();
438+
logger.Information("unauthorizedResponse == anotherUnauthorized: {IsEqual}", unauthorizedResponse == anotherUnauthorized);
439+
440+
logger.Information("notFoundResponse == unauthorizedResponse: {IsEqual}", notFoundResponse == unauthorizedResponse);
441+
}
356442
}

0 commit comments

Comments
 (0)