Skip to content

feat: add IValidateOptions and ValidateOnStart for existing options classes - #676

Merged
samtrion merged 5 commits into
feature/241-rabbitmq-channel-poolfrom
feature/238-options-validation
Aug 3, 2026
Merged

feat: add IValidateOptions and ValidateOnStart for existing options classes#676
samtrion merged 5 commits into
feature/241-rabbitmq-channel-poolfrom
feature/238-options-validation

Conversation

@samtrion

@samtrion samtrion commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds IValidateOptions<T> validators for the following options classes, each registered with the fail-fast pattern (AddOptions<T>().ValidateOnStart() + TryAddEnumerable(... IValidateOptions<T> ...)) in their respective existing Add*/Use* extension method:

  • TimeoutRequestInterceptorOptions (AddRequestTimeout) — GlobalTimeout must be null or > TimeSpan.Zero.
  • QueryCachingOptions (AddQueryCaching) — DefaultExpiry must be null or > TimeSpan.Zero.
  • OutboxOptions (AddOutbox) — TableName must not be null/empty/whitespace.
  • OutboxProcessorOptions (AddOutbox) — BatchSize > 0, PollingInterval > 0, MaxRetryCount >= 0, ProcessingTimeout > 0; when EnableExponentialBackoff is true, also BackoffMultiplier > 1.0, BaseRetryDelay > 0, MaxRetryDelay >= BaseRetryDelay.
  • AzureServiceBusTransportOptions (UseAzureServiceBusTransport) — either ConnectionString or FullyQualifiedNamespace must be set.
  • RabbitMqTransportOptions (UseRabbitMqTransport) — ExchangeName not empty; MaxChannelPoolSize >= 1 (the latter property was added by feat: RabbitMQ channel pooling in RabbitMqMessageTransport #241).
  • DaprMessageTransportOptions (UseDaprTransport) — PubSubName not empty.

Each validator lives next to its options class, following the existing {OptionsClassName}Validator naming convention used by LoggingInterceptorOptionsValidator.

Deviations from the issue text

  • LoggingInterceptorOptions is intentionally left untouched. It already has a validator (LoggingInterceptorOptionsValidator) but registration doesn't call .ValidateOnStart(). This class was explicitly called out as out of scope for this change.
  • AzureServiceBusTransportOptions: the transport previously validated its options imperatively at ServiceBusClient-creation time via a private static ValidateOptions method, throwing InvalidOperationException. This is replaced by AzureServiceBusTransportOptionsValidator registered via the options pipeline. Behavior is equivalent (same required-field check, still throws at first options resolution / at host startup with ValidateOnStart), but the thrown exception type changes to Microsoft.Extensions.Options.OptionsValidationException. Existing tests were updated to expect the new exception type.
  • SQLiteOutboxOptions does not exist as a distinct type in the current codebase — the SQLite provider (src/NetEvolve.Pulse.SQLite/Outbox/OutboxOptionsExtensions.cs) reuses the shared OutboxOptions. This row from the issue is covered by the OutboxOptionsValidator added for OutboxOptions (TableName not empty). OutboxOptions.ConnectionString is intentionally not validated as required, since it is legitimately null for EF Core-based outbox usage (only ADO.NET-based providers need it) — making it mandatory would break existing valid configurations.

Dependency

This branch is based on feature/241-rabbitmq-channel-pool (not yet merged to main), since RabbitMqTransportOptions.MaxChannelPoolSize — validated here — was introduced there. The diff shown in this PR is scoped to the #238-specific changes on top of that branch.

Notes

Because IValidateOptions<T> runs validation whenever the options are resolved (via IOptions<T>/IOptionsMonitor<T>), not only through the ValidateOnStart() eager startup check, a couple of pre-existing tests that resolved default (and now invalid) option values needed small updates to assert the resulting OptionsValidationException instead of the previous default values / exception types.

Closes #238

Test plan

  • dotnet build Pulse.slnx succeeds with no errors.
  • csharpier format . run before commit.
  • New validator unit tests (one class per validator) covering valid configuration and each invalid field — 59 tests, all passing.
  • Existing/updated extension registration tests (OutboxExtensionsTests, RabbitMqExtensionsTests, AzureServiceBusExtensionsTests, etc.) — 557 tests, all passing.
  • Verified ValidateOnStart's effect end-to-end: resolving IOptions<OutboxOptions> (and similarly for RabbitMQ/AzureServiceBus) with an invalid configuration throws OptionsValidationException when the service provider builds the options.

…lasses

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.
@samtrion
samtrion requested a review from a team as a code owner August 3, 2026 09:18
@samtrion
samtrion requested review from Hnogared and removed request for a team August 3, 2026 09:18
@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: 482e9500-9da0-4d3f-a394-81e598ad59e5

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.

SonarAnalyzer flagged the explanatory comment as commented-out code
because it contained a code-like fragment; reworded in prose only.
@samtrion samtrion linked an issue Aug 3, 2026 that may be closed by this pull request
4 tasks
samtrion and others added 2 commits August 3, 2026 12:44
…ons 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
samtrion merged commit 41b24c0 into feature/241-rabbitmq-channel-pool Aug 3, 2026
2 checks passed
@samtrion
samtrion deleted the feature/238-options-validation branch August 3, 2026 11:08
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: IValidateOptions and ValidateOnStart for all existing options classes

1 participant