Changelog
This page records issues discovered and resolved while hardening the library. All entries are fully resolved — none represent open bugs. The detailed engineering notes for each fix live in docs/known-issues/ (mirrors the GitHub bug-report template; kept as a developer reference).
Core DI
| ID | Summary | Severity |
|---|---|---|
| 001 | Open-generic pipeline behavior registrations throw at runtime under Native AOT when TResponse is a value type | High |
| 002 | ValidateOnBuild = false does not suppress the issue-001 runtime error | Medium |
| 004 | AddValidator registers a validator that is never wired into the pipeline — validation silently never runs | High |
| 015 | EnableCqrsBoundaryEnforcement() was an [Obsolete] no-op; runtime-API callers silently lost all enforcement | High |
| 019 | Behavior dedup compared (ServiceType, ImplementationType) only — lifetime-blind, skipped factory/instance registrations | Medium |
| 029 | Scoped IContext cached a boxed snapshot while WithCorrelationId returned a new box, so a pre-middleware reader and the response header could report different correlation IDs; identity is now fixed at creation via IInboundContextStore | Medium |
| 035 | Context held baggage and features in plain dictionaries while the concurrent event orchestrator runs every handler against that one instance, so entries were lost, the byte counter drifted and enumeration threw; both stores are now copy-on-write | High |
| 036 | ContextHandler built its context lazily without synchronization, so concurrent handlers in one scope each minted their own trace id and only one won the field; creation is now locked and double-checked | High |
Source Generator
| ID | Summary | Severity |
|---|---|---|
| 005 | Generator emits an uncompilable type name for nested handler/behavior classes (drops enclosing-type chain) | High |
| 006 | Open-generic behavior closed with the interface's arity, not the class's own → CS0305 | High |
| 012 | GlobalizeType emits invalid global:: prefix for tuple / pointer / function-pointer types | Medium |
| 017 | SplitTopLevelArgs ignored tuple () — a tuple nested as a generic argument emitted uncompilable code | Medium |
| 022 | [Validator] derived the response type from the first IRequest<T> found; multi-IRequest requests now emit diagnostic MDG011 | Low |
| 024 | Open-generic behavior special constraints (class/struct/unmanaged/notnull/new()) were dropped, closing the behavior over non-conforming handlers → uncompilable code (CS0453); now captured as equatable flags and enforced in the cross-product | High |
| 025 | Generated RegisterGroup namespace was derived from the assembly name, ignoring MSBuild RootNamespace; it now reads build_property.RootNamespace first (and a new [RegisterGroup] partial class lets users pick the namespace/name explicitly) | Medium |
| 030 | A generic IEvent/IEventHandler<T> declaration was collected for dispatcher registration, emitting typeof(Event<T>) — a type parameter not in scope — so RegisterGroup.g.cs did not compile; generic definitions are now skipped and the last hand-rolled globalizer was deleted | Medium |
| 034 | Issue 030's guard tested the event's own arity, so a non-generic event nested in a generic type still emitted typeof(Box<T>.Opened) and did not compile; the containing-type chain is now inspected too | Medium |
Pipeline & CQRS
| ID | Summary | Severity |
|---|---|---|
| 008 | Cross-assembly open-generic behavior is registered (and runs) twice — no dedup on the user-behavior path | High |
| 009 | Order is only a generator-local sort; runtime order follows DI registration across sources | Medium |
| 013 | Invoker overloads resolve handlers inconsistently (static TRequest vs runtime request.GetType()) | Medium |
| 016 | CQRS boundary marker leaked when a handler threw — spurious violation on later send in the same scope | Medium |
| 018 | CQRS enforcement was emitted per-discovered-handler only — manually registered handlers were not covered | Medium |
| 021 | EventDispatcher re-sorted behaviors via OrderBy on every publish (per-publish allocation) | Low |
Outbox
| ID | Summary | Severity |
|---|---|---|
| 010 | Outbox event lookup narrowed to ReferenceEquals — broke the public contract for equal-but-distinct events | Medium |
| 014 | Outbox lookup matched the first value-equal event across all scopes; stored items now carry a stable Guid identity via OutboxEntry | High |
| 020 | Outbox "failed count" counted retrying events (Attempts > 0), tripping the health check on transient retries | Medium |
| 023 | RecordOutbox* metric methods were dead no-ops on ISynapseMetrics (superseded by observable gauges); removed | Low |
| 040 | Inject wrote trace context only when Activity.Current existed, so on a host with no tracing an outbox entry was stored with no traceparent and its dispatch became a disconnected root; the header is now derived from IContext.TraceId | Medium |
| 041 | Rebuilding an entry's headers with ToDictionary threw on keys differing only by case, from outside the try, so one badly-shaped row aborted the whole batch with nothing marked processed or failed | Low |
| 042 | Dispatch used the entry's restored flow only to parent the activity: the stored baggage was dropped and handlers read the committing scope's IContext, so entries were dispatched under whichever request called CommitAsync; each entry now gets its own scope built from its own state | High |
Observability
| ID | Summary | Severity |
|---|---|---|
| 007 | Outbox observable gauges are never registered; Record* are no-ops so outbox metrics emit nothing | High |
| 011 | Tracing capture stores zero/invalid trace IDs (empty-string guard never fires) and stringifies twice | Medium |
| 028 | LoggingEnrichmentBehavior copied all context metadata into the log scope, so the internal __CQRSBoundaryEnforcement markers appeared in every log entry; the markers are now a context feature and enrichment reads typed identity plus baggage | Medium |
| 031 | ContextIdentity.ForUnitOfWork read Activity.Current.TraceId without checking it was set, and an all-zeros trace id stringifies non-empty, so contexts carried 000…0; the ambient id is now taken only when non-default | Low |
| 033 | SynapsePropagationHandler injected a scoped IContextAccessor, but IHttpClientFactory caches message handlers in a scope of its own, so outbound propagation silently stamped nothing; the context is now mirrored onto the execution context | High |
| 037 | Inject let the platform propagator write its own baggage header from Activity.Baggage, so inbound baggage untrusted mode had dropped was forwarded anyway; the platform's baggage output is now discarded and the context is the only source | High |
| 038 | IsValidValue refused commas in decoded values although values are percent-encoded, so SetBaggage("k", "a,b") was rejected and a conformant peer's Acme%2C%20Inc was silently dropped inbound; only control characters are refused now | Medium |
| 039 | Parse charged the byte budget for every repetition of a key even though a repetition overwrites, dropping valid entries after it, and MeasureEntry measured the decoded value so escaped-heavy baggage exceeded the limit on the wire | Medium |
| 043 | Inject wrote the ambient activity's trace id rather than IContext.TraceId, so an untrusted edge propagated the caller's forged trace id onto outbound calls and into outbox entries; the context now wins when the two diverge | Medium |
ASP.NET Core
| ID | Summary | Severity |
|---|---|---|
| 003 | Pipeline short-circuit via Result.Failure<T>() mapped to HTTP 500; behavior now returns a typed UnauthorizedFailure → 401 | Medium |
| 026 | Guid.Empty was accepted as an inbound correlation ID, colliding with the "not set" sentinel and the uninitialized outbox partition; extraction now requires a non-empty Guid and rejects duplicated headers | Medium |
| 027 | The response writer read IContextAccessor.Context, which creates a context, so non-mediator routes got a fabricated correlation ID and the guarding catch was unreachable; a new IsInitialized lets it skip instead | Low |
| 032 | TrustIncomingHeader = false cleared the inbound trace context but the host had already parented the request Activity to the caller, so the forged trace id was read back from Activity.Current; a new SuppressAmbientTrace flag disqualifies the ambient activity | High |
Discovery context: Issues 001–003 were found while building the pipeline-behavior showcase in
examples/MinimalApion branchfeature/typed-pipeline-behaviorsagainst .NET 10 withPublishAot=true. Issues 004–023 were found in successive high-effort code reviews of the same branch (diff + working-tree, multiple finder angles). All are resolved. Line numbers in the individual files are approximate and may drift as the branch evolves.