From 5c1f889719906ecb9475d70c23322ee003356b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Fija=C5=82kowski?= Date: Thu, 15 Feb 2024 17:04:45 +0100 Subject: [PATCH] Add docs for local execution --- docs/cqrs/local_execution/index.md | 104 ++++++++++++++++++++++++++ docs/cqrs/pipeline/index.md | 115 ++++++++++++++++++----------- mkdocs.yml | 2 + 3 files changed, 178 insertions(+), 43 deletions(-) create mode 100644 docs/cqrs/local_execution/index.md diff --git a/docs/cqrs/local_execution/index.md b/docs/cqrs/local_execution/index.md new file mode 100644 index 00000000..e0b8294b --- /dev/null +++ b/docs/cqrs/local_execution/index.md @@ -0,0 +1,104 @@ +# 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 mocked [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: + +1. [HttpContext.Connection], [HttpContext.WebSockets], [HttpContext.Session] are represented by empty objects - report null/empty data and all actions either throw or are no-op, +2. [HttpContext.User] is passed by the caller (and can, but does not have to be, real), +3. [HttpContext.Response] is an empty object, meaning that it ignores any writes to it (both body and headers will be lost), +4. [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. +5. 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. + +Local executors preserve the semantics of HTTP calls, meaning that: + +1. They are run in a separate DI scope, +2. They are stateless, and don't share anything with the parent call. + +## Packages + +| Package | Link | Application in section | +| ------------------------ | ----------- | ---------------------- | +| LeanCode.CQRS.AspNetCore | [![NuGet version (LeanCode.CQRS.AspNetCore)](https://img.shields.io/nuget/vpre/LeanCode.CQRS.AspNetCore.svg?style=flat-square&logo=nuget)](https://www.nuget.org/packages/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: + +```csharp +public override void ConfigureServices(IServiceCollection services) +{ + services + .AddCQRS(TypesCatalog.Of(), TypesCatalog.Of()) + .WithLocalCommands(c => c.Secure().Validate().TranslateExceptions().CommitTransaction()) + .WithLocalQueries(c => c.Validate().TranslateExceptions()) + .WithLocalOperations(c => c.Secure().TranslateExceptions().CommitTransaction()); +} +``` + +!!! 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: + +1. Object that will be executed, +2. The [ClaimsPrincipal] that the action will be executed as, +3. 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 [Query](../query/index.md) tutorial: + +```csharp +public class ProcessProjectDataCH : ICommandHandler +{ + 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: + +1. 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 as `CommandResult`s), +2. Not authenticated calls (`401 Unauthorized`) will be reported as `UnauthenticatedCQRSRequestException`, +3. Not authorized calls (`403 Forbidden`) will be reported as `UnauthorizedCQRSRequestException`, +4. All other status codes will be reported as `UnknownStatusCodeException`. + +This corresponds to the behavior of Remote CQRS calls. + +[HttpContext]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext +[HttpContext.Connection]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.connection +[HttpContext.WebSockets]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.websockets +[HttpContext.Session]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.session +[HttpContext.User]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user +[HttpContext.Response]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.response +[HttpContext.Request]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.request +[DefaultHttpContext]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.defaulthttpcontext +[AddCQRS(...)]: https://github.com/leancodepl/corelibrary/blob/HEAD/src/CQRS/LeanCode.CQRS.AspNetCore/ServiceCollectionCQRSExtensions.cs#L17 +[ClaimsPrincipal]: https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimsprincipal +[IHeaderDictionary]: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.iheaderdictionary diff --git a/docs/cqrs/pipeline/index.md b/docs/cqrs/pipeline/index.md index be354e61..1c70685a 100644 --- a/docs/cqrs/pipeline/index.md +++ b/docs/cqrs/pipeline/index.md @@ -11,42 +11,69 @@ The LeanCode CoreLibrary utilizes ASP.NET middlewares to create customized pipel ## Configuration -CQRS objects can only be registered in the ASP.NET request pipeline via endpoint routing. To register use `IEndpointRouteBuilder.MapRemoteCQRS(...)` extension method. In `MapRemoteCQRS(...)` you can configure the inner CQRS pipeline. In the following example, app is configured to handle: +CQRS objects need to be registered in two places: + +1. Handlers need to be registered in DI, +2. Routes for CQRS objects execution need to be registered in ASP.NET Core routing as endpoints. + +### DI + +To register handlers in DI, use [AddCQRS(...)] calls. There, you need to specify two `TypesCatalog`s: + +1. One that contains all available contracts, +2. One that contains all necessary handlers. + +You cannot call [AddCQRS(...)] twice - all objects need to be registered in one go. + +```csharp +public override void ConfigureServices(IServiceCollection services) +{ + services.AddCQRS(TypesCatalog.Of(), TypesCatalog.Of()); +} +``` + +[AddCQRS(...)] returns aa [CQRSServicesBuilder] that allows to further modify CQRS registration by, e.g., adding objects one-by-one, registering predefined libraries like [force update](../../features/force_update/index.md) or registering [local executors](../local_execution/index.md). + +### Endpoints + +CQRS objects can be registered in the ASP.NET request pipeline via endpoint routing. To register, use [MapRemoteCQRS(...)] extension method. In `MapRemoteCQRS(...)` you can configure the inner CQRS pipeline. In the following example, app is configured to handle: - [Commands] at `/api/command/FullyQualifiedName` - [Queries] at `/api/query/FullyQualifiedName` - [Operations] at `/api/operation/FullyQualifiedName` +Endpoint routing cannot execute handlers that are not in DI, thus the `MapRemoteCQRS(...)` call will ignore objects that were not found by `AddCQRS(...)`. + ```csharp - protected override void ConfigureApp(IApplicationBuilder app) - { - // . . . - app.UseEndpoints(endpoints => - { - endpoints.MapRemoteCQRS( - "/api", - cqrs => - { - cqrs.Commands = c => - c.CQRSTrace() - .Secure() - .Validate() - .CommitTransaction() - .PublishEvents(); - - cqrs.Queries = c => - c.CQRSTrace() - .Secure(); - - cqrs.Operations = c => - c.CQRSTrace() - .Secure() - .CommitTransaction() - .PublishEvents(); - } - ); - }); - } +protected override void ConfigureApp(IApplicationBuilder app) +{ + // . . . + app.UseEndpoints(endpoints => + { + endpoints.MapRemoteCQRS( + "/api", + cqrs => + { + cqrs.Commands = c => + c.CQRSTrace() + .Secure() + .Validate() + .CommitTransaction() + .PublishEvents(); + + cqrs.Queries = c => + c.CQRSTrace() + .Secure(); + + cqrs.Operations = c => + c.CQRSTrace() + .Secure() + .CommitTransaction() + .PublishEvents(); + } + ); + }); +} ``` !!! tip @@ -54,21 +81,21 @@ CQRS objects can only be registered in the ASP.NET request pipeline via endpoint In this code snippet, you can specify which middlewares to use for handling commands, queries, and operations. Several middlewares are added in the example: -| Method | Middleware | Responsibility | -|------------------------------- |--------------------------------------- |---------------------------- | -| [CQRSTrace()] | [CQRSTracingMiddleware] | Tracing | -| [Secure()] | [CQRSSecurityMiddleware] | Authorization | -| [Validate()] | [CQRSValidationMiddleware] | Validation | -| [CommitTransaction<T>()] | [CommitDatabaseTransactionMiddleware] | Saving changes to the database | -| [PublishEvents()] | [EventsPublisherMiddleware] | Publishing domain events to MassTransit | +| Method | Middleware | Responsibility | +|-------------------------------- |--------------------------------------- |------------------------------------------- | +| [CQRSTrace()] | [CQRSTracingMiddleware] | Tracing | +| [Secure()] | [CQRSSecurityMiddleware] | Authorization | +| [Validate()] | [CQRSValidationMiddleware] | Validation | +| [CommitTransaction<T>()] | [CommitDatabaseTransactionMiddleware] | Saving changes to the database | +| [PublishEvents()] | [EventsPublisherMiddleware] | Publishing domain events to MassTransit | The order in which these middlewares are added determines the sequence of execution. Additionally, there are a few other middlewares provided by library that can be incorporated into CQRS pipeline, although they are not covered in this basic example: -| Method | Middleware | Responsibility | -|----------------------------------- |----------------------------------------- |--------------------------------------------------------- | +| Method | Middleware | Responsibility | +|------------------------------------- |----------------------------------------- |---------------------------------------------------------- | | [LogCQRSResponses()] | [ResponseLoggerMiddleware] | Logging responses | | [LogCQRSResponsesOnNonProduction()] | [NonProductionResponseLoggerMiddleware] | Logging responses on non-production environments | -| [TranslateExceptions()] | [CQRSExceptionTranslationMiddleware] | Capturing and translating exceptions into error codes | +| [TranslateExceptions()] | [CQRSExceptionTranslationMiddleware] | Capturing and translating exceptions into error codes | ## Request handling @@ -96,15 +123,15 @@ sequenceDiagram Note over final: Execute handler Note over final: Set result in CQRSRequestPayload - final ->> middle: + final ->> middle: #0160; Note over middle: Custom middlewares, e.g. events publication - middle ->> start: + middle ->> start: #0160; Note over start: Set response headers Note over start: Serialize result - start ->> aspnet: + start ->> aspnet: #0160; ``` @@ -114,6 +141,8 @@ Subsequently, the pipeline executes additional custom middlewares, responsible f [EventsPublisherMiddleware] then facilitates the publication of events (assuming it's added to the pipeline in [MapRemoteCQRS(...)]). Towards the conclusion of the pipeline, response headers are configured, and the result is serialized inside [CQRSMiddleware]. Finally, the serialized result is returned to the client, completing the request handling process. +[AddCQRS(...)]: https://github.com/leancodepl/corelibrary/blob/HEAD/src/CQRS/LeanCode.CQRS.AspNetCore/ServiceCollectionCQRSExtensions.cs#L17 +[CQRSServicesBuilder]: https://github.com/leancodepl/corelibrary/blob/HEAD/src/CQRS/LeanCode.CQRS.AspNetCore/ServiceCollectionCQRSExtensions.cs#L46 [MapRemoteCQRS(...)]: https://github.com/leancodepl/corelibrary/blob/HEAD/src/CQRS/LeanCode.CQRS.AspNetCore/CQRSEndpointRouteBuilderExtensions.cs#L13 [CQRSTrace()]: https://github.com/leancodepl/corelibrary/blob/HEAD/src/CQRS/LeanCode.CQRS.AspNetCore/CQRSApplicationBuilder.cs#L62 [Validate()]: https://github.com/leancodepl/corelibrary/blob/HEAD/src/CQRS/LeanCode.CQRS.AspNetCore/CQRSApplicationBuilder.cs#L38 diff --git a/mkdocs.yml b/mkdocs.yml index 6c149914..7f96a893 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,6 +40,8 @@ nav: - ./cqrs/authorization/index.md - Validation: - ./cqrs/validation/index.md + - Local execution: + - ./cqrs/local_execution/index.md - Domain: - ./domain/index.md - Aggregate: