Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5993ad4
Adds more projections documentation
jsedlak Sep 21, 2025
1d5ecc5
Documentation update - formatting and sample code
jsedlak Sep 21, 2025
cbd1e03
Documentation update
jsedlak Sep 22, 2025
bbe7ab4
Documentation update
jsedlak Sep 22, 2025
bcc84d5
Projections docs updates
jsedlak Sep 22, 2025
0ce132c
First run at projections
jsedlak Sep 22, 2025
c3dead8
Fixes workflow
jsedlak Sep 22, 2025
21388fc
Removes Projections for now
jsedlak Sep 24, 2025
bf7082e
Updated test to deactivate the grain and test loading from storage
jsedlak Sep 24, 2025
2b0580d
Adds synchronous outbox messaging
jsedlak Sep 24, 2025
3f90419
Removes old code
jsedlak Sep 24, 2025
aa734bc
Removes old tests
jsedlak Sep 24, 2025
f65501f
Cleanup, readme
jsedlak Sep 24, 2025
7f3968e
Adds multiple grain test
jsedlak Sep 24, 2025
c5aa420
Readme update
jsedlak Sep 24, 2025
8075014
Readme
jsedlak Sep 24, 2025
8bf94b7
Fixes test oopsie
jsedlak Sep 24, 2025
ecc57ad
Fixes tests
jsedlak Sep 24, 2025
3b130a5
updates?
jsedlak Sep 27, 2025
37c8add
Updates to timer based solution
jsedlak Sep 27, 2025
3975481
Improves timer parameter clarity
jsedlak Sep 27, 2025
8c6287f
Cleans up code to not use constructor based services, simplifying imp…
jsedlak Sep 27, 2025
0907abb
test cleanup
jsedlak Sep 27, 2025
0e4962e
Adds sidecars, cleans up unit tests, adds unit tests for sidecars
jsedlak Sep 28, 2025
1f6b74a
updates readme
jsedlak Sep 28, 2025
5be5d5f
Comments out tests that need work
jsedlak Oct 22, 2025
9f2272d
Removes sidecar, needs more work
jsedlak Oct 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[*.cs]

# ORLEANSEXP005: Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
dotnet_diagnostic.ORLEANSEXP005.severity = suggestion
22 changes: 12 additions & 10 deletions .github/workflows/merge-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@ jobs:
runs-on: ubuntu-latest

steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Checkout Code
uses: actions/checkout@v2

- name: Setup .NET Core
uses: actions/setup-dotnet@v3
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x

- name: Install dependencies
run: dotnet restore
- name: Install dependencies
run: dotnet restore

- name: Build
run: dotnet build
- name: Build
run: dotnet build

- name: Test
run: dotnet test
- name: Test
run: dotnet test
208 changes: 195 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,205 @@
<div style="text-align:center;">

![Strata Logo](img/strata-logo.png)

</div>

# Strata

Strata is an opinionated Event Sourcing library built for Microsoft Orleans.

## Features
## Goals

- ♾️ Tentative & Confirmed State Models & Versioning
- 📷 Snapshotting
- ⏱️ Delayed Writes
- 🗃️ Orleans Provider Model
- ⚡ [Event Handlers](doc/event-handlers.md)
- 🗃️ Orleans Provider Model via Durable Framework
- ⚡ Outbox Recipients & Projections
- 🏁 Retry, Replay, and Dry-Run Capabilities
- 📨 OOB Recipients: Direct Grain Calls, Orleans Streams, Recipient Handling

# Event Sourcing

## Build an Aggregate Model

Create an aggregate object that can handle events being applied to it. This will typically represent an aggregate root in your domain model. It can contain objects,

```csharp
[GenerateSerializer]
public class AccountAggregate
{
public void Apply(BalanceAdjustedEvent @event)
{
Balance = @event.Balance;
}

[Id(0)]
public string Id { get; set; } = null!;

[Id(2)]
public double Balance { get; set; }
}
```

## Build an Aggregate Grain

Implement a Journaled Grain that converts commands into events.

```csharp
internal sealed class AccountGrain :
JournaledGrain<AccountAggregate, BaseAccountEvent>,
IAccountGrain
{
public Task<double> GetBalance() => Task.FromResult(ConfirmedState.Balance);

public async Task Deposit(double amount)
{
if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount), "Deposit amount must be positive.");
var newBalance = ConfirmedState.Balance + amount;
var @event = new BalanceAdjustedEvent(this.GetPrimaryKeyString()) { Balance = newBalance };
await RaiseEvent(@event);
}

public async Task Withdraw(double amount)
{
if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount), "Withdrawal amount must be positive.");
if (amount > ConfirmedState.Balance) throw new InvalidOperationException("Insufficient funds for withdrawal.");
var newBalance = ConfirmedState.Balance - amount;
var @event = new BalanceAdjustedEvent(this.GetPrimaryKeyString()) { Balance = newBalance };
await RaiseEvent(@event);
}
}
```

