Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions .claude/CLAUDE-ATTRIBUTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,11 @@ Ad-hoc unions for 2-5 types (generic syntax).

**Properties** (per generic parameter):

| Property | Type | Default | Description |
|---------------------------------------------------------------|-----------|-----------|--------------------------------------------------------------------------|
| `T1Name`, `T2Name`, ... | `string?` | Type name | Override member name for T1, T2, etc. |
| `T1IsNullableReferenceType`, `T2IsNullableReferenceType`, ... | `bool` | `false` | Mark T1, T2, etc. as nullable reference type (no effect for value types) |
| Property | Type | Default | Description |
|---------------------------------------------------------------|-----------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `T1Name`, `T2Name`, ... | `string?` | Type name | Override member name for T1, T2, etc. |
| `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` |
| `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.** |

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

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

**Properties**:

| Property | Type | Default | Description |
|--------------------------------------------------------------------------------------------|-----------|------------|--------------------------------------------------------------------------|
| `T1`, `T2` | `Type` | (required) | Required member types |
| `T3`, `T4`, `T5` | `Type?` | `null` | Optional member types |
| `T1Name`, `T2Name`, ..., `T5Name` | `string?` | Type name | Override member name for T1, T2, etc. |
| `T1IsNullableReferenceType`, `T2IsNullableReferenceType`, ..., `T5IsNullableReferenceType` | `bool` | `false` | Mark T1, T2, etc. as nullable reference type (no effect for value types) |
| Property | Type | Default | Description |
|--------------------------------------------------------------------------------------------|-----------|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `T1`, `T2` | `Type` | (required) | Required member types |
| `T3`, `T4`, `T5` | `Type?` | `null` | Optional member types |
| `T1Name`, `T2Name`, ..., `T5Name` | `string?` | Type name | Override member name for T1, T2, etc. |
| `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` |
| `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.** |

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

Expand Down
1 change: 1 addition & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ This is a .NET library providing **Smart Enums**, **Value Objects**, and **Discr

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

Expand Down
61 changes: 54 additions & 7 deletions .serena/project.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: csharp

# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
Expand Down Expand Up @@ -64,5 +58,58 @@ excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""

# the name by which the project can be referenced within Serena
project_name: "Thinktecture.Runtime.Extensions"

# list of mode names to that are always to be included in the set of active modes
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this setting overrides the global configuration.
# Set this to [] to disable base modes for this project.
# Set this to a list of mode names to always include the respective modes for this project.
base_modes:

# list of mode names that are to be activated by default.
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
# This setting can, in turn, be overridden by CLI parameters (--mode).
default_modes:

# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
included_optional_tools: []

# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
fixed_tools: []

# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: utf-8


# list of languages for which language servers are started; choose from:
# al bash clojure cpp csharp
# csharp_omnisharp dart elixir elm erlang
# fortran fsharp go groovy haskell
# java julia kotlin lua markdown
# matlab nix pascal perl php
# powershell python python_jedi r rego
# ruby ruby_solargraph rust scala swift
# terraform toml typescript typescript_vts vue
# yaml zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some languages require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- csharp
2 changes: 1 addition & 1 deletion docs
Submodule docs updated from 58e558 to b83366
30 changes: 30 additions & 0 deletions samples/Basic.Samples/Unions/ApiResponseWithStateless.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace Thinktecture.Unions;

/// <summary>
/// API response union demonstrating stateless types for memory optimization.
/// NotFound and Unauthorized are stateless types that carry no instance data.
/// </summary>
[Union<SuccessResponse, NotFound, Unauthorized>(
T1Name = "Success",
T2Name = "NotFound", T2IsStateless = true,
T3Name = "Unauthorized", T3IsStateless = true)]
public partial class ApiResponseWithStateless;

public sealed class SuccessResponse
{
public required string Data { get; init; }
}

/// <summary>
/// Stateless type for "not found" state.
/// No backing field is allocated - only the discriminator index is stored.
/// Using a struct avoids null-handling complexity since default(NotFound) is a valid value.
/// </summary>
public readonly record struct NotFound;

