feat(19): ConsumerDefinition<TConsumer> discovered through DI (public retry, opt-in topology) - #45
Merged
Merged
Conversation
…rConfigurator<TConsumer> facade Public abstract ConsumerDefinition<TConsumer> in BareWire.Abstractions.Configuration (zero-dep, transport-agnostic) co-locates per-consumer settings via a new single-param, message-agnostic IConsumerConfigurator<TConsumer> facade. The existing two-param IConsumerConfigurator<TConsumer, TMessage> now inherits the single-param one (additive, non-breaking). TMessage is bound at startup by inference, never on the base type signature. Abstractions public-API snapshot regenerates additively in 19.15.
Public IRetryConfigurator (Interval/Incremental/Exponential/Handle<TException>/Ignore<TException>, chained shape) now lives in BareWire.Abstractions so a consumer definition can compose a retry policy. Build()->RetryPolicy stays in the core (BareWire) — RetryPolicy is not referenced from the zero-dep Abstractions. The former internal IRetryConfigurator is removed; RetryConfigurator maps the public contract to RetryPolicy in the core. Abstractions public-API snapshot regenerates additively in 19.15.
…ndpointConfigurator Additive single-param overload for a consumer with a single IConsumer<T>; TMessage is resolved at startup by inference (19.6). Existing Consumer<TConsumer, TMessage>() unchanged. Abstractions public-API snapshot regenerates additively in 19.15.
…nt knobs Additive, default-off ConfigureRetry (Action<IRetryConfigurator>?) plus PrefetchCount/ ConcurrentMessageLimit. The retry carrier is typed on the public IRetryConfigurator contract and does not pull the core RetryPolicy type into the zero-dep Abstractions, nor degrade to an untyped handle (I-1). Abstractions public-API snapshot regenerates additively in 19.15.
RetryPolicyMaterializer bridges the ConsumerRegistration retry carrier (Action<IRetryConfigurator>) to a core RetryPolicy. Build()->RetryPolicy stays in the core; RetryMiddleware and settlement (ack/nack/dead-letter) are unchanged.
…lement Consumer<TConsumer>() Concrete Consumer<TConsumer>() on both the core and RabbitMQ endpoint configurators. TMessage is resolved once at startup (MakeGenericMethod -> baked closed delegate; zero per-message reflection, ADR-003); multiple IConsumer<T> fail fast at startup with an actionable message requiring the explicit Consumer<TConsumer, TMessage>() overload. Greens the full solution build.
The core resolves explicitly DI-registered ConsumerDefinition<TConsumer> instances at startup, invokes Configure(IReceiveEndpointConfigurator, IConsumerConfigurator<TConsumer>), and materializes the settings into ConsumerRegistration. No assembly scan and no IRegistrationConfigurator; consumers are still not auto-registered. An unregistered definition is not applied. Zero hot-path reflection.
…stration (core configurator) The core ConsumerConfigurator now materializes definition-sourced settings (retry carrier, prefetch/concurrency knobs) into ConsumerRegistration via internal setters and an extended Build(), coexisting with the existing routing-key / AcceptUntyped / envelope materialization. No public-API change. Per-project configurator (core and transport internals are not shared).
New IBareWireInstrumentation.RecordRetryAttempt(endpoint, messageType, errorType) with a Counter<long> (TagList, no heap alloc) in BareWireMetrics, a no-op in both NullInstrumentation implementers, invoked only inside the RetryMiddleware retry branch (post-exception path) — the 0-B/op success path is untouched and settlement/ack-nack is unchanged. Message-type tag is precompiled at endpoint build time. Abstractions public-API snapshot regenerates in 19.15.
… transport configurator Separate internal sealed ConsumerConfigurator in the RabbitMQ transport materializes definition settings into a ConsumerRegistration equivalent to the core path (19.8), per the dual-impl parity requirement (core and transport internals are not shared). No public-API change.
…inition MemoryDiagnoser benchmark proving the no-opt-in dispatch path stays 0 B/op: discovery and TMessage inference happen once at startup (baked closed delegate), so per-delivery there is no reflection and only precompiled ConsumerRegistration field reads. Confirms the <512 B/op consume budget is unchanged.
…ement Eight facts across the four ADR-036 enforcement guarantees: (1) the retry carrier maps to the correct core RetryPolicy while the Abstractions-resident carrier neither references RetryPolicy nor degrades to an untyped handle (I-1); (2) a DI-registered definition applies endpoint/per-consumer settings bit-for-bit on both core and transport; (3) single-IConsumer<T> infers TMessage, multiple fail fast at startup; (4) a consumer with no definition behaves bit-identically to before.
…t seam Opt-in helper (extension method in BareWire.Transport.RabbitMQ) declares exchange+queue+binding for a consumer through the transport adapter. Invariants held: opt-in only (ADR-002 not reversed — without the call topology is unchanged); routed through the transport seam, not a core->RabbitMQ call; the binding declaration and the dispatcher routing-key pattern stay separate axes (no silent coupling); the AMQP helper is absent from ConsumerDefinition<T> and the zero-dep Abstractions (I-2).
The SEC-13 advisory (BareWireBusControl) reads the materialized per-consumer flags on binding.Consumers and warns at bus build when a definition combines AcceptUntyped() with the MassTransit envelope and/or the opt-in topology helper without a configured SchemaValidationMiddleware. The definition does not by itself widen the attack surface; deserialization hardening (no polymorphic serializer, STJ MaxDepth, payload size limit) matches the type-less path, assuming broker-level publish ACL.
…tMQ broker Aspire-fixtured integration tests: a ConsumerDefinition with retry + dead-letter + routing keys applied on a real broker; the opt-in topology helper declares exchange+queue+binding through the adapter while without the opt-in call topology stays unchanged (ADR-002); binding and dispatcher routing-key pattern are independent axes. Compiles green; the live broker run is pending a Docker-hosted RabbitMQ.
…t tests Completes the single-param facade with a public Retry(Action<IRetryConfigurator>) so a ConsumerDefinition composes retry ergonomically (consumer.Retry(r => r.Exponential(...))) — the method is promoted onto IConsumerConfigurator<TConsumer>, implemented on the core and RabbitMQ two-param configurators and on the definition facade (flowing into ConsumerRegistration.ConfigureRetry). Regenerates the additive BareWire.Abstractions public-API baseline for the whole feature, adds the I-2 topology-seam assertion (the AMQP topology helper is absent from ConsumerDefinition<T>/Abstractions) and the M-1 fitness test (auto-topology is not the default). ContractTests green; layer rules unchanged.
New English DocFX article covering: defining a single-param ConsumerDefinition<TConsumer> and registering it through DI; composing per-consumer settings in Configure (routing keys, AcceptUntyped, MassTransit envelope, and the fluent consumer.Retry(r => ...)); the opt-in topology helper with manual topology as the default; and TMessage inference for a single IConsumer<T>. Includes a Differences-vs- MassTransit note warning that DI-explicit discovery (no assembly scan) silently skips an unregistered definition. Described inline (no ADR numbers or internal paths).
…t-in topology New BareWire.Samples.ConsumerDefinitionShowcase: a ConsumerDefinition<TransferConsumer> discovered through explicit DI registration that co-locates a retry policy (consumer.Retry(r => r.Exponential(...))), routing-key patterns, and the opt-in transport topology helper. Registered in the Aspire AppHost with a wired E2E smoke test (SamplesAppFixture) asserting routing + retry + topology on a real broker. Code and README are English and self-contained. The live E2E run is pending a Docker-hosted RabbitMQ (unavailable in this environment); the sample compiles and the test is discoverable.
The discovery previously passed a no-op IReceiveEndpointConfigurator to a definition's Configure, so endpoint-level settings a user applied through the endpoint argument (prefetch, concurrency, retry count/interval, serializer overrides) were silently dropped. A capturing configurator now records the settings that map onto EndpointBinding and materializes them back into the binding, and it throws a clear NotSupportedException for endpoint operations a per-consumer definition cannot express (nested consumer/raw/saga registration, per-endpoint ordering, the consume-topology toggle, default content type, raw-serializer options) instead of dropping them. Reflection-wrapped exceptions are unwrapped so the real NotSupportedException surfaces.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Feature 19 —
ConsumerDefinition<TConsumer>discovered through DIAdds a public
ConsumerDefinition<TConsumer>base type that co-locates per-consumer configuration (routing keys, retry, dead-letter,AcceptUntyped, MassTransit envelope, prefetch/concurrency) close to the consumer, discovered through explicit DI registration (no assembly scan). Fully additive and non-breaking; the raw-first and manual-topology defaults are unchanged.Highlights
ConsumerDefinition<TConsumer>+ a new message-agnosticIConsumerConfigurator<TConsumer>that the existing two-paramIConsumerConfigurator<TConsumer, TMessage>now inherits (additive).IRetryConfigurator(Interval/Incremental/Exponential/Handle<TException>/Ignore<TException>) with a fluentconsumer.Retry(r => r.Exponential(...))on the façade;Build() → RetryPolicystays in the core (zero-dep Abstractions preserved).Consumer<TConsumer>()inference overload —TMessageresolved once at startup (MakeGenericMethod, zero per-message reflection); multipleIConsumer<T>fail fast at startup.AcceptUntyped()with the envelope/topology helper without a schema-validation middleware.Subtasks
ConsumerDefinition<TConsumer>+IConsumerConfigurator<TConsumer>façadeIRetryConfiguratorinto AbstractionsConsumer<TConsumer>()inference overloadConsumerRegistrationwith retry carrier + endpoint knobsIRetryConfiguratorto coreRetryPolicyTMessageinference + concreteConsumer<TConsumer>()ConsumerDefinition<TConsumer>through DI at startupConsumerRegistration(core)Retry)Build & Test
dotnet build BareWire.slnx -c Release— 0 warnings / 0 errors (TreatWarningsAsErrors).Caveats
ConsumerDefinitionis single-parameter (ConsumerDefinition<TConsumer>); the two-parameter variant was rejected because a single-param class cannot reference the two-param configurator in itsConfiguresignature (CS0311). The façade gained a fluentRetry(...)so retry composes ergonomically.