Communityコーディング&開発github.com

akka-hosting-actor-patterns

Akka.Hosting を使用してエンティティアクターを構築するためのパターン - GenericChildPerEntityParent、メッセージ抽出、クラスターシャーディング抽象化、akka-reminders、ITimeProvider。ローカルテストとクラスタ運用モードの両方をサポートします。

akka-hosting-actor-patterns とは?

このスキルは、Akka.NET の Akka.Hosting ライブラリを使用して、エンティティアクター(ユーザー、注文、請求書などのドメインオブジェクト)を構築するための一連のパターンとベストプラクティスを提供します。主な目的は、同一のアクターコードが単体テスト(クラスタリングなし)と本番環境(クラスターシャーディング)の両方で動作するようにする実行モードの抽象化です。このスキルでは、GenericChildPerEntityParent アクターを使用してシャーディングセマンティクスを模倣するローカルテストモードと、完全なクラスタリングを使用する運用モードを切り替えるための AkkaExecutionMode 列挙型を定義します。また、Akka.Cluster.Sharding の IMessageExtractor インターフェースを再利用したメッセージ抽出によるルーティング、Akka.Hosting 拡張メソッドによる流暢な設定、テスト容易性のための ITimeProvider の使用、重複したスケジュールタスクのための akka-reminders の統合などをカバーしています。さらに、アクターにおける DI スコープ管理のパターン(メッセージごとのスコープ作成)や、アクターレジストリのマーカータイプも含まれています。このスキルは、Claude Code、Cursor、Codex などの AI エージェント開発ツールで活用でき、堅牢でテスト可能な Akka.NET アクターシステムを効率的に構築するための実用的なコード例とベストプラクティスを提供します。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Aaronontheweb/dotnet-skills/tree/main/skills/akka-hosting-actor-patterns

Installed? Explore more コーディング&開発 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Akka.Hosting アクターパターン

このスキルを使用するタイミング

以下の場合にこのスキルを使用します:

  • ドメインオブジェクト(ユーザー、注文、請求書など)を表すエンティティアクターを構築する場合
  • 単体テスト(クラスタリングなし)と本番環境(クラスターシャーディング)の両方で動作するアクターが必要な場合
  • akka-reminders を使用したスケジュールタスクを設定する場合
  • Akka.Hosting 拡張メソッドでアクターを登録する場合
  • 再利用可能なアクター設定パターンを作成する場合

基本原則

  1. 実行モード抽象化 - 同じアクターコードがローカル(テスト)とクラスタ(本番)の両方で実行される
  2. ローカル用 GenericChildPerEntityParent - クラスタオーバーヘッドなしでシャーディングセマンティクスを模倣
  3. ルーティング用メッセージ抽出 - Akka.Cluster.Sharding の IMessageExtractor インターフェースを再利用
  4. Akka.Hosting 拡張メソッド - 流暢な設定で構成が容易
  5. テスト容易性のための ITimeProvider - DateTime.Now の代わりに ActorSystem.Scheduler を使用

実行モード

アクターの動作を制御するための列挙型を定義します:

/// <summary>
/// Akka.NET の設定方法を決定します
/// </summary>
public enum AkkaExecutionMode
{
    /// <summary>
    /// 純粋なローカルアクターシステム - リモートなし、クラスタリングなし。
    /// ShardRegion の代わりに GenericChildPerEntityParent を使用します。
    /// 単体テストやシンプルなシナリオに最適です。
    /// </summary>
    LocalTest,

    /// <summary>
    /// ShardRegion を使用した完全なクラスタリング。
    /// 統合テストや本番環境で使用します。
    /// </summary>
    Clustered
}

GenericChildPerEntityParent

子エンティティにメッセージをルーティングする軽量な親アクターで、クラスタを必要とせずにクラスターシャーディングのセマンティクスを模倣します:

using Akka.Actor;
using Akka.Cluster.Sharding;

/// <summary>
/// エンティティごとに子アクターを持つ汎用親アクター。
/// </summary>
/// <remarks>
/// 一貫したルーティングのために Akka.Cluster.Sharding の IMessageExtractor を再利用します。
/// クラスタリングのオーバーヘッドが不要な単体テストに最適です。
/// </remarks>
public sealed class GenericChildPerEntityParent : ReceiveActor
{
    public static Props CreateProps(
        IMessageExtractor extractor,
        Func<string, Props> propsFactory)
    {
        return Props.Create(() =>
            new GenericChildPerEntityParent(extractor, propsFactory));
    }

    private readonly IMessageExtractor _extractor;
    private readonly Func<string, Props> _propsFactory;

    public GenericChildPerEntityParent(
        IMessageExtractor extractor,
        Func<string, Props> propsFactory)
    {
        _extractor = extractor;
        _propsFactory = propsFactory;

        ReceiveAny(message =>
        {
            var entityId = _extractor.EntityId(message);
            if (entityId is null) return;

            // 既存の子を取得するか、新しい子を作成
            Context.Child(entityId)
                .GetOrElse(() => Context.ActorOf(_propsFactory(entityId), entityId))
                .Forward(_extractor.EntityMessage(message));
        });
    }
}

メッセージ抽出

Akka.Cluster.Sharding の IMessageExtractor を実装する抽出子を作成します:

using Akka.Cluster.Sharding;

/// <summary>
/// 厳密に型指定された ID に基づいてメッセージをエンティティアクターにルーティングします。
/// </summary>
public sealed class OrderMessageExtractor : HashCodeMessageExtractor
{
    public const int DefaultShardCount = 40;

    public OrderMessageExtractor(int maxNumberOfShards = DefaultShardCount)
        : base(maxNumberOfShards)
    {
    }

    public override string? EntityId(object message)
    {
        return message switch
        {
            IWithOrderId msg => msg.OrderId.Value.ToString(),
            _ => null
        };
    }
}

// 特定のエンティティを対象とするメッセージのインターフェースを定義
public interface IWithOrderId
{
    OrderId OrderId { get; }
}

// 厳密に型指定された ID を使用
public readonly record struct OrderId(Guid Value)
{
    public static OrderId New() => new(Guid.NewGuid());
    public override string ToString() => Value.ToString();
}

Akka.Hosting 拡張メソッド

実行モードを抽象化する拡張メソッドを作成します:

using Akka.Cluster.Hosting;
using Akka.Cluster.Sharding;
using Akka.Hosting;

public static class OrderActorHostingExtensions
{
    /// <summary>
    /// OrderActor をローカルモードとクラスタモードの両方をサポートして追加します。
    /// </summary>
    public static AkkaConfigurationBuilder WithOrderActor(
        this AkkaConfigurationBuilder builder,
        AkkaExecutionMode executionMode = AkkaExecutionMode.Clustered,
        string? clusterRole = null)
    {
        if (executionMode == AkkaExecutionMode.LocalTest)
        {
            // 非クラスタモード: GenericChildPerEntityParent を使用
            builder.WithActors((system, registry, resolver) =>
            {
                var parent = system.ActorOf(
                    GenericChildPerEntityParent.CreateProps(
                        new OrderMessageExtractor(),
                        entityId => resolver.Props<OrderActor>(entityId)),
                    "orders");

                registry.Register<OrderActor>(parent);
            });
        }
        else
        {
            // クラスタモード: ShardRegion を使用
            builder.WithShardRegion<OrderActor>(
                "orders",
                (system, registry, resolver) =>
                    entityId => resolver.Props<OrderActor>(entityId),
                new OrderMessageExtractor(),
                new ShardOptions
                {
                    StateStoreMode = StateStoreMode.DData,
                    Role = clusterRole
                });
        }

        return builder;
    }
}

複数アクターの構成

すべてのドメインアクターを登録する便利なメソッドを作成します:

