Skip to content

feat: IConfigureOptions with IConfiguration binding for existing options classes - #677

Merged
samtrion merged 4 commits into
feature/238-options-validationfrom
feature/239-configure-options-binding
Aug 3, 2026
Merged

feat: IConfigureOptions with IConfiguration binding for existing options classes#677
samtrion merged 4 commits into
feature/238-options-validationfrom
feature/239-configure-options-binding

Conversation

@samtrion

@samtrion samtrion commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds an IConfigureOptions<TOptions> implementation for each existing options class, binding the documented Pulse:* configuration section (configuration.GetSection("Pulse:...").Bind(options)), and registers each via services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<TOptions>, TOptionsConfiguration>()) inside the existing Add*/Use* extension method (no new public API):
    • LoggingInterceptorOptionsPulse:Logging (AddLogging)
    • TimeoutRequestInterceptorOptionsPulse:Timeout (AddRequestTimeout)
    • QueryCachingOptionsPulse:QueryCaching (AddQueryCaching)
    • OutboxOptionsPulse:Outbox (AddOutbox)
    • OutboxProcessorOptionsPulse:OutboxProcessor (AddOutbox)
    • AzureServiceBusTransportOptionsPulse:Transports:AzureServiceBus (UseAzureServiceBusTransport)
    • RabbitMqTransportOptionsPulse:Transports:RabbitMq (UseRabbitMqTransport)
    • DaprMessageTransportOptionsPulse:Transports:Dapr (UseDaprTransport)
  • Since IConfigureOptions runs before IValidateOptions in the options pipeline, a bad Pulse:* config section now fails at startup via the feat: IValidateOptions and ValidateOnStart for all existing options classes #238 validators, once bound.
  • The configuration-bound configurator is registered before any explicit Configure(...) call in each method, so an explicit code-based configuration action still takes precedence over configuration-bound values (config supplies the base, code can override it).
  • AddRequestTimeout(TimeSpan? globalTimeout = null) now only applies its globalTimeout parameter to GlobalTimeout when a value is actually supplied, instead of unconditionally overwriting it with the default null argument. Without this change, a configuration-bound GlobalTimeout would always be wiped out by the parameterless AddRequestTimeout() call.

Deviation from the issue text: SQLiteOutboxOptions

The issue lists a SQLiteOutboxOptions type bound from Pulse:Outbox:SQLite. As already established in #238, no such type exists — the SQLite provider reuses the shared OutboxOptions (see src/NetEvolve.Pulse.SQLite/). This row is skipped rather than inventing a new options type or a duplicate configurator: OutboxOptions is already bound from Pulse:Outbox by item 4 above, which covers the SQLite provider as well. Introducing a SQLite-specific Pulse:Outbox:SQLite override section would only add a second, harder-to-reason-about binding path onto the same options instance for no documented benefit, so it was intentionally left out.

Dependency

Depends on #238 (options validators), which this branch is based on (feature/238-options-validation). This PR's diff should only contain the #239-specific changes once #238 is merged first.

Closes #239

Test plan

  • dotnet build Pulse.slnx succeeds with no new warnings/errors
  • dotnet run --project Tests/NetEvolve.Pulse.Tests.Unit -f net10.0 -- --treenode-filter "/*/*/*ConfigurationTests*/*" — new configurator tests (binding + default-preservation + null-configuration guard) all pass
  • Full NetEvolve.Pulse.Tests.Unit suite (1629 tests) passes, including existing extension-method tests updated to register a minimal IConfiguration (previously implicit, now required since IConfiguration must be resolvable from DI for the new configurators)
  • csharpier format . applied

Add IConfigureOptions<TOptions> implementations that bind LoggingInterceptorOptions,
TimeoutRequestInterceptorOptions, QueryCachingOptions, OutboxOptions,
OutboxProcessorOptions, AzureServiceBusTransportOptions, RabbitMqTransportOptions,
and DaprMessageTransportOptions from documented Pulse:* configuration sections,
registered inside the respective existing Add*/Use* extension methods so
IConfiguration-backed values are validated at startup by the #238 validators.

