Skip to content

Commit

Permalink
Add docs for local execution
Browse files Browse the repository at this point in the history
  • Loading branch information
jakubfijalkowski committed Feb 15, 2024
1 parent 3504489 commit 5c1f889
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 43 deletions.
104 changes: 104 additions & 0 deletions docs/cqrs/local_execution/index.md
Original file line number Diff line number Diff line change
@@ -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<ExampleCommand>(), TypesCatalog.Of<ExampleHandler>())
.WithLocalCommands(c => c.Secure().Validate().TranslateExceptions().CommitTransaction<CoreDbContext>())
.WithLocalQueries(c => c.Validate().TranslateExceptions())
.WithLocalOperations(c => c.Secure().TranslateExceptions().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:

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<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:

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
115 changes: 72 additions & 43 deletions docs/cqrs/pipeline/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,64 +11,91 @@ 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<ExampleCommand>(), TypesCatalog.Of<ExampleHandler>());
}
```

[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<CoreDbContext>()
.PublishEvents();

cqrs.Queries = c =>
c.CQRSTrace()
.Secure();

cqrs.Operations = c =>
c.CQRSTrace()
.Secure()
.CommitTransaction<CoreDbContext>()
.PublishEvents();
}
);
});
}
protected override void ConfigureApp(IApplicationBuilder app)
{
// . . .
app.UseEndpoints(endpoints =>
{
endpoints.MapRemoteCQRS(
"/api",
cqrs =>
{
cqrs.Commands = c =>
c.CQRSTrace()
.Secure()
.Validate()
.CommitTransaction<CoreDbContext>()
.PublishEvents();

cqrs.Queries = c =>
c.CQRSTrace()
.Secure();

cqrs.Operations = c =>
c.CQRSTrace()
.Secure()
.CommitTransaction<CoreDbContext>()
.PublishEvents();
}
);
});
}
```

!!! tip
To learn about ASP.NET middlewares and how you can implement them, visit [here](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/).

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&lt;T&gt;()] | [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&lt;T&gt;()] | [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

Expand Down Expand Up @@ -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;
```

Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 5c1f889

Please sign in to comment.