public static class DomainActorHostingExtensions
{
    /// <summary>
    /// すべての注文ドメインアクターをシャーディングサポート付きで追加します。
    /// </summary>
    public static AkkaConfigurationBuilder WithOrderDomainActors(
        this AkkaConfigurationBuilder builder,
        AkkaExecutionMode executionMode = AkkaExecutionMode.Clustered,
        string? clusterRole = null)
    {
        return builder
            .WithOrderActor(executionMode, clusterRole)
            .WithPaymentActor(executionMode, clusterRole)
            .WithShipmentActor(executionMode, clusterRole)
            .WithNotificationActor(); // シングルトン、シャーディング不要
    }
}

スケジューリングでの ITimeProvider の使用

テスト可能な時間ベースのロジックのために、ActorSystem の Scheduler を ITimeProvider として登録します:

public static class SharedAkkaHostingExtensions
{
    public static IServiceCollection AddAkkaWithTimeProvider(
        this IServiceCollection services,
        Action<AkkaConfigurationBuilder, IServiceProvider> configure)
    {
        // ActorSystem の Scheduler を使用して ITimeProvider を登録
        services.AddSingleton<ITimeProvider>(sp =>
            sp.GetRequiredService<ActorSystem>().Scheduler);

        return services.ConfigureAkka((builder, sp) =>
        {
            configure(builder, sp);
        });
    }
}

// アクター内で ITimeProvider を注入
public class SubscriptionActor : ReceiveActor
{
    private readonly ITimeProvider _timeProvider;

    public SubscriptionActor(ITimeProvider timeProvider)
    {
        _timeProvider = timeProvider;

        // DateTime.UtcNow の代わりに _timeProvider.GetUtcNow() を使用
        // これによりテストで時間を制御できる
    }
}

Akka.Reminders 統合

再起動後も持続するスケジュールタスクには akka-reminders を使用します:

using Akka.Reminders;
using Akka.Reminders.Sql;
using Akka.Reminders.Sql.Configuration;
using Akka.Reminders.Storage;

public static class ReminderHostingExtensions
{
    /// <summary>
    /// PostgreSQL ストレージを使用して akka-reminders を設定します。
    /// </summary>
    public static AkkaConfigurationBuilder WithPostgresReminders(
        this AkkaConfigurationBuilder builder,
        string connectionString,
        string schemaName = "reminders",
        string tableName = "scheduled_reminders",
        bool autoInitialize = true)
    {
        return builder.WithLocalReminders(reminders => reminders
            .WithResolver(sys => new GenericChildPerEntityResolver(sys))
            .WithStorage(system =>
            {
                var settings = SqlReminderStorageSettings.CreatePostgreSql(
                    connectionString,
                    schemaName,
                    tableName,
                    autoInitialize);
                return new SqlReminderStorage(settings, system);
            })
            .WithSettings(new ReminderSettings
            {
                MaxSlippage = TimeSpan.FromSeconds(30),
                MaxDeliveryAttempts = 3,
                RetryBackoffBase = TimeSpan.FromSeconds(10)
            }));
    }

    /// <summary>
    /// テスト用にインメモリストレージを使用して akka-reminders を設定します。
    /// </summary>
    public static AkkaConfigurationBuilder WithInMemoryReminders(
        this AkkaConfigurationBuilder builder)
    {
        return builder.WithLocalReminders(reminders => reminders
            .WithResolver(sys => new GenericChildPerEntityResolver(sys))
            .WithStorage(system => new InMemoryReminderStorage())
            .WithSettings(new ReminderSettings
            {
                MaxSlippage = TimeSpan.FromSeconds(1),
                MaxDeliveryAttempts = 3,
                RetryBackoffBase = TimeSpan.FromMilliseconds(100)
            }));
    }
}

子エンティティごとのカスタムリマインダーリゾルバー

リマインダーコールバックを GenericChildPerEntityParent アクターにルーティングします:

using Akka.Actor;
using Akka.Hosting;
using Akka.Reminders;

