Basic Configuration

Introduction

Darker is the query-side counterpart to Brighter, implementing the Query Object pattern for CQRS (Command Query Responsibility Segregation) architectures. While Brighter handles commands and events that change state, Darker provides a pipeline for executing queries that read state. Together, they form a complete CQRS solution for .NET applications.

You use Darker when you want to separate the parameters of a query from the execution of that query, typically when you need to add cross-cutting concerns such as logging, retry policies, or circuit breakers to your query handling. For more information on CQRS patterns and how Brighter and Darker work together, see CQRS with Brighter and Darker.

Prerequisites

.NET Version Requirements

Darker supports .NET 8.0 and later versions. The sample code in this documentation is compatible with both .NET 8.0 and .NET 9.0.

NuGet Packages

You will need the following NuGet packages to use Darker:

  • Paramore.Darker - Core library providing the IQueryProcessor and query handler infrastructure

  • Paramore.Darker.AspNetCore - ASP.NET Core integration providing the AddDarker extension method for dependency injection

  • Paramore.Darker.QueryLogging - Optional package for JSON-based query logging

  • Paramore.Darker.Policies - Optional package for integrating Polly resilience policies (retry, circuit breaker, fallback)

Install the packages using the .NET CLI:

dotnet add package Paramore.Darker
dotnet add package Paramore.Darker.AspNetCore
dotnet add package Paramore.Darker.QueryLogging
dotnet add package Paramore.Darker.Policies

Or using Package Manager Console:

Quick Start with ASP.NET Core

Basic Setup

To configure Darker in an ASP.NET Core application, use the AddDarker extension method in your Program.cs or Startup.cs file. Darker integrates with the ASP.NET Core dependency injection container, making the IQueryProcessor available for injection into your controllers and endpoints.

The AddHandlersFromAssemblies method scans the specified assembly for query handlers and registers them automatically. This is the recommended approach for registering handlers.

Minimal API Example

Here's a complete example of using Darker with ASP.NET Core Minimal APIs:

In this example, IQueryProcessor is injected directly into the endpoint handlers. The query processor dispatches the query to the appropriate query handler.

MVC Controller Example

If you're using MVC controllers, inject IQueryProcessor into your controller's constructor:

Working examples can be found in the Darker samples: Darker/samples/SampleMinimalApi/

Configuration Options

Query Processor Lifetime

By default, the IQueryProcessor is registered with a Transient lifetime, meaning a new instance is created each time it's requested. However, if you're using Entity Framework Core, you need to register the Query Processor with a Scoped lifetime to match the EF Core DbContext lifetime.

Default Configuration (Transient):

Scoped Configuration (Required for EF Core):

When using Entity Framework Core, the DbContext is registered as scoped by default. To ensure Darker works correctly with EF Core, you must configure the Query Processor to use the same scoped lifetime:

If you don't configure the scoped lifetime when using EF Core, you may encounter exceptions related to accessing a disposed DbContext.

Handler Registration Strategies

Darker provides two ways to register query handlers: automatic assembly scanning (recommended) and manual registration.

Assembly Scanning (Recommended):

The AddHandlersFromAssemblies method scans one or more assemblies and automatically registers all query handlers it finds:

This approach follows convention over configuration and is the easiest way to register handlers.

Manual Registration:

For more control over handler registration, you can use QueryHandlerRegistry to register handlers explicitly:

Manual registration is useful when you need fine-grained control over which handlers are registered or when you're not using ASP.NET Core's dependency injection.

Using IQueryProcessor

The IQueryProcessor is the central interface for executing queries in Darker. Once configured, you inject it into your controllers, endpoints, or services to dispatch queries to their handlers.

The asynchronous ExecuteAsync method is recommended for most scenarios, especially when your query involves I/O operations like database access:

Always pass the CancellationToken to ExecuteAsync when available. This allows query execution to be cancelled if the request is aborted, improving application responsiveness and resource usage.

Execute Pattern (Synchronous)

For synchronous query execution (rare cases where async is not needed), use the Execute method:

Use the synchronous Execute method only when:

  • Your query handler performs purely in-memory operations

  • You're working in a synchronous context that cannot be made async

  • You have a specific performance requirement for synchronous execution

For most modern applications, prefer ExecuteAsync.

Configuration with Decorators

Darker supports decorators (middleware) that wrap query handlers to provide cross-cutting concerns like logging and resilience policies. You can add decorators during configuration.

Adding Query Logging

The AddJsonQueryLogging extension adds a decorator that logs query execution details in JSON format:

This will log:

  • The query type and parameters

  • Execution time

  • Query results (summary)

Query logging is useful for debugging and monitoring query execution in your application. For more details on using the logging decorator in handlers, see Query Pipeline.

Adding Policies

Darker integrates with Polly to provide resilience and transient fault handling through policies like retry, circuit breaker, and fallback.

Using Default Policies:

The AddDefaultPolicies method adds a default set of policies:

Using Custom Policy Registry:

For more control, you can create a custom policy registry with specific retry strategies, circuit breakers, and timeout policies:

Once policies are configured, you can apply them to query handlers using attributes. For details on using policy decorators in handlers, see Query Pipeline.

Common Configuration Patterns

Pattern: Basic Web API Setup

This is the most common setup for an ASP.NET Core Web API using Darker:

Pattern: With EF Core DbContext

When using Entity Framework Core, you must configure the Query Processor with scoped lifetime to match the DbContext:

Without the scoped configuration, you'll encounter exceptions about disposed DbContext instances.

Pattern: Multiple Handler Assemblies

When your query handlers are spread across multiple assemblies, register all of them:

This pattern is useful in modular monoliths or when organizing queries by domain.

Troubleshooting

Common Issues

Handler not found errors

If you receive an exception that a handler cannot be found for a query:

  • Verify that the handler class implements QueryHandler<TQuery, TResult> or QueryHandlerAsync<TQuery, TResult>

  • Ensure the handler's assembly is registered with AddHandlersFromAssemblies

  • Check that the query and handler types match exactly (including generic type parameters)

  • Verify the handler class is public and not abstract

Lifetime scope issues with EF Core

If you see exceptions about a disposed DbContext:

  • Ensure you've configured QueryProcessorLifetime = ServiceLifetime.Scoped in the Darker options

  • Verify your DbContext is registered with scoped lifetime (default for EF Core)

  • Check that you're not trying to use the query result after the scope has been disposed

Assembly scanning not finding handlers

If handlers aren't being registered automatically:

  • Verify you're passing the correct assembly to AddHandlersFromAssemblies

  • Ensure handlers are in the same assembly or you've registered all relevant assemblies

  • Check that handler classes are public and not nested within other classes

  • Verify handlers follow the naming conventions (end with "Handler" or "QueryHandler")

Policy not found errors

If you see exceptions about missing policies:

  • Ensure you've called AddDefaultPolicies() or AddPolicies(registry)

  • Verify the policy name in your attribute matches the name in the policy registry

  • Check that the policy registry is correctly configured before being passed to AddPolicies

Further Reading

Last updated

Was this helpful?