AddRequestTimeout only applies its explicit globalTimeout parameter when a value
is provided, so a configuration-bound GlobalTimeout is no longer unconditionally
overwritten by the method's default null argument.
@samtrion
samtrion requested a review from a team as a code owner August 3, 2026 10:01
@samtrion
samtrion requested review from benwirren and removed request for a team August 3, 2026 10:01
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • state:ready for merge

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7f9a8fcf-916b-41b6-85e1-f72b6c668509

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Resolving IRabbitMqChannelPool now requires IConfiguration to be
resolvable, since RabbitMqTransportOptionsConfiguration (added by the
IConfigureOptions binding work) depends on it.
@samtrion
samtrion merged commit 4ba5faa into feature/238-options-validation Aug 3, 2026
2 checks passed
@samtrion
samtrion deleted the feature/239-configure-options-binding branch August 3, 2026 11:07
samtrion added a commit that referenced this pull request Aug 3, 2026
…lasses (#676)

* feat: add IValidateOptions and ValidateOnStart for existing options classes

Add IValidateOptions<T> validators for TimeoutRequestInterceptorOptions,
QueryCachingOptions, OutboxOptions, OutboxProcessorOptions,
AzureServiceBusTransportOptions, RabbitMqTransportOptions, and
DaprMessageTransportOptions, each registered with AddOptions<T>().ValidateOnStart()
so misconfiguration is caught at startup instead of at first use.

The AzureServiceBus transport's imperative ValidateOptions() check is replaced
by AzureServiceBusTransportOptionsValidator with equivalent behavior, now
surfaced as an OptionsValidationException instead of an InvalidOperationException.

LoggingInterceptorOptions is intentionally left untouched: it already has a
validator but is out of scope for this change. SQLiteOutboxOptions does not
exist as a distinct type; the SQLite provider reuses the shared OutboxOptions,
which is already covered by OutboxOptionsValidator (TableName not empty).
ConnectionString remains unvalidated there since it is legitimately null for
EF Core-based outbox usage.

* fix(outbox): reword test comment to avoid false-positive S125 match

SonarAnalyzer flagged the explanatory comment as commented-out code
because it contained a code-like fragment; reworded in prose only.

* feat: IConfigureOptions with IConfiguration binding for existing options classes (#677)

* feat: bind options from configuration via IConfigureOptions

Add IConfigureOptions<TOptions> implementations that bind LoggingInterceptorOptions,
TimeoutRequestInterceptorOptions, QueryCachingOptions, OutboxOptions,
OutboxProcessorOptions, AzureServiceBusTransportOptions, RabbitMqTransportOptions,
and DaprMessageTransportOptions from documented Pulse:* configuration sections,
registered inside the respective existing Add*/Use* extension methods so
IConfiguration-backed values are validated at startup by the #238 validators.

AddRequestTimeout only applies its explicit globalTimeout parameter when a value
is provided, so a configuration-bound GlobalTimeout is no longer unconditionally
overwritten by the method's default null argument.

* fix(rabbitmq): register IConfiguration in channel pool resolution test

Resolving IRabbitMqChannelPool now requires IConfiguration to be
resolvable, since RabbitMqTransportOptionsConfiguration (added by the
IConfigureOptions binding work) depends on it.
samtrion added a commit that referenced this pull request Aug 3, 2026
)

* feat(rabbitmq): pool RabbitMQ channels in RabbitMqMessageTransport

Replace the single lazily-created, publish-serialized channel in
RabbitMqMessageTransport with a pooled IRabbitMqChannelPool /
RabbitMqChannelPool backed by a ConcurrentQueue of idle channels and a
SemaphoreSlim capped at the new RabbitMqTransportOptions.MaxChannelPoolSize
(default 10). SendAsync rents a channel per call; SendBatchAsync rents a
single channel for the whole batch and publishes sequentially on it, since
only one thread ever touches that channel. Both always return the channel
in a finally block. IsHealthyAsync now delegates to the pool. The channel
pool is registered as a singleton via TryAddSingleton in
UseRabbitMqTransport so repeated calls do not duplicate it.

* fix(rabbitmq): suppress false-positive S5034 on per-iteration ValueTask conversion

RentAsync_ConcurrentCalls_AreCappedAtMaxChannelPoolSize converts a fresh
ValueTask returned by RentAsync to a Task exactly once per loop iteration,
but SonarAnalyzer's cross-iteration analysis cannot tell the instances
apart and flags a false double-consumption.

* test(rabbitmq): cover channel-creation failure and pool resolution paths

Adds coverage for RentAsync releasing its rental slot when channel
creation fails, and for resolving IRabbitMqChannelPool from a built
service provider, raising patch coverage to the required threshold.

* feat: add IValidateOptions and ValidateOnStart for existing options classes (#676)

* feat: add IValidateOptions and ValidateOnStart for existing options classes

Add IValidateOptions<T> validators for TimeoutRequestInterceptorOptions,
QueryCachingOptions, OutboxOptions, OutboxProcessorOptions,
AzureServiceBusTransportOptions, RabbitMqTransportOptions, and
DaprMessageTransportOptions, each registered with AddOptions<T>().ValidateOnStart()
so misconfiguration is caught at startup instead of at first use.

The AzureServiceBus transport's imperative ValidateOptions() check is replaced
by AzureServiceBusTransportOptionsValidator with equivalent behavior, now
surfaced as an OptionsValidationException instead of an InvalidOperationException.

LoggingInterceptorOptions is intentionally left untouched: it already has a
validator but is out of scope for this change. SQLiteOutboxOptions does not
exist as a distinct type; the SQLite provider reuses the shared OutboxOptions,
which is already covered by OutboxOptionsValidator (TableName not empty).
ConnectionString remains unvalidated there since it is legitimately null for
EF Core-based outbox usage.

* fix(outbox): reword test comment to avoid false-positive S125 match

SonarAnalyzer flagged the explanatory comment as commented-out code
because it contained a code-like fragment; reworded in prose only.

* feat: IConfigureOptions with IConfiguration binding for existing options classes (#677)

* feat: bind options from configuration via IConfigureOptions

Add IConfigureOptions<TOptions> implementations that bind LoggingInterceptorOptions,
TimeoutRequestInterceptorOptions, QueryCachingOptions, OutboxOptions,
OutboxProcessorOptions, AzureServiceBusTransportOptions, RabbitMqTransportOptions,
and DaprMessageTransportOptions from documented Pulse:* configuration sections,
registered inside the respective existing Add*/Use* extension methods so
IConfiguration-backed values are validated at startup by the #238 validators.

AddRequestTimeout only applies its explicit globalTimeout parameter when a value
is provided, so a configuration-bound GlobalTimeout is no longer unconditionally
overwritten by the method's default null argument.

* fix(rabbitmq): register IConfiguration in channel pool resolution test

Resolving IRabbitMqChannelPool now requires IConfiguration to be
resolvable, since RabbitMqTransportOptionsConfiguration (added by the
IConfigureOptions binding work) depends on it.

* fix(rabbitmq): register IConfiguration in channel pool resolution test

Resolving IOptions<RabbitMqTransportOptions> now requires IConfiguration
to be resolvable, since RabbitMqTransportOptionsConfiguration depends
on it; the raw ServiceCollection built by this integration test did not
register one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: IConfigureOptions with IConfiguration binding for all existing options classes

1 participant