/// <summary>
/// リマインダーターゲットを GenericChildPerEntityParent アクターに解決します。
/// </summary>
public sealed class GenericChildPerEntityResolver : IReminderActorResolver
{
    private readonly ActorSystem _system;

    public GenericChildPerEntityResolver(ActorSystem system)
    {
        _system = system;
    }

    public IActorRef ResolveActorRef(ReminderEntry entry)
    {
        var registry = ActorRegistry.For(_system);

        return entry.Key switch
        {
            var k when k.StartsWith("order-") =>
                registry.Get<OrderActor>(),
            var k when k.StartsWith("subscription-") =>
                registry.Get<SubscriptionActor>(),
            _ => throw new InvalidOperationException(
                $"不明なリマインダーキー形式: {entry.Key}")
        };
    }
}

シングルトンアクター(シャーディングなし)

インスタンスが1つだけのアクターの場合:

public static AkkaConfigurationBuilder WithEmailSenderActor(
    this AkkaConfigurationBuilder builder)
{
    return builder.WithActors((system, registry, resolver) =>
    {
        var actor = system.ActorOf(
            resolver.Props<EmailSenderActor>(),
            "email-sender");
        registry.Register<EmailSenderActor>(actor);
    });
}

レジストリのマーカータイプ

親として登録されているアクターを参照する必要がある場合:

/// <summary>
/// OrderActor の GenericChildPerEntityParent を取得するための ActorRegistry のマーカータイプ。
/// </summary>
public sealed class OrderManagerActor;

// 拡張メソッドでの使用
registry.Register<OrderManagerActor>(parent);

// コントローラー/サービスでの使用
public class OrderService
{
    private readonly IActorRef _orderManager;

    public OrderService(IRequiredActor<OrderManagerActor> orderManager)
    {
        _orderManager = orderManager.ActorRef;
    }

    public async Task<OrderResponse> CreateOrder(CreateOrderCommand cmd)
    {
        return await _orderManager.Ask<OrderResponse>(cmd);
    }
}

アクターにおける DI スコープ管理

アクターには自動的な DI スコープがありません。 ASP.NET コントローラー(各 HTTP リクエストがスコープを作成する)とは異なり、アクターは長寿命です。スコープサービス(DbContext など)が必要な場合は、IServiceProvider を注入して手動でスコープを作成します。

パターン: メッセージごとのスコープ

public sealed class OrderProcessingActor : ReceiveActor
{
    private readonly IServiceProvider _serviceProvider;
    private readonly IActorRef _notificationActor;

    public OrderProcessingActor(
        IServiceProvider serviceProvider,
        IRequiredActor<NotificationActor> notificationActor)
    {
        _serviceProvider = serviceProvider;
        _notificationActor = notificationActor.ActorRef;

        ReceiveAsync<ProcessOrder>(HandleProcessOrder);
    }

    private async Task HandleProcessOrder(ProcessOrder msg)
    {
        // このメッセージ用のスコープを作成 - 処理後に破棄
        using var scope = _serviceProvider.CreateScope();

        // スコープ内でスコープサービスを解決
        var orderRepository = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
        var paymentService = scope.ServiceProvider.GetRequiredService<IPaymentService>();
        var emailComposer = scope.ServiceProvider.GetRequiredService<IOrderEmailComposer>();

        // スコープサービスで作業を実行
        var order = await orderRepository.GetByIdAsync(msg.OrderId);
        var payment = await paymentService.ProcessAsync(order);

        // スコープが破棄されると DbContext の変更がコミットされる
    }
}

このパターンの利点

利点説明
メッセージごとに新しい DbContextメッセージ間で古いエンティティトラッキングがない
適切な破棄各メッセージ後にデータベース接続が解放される
分離あるメッセージのエラーが別のメッセージの状態を破損しない
テスト容易性テストでモックの IServiceProvider を注入できる

シングルトンサービス - 直接注入

