Skip to content

Commit

Permalink
Moving away from aggregate store (#368)
Browse files Browse the repository at this point in the history
* Added truncation to SQL stores
* Added tiered event store
* Document persistence changes
* Added the last change in subscriptions to the docs
* Add RMQ docs
* Add queue name to RMQ, handle empty stream for ExpectedNew as non-existent.
---------

Co-authored-by: Ruben Bartelink <ruben@bartelink.com>
  • Loading branch information
alexeyzimarev and bartelink authored Jul 19, 2024
1 parent 526a4a4 commit 21e940c
Show file tree
Hide file tree
Showing 287 changed files with 4,961 additions and 2,656 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ root = true
[*]

indent_style = space
max_line_length = 160
max_line_length = 200
brace_style = end_of_line

# IntelliJ IDEA properties
Expand Down
19 changes: 11 additions & 8 deletions Eventuous.sln
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Eventuous.Tests.Projections
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{CCD807D2-F4F2-4EC2-A03D-8943F73993A6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Eventuous.AspNetCore.Web", "src\Extensions\src\Eventuous.AspNetCore.Web\Eventuous.AspNetCore.Web.csproj", "{E387CC89-42B6-40BD-800B-C369AE971FCC}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Eventuous.Extensions.AspNetCore", "src\Extensions\src\Eventuous.Extensions.AspNetCore\Eventuous.Extensions.AspNetCore.csproj", "{E387CC89-42B6-40BD-800B-C369AE971FCC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Experimental", "Experimental", "{B64D59D5-7935-4DFC-8D90-D6EF3D6F2ABA}"
EndProject
Expand All @@ -107,7 +107,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElasticPlayground", "src\Ex
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Eventuous.Tests.DependencyInjection", "src\Extensions\test\Eventuous.Tests.DependencyInjection\Eventuous.Tests.DependencyInjection.csproj", "{152E27CE-35F1-4F65-B53A-C7B710F1B310}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Eventuous.Tests.AspNetCore.Web", "src\Extensions\test\Eventuous.Tests.AspNetCore.Web\Eventuous.Tests.AspNetCore.Web.csproj", "{B3F782EE-FBEF-47E2-8379-8A91B11363B8}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Eventuous.Tests.Extensions.AspNetCore", "src\Extensions\test\Eventuous.Tests.Extensions.AspNetCore\Eventuous.Tests.Extensions.AspNetCore.csproj", "{B3F782EE-FBEF-47E2-8379-8A91B11363B8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Eventuous.Sut.AspNetCore", "src\Extensions\test\Eventuous.Sut.AspNetCore\Eventuous.Sut.AspNetCore.csproj", "{1C1033D6-059B-4CEE-A7D8-9EE470053145}"
EndProject
Expand Down Expand Up @@ -213,6 +213,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bookings.Payments", "sample
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bookings", "samples\esdb\Bookings\Bookings.csproj", "{C666D8E7-FB55-4435-A8D4-CF9815660E85}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Eventuous.Tests.Persistence", "src\Core\test\Eventuous.Tests.Persistence\Eventuous.Tests.Persistence.csproj", "{F24F066B-FC7A-4298-B007-19CC86BB31E1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -533,27 +535,27 @@ Global
{17EC98BE-0738-4F65-8ACD-39150BB134ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{17EC98BE-0738-4F65-8ACD-39150BB134ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17EC98BE-0738-4F65-8ACD-39150BB134ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17EC98BE-0738-4F65-8ACD-39150BB134ED}.Debug CI|Any CPU.ActiveCfg = Debug CI|Any CPU
{4020E68A-B17C-4FFA-9687-C189F5D2851C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4020E68A-B17C-4FFA-9687-C189F5D2851C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4020E68A-B17C-4FFA-9687-C189F5D2851C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4020E68A-B17C-4FFA-9687-C189F5D2851C}.Debug CI|Any CPU.ActiveCfg = Debug CI|Any CPU
{246E53C4-BF6B-4B7E-8C45-788B31EFC0A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{246E53C4-BF6B-4B7E-8C45-788B31EFC0A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{246E53C4-BF6B-4B7E-8C45-788B31EFC0A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{246E53C4-BF6B-4B7E-8C45-788B31EFC0A2}.Debug CI|Any CPU.ActiveCfg = Debug CI|Any CPU
{7D86A33D-7C1A-45F7-BEFF-1B95525716D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D86A33D-7C1A-45F7-BEFF-1B95525716D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D86A33D-7C1A-45F7-BEFF-1B95525716D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D86A33D-7C1A-45F7-BEFF-1B95525716D6}.Debug CI|Any CPU.ActiveCfg = Debug CI|Any CPU
{7D24DAB3-FD49-443C-811A-96F0CA6A6F9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D24DAB3-FD49-443C-811A-96F0CA6A6F9A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D24DAB3-FD49-443C-811A-96F0CA6A6F9A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D24DAB3-FD49-443C-811A-96F0CA6A6F9A}.Debug CI|Any CPU.ActiveCfg = Debug CI|Any CPU
{C666D8E7-FB55-4435-A8D4-CF9815660E85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C666D8E7-FB55-4435-A8D4-CF9815660E85}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C666D8E7-FB55-4435-A8D4-CF9815660E85}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C666D8E7-FB55-4435-A8D4-CF9815660E85}.Debug CI|Any CPU.ActiveCfg = Debug CI|Any CPU
{F24F066B-FC7A-4298-B007-19CC86BB31E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F24F066B-FC7A-4298-B007-19CC86BB31E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F24F066B-FC7A-4298-B007-19CC86BB31E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F24F066B-FC7A-4298-B007-19CC86BB31E1}.Release|Any CPU.Build.0 = Release|Any CPU
{F24F066B-FC7A-4298-B007-19CC86BB31E1}.Debug CI|Any CPU.ActiveCfg = Debug CI|Any CPU
{F24F066B-FC7A-4298-B007-19CC86BB31E1}.Debug CI|Any CPU.Build.0 = Debug CI|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -649,6 +651,7 @@ Global
{7D86A33D-7C1A-45F7-BEFF-1B95525716D6} = {75F337AF-7E15-4ED1-8E4F-A582DABEA373}
{7D24DAB3-FD49-443C-811A-96F0CA6A6F9A} = {75F337AF-7E15-4ED1-8E4F-A582DABEA373}
{C666D8E7-FB55-4435-A8D4-CF9815660E85} = {75F337AF-7E15-4ED1-8E4F-A582DABEA373}
{F24F066B-FC7A-4298-B007-19CC86BB31E1} = {0ED6785B-60EF-46B4-B938-EF04189FC8BC}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0691467B-C257-46DB-BC4F-88EB7CD615B8}
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Website

This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.

### Installation

Expand Down
31 changes: 31 additions & 0 deletions docs/versioned_docs/version-0.15/application/_result.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
### Result

The command service will return an instance of `Result<TState>`. It can be inspected using the following members:

| Signature | Description |
|----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `bool TryGet(out Result<TState>.Ok ok)` | Returns `true` if the result is successful and also returns `Result<TState>.Ok` as the `out` variable. |
| `bool TryGetError(out Result<TState>.Error error)` | Returns `true` if there was an error. The `error` then gets assigned to an instance of `Error` that contains more details about what went wrong. |
| `bool Success` | Returns `true` if the result is successful. |
| `Exception? Exception { get; }` | Returns an exception instance if there was an error, or `null` if there was no exception. |
| `void ThrowIfError()` | Throws the recorded exception if there was an error, does nothing otherwise. |
| `T Match<T>(Func<Ok, T> matchOk, Func<Error, T> matchError)` | Can be used for pattern matching success and error if the output has the same type. Eventuous uses this function for producing `IResult` and `ActionResult` in HTTP API extensions. |
| `void Match<T>(Action<Ok> matchOk, Action<Error> matchError)` | Allows to execute code branches based on the result success. |
| `Task MatchAsync<T>(Func<Ok, Task> matchOk, Func<Error, Task> matchError)` | Allows to execute async code branches based on the result success. |

When using `TryGet`, you get the `Ok` instance back, which contains the following properties:

| Property | Description |
|------------------------|----------------------------------------------------------------------------------------------------------|
| `TState State` | New state instance |
| `Change[] Changes` | List of new events. `Change` struct contains both the event payload and its type. |
| `ulong StreamPosition` | Position of the last event in the stream that can be used for tracking, for example, read model updates. |

The `Match` function also provides `Ok` for the `matchOk` function to use.

When using `TryGetError`, you get the `Error` instance back, which contains the following properties:

| Property | Description |
|------------------------|---------------------------------------------------------------------|
| `string ErrorMessage` | The error message, which can be custom or taken from the exception. |
| `Exception? Exception` | Exception details if available. |
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Application HTTP API

The most common use case is to connect the command service to an HTTP API using controllers or minimal API mappings.

Read the [Command API](./command-api) feature documentation for more details.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:::caution Handling failures
The last point above translates to: the command service **does not throw exceptions**. It [returns](#result) an instance of `Result<TState>.Error` instead. It is your responsibility to handle the error.
:::
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Bootstrap

If you registered an implementation of `IEventStore` in the DI container, you can also register the command service:

```csharp title="Program.cs"
builder.Services.AddCommandService<BookingCommandService, BookingState>();
```

The `AddCommandService` extension will register the `BookingService`, and also as `ICommandService<BookingState>`, as a singleton. Remember that all the DI extensions are part of the `Eventuous.Extensions.DependencyInjection` NuGet package.

When you also use `AddControllers`, you get the command service injected to your controllers.

You can simplify your application and avoid creating HTTP endpoints explicitly (as controllers or minimal API endpoints) if you use the [command API feature](command-api.md).
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
---
title: "Command service"
title: "Service with aggregate"
description: "Command service and unit of work for aggregates"
sidebar_position: 1
---

import NoThrowWarning from './_service_no_throw.mdx';
import Result from './_result.mdx';
import Bootstrap from "./_service_registration.mdx";
import HttpApi from "./_service_http.mdx";

## Concept

:::note
This page describes command services that work with [aggregates](../domain/aggregate.md). If you are not using aggregates, check the [Functional services](func-service.mdx) page.
:::


The command service itself performs the following operations when handling one command:
1. Extract the aggregate id from the command, if necessary.
2. Instantiate all the necessary value objects. This could effectively reject the command if value objects cannot be constructed. The command service could also load some other aggregates, or any other information, which is needed to execute the command but won't change state.
3. If the command expects to operate on an existing aggregate instance, this instance gets loaded from the [Aggregate Store](../persistence/aggregate-store).
3. If the command expects to operate on an existing aggregate instance, this instance gets loaded from the [event store](../persistence/event-store.mdx).
4. Execute an operation on the loaded (or new) aggregate, using values from the command, and the constructed value objects.
5. The aggregate either performs the operation and changes its state by producing new events, or rejects the operation.
6. If the operation was successful, the service persists new events to the store. Otherwise, it returns a failure to the edge.
Expand All @@ -35,14 +45,18 @@ sequenceDiagram
API Endpoint-->>-Client: Return result
```

:::caution Handling failures
The last point above translates to: the command service **does not throw exceptions**. It [returns](#result) an instance of `ErrorResult` instead. It is your responsibility to handle the error.
:::
<NoThrowWarning/>

## Implementation

Eventuous provides a base class for you to build command services. It is a generic abstract class, which is typed to the aggregate type. You should create your own implementation of a command service for each aggregate type. As command execution is transactional, it can only operate on a single aggregate instance, and, logically, only one aggregate type.

:::note
Add `Eventuous.Application` NuGet package to your project.
:::

The base class for aggregate-based command services is `CommandService<TAggregate, TState, TId>`.

### Handling commands

The base class has one function that must be used in the service class constructor to define how the service will handle commands. The function is called `On<TCommand>` where `TCommand` is the command type. You can add as many command handlers as you need. The `On` function composes a command handler builder that allows to chain further details to describe how the command needs to be processed.
Expand All @@ -59,7 +73,7 @@ Here is an example of a command service form our test project:
public class BookingsCommandService
: CommandService<Booking, BookingState, BookingId> {
public BookingsCommandService(
IAggregateStore store,
IEventStore store,
Services.IsRoomAvailable isRoomAvailable
) : base(store) {
On<BookRoom>()
Expand Down Expand Up @@ -101,30 +115,8 @@ public class BookingsCommandService
Check the [stream name](../persistence/aggregate-stream#stream-name) documentation if you need to use custom stream names.
:::

### Result

The command service will return an instance of `Result<TState>`.

It could be an `OkResult<TState>`, which contains the new aggregate state and the list of new events. You use the data in the result to pass it over to the caller, if needed.

If the operation was not successful, the command service will return an instance of `ErrorResult` that contains the error message and the exception details.

### Bootstrap

If you registered the `EsdbEventStore` and the `AggregateStore` in your `Startup` as described on the [Aggregate store](../persistence/aggregate-store) page, you can also register the command service:

```csharp title="Program.cs"
builder.Services.AddCommandService<BookingCommandService, BookingState>();
```

The `AddCommandService` extension will register the `BookingService`, and also as `ICommandService<BookingState>`, as a singleton. Remember that all the DI extensions are part of the `Eventuous.Extensions.DependencyInjection` NuGet package.

When you also use `AddControllers`, you get the command service injected to your controllers.

You can simplify your application and avoid creating HTTP endpoints explicitly (as controllers or minimal API endpoints) if you use the [command API feature](command-api.md).

## Application HTTP API
<Result/>

The most common use case is to connect the command service to an HTTP API.
<Bootstrap/>

Read the [Command API](./command-api) feature documentation for more details.
<HttpApi/>
Loading

0 comments on commit 21e940c

Please sign in to comment.