Skip to content

eventstorage/eventstorage

Repository files navigation

eventstorage

A lightweight event sourcing framework for .NET with event storage of choice.

Github follow Nuget Package Nuget Github follow In follow build Status

eventstorage

Overview

eventstorage is a high-performance event sourced infrastructure out of the box, powered by latest innovative C# that allows selecting event storage of choice with Azure Sql, Postgres and Sql Server, and offers multiple projection modes with support to Redis for high-performance asynchronous projections.

Key benefits of eventstorage

  • Runs plain sql resulting in high-performance storage infrastructure
  • Flexible schema gains with denormalization and throwing away ORM
  • Identifies aggregates with GUID and long and allows switching back and forth
  • Lightning-fast storage operations by selecting long aggregate identifier
  • Allows event storage selection with Azure Sql, Postgre Sql and Sql Server
  • Offers transient/runtime, ACID-compliant and async projections modes
  • Allows projection source as either selected storage or high-performance Redis
  • Lightweight asynchronous projection engine that polls for async projections
  • Restores projections at startup ensuring consistency without blocking business
  • Designed to survive failures and prevents possible inconsistencies

Environment setup

My Skills

eventstorage runs on .Net 8 and requires the SDK installed:

https://dotnet.microsoft.com/en-us/download/dotnet/8.0

My Skills

Use docker to run mssql or postgres databases, execute docker-compose or docker run:

docker compose --project-name eventstorage up -d

Postgres

docker run --name some-postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgres

Sql Server

docker run --name some-mssql -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=sysadmin@1234" -d mcr.microsoft.com/mssql/server:2019-latest

Optionally run Redis for high-performance projections:

docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest

Getting started

My Skills

Install the package

Simply install EventStorage package.
dotnet add package EventStorage --prerelease

Configure event storage

Use AddEventStorage service collection extension.
builder.Services.AddEventStorage(storage =>
{
    storage.Schema = "es";
    storage.AddEventSource(source =>
    {
        source.Select(EventStore.PostgresSql, connectionString);
    });
});

Use configuration options to select your event storage of choice and make sure connection string is defined.

Create aggregate root

Add your aggregate with EventSource<TId>
public class OrderBooking : EventSource<long> // or Guid
{
    public OrderStatus OrderStatus { get; private set; }
    public void Apply(OrderPlaced e)
    {
        OrderStatus = OrderStatus.Placed;
    }
    public void PlaceOrder(PlaceOrder command)
    {
        if(OrderStatus == OrderStatus.Placed)
            return;
        RaiseEvent(new OrderPlaced(command));
    }
}

public enum OrderStatus
{
    Draft = 0,
    Placed = 1,
    Confirmed = 2,
}
public record PlaceOrder(string ProductName, int Quantity, string UserId);
public record OrderPlaced(PlaceOrder Command) : SourcedEvent;

EventSource<TId> allows selecting long or Guid to identify event streams.

While selecting long offers lightnening-fast queries and is human readable, we can always switch back and forth between the two!

Use IEventStorage<T> service

app.MapPost("api/placeorder", 
async(IEventStorage<OrderBooking> eventStorage, PlaceOrder command) =>
{
    var aggregate = await eventStorage.CreateOrRestore();
    aggregate.PlaceOrder(command);

    await eventStorage.Commit(aggregate);
    return Results.Ok(aggregate.SourceId);
});

Similar to placing order, add two more methods to the aggregate to confirm an order, ConfirmOrder command and Apply(OrderConfirmed) method:

app.MapPost("api/confirmorder/{orderId}", 
async(IEventStorage<OrderBooking> eventStorage, string orderId, ConfirmOrder command) =>
{
    var aggregate = await eventStorage.CreateOrRestore(orderId);
    aggregate.ConfirmOrder(command);

    await eventStorage.Commit(aggregate);
});

Configure projections

Transient (runtime), consistent and async projection modes are supported.

eventstorage.AddEventSource(source =>
{
    source.Select(EventStore.PostgresSql, connectionString)
    .Project<OrderProjection>(ProjectionMode.Transient)
    .Project<OrderDetailProjection>(ProjectionMode.Consistent)
    .Project<OrderInfoProjection>(ProjectionMode.Async)
    .Project<OrderDocumentProjection>(ProjectionMode.Async, src => src.Redis("redis://localhost"));
});

When projection mode set to async, optional projection source can be selected and is by default set to selected event store.

Sample projection
public record Order(string SourceId, OrderStatus Status, long Version);

public class OrderProjection : Projection<Order>
{
    public static Order Project(OrderPlaced orderPlaced) => 
        new(orderPlaced.SourceId?.ToString()?? "", OrderStatus.Placed, orderPlaced.Version);
    public static Order Project(OrderConfirmed orderConfirmed, Order order) =>
        order with { Status = OrderStatus.Confirmed, Version = orderConfirmed.Version };
}
Sample Redis projection
[Document(StorageType = StorageType.Json, Prefixes = ["OrderDocument"])]
public class OrderDocument
{
    [RedisIdField][Indexed]
    public string? SourceId { get; set; } = string.Empty;
    // [Searchable]
    public OrderStatus Status { get; set; }
    public long Version { get; set; }
}

public class OrderDocumentProjection : Projection<OrderDocument>
{
    public static OrderDocument Project(OrderPlaced orderPlaced) => new()
    {
        SourceId = orderPlaced.SourceId?.ToString(),
        Version = orderPlaced.Version,
        Status = OrderStatus.Placed
    };
    public static OrderDocument Project(OrderConfirmed orderConfirmed, OrderDocument orderDocument)
    {
        orderDocument.Status = OrderStatus.Confirmed;
        orderDocument.Version = orderConfirmed.Version;
        return orderDocument;
    }
}

Define endpoints to project

// endpoint for transient OrderProjection
app.MapGet("api/order/{orderId}",
async(IEventStorage<OrderBooking> eventStorage, string orderId) =>
{
    var order = await eventStorage.Project<Order>(orderId);
    return Results.Ok(order);
});
// endpoint for async OrderDocumentProjection on Redis
app.MapGet("api/orderDocument/{orderId}",
    async(IEventStorage<OrderBooking> eventStorage, string orderId) =>
    await eventStorage.Project<OrderDocument>(orderId)
);

Documentation

For detailed walkthrough and guides, visit our beautiful documentation site.

Give us a ⭐

If you are an event sourcer and love OSS, give eventstorage a star. 💜

License

This project is licensed under the terms of MIT license.

Packages

 
 
 

Contributors 2

  •  
  •