Query Pipeline and Decorators
Introduction
The Query Pipeline in Darker provides a powerful way to add cross-cutting concerns to your query handlers without modifying the handler code itself. Using decorators (also called middleware), you can add capabilities like logging, retry logic, circuit breakers, and fallback behavior to any query handler through simple attribute annotations.
Darker's pipeline uses the same Russian Doll Model as Brighter, where each decorator in the pipeline encompasses the call to the next decorator or handler, allowing the chain to behave like a call stack. This architectural pattern enables you to compose complex behavior from simple, focused decorators that remain independent and testable.
This approach follows the Decorator Pattern and allows you to separate cross-cutting concerns from your core query logic. For more information on pipelines and the Russian Doll Model, see Basic Concepts.
How the Query Pipeline Works
Pipeline Execution Flow
When you call IQueryProcessor.ExecuteAsync(query), Darker constructs a pipeline of decorators around your query handler based on the attributes you've applied to the handler's ExecuteAsync method. The execution flows through each decorator in order before reaching your handler:
QueryProcessor.ExecuteAsync(query)
↓
[QueryLogging Decorator - Step 1]
Logs query details
↓
[FallbackPolicy Decorator - Step 2]
Catches exceptions, provides fallback
↓
[RetryableQuery Decorator - Step 3]
Retries on transient failures
Circuit breaker protection
↓
[Target Query Handler]
Your ExecuteAsync implementation
↓
Result (flows back up through decorators)
↓
[RetryableQuery completes]
↓
[FallbackPolicy completes]
↓
[QueryLogging completes]
↓
Result returned to callerEach decorator in the pipeline can:
Execute logic before calling the next handler in the chain
Execute logic after the next handler completes
Transform the query or result
Handle exceptions from downstream handlers
Short-circuit the pipeline and return early
Decorator Ordering
The order in which decorators execute is controlled by the step number specified in each attribute. Decorators execute in ascending order by step number:
using Paramore.Darker;
using Paramore.Darker.Policies;
using Paramore.Darker.QueryLogging;
using System.Threading;
using System.Threading.Tasks;
public sealed class GetPersonQueryHandler : QueryHandlerAsync<GetPersonNameQuery, string>
{
[QueryLogging(1)] // Executes FIRST (step 1)
[FallbackPolicy(2)] // Executes SECOND (step 2)
[RetryableQuery(3, "MyCircuitBreaker")] // Executes THIRD (step 3)
public override async Task<string> ExecuteAsync(
GetPersonNameQuery query,
CancellationToken cancellationToken = default)
{
// Your query logic here
// This executes LAST, after all decorators
}
}Why ordering matters:
Logging should typically be first (step 1): This ensures all operations are logged, including retries and fallbacks
Fallback before retry (step 2 before 3): If a retry exhausts its attempts, the fallback can provide a default result
Retry with circuit breaker should be last (step 3): This ensures retries happen after other decorators have a chance to handle the request
However, you can adjust the ordering to suit your specific needs. For example, you might want retry before fallback if you only want to use the fallback when all retries are exhausted.
Available Decorators
QueryLogging Decorator
The QueryLogging decorator provides JSON-based logging of query execution, including query parameters, execution time, and result summaries. This is invaluable for debugging, monitoring, and auditing query operations.
Configuration
First, add the query logging decorator to your Darker configuration in Program.cs:
using Paramore.Darker;
using Paramore.Darker.AspNetCore;
using Paramore.Darker.QueryLogging;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDarker()
.AddHandlersFromAssemblies(typeof(Program).Assembly)
.AddJsonQueryLogging(); // Add logging decorator
var app = builder.Build();
app.Run();The AddJsonQueryLogging() method registers the logging decorator in the Darker pipeline, making it available for use in your query handlers.
Usage
Apply the [QueryLogging] attribute to your query handler's ExecuteAsync method with a step number:
using Paramore.Darker;
using Paramore.Darker.QueryLogging;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
public sealed class GetPeopleQueryHandler : QueryHandlerAsync<GetPeopleQuery, IReadOnlyDictionary<int, string>>
{
private readonly IPersonRepository _repository;
public GetPeopleQueryHandler(IPersonRepository repository)
{
_repository = repository;
}
[QueryLogging(1)] // Execute logging as the first decorator
public override async Task<IReadOnlyDictionary<int, string>> ExecuteAsync(
GetPeopleQuery query,
CancellationToken cancellationToken = default)
{
var people = await _repository.GetAllAsync(cancellationToken);
return people;
}
}What Gets Logged
The QueryLogging decorator logs:
Query type: The full type name of the query being executed
Query parameters: JSON serialization of the query object and its properties
Execution time: The time taken to execute the query
Result summary: A summary of the result (typically type and count for collections)
Timestamp: When the query was executed
This information is written to your application's configured logging output (console, file, Application Insights, etc.) at the Information level by default.
Policy Decorators (Resilience)
Darker integrates with Polly to provide resilience and transient fault handling through policy decorators. These decorators allow you to add retry logic, circuit breakers, and fallback behavior to your query handlers.
RetryableQuery Decorator
The RetryableQuery decorator automatically retries a query when it encounters transient failures. It integrates with Polly circuit breakers to prevent overwhelming failing systems.
Purpose:
Retry queries that fail due to transient errors (network issues, temporary unavailability)
Protect downstream services with circuit breaker patterns
Improve application resilience without changing query handler code
Use Cases:
Querying external HTTP APIs that may have intermittent connectivity issues
Database queries that may experience transient connection failures
Any query operation that interacts with unreliable external dependencies
Configuration:
First, add policies to your Darker configuration:
using Paramore.Darker;
using Paramore.Darker.AspNetCore;
using Paramore.Darker.Policies;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDarker()
.AddHandlersFromAssemblies(typeof(Program).Assembly)
.AddDefaultPolicies(); // Adds default retry and circuit breaker policies
var app = builder.Build();
app.Run();Usage:
Apply the [RetryableQuery] attribute with a step number and the name of a circuit breaker policy:
using Paramore.Darker;
using Paramore.Darker.Policies;
using Paramore.Darker.QueryLogging;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
public sealed class GetPeopleQueryHandler : QueryHandlerAsync<GetPeopleQuery, IReadOnlyDictionary<int, string>>
{
private readonly IPersonRepository _repository;
public GetPeopleQueryHandler(IPersonRepository repository)
{
_repository = repository;
}
[QueryLogging(1)]
[RetryableQuery(2, "DefaultCircuitBreaker")] // Retry with circuit breaker
public override async Task<IReadOnlyDictionary<int, string>> ExecuteAsync(
GetPeopleQuery query,
CancellationToken cancellationToken = default)
{
var people = await _repository.GetAllAsync(cancellationToken);
return people;
}
}The RetryableQuery attribute takes two parameters:
Step number: Controls when this decorator executes in the pipeline (typically after logging and fallback)
Circuit breaker name: The name of a circuit breaker policy in your policy registry
When a query fails, the retry policy will attempt to execute it again based on your policy configuration (see Configuring Polly Policies). If failures continue, the circuit breaker will open, preventing further attempts until the circuit closes again.
FallbackPolicy Decorator
The FallbackPolicy decorator provides a way to return a default or degraded result when a query fails, rather than propagating the exception to the caller. This is essential for providing graceful degradation in user-facing applications.
Purpose:
Provide default values when queries fail
Enable degraded service modes
Improve user experience by avoiding error messages
Use Cases:
Returning cached or default data when a primary data source is unavailable
Providing placeholder content when real-time data cannot be retrieved
Implementing graceful degradation for non-critical queries
Configuration:
The FallbackPolicy decorator is available when you add policies to Darker:
using Paramore.Darker;
using Paramore.Darker.AspNetCore;
using Paramore.Darker.Policies;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDarker()
.AddHandlersFromAssemblies(typeof(Program).Assembly)
.AddDefaultPolicies(); // Includes fallback support
var app = builder.Build();
app.Run();Usage:
Apply the [FallbackPolicy] attribute and implement a FallbackAsync method in your handler:
using Paramore.Darker;
using Paramore.Darker.Policies;
using Paramore.Darker.QueryLogging;
using System.Threading;
using System.Threading.Tasks;
public sealed class GetPersonQueryHandler : QueryHandlerAsync<GetPersonNameQuery, string>
{
private readonly IPersonRepository _repository;
public GetPersonQueryHandler(IPersonRepository repository)
{
_repository = repository;
}
[QueryLogging(1)]
[FallbackPolicy(2)] // Provide fallback if query fails
[RetryableQuery(3, "DefaultCircuitBreaker")]
public override async Task<string> ExecuteAsync(
GetPersonNameQuery query,
CancellationToken cancellationToken = default)
{
var name = await _repository.GetNameByIdAsync(query.PersonId, cancellationToken);
return name;
}
// Fallback method - called when ExecuteAsync throws an exception
public override Task<string> FallbackAsync(
GetPersonNameQuery query,
CancellationToken cancellationToken = default)
{
// Return a default value
return Task.FromResult("Unknown");
}
}Important: When you use the [FallbackPolicy] attribute, you must implement the FallbackAsync method with the same signature (except method name) as ExecuteAsync. The fallback method is called when:
The primary
ExecuteAsyncmethod throws an exceptionAll retries have been exhausted (if using
RetryableQuery)The circuit breaker is open
The fallback method should:
Return a sensible default value
Execute quickly (no expensive operations)
Not throw exceptions (wrap any operations in try-catch)
Be deterministic and predictable
Circuit Breaker Integration
Both RetryableQuery and custom policies can integrate with Polly circuit breakers. A circuit breaker prevents your application from repeatedly attempting operations that are likely to fail, giving failing systems time to recover.
Circuit Breaker States:
Closed: Normal operation, requests flow through
Open: Too many failures occurred, requests are blocked immediately
Half-Open: Testing if the system has recovered, allowing limited requests
How it works:
The circuit breaker tracks failures
After a threshold of consecutive failures, the circuit "opens"
While open, requests fail immediately without attempting the operation
After a timeout period, the circuit enters "half-open" state
A successful request closes the circuit; a failure reopens it
Usage:
Circuit breakers are specified by name in the RetryableQuery attribute:
[RetryableQuery(2, "ExternalApiCircuitBreaker")]
public override async Task<OrderData> ExecuteAsync(
GetOrderQuery query,
CancellationToken cancellationToken = default)
{
// Query external API
}You can use different circuit breakers for different types of failures or different external dependencies. See Configuring Polly Policies for how to define circuit breakers.
Custom Decorators
Darker's decorator system is extensible, allowing you to create custom decorators for your specific cross-cutting concerns. However, the mechanism for creating custom decorators is not prominently documented in the core Darker library.
Based on the Darker architecture, custom decorators would need to:
Implement the decorator pattern around
IQueryHandler<TQuery, TResult>Integrate with the Darker pipeline registration
Support attribute-based configuration with step ordering
Note: If you need custom cross-cutting behavior not provided by the built-in decorators, consider:
Using Polly policies: Many custom behaviors can be implemented as Polly policies (timeout, rate limiting, caching, etc.)
Wrapping the IQueryProcessor: For application-wide concerns, you can create a wrapper around
IQueryProcessorContributing to Darker: If you develop a useful custom decorator pattern, consider contributing it back to the Darker project
For most scenarios, the combination of QueryLogging, RetryableQuery, and FallbackPolicy decorators with custom Polly policies provides sufficient flexibility.
Decorator Patterns
Pattern: Logging + Retry
This is the most common pattern for query handlers that interact with external dependencies. Logging provides visibility into query execution and retries, while the retry policy handles transient failures:
using Paramore.Darker;
using Paramore.Darker.Policies;
using Paramore.Darker.QueryLogging;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
public sealed class GetPeopleQueryHandler : QueryHandlerAsync<GetPeopleQuery, IReadOnlyDictionary<int, string>>
{
private readonly IPersonRepository _repository;
public GetPeopleQueryHandler(IPersonRepository repository)
{
_repository = repository;
}
[QueryLogging(1)] // Log all executions, including retries
[RetryableQuery(2, "DefaultCircuitBreaker")] // Retry on transient failures
public override async Task<IReadOnlyDictionary<int, string>> ExecuteAsync(
GetPeopleQuery query,
CancellationToken cancellationToken = default)
{
var people = await _repository.GetAllAsync(cancellationToken);
return people;
}
}When to use:
Queries that access databases or external APIs
Any query that may experience transient failures
Production queries where you need observability
Benefits:
Complete visibility into query execution, including retries
Automatic recovery from transient failures
Circuit breaker protection against cascading failures
Pattern: Logging + Fallback + Retry
This pattern adds fallback behavior to provide graceful degradation when all retries are exhausted. This is especially valuable for user-facing queries where you want to avoid showing error messages:
using Paramore.Darker;
using Paramore.Darker.Policies;
using Paramore.Darker.QueryLogging;
using System.Threading;
using System.Threading.Tasks;
public sealed class GetPersonQueryHandler : QueryHandlerAsync<GetPersonNameQuery, string>
{
private readonly IPersonRepository _repository;
public GetPersonQueryHandler(IPersonRepository repository)
{
_repository = repository;
}
[QueryLogging(1)] // Log everything
[FallbackPolicy(2)] // Provide fallback if needed
[RetryableQuery(3, "DefaultCircuitBreaker")] // Retry before falling back
public override async Task<string> ExecuteAsync(
GetPersonNameQuery query,
CancellationToken cancellationToken = default)
{
var name = await _repository.GetNameByIdAsync(query.PersonId, cancellationToken);
return name;
}
public override Task<string> FallbackAsync(
GetPersonNameQuery query,
CancellationToken cancellationToken = default)
{
// Return a friendly default when the query fails
return Task.FromResult("Unknown Person");
}
}Execution flow:
QueryLogging logs the query attempt
FallbackPolicy wraps the inner execution
RetryableQuery attempts the query, retrying on failure
If all retries fail, FallbackPolicy catches the exception and calls
FallbackAsyncQueryLogging logs the final result (either from successful query or fallback)
When to use:
User-facing queries where errors should be handled gracefully
Queries where a default value is acceptable when the primary source fails
Critical paths where you want to maintain service even during partial failures
Benefits:
User experience remains smooth even during failures
Retries happen first, fallback is last resort
Complete logging of the entire flow
Pattern: Multiple Circuit Breakers for Different Dependencies
When your query handler interacts with multiple external systems, you can apply different circuit breakers to different failure scenarios:
using Paramore.Darker;
using Paramore.Darker.Policies;
using Paramore.Darker.QueryLogging;
using System.Threading;
using System.Threading.Tasks;
public sealed class GetCustomerOrderSummaryQueryHandler :
QueryHandlerAsync<GetCustomerOrderSummaryQuery, CustomerOrderSummary>
{
private readonly ICustomerRepository _customerRepository;
private readonly IOrderRepository _orderRepository;
public GetCustomerOrderSummaryQueryHandler(
ICustomerRepository customerRepository,
IOrderRepository orderRepository)
{
_customerRepository = customerRepository;
_orderRepository = orderRepository;
}
[QueryLogging(1)]
[RetryableQuery(2, "CustomerDatabaseCircuitBreaker")]
public override async Task<CustomerOrderSummary> ExecuteAsync(
GetCustomerOrderSummaryQuery query,
CancellationToken cancellationToken = default)
{
// This entire method is protected by CustomerDatabaseCircuitBreaker
var customer = await _customerRepository.GetByIdAsync(query.CustomerId, cancellationToken);
var orders = await _orderRepository.GetByCustomerIdAsync(query.CustomerId, cancellationToken);
return new CustomerOrderSummary
{
CustomerId = customer.Id,
CustomerName = customer.Name,
OrderCount = orders.Count,
TotalValue = orders.Sum(o => o.Total)
};
}
}For more fine-grained control, you might create separate query handlers for each external dependency, each with its own circuit breaker, and compose them at a higher level.
When to use:
Handlers that query multiple external systems
When different dependencies have different reliability characteristics
When you want to isolate failures to specific systems
Configuring Polly Policies
Darker's policy decorators are powered by Polly, a .NET resilience and transient-fault-handling library. You can configure policies to control retry behavior, circuit breaker thresholds, and timeouts.
Default Policies
The simplest way to add policies is to use AddDefaultPolicies():
using Paramore.Darker;
using Paramore.Darker.AspNetCore;
using Paramore.Darker.Policies;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDarker()
.AddHandlersFromAssemblies(typeof(Program).Assembly)
.AddDefaultPolicies(); // Adds default retry and circuit breaker policies
var app = builder.Build();
app.Run();The default policies provide:
Default retry policy: Retries with exponential backoff
Default circuit breaker: Opens after consecutive failures, closes after a timeout period
These policies are sufficient for many applications and provide a good starting point for resilience.
Custom Policy Registry
For more control over resilience policies, you can create a custom policy registry with specific retry strategies, circuit breakers, and timeout policies:
using Paramore.Darker;
using Paramore.Darker.AspNetCore;
using Paramore.Darker.Policies;
using Polly;
using Polly.Registry;
using System;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDarker()
.AddHandlersFromAssemblies(typeof(Program).Assembly)
.AddPolicies(ConfigurePolicies());
var app = builder.Build();
app.Run();
static IPolicyRegistry<string> ConfigurePolicies()
{
// Retry policy with exponential backoff
var defaultRetryPolicy = Policy
.Handle<Exception>()
.WaitAndRetryAsync(new[]
{
TimeSpan.FromMilliseconds(50), // First retry after 50ms
TimeSpan.FromMilliseconds(100), // Second retry after 100ms
TimeSpan.FromMilliseconds(150) // Third retry after 150ms
});
// Circuit breaker that opens after 1 failure, stays open for 500ms
var defaultCircuitBreaker = Policy
.Handle<Exception>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 1,
durationOfBreak: TimeSpan.FromMilliseconds(500));
// Specific circuit breaker for critical operations
var criticalCircuitBreaker = Policy
.Handle<Exception>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 3, // More tolerant
durationOfBreak: TimeSpan.FromSeconds(30)); // Longer break
// Register policies with names
var policyRegistry = new PolicyRegistry
{
{ Constants.RetryPolicyName, defaultRetryPolicy },
{ Constants.CircuitBreakerPolicyName, defaultCircuitBreaker },
{ "CriticalCircuitBreaker", criticalCircuitBreaker }
};
return policyRegistry;
}Policy Naming Convention
Darker provides constants for common policy names in the Paramore.Darker.Policies.Constants class:
Constants.RetryPolicyName- Default retry policy nameConstants.CircuitBreakerPolicyName- Default circuit breaker policy name
Best practices:
Use the provided constants for default policies
Use descriptive names for custom circuit breakers (e.g., "ExternalApiCircuitBreaker", "DatabaseCircuitBreaker")
Document your policy names in a central configuration class
Consider creating a constants class for policy names used across your application:
public static class QueryPolicies
{
public const string DatabaseCircuitBreaker = "DatabaseCircuitBreaker";
public const string ExternalApiCircuitBreaker = "ExternalApiCircuitBreaker";
public const string CacheCircuitBreaker = "CacheCircuitBreaker";
}Advanced Policy Configurations
Polly supports many advanced resilience patterns:
Handle specific exceptions:
var retryPolicy = Policy
.Handle<HttpRequestException>()
.Or<TimeoutException>()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));Retry with callback:
var retryPolicy = Policy
.Handle<Exception>()
.WaitAndRetryAsync(
new[] { TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(200) },
onRetry: (exception, timeSpan, retryCount, context) =>
{
// Log retry attempts
Console.WriteLine($"Retry {retryCount} after {timeSpan}");
});Circuit breaker with callbacks:
var circuitBreaker = Policy
.Handle<Exception>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30),
onBreak: (exception, duration) =>
{
// Log when circuit opens
Console.WriteLine($"Circuit breaker opened for {duration}");
},
onReset: () =>
{
// Log when circuit closes
Console.WriteLine("Circuit breaker reset");
});For more information on Polly policies, see the Polly documentation and the Brighter documentation on Supporting Retry and Circuit Breaker.
Comparison with Brighter Pipeline
Darker's query pipeline shares many similarities with Brighter's request pipeline, as both implement the same Russian Doll Model for middleware composition. However, there are some key differences to be aware of when working with both frameworks.
Similarities
Russian Doll Model: Both frameworks use the same pipeline architecture where each handler/decorator wraps the next one in the chain, allowing cross-cutting concerns to execute before and after the core handler logic.
Attribute-Based Ordering: Both use attributes with step numbers to control decorator execution order:
// Brighter
[RequestLogging(1)]
[UsePolicy("RetryPolicy", 2)]
public override Task<AddGreetingResponse> HandleAsync(AddGreetingCommand command, ...)
// Darker
[QueryLogging(1)]
[RetryableQuery(2, "DefaultCircuitBreaker")]
public override Task<string> ExecuteAsync(GetPersonNameQuery query, ...)Policy Integration: Both integrate with Polly for resilience policies (retry, circuit breaker, timeout).
Extensible Architecture: Both support custom decorators for application-specific cross-cutting concerns.
Differences
Return Values:
Darker: Query handlers return results (
TResult), and the pipeline preserves and returns these resultsBrighter: Command handlers typically return
voidor the command itself; events are used to signal results
External Bus Support:
Brighter: Supports external messaging through service activators, message mappers, and external bus integration
Darker: Focuses on in-process query handling only; no external messaging support
Decorator Focus:
Darker: Decorators are query-specific (QueryLogging, RetryableQuery, FallbackPolicy)
Brighter: Decorators are request-specific (RequestLogging, UsePolicy, UseInbox, UseOutbox)
Use Cases:
Darker: Read-side operations, queries that don't change state
Brighter: Write-side operations, commands that change state, event publishing
When using both Brighter and Darker together in a CQRS architecture, you'll apply similar patterns but with framework-specific decorators. For more information on using both frameworks together, see CQRS with Brighter and Darker.
Best Practices
1. Order decorators logically
Place logging first (step 1) so all operations are logged, including retries and fallbacks:
[QueryLogging(1)]
[FallbackPolicy(2)]
[RetryableQuery(3, "CircuitBreaker")]2. Use circuit breakers for external dependencies
Whenever your query interacts with external systems (databases, APIs, microservices), protect them with circuit breakers to prevent cascading failures and give failing systems time to recover.
3. Implement fallbacks for user-facing queries
For queries that directly serve user requests, implement fallback logic to provide graceful degradation rather than error messages:
[FallbackPolicy(2)]
public override async Task<Result> ExecuteAsync(Query query, ...)
{
// primary logic
}
public override Task<Result> FallbackAsync(Query query, ...)
{
return Task.FromResult(GetDefaultValue());
}4. Keep decorators focused and composable
Each decorator should have a single responsibility. Compose multiple simple decorators rather than creating complex custom decorators.
5. Use named circuit breakers for different failure types
Create separate circuit breakers for different external dependencies or failure scenarios:
[RetryableQuery(2, "DatabaseCircuitBreaker")] // For database queries
[RetryableQuery(2, "ExternalApiCircuitBreaker")] // For API queries6. Configure appropriate retry delays
Use exponential backoff for retries to avoid overwhelming recovering systems:
TimeSpan.FromMilliseconds(50), // 50ms
TimeSpan.FromMilliseconds(100), // 100ms
TimeSpan.FromMilliseconds(200), // 200ms7. Monitor circuit breaker state changes
Use Polly's callback methods to log or alert when circuit breakers open or close, as these indicate systemic issues.
8. Test your fallback logic
Ensure your FallbackAsync methods are tested and return appropriate default values. Fallback logic should be simple and not throw exceptions.
Common Pitfalls
1. Wrong decorator ordering
Putting retry before logging means individual retry attempts won't be logged. Putting fallback before retry means the fallback will be used before retries are exhausted.
❌ Bad:
[RetryableQuery(1, "CB")]
[QueryLogging(2)] // Won't log individual retries✅ Good:
[QueryLogging(1)] // Logs everything including retries
[RetryableQuery(2, "CB")]2. Forgetting to configure policies
Using [RetryableQuery] without calling AddDefaultPolicies() or AddPolicies() will cause runtime errors.
❌ Bad:
builder.Services.AddDarker()
.AddHandlersFromAssemblies(typeof(Program).Assembly);
// No policies configured!✅ Good:
builder.Services.AddDarker()
.AddHandlersFromAssemblies(typeof(Program).Assembly)
.AddDefaultPolicies();3. Circuit breaker naming mismatches
Referencing a circuit breaker name that doesn't exist in the policy registry will cause runtime errors.
❌ Bad:
[RetryableQuery(2, "MyCircuitBreaker")] // Policy name doesn't exist✅ Good:
// Define policy
policyRegistry.Add("MyCircuitBreaker", circuitBreakerPolicy);
// Reference it
[RetryableQuery(2, "MyCircuitBreaker")]4. Fallback not implemented when using FallbackPolicy
If you use the [FallbackPolicy] attribute, you must implement the FallbackAsync method, or you'll get a runtime error.
❌ Bad:
[FallbackPolicy(2)]
public override async Task<Result> ExecuteAsync(Query query, ...)
{
// ...
}
// Missing FallbackAsync method!✅ Good:
[FallbackPolicy(2)]
public override async Task<Result> ExecuteAsync(Query query, ...)
{
// ...
}
public override Task<Result> FallbackAsync(Query query, ...)
{
return Task.FromResult(defaultValue);
}5. Expensive operations in fallback logic
Fallback methods should be fast and not call external services. The purpose is to provide a quick default, not to attempt alternative implementations.
❌ Bad:
public override async Task<string> FallbackAsync(Query query, ...)
{
// Calling another external service in fallback!
return await _alternativeService.GetDataAsync();
}✅ Good:
public override Task<string> FallbackAsync(Query query, ...)
{
// Return a simple default value
return Task.FromResult("Default Value");
}6. Not handling CancellationToken in decorators
Always pass the CancellationToken through the pipeline to allow graceful cancellation of long-running queries.
Further Reading
Implementing a Query Handler - Learn how to implement query handlers that use decorators
Basic Configuration - Configure Darker with policies and decorators
Building a Pipeline of Request Handlers - Brighter's equivalent pipeline documentation
Supporting Retry and Circuit Breaker - Detailed guide to Polly policies in Brighter (patterns apply to Darker)
Polly Documentation - Comprehensive guide to Polly resilience policies
Working examples can be found in the Darker samples: Darker/samples/SampleMinimalApi/QueryHandlers/
Last updated
Was this helpful?
