https://github.com/thenativeweb/eventsourcingdb-client-dotnet
The official .NET client SDK for EventSourcingDB.
https://github.com/thenativeweb/eventsourcingdb-client-dotnet
Last synced: 8 months ago
JSON representation
The official .NET client SDK for EventSourcingDB.
- Host: GitHub
- URL: https://github.com/thenativeweb/eventsourcingdb-client-dotnet
- Owner: thenativeweb
- License: mit
- Created: 2025-05-08T20:45:09.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2025-08-18T18:51:35.000Z (9 months ago)
- Last Synced: 2025-08-18T19:02:41.506Z (9 months ago)
- Language: C#
- Homepage: https://www.eventsourcingdb.io
- Size: 71.3 KB
- Stars: 6
- Watchers: 3
- Forks: 3
- Open Issues: 5
-
Metadata Files:
- Readme: README.md
- License: LICENSE.md
- Codeowners: .github/CODEOWNERS
Awesome Lists containing this project
README
# eventsourcingdb
The official .NET client SDK for [EventSourcingDB](https://www.eventsourcingdb.io) – a purpose-built database for event sourcing.
EventSourcingDB enables you to build and operate event-driven applications with native support for writing, reading, and observing events. This client SDK provides convenient access to its capabilities in .NET.
For more information on EventSourcingDB, see its [official documentation](https://docs.eventsourcingdb.io/).
This client SDK includes support for [Testcontainers](https://testcontainers.com/) to spin up EventSourcingDB instances in integration tests. For details, see [Using Testcontainers](#using-testcontainers).
## Getting Started
Install the client SDK:
```shell
dotnet add package EventSourcingDb
```
Import the `Client` class and create an instance by providing the URL of your EventSourcingDB instance and the API token to use:
```csharp
using EventSourcingDb;
var url = new Uri("http://localhost:3000");
var apiToken = "secret";
var client = new Client(url, apiToken);
```
Then call the `PingAsync` method to check whether the instance is reachable. If it is not, the method will throw an exception:
```csharp
await client.PingAsync();
```
Optionally, you might provide a `CancellationToken`.
*Note that `PingAsync` does not require authentication, so the call may succeed even if the API token is invalid.*
If you want to verify the API token, call `VerifyApiTokenAsync`. If the token is invalid, the function will throw an exception:
```csharp
await client.VerifyApiTokenAsync();
```
Optionally, you might provide a `CancellationToken`.
## Writing Events
Call the `WriteEventsAsync` method and provide a collection of events. You do not have to set all event fields – some are automatically added by the server.
Specify `Source`, `Subject`, `Type`, and `Data` according to the [CloudEvents](https://docs.eventsourcingdb.io/fundamentals/cloud-events/) format.
For `Data`, you may provide any object that is serializable to JSON. It is recommended to use properties with JSON attributes to control the serialization.
The method returns a list of written events, including the fields added by the server:
```csharp
var @event = new EventCandidate(
Source: "https://library.eventsourcingdb.io",
Subject: "/books/42",
Type: "io.eventsourcingdb.library.book-acquired",
Data: new {
title = "2001 – A Space Odyssey",
author = "Arthur C. Clarke",
isbn = "978-0756906788"
}
);
var writtenEvents = await client.WriteEventsAsync(new[] { @event });
```
*Optionally, you might provide a `CancellationToken`.*
### Using the `IsSubjectPristine` precondition
If you only want to write events in case a subject (such as `/books/42`) does not yet have any events, use the `IsSubjectPristinePrecondition`:
```csharp
var writtenEvents = await client.WriteEventsAsync(
new[] { @event },
new[] { Precondition.IsSubjectPristinePrecondition("/books/42") }
);
```
### Using the `IsSubjectOnEventId` precondition
If you only want to write events in case the last event of a subject (such as `/books/42`) has a specific ID (e.g., `"0"`), use the `IsSubjectOnEventIdPrecondition`:
```csharp
var writtenEvents = await client.WriteEventsAsync(
new[] { @event },
new[] { Precondition.IsSubjectOnEventIdPrecondition("/books/42", "0") }
);
```
*Note that according to the CloudEvents standard, event IDs must be of type string.*
### Using the `IsEventQlQueryTrue` precondition
If you want to write events depending on an EventQL query, use the `IsEventQlQueryTruePrecondition`:
```csharp
var writtenEvents = await client.WriteEventsAsync(
new[] { @event },
new[] { Precondition.IsEventQlQueryTruePrecondition("FROM e IN events WHERE e.type == 'io.eventsourcingdb.library.book-borrowed' PROJECT INTO COUNT() < 10") }
);
```
*Note that the query must return a single row with a single value, which is interpreted as a boolean.*
## Reading Events
To read all events of a subject, call the `ReadEventsAsync` method and pass the subject and an options object. Set `Recursive` to `false` to ensure that only events of the given subject are returned, not events of nested subjects.
The method returns an async stream, which you can iterate over using `await foreach`:
```csharp
await foreach (var @event in client.ReadEventsAsync(
"/books/42",
new ReadEventsOptions(Recursive: false)))
{
// Handle event
}
```
If an error occurs, the stream will terminate with an exception.
*Optionally, you might provide a `CancellationToken`.*
#### Deserializing Event Data
Each event contains a `Data` property, which holds the event payload as JSON. To deserialize this payload into a strongly typed object, call `GetData()`:
```csharp
var book = @event.GetData();
```
Alternatively, you can use the non-generic overload `GetData(Type)` to resolve the type at runtime:
```csharp
var type = typeof(BookAcquired);
var book = (BookAcquired)@event.GetData(type)!;
```
If you prefer to work directly with the JSON structure, access the `Data` property as a `JsonElement`:
```csharp
var title = @event.Data.GetProperty("title").GetString();
```
### Reading from subjects recursively
If you want to read not only all events of a subject, but also the events of all nested subjects, set `Recursive` to `true`:
```csharp
await foreach (var @event in client.ReadEventsAsync(
"/books/42",
new ReadEventsOptions(Recursive: true)))
{
// ...
}
```
This also allows you to read *all* events ever written by using `/` as the subject.
### Reading in anti-chronological order
By default, events are read in chronological order. To read in anti-chronological order, use the `Order` option:
```csharp
await foreach (var @event in client.ReadEventsAsync(
"/books/42",
new ReadEventsOptions(
Recursive: false,
Order: Order.Antichronological)))
{
// ...
}
```
*Note that you can also use `Order.Chronological` to explicitly enforce the default order.*
### Specifying bounds
If you only want to read a range of events, set the `LowerBound` and `UpperBound` options — either one of them or both:
```csharp
await foreach (var @event in client.ReadEventsAsync(
"/books/42",
new ReadEventsOptions(
Recursive: false,
LowerBound: new Bound("100", BoundType.Inclusive),
UpperBound: new Bound("200", BoundType.Exclusive))))
{
// ...
}
```
### Starting from the latest event of a given type
To start reading from the latest event of a specific type, set the `FromLatestEvent` option:
```csharp
await foreach (var @event in client.ReadEventsAsync(
"/books/42",
new ReadEventsOptions(
Recursive: false,
FromLatestEvent: new ReadFromLatestEvent(
Subject: "/books/42",
Type: "io.eventsourcingdb.library.book-borrowed",
IfEventIsMissing: ReadIfEventIsMissing.ReadEverything))))
{
// ...
}
```
*Note that `FromLatestEvent` and `LowerBound` cannot be used at the same time.*
## Running EventQL Queries
To run an EventQL query, call the `RunEventQlQueryAsync` method and provide the query as an argument. The method returns an async stream, which you can iterate over using `await foreach`:
```csharp
await foreach (var row in client.RunEventQlQueryAsync(
"FROM e IN events PROJECT INTO e"))
{
// ...
}
```
Each row is returned as a `JsonElement`.
*Optionally, you might provide a `CancellationToken`.*
### Typed Results
If you want results to be deserialized automatically, use the generic overload `RunEventQlQueryAsync`. Each row is deserialized into `TRow` according to your projection.
*When using the non-generic overload, each row is a `JsonElement`. With the generic overload, each row is a `TRow` instance. Ensure your projection matches the shape of `TRow`.*
## Observing Events
To observe all future events of a subject, call the `ObserveEventsAsync` method and pass the subject and an options object. Set `Recursive` to `false` to observe only the events of the given subject.
The method returns an async stream:
```csharp
await foreach (var @event in client.ObserveEventsAsync(
"/books/42",
new ObserveEventsOptions(Recursive: false)))
{
// Handle event
}
```
If an error occurs, the stream will terminate with an exception.
*Optionally, you might provide a `CancellationToken`.*
#### Deserializing Event Data
Each event contains a `Data` property, which holds the event payload as JSON. To deserialize this payload into a strongly typed object, call `GetData()`:
```csharp
var book = @event.GetData();
```
Alternatively, you can use the non-generic overload `GetData(Type)` to resolve the type at runtime:
```csharp
var type = typeof(BookAcquired);
var book = (BookAcquired)@event.GetData(type)!;
```
If you prefer to work directly with the JSON structure, access the `Data` property as a `JsonElement`:
```csharp
var title = @event.Data.GetProperty("title").GetString();
```
### Observing from subjects recursively
If you want to observe not only the events of a subject, but also events of all nested subjects, set `Recursive` to `true`:
```csharp
await foreach (var @event in client.ObserveEventsAsync(
"/books/42",
new ObserveEventsOptions(Recursive: true)))
{
// ...
}
```
This also allows you to observe *all* events ever written by using `/` as the subject.
### Specifying bounds
If you want to start observing from a certain point, set the `LowerBound` option:
```csharp
await foreach (var @event in client.ObserveEventsAsync(
"/books/42",
new ObserveEventsOptions(
Recursive: false,
LowerBound: new Bound("100", BoundType.Inclusive))))
{
// ...
}
```
### Starting from the latest event of a given type
To observe starting from the latest event of a specific type, use the `FromLatestEvent` option:
```csharp
await foreach (var @event in client.ObserveEventsAsync(
"/books/42",
new ObserveEventsOptions(
Recursive: false,
FromLatestEvent: new ObserveFromLatestEvent(
Subject: "/books/42",
Type: "io.eventsourcingdb.library.book-borrowed",
IfEventIsMissing: ObserveIfEventIsMissing.ReadEverything))))
{
// ...
}
```
*Note that `FromLatestEvent` and `LowerBound` cannot be used at the same time.*
## Listing Event Types
To list all event types, call the `ReadEventTypesAsync` method. The method returns an async stream:
```csharp
await foreach (var eventType in client.ReadEventTypesAsync())
{
// ...
}
```
## Listing a Specific Event Type
To list a specific event type, call the `ReadEventTypeAsync` method with the event type as an argument. The method returns the detailed event type, which includes the schema:
```csharp
var eventType = await client.ReadEventTypeAsync("io.eventsourcingdb.library.book-acquired");
```
## Using Testcontainers
Import the `Container` class, create an instance, call the `StartAsync` method to run a test container, get a client, run your test code, and finally call the `StopAsync` method to stop the test container:
```csharp
using EventSourcingDb;
var container = new Container();
await container.StartAsync();
var client = container.GetClient();
// ...
await container.StopAsync();
```
Optionally, you might provide a `CancellationToken` to the `StartAsync` and `StopAsync` methods.
To check if the test container is running, call the `IsRunning` method:
```csharp
var isRunning = container.IsRunning();
```
#### Configuring the Container Instance
By default, `Container` uses the `latest` tag of the official EventSourcingDB Docker image. To change that, call the `WithImageTag` method:
```csharp
var container = new Container()
.WithImageTag("1.0.0");
```
Similarly, you can configure the port to use and the API token. Call the `WithPort` or the `WithApiToken` method respectively:
```csharp
var container = new Container()
.WithPort(4000)
.WithApiToken("secret");
```
#### Configuring the Client Manually
In case you need to set up the client yourself, use the following methods to get details on the container:
- `GetHost()` returns the host name
- `GetMappedPort()` returns the port
- `GetBaseUrl()` returns the full URL of the container
- `GetApiToken()` returns the API token