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.
- Runs plain sql resulting in high-performance storage infrastructure
- Flexible schema gains with denormalization and throwing away ORM
- Identifies aggregates with
GUIDandlongand allows switching back and forth - Lightning-fast storage operations by selecting
longaggregate identifier - Allows event storage selection with
Azure Sql,Postgre SqlandSql 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
eventstorage runs on .Net 8 and requires the SDK installed:
https://dotnet.microsoft.com/en-us/download/dotnet/8.0
Use docker to run mssql or postgres databases, execute docker-compose or docker run:
docker compose --project-name eventstorage up -dPostgres
docker run --name some-postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgresSql 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-latestOptionally run Redis for high-performance projections:
docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latestdotnet add package EventStorage --prereleasebuilder.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.
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!
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);
});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.
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 };
}[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;
}
}// 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)
);For detailed walkthrough and guides, visit our beautiful documentation site.
If you are an event sourcer and love OSS, give eventstorage a star. 💜
This project is licensed under the terms of MIT license.