ステートレスでスレッドセーフなサービスの場合は、直接注入します(スコープ不要):

public sealed class NotificationActor : ReceiveActor
{
    private readonly IEmailLinkGenerator _linkGenerator;  // シングルトン - OK!
    private readonly IMjmlTemplateRenderer _renderer;     // シングルトン - OK!

    public NotificationActor(
        IEmailLinkGenerator linkGenerator,
        IMjmlTemplateRenderer renderer)
    {
        _linkGenerator = linkGenerator;
        _renderer = renderer;
        // ...
    }
}

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

akka-net-aspire-configuration

Configure Akka.NET with .NET Aspire for local development and production deployments. Covers actor system setup, clustering, persistence, Akka.Management integration, and Aspire orchestration patterns.

akka-net-best-practices

Critical Akka.NET best practices including EventStream vs DistributedPubSub, supervision strategies, error handling, Props vs DependencyResolver, work distribution patterns, and cluster/local mode abstractions for testability.

akka-net-management

Akka.Management for cluster bootstrapping, service discovery (Kubernetes, Azure, Config), health checks, and dynamic cluster formation without static seed nodes.

akka-net-testing-patterns

Write unit and integration tests for Akka.NET actors using modern Akka.Hosting.TestKit patterns. Covers dependency injection, TestProbes, persistence testing, and actor interaction verification. Includes guidance on when to use traditional TestKit.

api-design

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

aspire-configuration

Configure Aspire AppHost to emit explicit app config via environment variables; keep app code free of Aspire clients and service discovery.

aspire-integration-testing

Write integration tests using .NET Aspire

aspire-service-defaults

Create a shared ServiceDefaults project for Aspire applications. Centralizes OpenTelemetry, health checks, resilience, and service discovery configuration across all services.

crap-analysis

Analyze code coverage and CRAP (Change Risk Anti-Patterns) scores to identify high-risk code. Use OpenCover format with ReportGenerator for Risk Hotspots showing cyclomatic complexity and untested code paths.

csharp-concurrency-patterns

Choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoid locks and manual synchronization unless absolutely necessary.

database-performance

Database access patterns for performance. Separate read/write models, avoid N+1 queries, use AsNoTracking, apply row limits, and never do application-side joins. Works with EF Core and Dapper.

dependency-injection-patterns

Organize DI registrations using IServiceCollection extension methods. Group related services into composable Add* methods for clean Program.cs and reusable configuration in tests.

dotnet-devcert-trust

Diagnose and fix .NET HTTPS dev certificate trust issues on Linux. Covers the full certificate lifecycle from generation to system CA bundle inclusion, with distro-specific guidance for Ubuntu, Fedora, Arch, and WSL2.

dotnet-local-tools

Managing local .NET tools with dotnet-tools.json for consistent tooling across development environments and CI/CD pipelines.

dotnet-project-structure

Modern .NET project structure including .slnx solution format, Directory.Build.props, central package management, SourceLink, version management with RELEASE_NOTES.md, and SDK pinning with global.json.

dotnet-slopwatch

Use Slopwatch to detect LLM reward hacking in .NET code changes. Run after every code modification to catch disabled tests, suppressed warnings, empty catch blocks, and other shortcuts that mask real problems.

efcore-patterns

Entity Framework Core best practices including NoTracking by default, query splitting for navigation collections, migration management, dedicated migration services, and common pitfalls to avoid.

ilspy-decompile

Understand implementation details of .NET code by decompiling assemblies. Use when you want to see how a .NET API works internally, inspect NuGet package source, view framework implementation, or understand compiled .NET binaries.

mailpit-integration

Test email sending locally using Mailpit with .NET Aspire. Captures all outgoing emails without sending them. View rendered HTML, inspect headers, and verify delivery in integration tests.

marketplace-publishing

Workflow for publishing skills and agents to the dotnet-skills Claude Code marketplace. Covers adding new content, updating plugin.json, validation, and release tagging.

関連スキル