![]() |
VOOZH | about |
dotnet add package ErikLieben.FA.ES --version 1.4.0
NuGet\Install-Package ErikLieben.FA.ES -Version 1.4.0
<PackageReference Include="ErikLieben.FA.ES" Version="1.4.0" />
<PackageVersion Include="ErikLieben.FA.ES" Version="1.4.0" />Directory.Packages.props
<PackageReference Include="ErikLieben.FA.ES" />Project file
paket add ErikLieben.FA.ES --version 1.4.0
#r "nuget: ErikLieben.FA.ES, 1.4.0"
#:package ErikLieben.FA.ES@1.4.0
#addin nuget:?package=ErikLieben.FA.ES&version=1.4.0Install as a Cake Addin
#tool nuget:?package=ErikLieben.FA.ES&version=1.4.0Install as a Cake Tool
👁 Quality Gate Status
👁 Maintainability Rating
👁 Security Rating
👁 Technical Debt
👁 Lines of Code
👁 Coverage
👁 Known Vulnerabilities
A lightweight, AOT-friendly Event Sourcing toolkit for .NET. Build aggregates, append and read events, create snapshots, upcast historical data, and integrate with Azure storage and Functions.
This is an opinionated library built primarily for my own projects and coding style. You're absolutely free to use it (it's MIT licensed!), but please don't expect free support or feature requests. If it works for you, great! If not, there are many other excellent libraries in the .NET ecosystem. For commercially supported event-sourcing platforms, consider EventStoreDB (https://www.eventstore.com/eventstoredb) or AxonIQ's Axon Server/Axon Framework (https://www.axoniq.io/).
That said, I do welcome bug reports and thoughtful contributions. If you're thinking about a feature or change, please open an issue first to discuss it - this helps avoid disappointment if it doesn't align with the library's direction. 😊
🚧 Still a bit under construction while moving from in-process Azure Function support to isolated (out-of-process) Azure Function support and full support for AOT. API isn't compatible with older versions, versions before 1.0.0 🚧
ErikLieben.FA.ES is an event sourcing toolkit/framework designed to be:
Install the CLI tool (locally):
dotnet new tool-manifest
dotnet tool install ErikLieben.FA.ES.CLI --local
Add the NuGet package to your domain class library:
dotnet add package ErikLieben.FA.ES
Decide upon a storage provider and add the corresponding package:
dotnet add package ErikLieben.FA.ES.AzureStorage
Currently only Azure Storage support is released due to lacking support for AOT in the Azure SDK's for Azure Table Storage & CosmosDB.
For Azure Functions:
dotnet add package ErikLieben.FA.ES.Azure.Functions.Worker.Extensions
For your unit test projects (inMemory):
dotnet add package ErikLieben.FA.ES.Testing
Requirements: .NET 9.0+
🚧 More documentation needs to be added, these are some of the basics. 🚧
Start with creating an aggregate:
public partial class Customer(IEventStream stream) : Aggregate(stream)
{
}
This class is partial, the CLI will generate the remaining supporting code.
Because incremental generators can’t be ordered, running our generator alongside System.Text.Json’s JsonSerializable source generator results in conflicts, because the generators would run at the same time. So this code isn’t generated via a Roslyn incremental generator, but needs to be manually generator through the CLI tool.
What this means:
Customer/123). The storage provider implements the stream mechanics..Generated.cs file so your hand-written code stays clean and small.Next, define an event that represents the business action:
[EventName("Customer.Registered.ThroughWebsite")]
public record CustomerRegisteredThroughWebsite(string CustomerName);
ℹ️ Note: This sample uses a minimal event that includes only the customer's name to keep the example focused and easier to understand.
A few event tips:
Customer.Registered.ThroughWebsite not Register.Customer) so the stream reads like a history of facts; the dots are optional in the event name.In the Customer aggregate, we're now adding a method to apply the event to the state when it's appended to/ replayed from the eventstream:
public partial class Customer(IEventStream stream) : Aggregate(stream)
{
public string? CustomerName { get; private set; }
private void When(CustomerRegisteredThroughWebsite @event)
{
this.CustomerName = @event.CustomerName;
}
}
We add a When method to the aggregate; it's invoked when the stream of events is folded to rebuild the latest state; add one When per event type.
More about folding:
When methods are called in-order.Next up, we add a method to perform a command/action:
public partial class Customer(IEventStream stream) : Aggregate(stream)
{
public string? CustomerName { get; private set; }
public Task RegisterCustomerThroughWebsite(string customerName)
{
ArgumentNullException.ThrowIfNull(customerName);
return Stream.Session(context =>
Fold(context.Append(
new CustomerRegisteredThroughWebsite(customerName))));
}
private void When(CustomerRegisteredThroughWebsite @event)
{
this.CustomerName = @event.CustomerName;
}
}
In RegisterCustomerThroughWebsite, we validate inputs, append the event, and immediately fold it over the current state.
This simple example omits domain validation; in a real system you could also check whether the current state allows the change (for example, the customer's account isn't blocked).
What the call does:
Stream.Session: Opens a short-lived unit of work with the event stream. Providers can batch writes and handle concurrency.context.Append(...): Records the new event as a fact to the stream.Fold(...): Applies the newly appended event(s) to your in-memory state so the aggregate is up-to-date after the command.Next, generate the supporting code with the CLI tool:
dotnet tool run faes
This generates supporting code for the Customer aggregate (in Customer.Generated.cs):
Fold method from the event name Customer.Registered.ThroughWebsite to the When method handling it.ICustomer interface containing the public properties of your aggregate.JsonSerializerContext for the aggregate and all events (this enables AOT-friendly serialization).CustomerFactory class to create aggregate instances.ICustomerFactory interface.It also generates a library-wide helper DemoApp.DomainExtensions.Generated.cs that contains:
IServiceCollection to register the factories and interfaces as singletons to DI.JsonSerializerContext for the events (this enables AOT-friendly serialization).After generation, next steps:
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddAzureClients(clientBuilder =>
{
var store = builder.Configuration.GetConnectionString("Store");
if (string.IsNullOrEmpty(store))
{
throw new InvalidOperationException("AzureWebJobsStorage connection string not found");
}
// Create a Azure client with the "Store" name, later used in EventStreamBlobSettings below
clientBuilder
.AddBlobServiceClient(store)
.WithName("Store");
});
// Register the generated factories and code from DemoApp.Domain class library
builder.Services.ConfigureDemoAppDomainFactory();
// Register services of the Azure Blob storage provider and set it up
builder.Services.ConfigureBlobEventStore(new EventStreamBlobSettings("Store", autoCreateContainer: true));
// Setup the framework to use the blob storage provider by default
builder.Services.ConfigureEventStore(new EventStreamDefaultTypeSettings("blob"));
var app = builder.Build();
await app.RunAsync();
A version token points to an exact position in an event stream while also anchoring that position to the object and the specific stream instance used at the time.
Key terms:
Why it matters:
Example scenario:
In short, the version token unambiguously identifies an event position, and the object document ensures your application always routes to the current active stream for new work—without losing access to historical streams.
Projections are read models that materialize one or more streams into a shape that’s fast to query. They’re ideal for list pages, lookups, dashboards, and cross-aggregate views. Instead of replaying every event from every aggregate to render a page (which can be slow), you incrementally fold events into a compact structure.
Key characteristics:
Define a projection by declaring a partial class and When handlers. The CLI will generate the Fold method and JSON serializer context for you.
public partial class CustomerListProjection : Projection
{
public List<string> CustomerNames { get; set; } = new();
// Called when a new customer is registered
public void When(CustomerRegisteredThroughWebsite @event)
{
if (!CustomerNames.Contains(@event.CustomerName))
{
CustomerNames.Add(@event.CustomerName);
}
}
}
// A versionToken can be created manually or captured from your aggregate metadata
var versionToken = new VersionToken("Customer", "12345", "0000000001", 1);
// Resolve dependencies (in production you’d typically get these from DI)
var documentFactory = serviceProvider.GetRequiredService<IObjectDocumentFactory>();
var eventStreamFactory = serviceProvider.GetRequiredService<IEventStreamFactory>();
var projection = new CustomerListProjection(documentFactory, eventStreamFactory);
// Bring the projection up to the specific version; and keep a reference to the object and latest version in the checkpoint
await projection.UpdateToVersion(versionToken);
// Later, you can try to advance it to the latest version (for all tracked streams)
await projection.UpdateToLatestVersion();
Tips
When methods idempotent and side-effect free. They should only update in-memory state.Checkpoint dictionary and a CheckpointFingerprint. You can persist the projection (e.g., as JSON via ToJson()) together with the checkpoint to resume efficiently.ProjectionWithExternalCheckpointAttribute.When method per event type.The CLI scans your aggregates and events to generate:
Run it during development whenever you add or modify aggregates or events.
This is not working with incremental source generators, because there is no way to order incremental source generators and it will run into conflict with the incremental source generators for JSON serialization.
Testing an aggregate is straightforward. Use the TestSetup class to create an in-memory event stream and assert on the results:
[Fact]
public async Task Should_append_event()
{
// Arrange
var serviceProvider = Substitute.For<IServiceProvider>();
// The context can be used for projections, when using multiple seperate streams
var context = TestSetup.GetContext(serviceProvider, DemoAppDomainFactory.Get);
var eventStream = await context.GetEventStreamFor("Customer", "12345");
var sut = new Account(eventStream);
// if you want to test based upon previously added events add them here:
// await eventStream.Session(ctx => ctx.Append(new CustomerRegisteredThroughWebsite(customerName)));
// sut.Fold();
var customerName = "ABC";
// Act
await sut.RegisterCustomerThroughWebsite(customerName);
// Assert
context.Assert
.ShouldHaveObject("Customer", "12345")
.WithEventCount(1)
.WithEventAtLastPosition(new CustomerRegisteredThroughWebsite(customerName));
}
Use the Testing package for fast, deterministic unit tests.
Currently only in development branch:
Azure.Data.Tables, which does not currently provide full Native AOT/trimming support (e.g., lacks complete trimming annotations and uses reflection-based patterns in key areas).Azure.Cosmos, which does not currently offer full Native AOT support; relevant work is pending in the Azure SDK ecosystem.MIT License - see the file for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 net9.0 is compatible. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 was computed. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
Showing the top 5 NuGet packages that depend on ErikLieben.FA.ES:
| Package | Downloads |
|---|---|
|
ErikLieben.FA.ES.AzureStorage
Azure Blob and Table storage provider for ErikLieben.FA.ES event sourcing framework. Includes snapshots, tiering, and projection coordination. |
|
|
ErikLieben.FA.ES.Azure.Functions.Worker.Extensions
Azure Functions Worker Extensions for ErikLieben.FA.ES event sourcing framework. Provides input and trigger bindings for aggregates and projections. |
|
|
ErikLieben.FA.ES.Testing
Testing utilities and builders for ErikLieben.FA.ES event sourcing framework. Provides in-memory test contexts and aggregate test builders. |
|
|
ErikLieben.FA.ES.EventStreamManagement
Event stream migration, transformation, and management toolkit with zero-downtime support for distributed systems |
|
|
ErikLieben.FA.ES.CosmosDb
Azure Cosmos DB storage provider for ErikLieben.FA.ES event sourcing framework. Optimized for RU efficiency with proper partition key strategies. |
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.0.0-preview.17 | 117 | 5/19/2026 |
| 2.0.0-preview.16 | 88 | 5/15/2026 |
| 2.0.0-preview.15 | 84 | 5/15/2026 |
| 2.0.0-preview.14 | 87 | 5/12/2026 |
| 2.0.0-preview.12 | 77 | 4/17/2026 |
| 2.0.0-preview.11 | 83 | 4/17/2026 |
| 2.0.0-preview.10 | 157 | 3/1/2026 |
| 2.0.0-preview.9 | 338 | 2/22/2026 |
| 2.0.0-preview.8 | 109 | 1/7/2026 |
| 2.0.0-preview.7 | 92 | 1/7/2026 |
| 2.0.0-preview.6 | 96 | 1/5/2026 |
| 2.0.0-preview.5 | 85 | 1/5/2026 |
| 2.0.0-preview.4 | 1,110 | 1/5/2026 |
| 2.0.0-preview.3 | 79 | 1/5/2026 |
| 2.0.0-preview.2 | 278 | 12/30/2025 |
| 2.0.0-preview.1 | 374 | 12/7/2025 |
| 1.4.0 | 744 | 2/24/2026 |
| 1.3.7 | 192 | 2/13/2026 |
| 1.3.6 | 3,472 | 11/12/2025 |