/// <summary>
/// Stateless type for "unauthorized" state.
/// No backing field is allocated - only the discriminator index is stored.
/// Using a struct avoids null-handling complexity since default(Unauthorized) is a valid value.
/// </summary>
public readonly record struct Unauthorized;
90 changes: 88 additions & 2 deletions samples/Basic.Samples/Unions/DiscriminatedUnionsDemos.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public static void Demo(ILogger logger)
DemoForJurisdiction(logger);
DemoForPartiallyKnownDate(logger);
DemoForNestedUnionsAndSwitchMapOverloads(logger);
DemoForStatelessTypes(logger);
}

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

// Non-exhaustive overload (stopped at Failure level)
Expand All @@ -353,4 +354,89 @@ void HandleFailureSimple(ApiResponseWithSimpleParameterNames.Failure failure)
);
}
}

private static void DemoForStatelessTypes(ILogger logger)
{
logger.Information("""


==== Demo for Stateless Types (Memory Optimization) ====

""");

// Creating different API responses
ApiResponseWithStateless successResponse = new SuccessResponse { Data = "Hello, World!" };
ApiResponseWithStateless notFoundResponse = new NotFound();
ApiResponseWithStateless unauthorizedResponse = new Unauthorized();

logger.Information("--- Type Checking ---");
logger.Information("Success response - IsSuccess: {IsSuccess}, IsNotFound: {IsNotFound}, IsUnauthorized: {IsUnauthorized}",
successResponse.IsSuccess, successResponse.IsNotFound, successResponse.IsUnauthorized);
logger.Information("NotFound response - IsSuccess: {IsSuccess}, IsNotFound: {IsNotFound}, IsUnauthorized: {IsUnauthorized}",
notFoundResponse.IsSuccess, notFoundResponse.IsNotFound, notFoundResponse.IsUnauthorized);
logger.Information("Unauthorized response - IsSuccess: {IsSuccess}, IsNotFound: {IsNotFound}, IsUnauthorized: {IsUnauthorized}",
unauthorizedResponse.IsSuccess, unauthorizedResponse.IsNotFound, unauthorizedResponse.IsUnauthorized);

logger.Information("--- Accessing Values ---");
// For stateless types, accessors return default(T)
var notFoundValue = notFoundResponse.AsNotFound;
logger.Information("NotFound accessor returns: {Value} (default struct)", notFoundValue);

var unauthorizedValue = unauthorizedResponse.AsUnauthorized;
logger.Information("Unauthorized accessor returns: {Value} (default struct)", unauthorizedValue);

// Regular member still returns the actual instance
var successValue = successResponse.AsSuccess;
logger.Information("Success accessor returns: {Data}", successValue.Data);

logger.Information("--- Switch Pattern Matching ---");
successResponse.Switch(
success: s => logger.Information("[Switch] Success with data: {Data}", s.Data),
notFound: _ => logger.Information("[Switch] Not Found"),
unauthorized: _ => logger.Information("[Switch] Unauthorized")
);

notFoundResponse.Switch(
success: s => logger.Information("[Switch] Success with data: {Data}", s.Data),
notFound: _ => logger.Information("[Switch] Not Found"),
unauthorized: _ => logger.Information("[Switch] Unauthorized")
);

unauthorizedResponse.Switch(
success: s => logger.Information("[Switch] Success with data: {Data}", s.Data),
notFound: _ => logger.Information("[Switch] Not Found"),
unauthorized: _ => logger.Information("[Switch] Unauthorized")
);

logger.Information("--- Map Pattern Matching ---");
var statusCode = successResponse.Map(
success: 200,
notFound: 404,
unauthorized: 401
);
logger.Information("Success response maps to status code: {StatusCode}", statusCode);

statusCode = notFoundResponse.Map(
success: 200,
notFound: 404,
unauthorized: 401
);
logger.Information("NotFound response maps to status code: {StatusCode}", statusCode);

statusCode = unauthorizedResponse.Map(
success: 200,
notFound: 404,
unauthorized: 401
);
logger.Information("Unauthorized response maps to status code: {StatusCode}", statusCode);

logger.Information("--- Equality Comparison ---");
var anotherNotFound = new NotFound();
logger.Information("notFoundResponse == anotherNotFound: {IsEqual}", notFoundResponse == anotherNotFound);

var anotherUnauthorized = new Unauthorized();
logger.Information("unauthorizedResponse == anotherUnauthorized: {IsEqual}", unauthorizedResponse == anotherUnauthorized);

logger.Information("notFoundResponse == unauthorizedResponse: {IsEqual}", notFoundResponse == unauthorizedResponse);
}
}
Loading
Loading