Skip to content

Fix CloudEvents Extension Attributes - #69

Merged
feO2x merged 12 commits into
mainfrom
53-fix-cloud-events-extension-attributes
Aug 2, 2026
Merged

Fix CloudEvents Extension Attributes#69
feO2x merged 12 commits into
mainfrom
53-fix-cloud-events-extension-attributes

Conversation

@feO2x

@feO2x feO2x commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Closes #53

feO2x and others added 12 commits August 2, 2026 07:35
CloudEvents closes its attribute type system to seven types and maps them
onto three JSON forms, so Double, Single, Decimal, and out-of-range Int64
are currently written in a form no CloudEvents type maps to. The plan
records the decision to map through the type system rather than deviate,
using String as the spec's escape hatch.

Also covers the null rule in both directions, the character contract that
String imposes, and the value-dependent Int64 encoding that is kept for
its round trip and documented as a deviation.

Refs #53

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrQRRJp3wRtHPgoKfngajd
… boundary

Validating only in DefaultCloudEventsAttributeConversionService left the
rule advisory: a custom conversion service or a directly constructed
CloudEventsEnvelopeForWriting reaches the writer without it, while the
complex-kind rule is absolute at the same site.

Move enforcement into WriteExtensionAttributes ahead of the property name,
share a private core with the public value writer so no attribute is
scanned twice, and keep the validator public for services that prefer to
fail before serialization starts.

Refs #53

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrQRRJp3wRtHPgoKfngajd
The value-dependent encoding was justified against an alternative that was
described as more costly than it is: TryGetInt64 could gain canonical-string
parsing like TryGetUInt64, TryGetSingle, TryGetChar, and TryGetDecimal
already have, which makes a stable String encoding cheap for consumers.

Present both options honestly and decide for the threshold plus the parsing
arm. The arm is what makes the threshold defensible: without it an
out-of-range value writes as a string that TryGetInt64 refuses, so the
accessor is now total across both encodings and only Kind stays observable.

Refs #53

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrQRRJp3wRtHPgoKfngajd
0055-0 assigned the deferred Bytes kind to #53 on the premise that this
issue would map MetadataKind onto the abstract CloudEvents types, with
Binary as the first consumer. This plan models the JSON encoding instead,
and Binary is not one: the JSON Event Format renders it as a base64 string,
which is what a MetadataKind.String already produces.

Re-defer Bytes explicitly to its own issue rather than leaving the earlier
assignment orphaned, and note that its blockers - equality semantics and
defensive copying - are metadata-system decisions that a conformance fix is
the wrong venue to settle.

Refs #53

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrQRRJp3wRtHPgoKfngajd
The plan said Utf8JsonWriter's handling of ill-formed UTF-16 is not this
library's contract and then asked for a test pinning it. Such a test
asserts an incidental and breaks on a System.Text.Json update that changes
nothing here. Assert the rejection through the public serialization API
instead, which is the behavior the library owns.

Also scope the kind matrix to non-null primitives: an omitted null property
has no JsonValueKind or text, and its coverage lives in the omission and
unset criteria.

Refs #53

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrQRRJp3wRtHPgoKfngajd
…e value

A value-only overload cannot meet the failure contract: the caller must
write the property name first, so validation cannot name the attribute,
a failure leaves an incomplete property behind, and null omission is
already impossible by the time the value is reached.

Take the name and value together. That method owns validation, omission,
WritePropertyName, and emission, which removes both the shared private core
and the carve-out that wrote null when the value writer was called directly.
Name policy splits on legitimacy: never-valid names are rejected there,
while skipping standard names stays with the envelope writer that knows
they were already emitted.

Refs #53

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrQRRJp3wRtHPgoKfngajd
The criterion promised that no conformant attribute is scanned twice, while
the design offers preflight validation in a custom conversion service. Since
writer validation stays mandatory, preflighting necessarily adds a second
pass, so the unscoped promise was unkeepable.

