Sekibanを使ったイベントソーシング+アクターモデルの現代的アプリケーション開発
株式会社ジェイテックジャパンのCTO@tomohisaです。最近は.NETでのイベントソーシングとアクターモデルの組み合わせによる分散システム開発について取り組んでいます。
今回は、私たちが開発しているSekibanフレームワークについて、その概要と実際の開発方法を解説していきたいと思います。
Sekibanとは何か
Sekibanは、ジェイテックジャパンが開発した.NETベースのイベントソーシング/CQRSフレームワークです。現代的な分散システム開発において重要となる、以下の技術要素を統合的に提供しています。
- イベントソーシング: システムの状態変化をイベントとして記録し、現在の状態はイベントを再生して導き出すアーキテクチャパターン
- CQRS: コマンド(書き込み)とクエリ(読み取り)を分離することで、それぞれの関心事を独立して最適化できる設計原則
- Microsoft Orleans: アクターモデルベースの分散システムフレームワークとの統合
- .NET Aspire: クラウドネイティブアプリケーション開発を簡素化する統合スタック
従来のCRUDアーキテクチャでは、データの直接更新により過去の変更履歴が失われてしまいますが、Sekibanでは全ての変更をイベントとして記録することで、完全な監査証跡と柔軟なシステム進化を実現できます。
アクターモデルとMicrosoft Orleansの魅力
アクターモデルは、並行処理と分散システムを扱うための設計パターンです。システムを独立したアクター(処理単位)として構成し、アクター間はメッセージパッシングでのみ通信を行います。
Microsoft Orleansは、このアクターモデルを.NET環境で実現するフレームワークとして、以下の特徴を持っています。
仮想アクター(Grain)の概念
Orleansでは、アクターを「Grain」と呼びます。Grainは以下の特徴を持つ分散オブジェクトです。
- 一意のIDで識別: 各Grainは一意のIDを持ち、クラスタ内のどこからでもアクセス可能
- シングルスレッド実行: 各Grainは同時に一つのメソッドのみを実行し、状態の整合性を保証
- 透過的なアクティベーション: 必要に応じて自動的にアクティブ化・非アクティブ化される
- 場所透過性: Grainがクラスタ内のどのノードで実行されているかを意識する必要がない
他のアクターフレームワークとの違い
Orleansは、Akka.NETなどの他のアクターフレームワークと比較して、より開発者フレンドリーな設計になっています。物理的な配置を抽象化し、標準的な.NET開発体験との統合度が高いのが特徴です。
Sekibanの主要コンポーネント
Sekibanでは、以下のコンポーネントを組み合わせてアプリケーションを構築します。実際のコード例を交えながら説明していきましょう。
1. アグリゲート(IAggregatePayload)
アグリゲートは、データの一貫性境界となる単位です。天気予報を管理するアプリケーションを例に見てみましょう。
[GenerateSerializer]
public record WeatherForecast(
string Location,
DateOnly Date,
int TemperatureC,
string Summary
) : IAggregatePayload;
2. コマンド(ICommandWithHandler)
コマンドは、システムに対する命令を表現します。失敗の可能性があり、英語の命令形で表現するのが慣例です。
[GenerateSerializer]
public record CreateWeatherForecast(
string Location,
DateOnly Date,
int TemperatureC,
string Summary
) : ICommandWithHandler<CreateWeatherForecast, WeatherForecastProjector>
{
public PartitionKeys SpecifyPartitionKeys(params object[] keys) =>
PartitionKeys.Generate<WeatherForecastProjector>(keys);
public ResultBox<EventOrNone> Handle(
IAggregatePayload aggregatePayload,
ICommand command,
object context) =>
EventOrNone.Event(new WeatherForecastCreated(Location, Date, TemperatureC, Summary));
}
3. イベント(IEventPayload)
イベントは、システムで確定した事実を記録します。過去形の動詞で表現し、一度記録されると変更されることはありません。
[GenerateSerializer]
public record WeatherForecastCreated(
string Location,
DateOnly Date,
int TemperatureC,
string Summary
) : IEventPayload;
4. アグリゲートプロジェクター(IAggregateProjector)
プロジェクターは、イベントから現在の状態を構築する役割を担います。
public class WeatherForecastProjector : IAggregateProjector
{
public IAggregatePayload Project(IAggregatePayload payload, IEvent ev)
=> (payload, ev.GetPayload()) switch
{
(EmptyAggregatePayload, WeatherForecastCreated e) =>
new WeatherForecast(e.Location, e.Date, e.TemperatureC, e.Summary),
(WeatherForecast wf, WeatherForecastUpdated e) =>
wf with { Summary = e.Summary },
_ => payload
};
}
5. クエリ(IMultiProjectionListQuery)
クエリは、データを読み取るための仕組みです。複数のアグリゲートから必要な情報を抽出できます。
[GenerateSerializer]
public record WeatherForecastQuery(string? Location = null)
: IMultiProjectionListQuery<
AggregateListProjector<WeatherForecastProjector>,
WeatherForecastQuery,
WeatherForecastRecord>
{
public static ResultBox<IEnumerable<WeatherForecastRecord>> HandleFilter(
AggregateListProjector<WeatherForecastProjector> projection,
WeatherForecastQuery query)
=> projection.Payload.Aggregates
.Where(m => m.Value.GetPayload() is WeatherForecast)
.Where(m => query.Location == null ||
((WeatherForecast)m.Value.GetPayload()).Location.Contains(query.Location))
.Select(m => new WeatherForecastRecord(
m.Key,
((WeatherForecast)m.Value.GetPayload()).Location,
((WeatherForecast)m.Value.GetPayload()).Date,
((WeatherForecast)m.Value.GetPayload()).TemperatureC,
((WeatherForecast)m.Value.GetPayload()).Summary))
.ToResultBox();
[GenerateSerializer]
public record WeatherForecastRecord(
Guid Id,
string Location,
DateOnly Date,
int TemperatureC,
string Summary);
}
6. Web APIエンドポイント
これらのコンポーネントをWeb APIとして公開するのも簡単です。
app.MapGet("/weatherforecasts", async (
[FromQuery] string? location,
[FromServices] SekibanOrleansExecutor executor) =>
await executor.QueryAsync(new WeatherForecastQuery(location)).UnwrapBox());
app.MapPost("/weatherforecasts", async (
[FromBody] CreateWeatherForecast cmd,
[FromServices] SekibanOrleansExecutor executor) =>
await executor.CommandAsync(cmd).UnwrapBox());
Sekibanでの開発を始める方法
1. 開発環境の準備
Sekibanでの開発を始めるには、以下の環境が必要です。
- .NET 9 SDK
- Visual Studio 2022以上またはVS Code(C#拡張機能)
- Docker Desktop(ローカル開発用)
2. プロジェクトの作成
まず、Sekibanのプロジェクトテンプレートをインストールします。
# Sekibanテンプレートのインストール
dotnet new install Sekiban.Pure.Templates
# Sekiban+Orleans+Aspireプロジェクトの作成
dotnet new sekiban-orleans-aspire -n MyWeatherApp
プロジェクト一式がこれで生成されます。このプロジェクトには、ドメイン、API、Blazorによるフロントエンドが組み込まれていて、すでに動作する形で実行することができますので、このプロジェクトに機能を追加していくことで、すぐにウェブアプリの開発をすることができます。
3. プロジェクト構造の理解
作成されたプロジェクトは以下の構造になっています。
- MyWeatherApp.Domain: ドメインモデル、コマンド、イベント、クエリ
- MyWeatherApp.ApiService: Web APIエンドポイント
- MyWeatherApp.AppHost: Aspireホスト(オーケストレーション)
- MyWeatherApp.ServiceDefaults: 共通サービス設定
- MyWeatherApp.Web: Webフロントエンド(Blazor)
4. ローカル実行とデバッグ
.NET Aspireの恩恵により、ローカル開発環境の構築は非常に簡単です。
cd MyWeatherApp
dotnet build
dotnet run --project MyWeatherApp.AppHost --launch-profile https
これにより、Aspireダッシュボードが起動し、以下の機能を利用できます。
- リソース監視とログの表示
- エンドポイントへのアクセス
- 分散トレースの確認
- アプリケーション構成の管理
ローカル環境では、以下のストレージエミュレーターが自動的に起動されます。
- Azurite: Azure Blob、Queue、Tableストレージのエミュレーター
- PostgreSQL: イベントストア用データベース
Webフロントエンドにはすでに機能が組み込まれているので、動かして試すことができます。
5. テストの書き方
Sekibanでは、インメモリでのテストが簡単に書けます。
public class WeatherForecastTests : SekibanInMemoryTestBase
{
protected override SekibanDomainTypes GetDomainTypes() =>
MyWeatherAppDomainTypes.Generate(
MyWeatherAppEventsJsonContext.Default.Options);
[Fact]
public void CanCreateWeatherForecast()
{
var response = GivenCommand(new CreateWeatherForecast(
"Tokyo",
DateOnly.FromDateTime(DateTime.Today),
25,
"Sunny"));
Assert.Equal(1, response.Version);
var aggregate = ThenGetAggregate<WeatherForecastProjector>(
response.PartitionKeys);
var forecast = (WeatherForecast)aggregate.Payload;
Assert.Equal("Tokyo", forecast.Location);
Assert.Equal(25, forecast.TemperatureC);
}
}
Sekibanの活用例とサンプル
Sekibanは、すでに実際のプロジェクトで活用されており、いくつかのサンプルも公開されています。
リアルタイムアンケートシステム
管理サイトとクライアントサイトが分かれていて、SignalRによるリアルタイムでイベントを配信する機能が実装されたサンプルです。イベントソーシングの特性を活かして、アンケートの回答状況をリアルタイムで反映できます。
リードモデルサンプル
イベントの更新からPostgreSQLのテーブルを更新するサンプルです。イベントソーシングとRDBMSを組み合わせた読み取り最適化パターンの実装例として参考になります。
まとめ
Sekibanを使うことで、イベントソーシングとアクターモデルを組み合わせた現代的な分散システムを、従来よりもはるかに簡単に開発できるようになります。
特に以下の点が魅力的です。
- 完全な監査証跡: 全ての変更がイベントとして記録されるため、いつ何が変更されたかを完全に追跡できる
- 柔軟なシステム進化: 既存のイベントを変更することなく、新しいビジネスロジックを追加できる
- 高いスケーラビリティ: Microsoft Orleansによる分散処理のサポート
- テスト容易性: イベントベースの設計により、システムの動作をテストしやすい
- 現代的な開発体験: .NET AspireとOrleansの統合により、ローカル開発からクラウドデプロイまでの一貫した体験
次回は、実際にAzureにデプロイする方法と、CI/CDパイプラインの構築について解説していきたいと思います。
Sekibanに興味を持たれた方は、ぜひテンプレートを使って実際に触ってみてください。きっと、従来のCRUDアプリケーションとは異なる、新しい開発体験を感じられるはずです。
AIとの親和性
イベントソーシングは、データを消さないため、AIコーディング時代に、特に向いているアーキテクチャです。また、SekibanにもMCPがあり、コーディングルールを渡して開発をする手法に関しても研究を進めています。それらに関してもこれから記事を書いていきたいと思いますので、ぜひご覧ください。
AIコーディング革命とシステム設計の詳細については、こちらの記事も併せてご覧ください:
AIコーディング革命に備えるシステム設計:イベントソーシング×アクターモデルで実現する後悔なきアーキテクチャ