Skip to content
This repository was archived by the owner on Jun 30, 2024. It is now read-only.

Commit 1c6b3c9

Browse files
committed
Add versions
1 parent d42eb3d commit 1c6b3c9

File tree

85 files changed

+4441
-20
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+4441
-20
lines changed

docusaurus.config.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
const lightCodeTheme = require('prism-react-renderer/themes/vsLight');
55
const darkCodeTheme = require('prism-react-renderer/themes/vsDark');
6+
// @ts-ignore
7+
const versions = require('./versions.json');
68

79
/** @type {import('@docusaurus/types').Config} */
810
const config = {
@@ -31,7 +33,7 @@ const config = {
3133

3234
plugins: [
3335
[
34-
"posthog",
36+
"./src/plugins/posthog",
3537
{
3638
apiKey: "phc_5SQazjqM8Sy6JYz6eN5hs8pe7BrwQEo2qQbdI1FOhPV",
3739
appUrl: "https://eu.posthog.com",
@@ -94,6 +96,10 @@ const config = {
9496
position: 'left',
9597
label: 'Connector',
9698
},
99+
{
100+
type: 'docsVersionDropdown',
101+
position: 'right'
102+
},
97103
{
98104
href: 'https://github.com/sponsors/Eventuous',
99105
position: 'right',

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
"@docusaurus/theme-mermaid": "^2.4.1",
2222
"@mdx-js/react": "^1.6.22",
2323
"clsx": "^1.2.1",
24-
"posthog-docusaurus": "^2.0.0",
2524
"prism-react-renderer": "^1.3.5",
2625
"react": "^17.0.2",
2726
"react-dom": "^17.0.2"

sidebars.js

-13
Original file line numberDiff line numberDiff line change
@@ -151,19 +151,6 @@ const sidebars = {
151151
},
152152
],
153153
connectorSidebar: [{type: 'autogenerated', dirName: 'connector'}],
154-
155-
// But you can create a sidebar manually
156-
/*
157-
tutorialSidebar: [
158-
'intro',
159-
'hello',
160-
{
161-
type: 'category',
162-
label: 'Tutorial',
163-
items: ['tutorial-basics/create-a-document'],
164-
},
165-
],
166-
*/
167154
};
168155

169156
module.exports = sidebars;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
---
2+
title: "Command service"
3+
description: "Command service and unit of work for aggregates"
4+
sidebar_position: 1
5+
---
6+
7+
:::note
8+
The Command Service base class is **optional**, it just makes your life a bit easier.
9+
:::
10+
11+
## Concept
12+
13+
The command service itself performs the following operations when handling one command:
14+
1. Extract the aggregate id from the command, if necessary.
15+
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.
16+
3. If the command expects to operate on an existing aggregate instance, this instance gets loaded from the [Aggregate Store](../persistence/aggregate-store).
17+
4. Execute an operation on the loaded (or new) aggregate, using values from the command, and the constructed value objects.
18+
5. The aggregate either performs the operation and changes its state by producing new events, or rejects the operation.
19+
6. If the operation was successful, the service persists new events to the store. Otherwise, it returns a failure to the edge.
20+
21+
```mermaid
22+
sequenceDiagram
23+
participant Client
24+
participant API Endpoint
25+
participant Command Service
26+
participant Aggregate
27+
participant Aggregate Store
28+
29+
Client->>+API Endpoint: Request
30+
API Endpoint->>API Endpoint: Deserialize request
31+
API Endpoint->>+Command Service: Command
32+
Command Service->>+Aggregate Store: Load
33+
Aggregate Store-->>-Command Service: Aggregate
34+
Command Service->>+Aggregate: Execute
35+
Aggregate-->>-Command Service: Updated aggregate
36+
Command Service->>+Aggregate Store: Store changes
37+
Aggregate Store-->>-Command Service: Return result
38+
Command Service-->>-API Endpoint: Return result
39+
API Endpoint-->>-Client: Return result
40+
```
41+
42+
:::caution Handling failures
43+
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.
44+
:::
45+
46+
## Implementation
47+
48+
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.
49+
50+
### Handling commands
51+
52+
The base class has six methods, which you call in your class constructor to register the command handlers:
53+
54+
| Function | What's it for |
55+
|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
56+
| `OnNew` | Registers the handler, which expects no instance aggregate to exist (create, register, initialise, etc). It will get a new aggregate instance. The operation will fail when it will try storing the aggregate state due to version mismatch. |
57+
| `OnNewAsync` | The same as `OnNew` but expect an asynchronous command handler. |
58+
| `OnExisting` | Registers the handler, which expect an aggregate instance to exist. You need to provide a function to extract the aggregate id from the command. The handler will get the aggregate instance loaded from the store, and will throw if there's no aggregate to load. |
59+
| `OnExistingAsync` | The same as `OnExisting` but expect an asynchronous command handler. |
60+
| `OnAny` | Used for handlers, which can operate both on new and existing aggregate instances. The command service will _try_ to load the aggregate, but won't throw if the load fails, and will pass a new instance instead. |
61+
| `OnAnyAsync` | The same as `OnAny` but expect an asynchronous command handler. |
62+
63+
Here is an example of a command service form our test project:
64+
65+
```csharp title="BookingService.cs"
66+
public class BookingService
67+
: CommandService<Booking, BookingState, BookingId> {
68+
public BookingService(IAggregateStore store) : base(store) {
69+
OnNew<Commands.BookRoom>(
70+
cmd => new BookingId(cmd.BookingId),
71+
(booking, cmd)
72+
=> booking.BookRoom(
73+
cmd.RoomId,
74+
new StayPeriod(cmd.CheckIn, cmd.CheckOut),
75+
cmd.Price,
76+
cmd.BookedBy,
77+
cmd.BookedAt
78+
)
79+
);
80+
81+
OnAny<Commands.ImportBooking>(
82+
cmd => new BookingId(cmd.BookingId),
83+
(booking, cmd)
84+
=> booking.Import(
85+
cmd.RoomId,
86+
new StayPeriod(cmd.CheckIn, cmd.CheckOut)
87+
)
88+
);
89+
}
90+
}
91+
```
92+
93+
You pass the command handler as a function to one of those methods. The function can be inline, like in the example, or it could be a method in the command service class.
94+
95+
In addition, you need to specify a function, which extracts the aggregate id from the command, as both of those methods will try loading the aggregate instance from the store.
96+
97+
:::caution Stream name
98+
Check the [stream name](../persistence/aggregate-stream#stream-name) documentation if you need to use custom stream names.
99+
:::
100+
101+
#### Async command handlers
102+
103+
If you need to get outside your process boundary when handling a command, you most probably would need to execute an asynchronous call to something like an external HTTP API or a database. For those cases you need to use async overloads:
104+
105+
- `OnNewAsync`
106+
- `OnExistingAsync`
107+
- `OnAnyAsync`
108+
109+
These overloads are identical to sync functions, but the command handler function needs to return `Task`, so it can be awaited.
110+
111+
### Result
112+
113+
The command service will return an instance of `Result`.
114+
115+
It could be an `OkResult`, 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.
116+
117+
If the operation was not successful, the command service will return an instance of `ErrorResult` that contains the error message and the exception details.
118+
119+
### Bootstrap
120+
121+
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:
122+
123+
```csharp title="Program.cs"
124+
builder.Services.AddCommandService<BookingCommandService, Booking>();
125+
```
126+
127+
The `AddCommandService` extension will register the `BookingService`, and also as `ICommandService<Booking>`, as a singleton. Remember that all the DI extensions are part of the `Eventuous.AspNetCore` NuGet package.
128+
129+
When you also use `AddControllers`, you get the command service injected to your controllers.
130+
131+
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).
132+
133+
## Application HTTP API
134+
135+
The most common use case is to connect the command service to an HTTP API.
136+
137+
Read the [Command API](./command-api) feature documentation for more details.

0 commit comments

Comments
 (0)