Scope it to the built-in write path and state the trade at the seam: the
second scan buys an ArgumentException at ToCloudEvent time, before any byte
is serialized.

Refs #53

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrQRRJp3wRtHPgoKfngajd
Standard-attribute writing was called spec-conformant while the same plan
records that their text is never scanned for the characters CloudEvents
excludes from a String. Claim only what holds: their JSON rendering is
unaffected by this plan.

The release-note section scoped every entry to extension attributes, but
TryGetInt64 reading canonical text applies to any MetadataValue of kind
String regardless of origin. Give that entry its own global scope.

Refs #53

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrQRRJp3wRtHPgoKfngajd
…riter

The public writer owned the whole attribute but only rejected data,
data_base64, and lproutcome, so an invalid name reached the envelope
whenever conversion was bypassed, and a standard name got the extension
mapping applied to it: "type" with an in-range Int64 emits a JSON number
where the specification requires a string.

Reject every reserved and standard name and enforce the lowercase
alphanumeric grammar. WriteExtensionAttributes still skips the standard
names ahead of the call, so the guard exists for external callers and is
tested directly.

Also narrow the validity guarantee to the extension attributes the library
emits, since standard-attribute character validation is deferred.

Refs #53

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrQRRJp3wRtHPgoKfngajd
Cut the plan from ~5.6k to ~2.3k words. Every acceptance criterion and
technical decision survives; the reduction comes from merging related
criteria and trimming rationale prose, not from removing scope.

Repairs three defects the condensing introduced: an inverted sentence
about the decimal test replacement, an acceptance criterion that read as
if TryGetInt64 had stopped accepting MetadataKind.Int64, and a reference
to a nonexistent MetadataValue.String.

Restores the implementation anchors and the two rejected-alternative
arguments that the shortening lost: MetadataValue.cs:385,
DefaultHttpHeaderParsingService, MetadataKindExtensions,
TreatWarningsAsErrors, the System.Text.Json reason for not pinning
Utf8JsonWriter behavior, the map-versus-document-the-deviation decision,
and the always-String failure-mode argument.

Records one new constraint found while checking those anchors:
IsValidExtensionAttributeName is private on
DefaultCloudEventsAttributeConversionService, so the name grammar has to
move somewhere both it and the writer can call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrQRRJp3wRtHPgoKfngajd
Map extension metadata through the CloudEvents JSON Event Format type system, omit null attributes, and validate names and string text before committing a property.

Support canonical Int64 text on read-back, document the value-dependent encoding and stable converter remedy, and cover the public write/read surfaces plus allocation contracts.

Closes #53

Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Remove an unreachable encoding fallback and close the warning suppression. Cover the public name and text validation gaps, and document reserved outcome handling in the release notes.
@feO2x feO2x self-assigned this Aug 2, 2026
@feO2x feO2x added the bug Something isn't working label Aug 2, 2026
@feO2x feO2x changed the title 53 fix cloud events extension attributes Fix CloudEvents Extension Attributes Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Complexity Health
Light.PortableResults 97% 94% 2981
Light.PortableResults.AspNetCore.MinimalApis 89% 75% 25
Light.PortableResults.AspNetCore.Mvc 89% 75% 25
Light.PortableResults.AspNetCore.OpenApi 94% 83% 505
Light.PortableResults.AspNetCore.Shared 100% 100% 28
Light.PortableResults.Validation 97% 89% 2954
Light.PortableResults.Validation.OpenApi 98% 91% 146
Light.PortableResults.Validation.OpenApi.SourceGeneration 92% 86% 1164
Summary 96% (14295 / 14885) 90% (6043 / 6695) 7828

Minimum allowed line rate is 60%

@feO2x
feO2x merged commit 1d959c6 into main Aug 2, 2026
2 checks passed
@feO2x
feO2x deleted the 53-fix-cloud-events-extension-attributes branch August 2, 2026 07:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CloudEvents extension attributes are written with JSON types outside the CloudEvents type system

1 participant