## Handle Outbox Messages

Register an `IOutboxRecipient<TEvent>` to handle outbox messages in the `OnActivateAsync` method of your grain. This allows you to act on events, connecting other grains or passing the message to a stream or bus.

```csharp
internal sealed class AccountGrain :
JournaledGrain<AccountAggregate, BaseAccountEvent>,
IAccountGrain
{
protected override void OnRegisterRecipients()
{
RegisterRecipient(nameof(AccountProjection), new AccountProjection(this.GrainFactory));
}
}

public sealed class AccountProjection : IOutboxRecipient<BaseAccountEvent>
{
private readonly IGrainFactory _grainFactory;

public AccountProjection(IGrainFactory grainFactory)
{
_grainFactory = grainFactory;
}

public async Task Handle(int version, BaseAccountEvent @event)
{
if (@event is BalanceAdjustedEvent balanceEvent)
{
var accountId = balanceEvent.Id;
var viewModelGrain = _grainFactory.GetGrain<IAccountViewModelGrain>(accountId);
await viewModelGrain.UpdateBalance(balanceEvent.Balance);
}
}
}
```

# Sidecars

Strata provides a **Sidecar** pattern that allows you to attach auxiliary grains to primary grains for cross-cutting concerns like monitoring, logging, data collection, or background processing. Sidecars are automatically managed by the framework and can be enabled or disabled dynamically.

## Key Concepts

- **Sidecar Grain**: A grain that implements `ISidecarGrain` and provides auxiliary functionality
- **Host Grain**: A grain that implements `ISidecarHost<TSidecar>` to indicate it supports a specific sidecar
- **Lifecycle Management**: Sidecars are automatically activated when their host grain activates (if enabled)
- **State Management**: Each sidecar has persistent state to track whether it's enabled or disabled

## Creating a Sidecar Grain

Implement the `ISidecarGrain` interface to create a sidecar:

```csharp
[GrainType("user-cdp")]
public class UserCdpGrain : Grain, IUserCdpGrain
{
public async Task InitializeSidecar()
{
// Perform sidecar initialization logic
// Set reminders, collect data, establish connections, etc.

var userGrain = GrainFactory.GetGrain<IUserGrain>(
this.GetPrimaryKeyString()
);

await userGrain.SetReferenceId(Guid.NewGuid().ToString());

// Sidecars can control their own lifecycle
await userGrain.DisableSidecar<IUserCdpGrain>();
}
}

public interface IUserCdpGrain : IGrainWithStringKey, ISidecarGrain
{
// Additional sidecar-specific methods can be added here
}
```

## Creating a Host Grain

Mark your grain as a sidecar host by implementing `ISidecarHost<TSidecar>`:

```csharp
[GrainType("user")]
public class UserGrain : Grain, IUserGrain, ISidecarHost<IUserCdpGrain>
{
private readonly IPersistentState<UserData> _state;

public UserGrain([FromKeyedServices("state")] IPersistentState<UserData> state)
{
_state = state;
}

public async ValueTask SetReferenceId(string referenceId)
{
_state.State.ReferenceId = referenceId;
await _state.WriteStateAsync();
}

// Other grain methods...
}

public interface IUserGrain : IGrainWithStringKey
{
ValueTask SetReferenceId(string referenceId);
// Other methods...
}
```

## Sidecar Lifecycle

1. **Activation**: When a host grain activates, the framework checks if any sidecars are enabled
2. **Initialization**: If enabled, the sidecar grain is retrieved and `InitializeSidecar()` is called
3. **Runtime**: Sidecars operate independently but can interact with their host grain
4. **Control**: Sidecars can be enabled/disabled dynamically using extension methods

## Controlling Sidecars

Use the provided extension methods to control sidecar state:

```csharp
// Enable a sidecar
await userGrain.EnableSidecar<IUserCdpGrain>();

// Disable a sidecar
await userGrain.DisableSidecar<IUserCdpGrain>();
```

## Setup and Configuration

## Coming Soon
Add sidecar support to your Orleans silo:

- 📦 Orleans Durable Support
- 📦 Aspire Support
- 📦 Projections
```csharp
builder.ConfigureSilo((options, siloBuilder) =>
{
siloBuilder.AddSidecars();
// Other configuration...
});
```
Loading