You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: .claude/CLAUDE-ATTRIBUTES.md
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -102,6 +102,7 @@ SmartEnumAttribute()
102
102
|`ConversionToKeyMemberType`|`ConversionOperatorsGeneration`|`Implicit`| Generate conversion operator from enum to key type |
103
103
|`ConversionFromKeyMemberType`|`ConversionOperatorsGeneration`|`Explicit`| Generate conversion operator from key type to enum |
104
104
|`SerializationFrameworks`|`SerializationFrameworks`|`All`| Which serialization frameworks to generate integration code for |
105
+
|`DisableSpanBasedJsonConversion`|`bool`|`false`| Disables ReadOnlySpan-based zero-allocation JSON conversion, falling back to string-based conversion (string keys only; NET9+) |
105
106
|`SwitchMapStateParameterName`|`string?`|`"state"`| Name of state parameter in Switch/Map methods |
106
107
107
108
**Note**: `ISpanParsable<T>` inherits from `IParsable<T>`, so setting `SkipISpanParsable = false` will automatically set `SkipIParsable = false` if needed.
@@ -278,7 +279,7 @@ public partial class ApiResponse
278
279
279
280
### ObjectFactoryAttribute<T>
280
281
281
-
For types with custom factories for parsing/serialization.
282
+
For types with custom factories for parsing/serialization. When `T` is `ReadOnlySpan<char>` and `UseForSerialization = SerializationFrameworks.SystemTextJson`, enables zero-allocation JSON deserialization on NET9+ by transcoding UTF-8 JSON bytes directly to `ReadOnlySpan<char>` instead of allocating a `string`.
Copy file name to clipboardExpand all lines: .claude/CLAUDE-FEATURE-DEV.md
+100Lines changed: 100 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -106,6 +106,8 @@ Separate code generator factories for each serialization framework:
106
106
107
107
**Pattern**: Each factory checks if the corresponding serialization package is referenced, then generates appropriate converter/formatter registration code.
108
108
109
+
-**`KeyedJsonCodeGenerator`**: Generates JSON converter attributes on the type. On NET9+, when `UseSpanBasedJsonConverter` is true, it emits `#if NET9_0_OR_GREATER` blocks that register `ThinktectureSpanParsableJsonConverterFactory<T, TValidationError>` for zero-allocation deserialization, with a fallback to the regular `ThinktectureJsonConverterFactory<T, TValidationError>` on older target frameworks.
110
+
109
111
## Type Information System
110
112
111
113
Rich type metadata is captured and passed through the generation pipeline:
@@ -275,6 +277,103 @@ The generator creates:
275
277
- Integration with serializers (JSON, MessagePack, etc.)
276
278
- Model binding for ASP.NET Core
277
279
280
+
### Zero-Allocation JSON via ObjectFactory (NET9+)
281
+
282
+
When `[ObjectFactory<ReadOnlySpan<char>>]` is present with `UseForSerialization = SerializationFrameworks.SystemTextJson`, the source generator sets `UseSpanBasedJsonConverter = true` for the type. This causes the generated JSON converter attribute to use `ThinktectureSpanParsableJsonConverterFactory` on NET9+, enabling zero-allocation deserialization by transcoding UTF-8 JSON bytes directly to `ReadOnlySpan<char>` without allocating a `string`. This is the opt-in mechanism for Value Objects (unlike Smart Enums with string keys, where it is automatic).
This feature enables zero-allocation JSON deserialization for string-keyed Smart Enums and Value Objects with `ReadOnlySpan<char>` object factories, by converting UTF-8 JSON bytes directly to `ReadOnlySpan<char>` instead of allocating an intermediate `string`.
287
+
288
+
### Architecture
289
+
290
+
The feature spans three layers:
291
+
292
+
1.**`Utf8JsonReaderHelper`** (internal, `Thinktecture.Internal` namespace, NET9+ only): The low-level engine that converts UTF-8 JSON bytes to `ReadOnlySpan<char>` without string allocation.
293
+
-**Fast path**: When the JSON value is contiguous in the buffer and unescaped, it transcodes the raw UTF-8 bytes directly via `Encoding.UTF8.GetChars(ReadOnlySpan<byte>, Span<char>)`.
294
+
-**Slow path**: When the value is escaped or fragmented across buffer segments, it uses `Utf8JsonReader.CopyString(Span<byte>)` to get unescaped UTF-8 bytes, then transcodes.
295
+
-**Memory strategy**: Uses `stackalloc` for values up to 128 chars; rents from `ArrayPool<char>.Shared` for larger values and returns the buffer after use.
296
+
297
+
2.**`ThinktectureSpanParsableJsonConverter<T, TValidationError>`** (NET9+ only): A `System.Text.Json` converter that uses `Utf8JsonReaderHelper` to obtain a `ReadOnlySpan<char>` from the JSON reader, then calls `IConvertible<ReadOnlySpan<char>>.ToValue()` on the target type to perform zero-allocation conversion.
- Paired with `ThinktectureSpanParsableJsonConverterFactory<T, TValidationError>` for registration.
300
+
301
+
3.**`ThinktectureJsonConverterFactory`** (runtime detection): Updated with a NET9+-only constructor accepting `Func<Type, bool>? skipSpanBasedDeserialization`. At runtime, it inspects metadata via its internal `CanUseSpanParsableConverter()` method to decide whether to return the span-based converter or the regular converter for a given type.
The `ThinktectureJsonConverterFactory` checks two paths to determine if a type supports span-based deserialization:
306
+
307
+
-**String-keyed Smart Enums**: Checks `Metadata.Keyed.SmartEnum.DisableSpanBasedJsonConversion`. If `false` (the default), the span-based converter is used.
308
+
-**Other types (Value Objects, etc.)**: Checks the type's `ObjectFactories` metadata for an entry whose `ValueType` is `ReadOnlySpan<char>` and whose `SerializationFrameworks` includes `SystemTextJson`.
309
+
310
+
The optional `skipSpanBasedDeserialization` delegate (passed via the constructor) provides an additional external override for consumers who need to disable span-based deserialization for specific types at the application level.
311
+
312
+
### How It Works for Smart Enums
313
+
314
+
**Automatic for string-keyed enums**: When a Smart Enum has a `string` key type, the source generator automatically enables span-based JSON deserialization (no user action needed).
315
+
316
+
**Opt-out**: Set `DisableSpanBasedJsonConversion = true` on the `[SmartEnum<string>]` attribute:
**Non-string keys**: Not applicable. The feature is only effective for `string`-keyed Smart Enums because `IConvertible<ReadOnlySpan<char>>` maps naturally to string-based key lookup.
327
+
328
+
### How It Works for Value Objects
329
+
330
+
**Opt-in via ObjectFactory**: Value Objects must explicitly declare `[ObjectFactory<ReadOnlySpan<char>>]` with `UseForSerialization = SerializationFrameworks.SystemTextJson`:
The `ObjectFactorySourceGenerator` detects the `ReadOnlySpan<char>` object factory and sets `UseSpanBasedJsonConverter = true` in the serializer generator state.
343
+
344
+
### Source Generator Behavior
345
+
346
+
**State tracking**: `KeyedSerializerGeneratorState` has a `UseSpanBasedJsonConverter` boolean property that is included in equality comparison and hash code computation (ensuring incremental generation correctness).
347
+
348
+
**Per-generator logic for setting `UseSpanBasedJsonConverter`**:
349
+
350
+
-**`SmartEnumSourceGenerator`**: Sets to `true` when `!state.Settings.DisableSpanBasedJsonConversion && state.KeyMember?.SpecialType == SpecialType.System_String`
351
+
-**`ValueObjectSourceGenerator`**: Always passes `false` (Value Objects require an explicit `[ObjectFactory<ReadOnlySpan<char>>]`)
352
+
-**`ObjectFactorySourceGenerator`**: Sets to `true` when `state.AttributeInfo.ObjectFactories.Any(f => f.IsReadOnlySpanOfChar)`
-`SmartEnumAttribute<TKey>.DisableSpanBasedJsonConversion`: New attribute property (default `false`)
371
+
-`AllEnumSettings` and `SmartEnumSettings`: Track the `DisableSpanBasedJsonConversion` value
372
+
-`Constants.DISABLE_SPAN_BASED_JSON_CONVERSION`: String constant for attribute property lookup
373
+
-`AttributeDataExtensions.FindDisableSpanBasedJsonConversion()`: Extension method to extract the setting from attribute data
374
+
375
+
**`IConvertible<T>` update**: On NET9+, the interface uses `allows ref struct` constraint to support `ReadOnlySpan<char>` as the type parameter.
376
+
278
377
## Runtime Metadata System
279
378
280
379
The runtime metadata system provides the bridge between compile-time source generation and runtime framework integration. This system is critical for serialization, model binding, type conversion, and other runtime operations.
@@ -338,6 +437,7 @@ public partial class MyEnum
338
437
339
438
**Concrete implementations**:
340
439
-`SmartEnumMetadata`: For Smart Enums
440
+
- Includes `Metadata.Keyed.SmartEnum.DisableSpanBasedJsonConversion` (`bool`, `init`): When `true`, prevents the runtime `ThinktectureJsonConverterFactory` from selecting the span-based JSON converter for this Smart Enum. Default `false`. Only meaningful for string-keyed enums. Set via `SmartEnumAttribute<TKey>.DisableSpanBasedJsonConversion` and emitted by `SmartEnumCodeGenerator` in metadata initialization.
341
441
-`KeyedValueObjectMetadata`: For simple Value Objects with single key
342
442
-`ComplexValueObjectMetadata`: For complex Value Objects with multiple members
Copy file name to clipboardExpand all lines: .claude/CLAUDE-TESTING.md
+18Lines changed: 18 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -138,6 +138,10 @@ The test suite is organized into multiple projects, each with a specific purpose
138
138
publicpartialstructIntBasedStructValueObject;
139
139
```
140
140
141
+
**Notable test types for span-based JSON deserialization**:
142
+
-`TestEnums/SmartEnum_StringBased_WithDisabledSpanBasedJsonConversion.cs`: Compilation test verifying that `DisableSpanBasedJsonConversion = true` on a string-based Smart Enum produces compilable generated code that falls back to string-based JSON deserialization
143
+
-`TestValueObjects/ComplexValueObjectWithReadOnlySpanBasedObjectFactoryForJson.cs`: Compilation test verifying that a complex value object with `[ObjectFactory<ReadOnlySpan<char>>]` produces compilable generated code supporting span-based JSON deserialization
144
+
141
145
**Best practices**:
142
146
- Create separate types for each meaningful feature variation or configuration
143
147
- Name types descriptively to indicate what feature/configuration they test
@@ -307,6 +311,20 @@ public void Should_serialize_and_deserialize_smart_enum()
String-based Smart Enums and Value Objects support zero-allocation JSON deserialization via `ThinktectureSpanParsableJsonConverter`. This converter avoids allocating a `string` during deserialization by reading the UTF-8 JSON payload directly into a `ReadOnlySpan<char>` and calling `ISpanParsable<T>.Parse`.
317
+
318
+
**Key test areas**:
319
+
320
+
-**`ThinktectureSpanParsableJsonConverter` tests** (`SpanParsableJsonConverterTests.cs`): Verify that types implementing `ISpanParsable<T>` are deserialized through the span-based path. Test both `ThinktectureSpanParsableJsonConverterFactory` (standalone) and the integration through `ThinktectureJsonConverterFactory`.
321
+
-**`Utf8JsonReaderHelper` tests** (`Utf8JsonReaderHelperTests.cs`): Verify correct UTF-8 byte to `ReadOnlySpan<char>` conversion, including multi-byte characters, empty strings, and edge cases. This helper is the core of the zero-allocation path.
322
+
-**Opt-out via `DisableSpanBasedJsonConversion`**: Smart Enums can disable span-based JSON conversion by setting `DisableSpanBasedJsonConversion = true` on the `[SmartEnum<T>]` attribute. Test that the generated JSON converter falls back to string-based deserialization. The compilation test type `SmartEnum_StringBased_WithDisabledSpanBasedJsonConversion` verifies the generated code compiles correctly.
323
+
-**Value objects with `[ObjectFactory<ReadOnlySpan<char>>]`**: Value objects that declare a `ReadOnlySpan<char>`-based object factory also get span-based JSON deserialization. The compilation test type `ComplexValueObjectWithReadOnlySpanBasedObjectFactoryForJson` verifies this path. Snapshot tests in `JsonObjectFactoryCodeGeneratorFactoryTests` verify the generated converter code.
324
+
-**Source generator snapshot tests**: `Generate.Snapshot_StringKeyWithSpanJsonConverter.verified.txt` verifies the generated JSON converter includes the span-based deserialization code path.
325
+
326
+
**Benchmarks**: `SmartEnumJsonDeserializationBenchmarks` and `ValueObjectJsonDeserializationBenchmarks` (in the `Benchmarking` sample project) measure the allocation reduction. These are not unit tests but are useful for validating performance characteristics.
-**Span-based JSON deserialization** (NET9+, string keys only): Zero-allocation JSON deserialization enabled by default; transcodes UTF-8 JSON bytes directly to `ReadOnlySpan<char>` instead of allocating a `string`. Opt out per enum via `DisableSpanBasedJsonConversion = true`
138
139
139
140
#### Value Objects
140
141
@@ -196,7 +197,7 @@ The `MetadataLookup` class provides cached metadata discovery via reflection. Ob
196
197
197
198
### Framework Integration Quick Reference
198
199
199
-
**Serialization**: System.Text.Json, MessagePack, Newtonsoft.Json, ProtoBuf - reference package for auto-generation or manually register converters
200
+
**Serialization**: System.Text.Json, MessagePack, Newtonsoft.Json, ProtoBuf - reference package for auto-generation or manually register converters. On NET9+, string-based Smart Enums and types with `[ObjectFactory<ReadOnlySpan<char>>(UseForSerialization = SerializationFrameworks.SystemTextJson)]` automatically use zero-allocation span-based JSON deserialization (via `Utf8JsonReaderHelper` and `ThinktectureSpanParsableJsonConverter`)
@@ -214,6 +215,7 @@ The `MetadataLookup` class provides cached metadata discovery via reflection. Ob
214
215
4.**Serialization not working**: Ensure integration package is referenced, or manually register converters/formatters
215
216
5.**EF Core not converting**: Call `.UseThinktectureValueConverters()` on DbContextOptionsBuilder
216
217
6.**ISpanParsable not available**: Requires NET9+; ensure project targets `net9.0` or later and key type implements `ISpanParsable<TKey>`
218
+
7.**Span-based JSON deserialization causing issues**: Disable per Smart Enum with `[SmartEnum<string>(DisableSpanBasedJsonConversion = true)]`. For `ThinktectureJsonConverterFactory`, pass `skipSpanBasedDeserialization` callback to the constructor (NET9+ overload) to opt out at runtime
217
219
218
220
### Best Practices
219
221
@@ -225,6 +227,7 @@ The `MetadataLookup` class provides cached metadata discovery via reflection. Ob
225
227
6.**Partial keyword**: Types must be marked `partial` for source generators to work
226
228
7.**Culture-specific parsing**: Always pass appropriate `IFormatProvider` when parsing/formatting culture-sensitive types
227
229
8.**Arithmetic operators**: Use unchecked arithmetic context - overflow/underflow wraps around
230
+
9.**Span-based JSON for value objects**: To enable zero-allocation JSON deserialization for value objects on NET9+, add `[ObjectFactory<ReadOnlySpan<char>>(UseForSerialization = SerializationFrameworks.SystemTextJson)]` to the type. Without this attribute, value objects use regular key-based JSON conversion
0 commit comments