Skip to main content

Migration Guide

v2.x → v3.0​

Core Vali-Mediator 3.0.0; AspNetCore, Caching, Idempotency, Observability and Resilience 2.0.0. The extension packages call the new IPipelineBehavior.Handle(..., Func<CancellationToken, Task<T>> next) signature, so they only work against core 3.x — update the core and all five extensions together. An old extension against the 3.x core fails at runtime with a MissingMethodException. Use [3.0.0, 4.0.0) as the recommended dependency range.

1. Required breaking change: next takes a CancellationToken​

// 2.x
public async Task<TResponse> Handle(TRequest request, Func<Task<TResponse>> next, CancellationToken ct)
=> await next();

// 3.0
public async Task<TResponse> Handle(TRequest request, Func<CancellationToken, Task<TResponse>> next, CancellationToken ct)
=> await next(ct); // or a linked token if the behavior cancels on its own

Same for IPipelineBehavior<TDispatch>: Func<CancellationToken, Task> next. Lambdas that used to ignore the token become _ => ....

2. Behavior changes​

AreaBeforeNowAction
Publish<T>Only static-type handlers ranUnion of static-type and runtime-type handlers (deduplicated)Check handlers on base types/INotification that previously didn't run
Assembly scanningScanning twice duplicated handlers; first registration wonSingle registration; last lifetime winsNone, unless you relied on the first-wins behavior
default(Result) / default(Result<T>)Error = null, ErrorType.NoneExplicit "not initialized" failure, ErrorType.Failure. Fail("x", ErrorType.None) still keeps NoneDon't treat default as success
TimeoutBehaviorHandler kept running after timeoutCancels the handler's token; TimeoutException wraps the handler's exception as InnerExceptionPass the token through your operations
Handler exceptionsWrapped in TargetInvocationExceptionOriginal type and stackRemove catch (TargetInvocationException)
Resilience: orderFallback → Retry → Timeout → CB → Bulkhead → Hedge → RateLimiter → ChaosFallback → Chaos → RateLimiter → Retry → Timeout → CB → Bulkhead → HedgeA rate-limit rejection is no longer retried or counted against the circuit breaker
Resilience: policy resolutionCached per type (first request wins)Resolved per requestReuse one instance, or use .WithSharedState("key") to share Bulkhead/RateLimiter/CB state between per-request policies
Resilience: HedgeReturned default when every attempt failedThrows the last exception; losers are cancelledHandle the exception
Resilience: optionsInvalid values were toleratedArgumentOutOfRangeException at .Build()Fix out-of-range values
Idempotency: key formatIdempotencyKeyType#len:scope#key; old entries are treated as a missThe handler runs once more right after the upgrade
Idempotency: payload conflictSame key always replayed the stored responseSame key + different payload → Conflict / IdempotencyConflictException (disable with VerifyRequestFingerprint = false)Avoid volatile fields (timestamps, correlation ids) in the request
Idempotency: Expiration == nullNo expirationStore's default expiration (24h)Set Expiration explicitly for a different value
Idempotency / CachingFailed Result responses were storedNever stored or invalidatedNone
AspNetCoreFailure (500) included Result.Error in DetailGeneric detail message; ResultHttpOptions.ExposeErrorDetails = true restores itOnly enable in development
ObservabilityAn observer error propagated as AggregateExceptionIsolated; visible via the Activity / IMetricsCollector.RecordObserverError. Exception message hidden unless IncludeExceptionMessage = trueSubscribe to the metrics collector if you relied on seeing it

3. New limits and their defaults​

OptionDefaultWhere
InMemoryCacheOptions.MaxEntries10,000 (was 1,000)Caching
InMemoryCacheOptions.MaxKeyLength / MaxGroups / MaxKeysPerGroup512 / 10,000 / 10,000Caching
InMemoryCacheOptions.CleanupInterval5 minCaching
CachingOptions.CoalescingWaitTimeout30s (Timeout.InfiniteTimeSpan allowed)Caching
IdempotencyOptions.MaxKeyLength / VerifyRequestFingerprint256 / trueIdempotency
InMemoryIdempotencyStoreOptions.MaxEntries / DefaultExpiration10,000 / 24hIdempotency
RateLimiterOptions.MaxPartitions / PartitionIdleTimeout10,000 / 5 minResilience
WithSharedState(key)capped at 10,000 keysResilience
IValiMediator.SendAll(requests, maxDegreeOfParallelism, ct)unbounded if omittedCore
InMemoryDeadLetterQueue(maxEntries)1,000Core
ObservabilityOptions.IncludeExceptionMessagefalseObservability
ResultHttpOptions.ExposeErrorDetailsfalseAspNetCore

