Local Execution
The LeanCode CoreLibrary utilizes ASP.NET Core to model the pipeline, which enables powerful HTTP execution of queries, commands and operations. Sometimes though, there is a need to run query/command/operation in-proc. This is enabled by so-called local execution of CQRS.
Theory
Being able to call CQRS objects locally without going through HTTP pipeline is achieved by using a separate ASP.NET Core pipeline that is used with custom HttpContext. This enables us to re-use all the middlewares created for normal HTTP execution (which applies to all middlewares provided by the CoreLibrary) without changes. This option also enables using the same handlers as the normal executions.
Nevertheless, the local execution only mimics the HTTP pipeline. Local execution does not involve proper request/response, thus it comes with limitations:
- HttpContext.Connection, HttpContext.WebSockets, HttpContext.Session are represented by empty objects - report null/empty data and all actions either throw or are no-op,
- HttpContext.User is passed by the caller (and can, but does not have to be, real),
- HttpContext.Response is an empty object, meaning that it ignores any writes to it (both body and headers will be lost),
- HttpContext.Request does not have any body, nor other HTTP metadata like path, method or so. The only thing that is implemented are headers, which can be passed when calling local execution.
- The features provided by DefaultHttpContext are only partly available and we don't guarantee any feature to be available.
We found that, albeit some features that rely on HTTP will be ignored, most of the middlewares will be fully working.
Output Caching Not Supported
ASP.NET Core's output caching is not supported in local execution. The OutputCaching middleware requires a real HTTP context with stream manipulation capabilities that are not available in the local execution pipeline. If you need caching with local execution, consider implementing an application-level cache instead.
Local executors preserve the semantics of HTTP calls, meaning that:
- They are run in a separate DI scope,
- They are stateless, and don't share anything with the parent call.
Packages
| Package | Link | Application in section |
|---|---|---|
| LeanCode.CQRS.AspNetCore | Configuration |
Configuration
To use local execution, you have to explicitly register local executors (query/command/operation separately). This can be done by chaining calls to AddCQRS(...), specifying pipelines for local execution. For example:
public override void ConfigureServices(IServiceCollection services)
{
services
.AddCQRS(
TypesCatalog.Of<ExampleCommand>(),
TypesCatalog.Of<ExampleHandler>())
// Registers executor for local commands
.WithLocalCommands(c => c
.CQRSTrace()
.Secure()
.Validate()
.TranslateExceptions()
.CommitTransaction<CoreDbContext>())
// Registers executor for local queries
.WithLocalQueries(q => q
.CQRSTrace()
.Secure())
// Registers executor for local operations
.WithLocalOperations(o => o
.CQRSTrace()
.Secure()
.CommitTransaction<CoreDbContext>());
}
Tip
There are keyed versions of WithLocal* calls. If you need to have different pipelines for different modules, you can register them under different keys.
Usage
To call local objects, use ILocalCommandExecutor/ILocalQueryExecutor/ILocalOperationExecutor. All require you to pass:
- Object that will be executed,
- The ClaimsPrincipal that the action will be executed as,
- And optionally a IHeaderDictionary with additional headers.
Executors return a value that corresponds to the result of the object being executed (e.g. CommandResult or query/operation result).
The example below uses query from the Query tutorial:
public class ProcessProjectDataCH : ICommandHandler<ProcessProjectData>
{
private readonly ILocalQueryExecutor queries;
public UpdateProjectNameCH(ILocalQueryExecutor queries)
{
this.queries = queries;
}
public Task ExecuteAsync(HttpContext context, ProcessProjectData command)
{
// We call external (local) query to gather data.
// We call the query as the same user that calls this command.
var projects = await queries.GetAsync(
new AllProjects { NameFilter = "[IMPORTANT]" },
context.User,
context.RequestAborted);
// We can do sth with `projects` here
}
}
Error reporting
All local executors handle results as follows:
- Success (
200 OK) and validation error (422 Unprocessable Entity) will be reported as the result of operation (meaning that validation errors in commands will be reported asCommandResults), - Not authenticated calls (
401 Unauthorized) will be reported asUnauthenticatedCQRSRequestException, - Not authorized calls (
403 Forbidden) will be reported asUnauthorizedCQRSRequestException, - All other status codes will be reported as
UnknownStatusCodeException.
This corresponds to the behavior of Remote CQRS calls.