Every limit is validated (throws ArgumentOutOfRangeException when invalid) and configured through the relevant package's DI registration.

4. Checklist​

  1. Bump Vali-Mediator to 3.0.0 and all five extensions to 2.0.0.
  2. Update your own IPipelineBehavior implementations to call next(ct).
  3. Check the behavior-changes table above against your actual usage.
  4. Run your tests. If you use Idempotency, expect one re-execution per key right after deploying (the key format changed).

v1.x → v2.0​

Version 2.0 introduces several breaking changes alongside new features. Follow this guide to upgrade.

Breaking Changes​

1. IPreProcessor and IPostProcessor now return Task​

Before (v1.x):

public class MyPreProcessor : IPreProcessor<MyRequest>
{
public void Process(MyRequest request, CancellationToken ct)
{
// sync implementation
}
}

After (v2.0):

public class MyPreProcessor : IPreProcessor<MyRequest>
{
public async Task Process(MyRequest request, CancellationToken ct)
{
// async implementation
await Task.CompletedTask;
}
}

For sync implementations, return Task.CompletedTask:

public Task Process(MyRequest request, CancellationToken ct)
{
// sync work here
return Task.CompletedTask;
}

2. INotificationHandler.Priority now has a default implementation​

Before (v1.x): Priority was an abstract property — every handler had to override it.

After (v2.0): Priority has a default implementation => 0 — override only when needed.

// v1.x — required
public class MyHandler : INotificationHandler<MyEvent>
{
public int Priority => 0; // had to be here

public async Task Handle(MyEvent n, CancellationToken ct) { ... }
}

// v2.0 — Priority can be omitted if 0
public class MyHandler : INotificationHandler<MyEvent>
{
public async Task Handle(MyEvent n, CancellationToken ct) { ... }
}

3. ValiMediatorConfiguration uses List (order preserved)​

In v1.x, behaviors were stored in a Dictionary<Type, Type>, which prevented registering multiple behaviors of the same open-generic type.

In v2.0, List<(Type, Type)> is used — registration order is preserved and duplicates are allowed.

No code change required unless you were relying on dictionary semantics.

New Features in v2.0​

Result Pattern​

// New in v2.0 — use Result<T> instead of throwing exceptions
public async Task<Result<ProductDto>> Handle(GetProductQuery req, CancellationToken ct)
{
var product = await _repo.FindAsync(req.Id, ct);
if (product is null)
return Result<ProductDto>.Fail("Not found.", ErrorType.NotFound);
return Result<ProductDto>.Ok(product.ToDto());
}

SendOrDefault​

// New in v2.0 — returns default instead of throwing HandlerNotFoundException
ProductDto? product = await _mediator.SendOrDefault(new GetProductQuery(id));

SendAll​

// New in v2.0 — parallel execution
Result<ProductDto>[] results = await _mediator.SendAll(queries);

Streaming​

// New in v2.0
IAsyncEnumerable<ProductDto> stream = _mediator.CreateStream(new GetProductsStream());

ResilientParallel + Dead Letter Queue​

// New in v2.0
await _mediator.Publish(notification, PublishStrategy.ResilientParallel);

IHasTimeout​

// New in v2.0 — declarative per-request timeout
public record SlowQuery() : IRequest<Result<string>>, IHasTimeout
{
public int TimeoutMs => 5000;
}

Migration Steps​

  1. Update packages to version 2.0.0
  2. Fix IPreProcessor/IPostProcessor — change void Process(...) to Task Process(...)
  3. Remove mandatory Priority => 0 overrides where not needed (optional cleanup)
  4. Adopt Result<T> — refactor handlers that throw business exceptions
  5. Install extension packages as needed (Resilience, Caching, etc.)
  6. Add behaviors to DI — config.AddCachingBehavior(), etc.
tip

The migration can be done incrementally. Old handlers without Result<T> continue to work. Adopt the Result pattern handler by handler.