Akka.Hosting Actor Patterns
Wann Diese Fähigkeit Verwenden
Verwenden Sie diese Fähigkeit, wenn:
- Sie Entity-Aktoren erstellen, die Domänenobjekte repräsentieren (Benutzer, Bestellungen, Rechnungen usw.)
- Sie Aktoren benötigen, die sowohl in Unit-Tests (kein Clustering) als auch in der Produktion (Cluster-Sharding) funktionieren
- Sie geplante Aufgaben mit Akka-Reminders einrichten
- Sie Aktoren mit Akka.Hosting-Erweiterungsmethoden registrieren
- Sie wiederverwendbare Aktorkonfigurationsmuster erstellen
Kernprinzipien
- Ausführungsmodus-Abstraktion - Derselbe Aktorcode läuft lokal (Tests) oder geclustert (Produktion)
- GenericChildPerEntityParent für Lokal - Bildet Sharding-Semantik ohne Cluster-Overhead nach
- Message-Extractoren für Routing - Wiederverwendung des
IMessageExtractor-Interfaces von Akka.Cluster.Sharding - Akka.Hosting-Erweiterungsmethoden - Fließende Konfiguration, die sich gut zusammensetzen lässt
- ITimeProvider für Testbarkeit - Verwenden Sie
ActorSystem.Scheduleranstelle vonDateTime.Now
Ausführungsmodi
Definieren Sie einen Enum, um das Aktorverhalten zu steuern:
/// <summary>
/// Bestimmt, wie Akka.NET konfiguriert werden soll
/// </summary>
public enum AkkaExecutionMode
{
/// <summary>
/// Reines lokales Actor-System - kein Remoting, kein Clustering.
/// Verwendet GenericChildPerEntityParent anstelle von ShardRegion.
/// Ideal für Unit-Tests und einfache Szenarien.
/// </summary>
LocalTest,
/// <summary>
/// Vollständiges Clustering mit ShardRegion.
/// Verwendung für Integrationstests und Produktion.
/// </summary>
Clustered
}
GenericChildPerEntityParent
Ein leichtgewichtiger Eltern-Aktor, der Nachrichten an Kind-Entities weiterleitet und die Cluster-Sharding-Semantik nachbildet, ohne dass ein Cluster erforderlich ist:
using Akka.Actor;
using Akka.Cluster.Sharding;
/// <summary>
/// Ein generischer "Kind pro Entity"-Eltern-Aktor.
/// </summary>
/// <remarks>
/// Verwendet `IMessageExtractor` von Akka.Cluster.Sharding für konsistentes Routing.
/// Ideal für Unit-Tests, bei denen der Cluster-Overhead unnötig ist.
/// </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;
// Vorhandenes Kind abrufen oder neues erstellen
Context.Child(entityId)
.GetOrElse(() => Context.ActorOf(_propsFactory(entityId), entityId))
.Forward(_extractor.EntityMessage(message));
});
}
}
Message-Extractoren
Erstellen Sie Extractoren, die IMessageExtractor von Akka.Cluster.Sharding implementieren:
using Akka.Cluster.Sharding;
/// <summary>
/// Leitet Nachrichten basierend auf einer stark typisierten ID an Entity-Aktoren weiter.
/// </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
};
}
}
// Definieren Sie ein Interface für Nachrichten, die auf eine bestimmte Entity abzielen
public interface IWithOrderId
{
OrderId OrderId { get; }
}
// Verwenden Sie stark typisierte IDs
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString();
}
Akka.Hosting-Erweiterungsmethoden
Erstellen Sie Erweiterungsmethoden, die den Ausführungsmodus abstrahieren:
using Akka.Cluster.Hosting;
using Akka.Cluster.Sharding;
using Akka.Hosting;
public static class OrderActorHostingExtensions
{
/// <summary>
/// Fügt OrderActor mit Unterstützung für lokale und geclusterte Modi hinzu.
/// </summary>
public static AkkaConfigurationBuilder WithOrderActor(
this AkkaConfigurationBuilder builder,
AkkaExecutionMode executionMode = AkkaExecutionMode.Clustered,
string? clusterRole = null)
{
if (executionMode == AkkaExecutionMode.LocalTest)
{
// Nicht geclusterter Modus: GenericChildPerEntityParent verwenden
builder.WithActors((system, registry, resolver) =>
{
var parent = system.ActorOf(
GenericChildPerEntityParent.CreateProps(
new OrderMessageExtractor(),
entityId => resolver.Props<OrderActor>(entityId)),
"orders");
registry.Register<OrderActor>(parent);
});
}
else
{
// Geclusterter Modus: ShardRegion verwenden
builder.WithShardRegion<OrderActor>(
"orders",
(system, registry, resolver) =>
entityId => resolver.Props<OrderActor>(entityId),
new OrderMessageExtractor(),
new ShardOptions
{
StateStoreMode = StateStoreMode.DData,
Role = clusterRole
});
}
return builder;
}
}
Zusammensetzen Mehrere Aktoren
Erstellen Sie eine praktische Methode, die alle Domänenaktoren registriert:
public static class DomainActorHostingExtensions
{
/// <summary>
/// Fügt alle Bestelldomänen-Aktoren mit Sharding-Unterstützung hinzu.
/// </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(); // Singleton, kein Sharding erforderlich
}
}
Verwenden von ITimeProvider für die Planung
Registrieren Sie den Scheduler des ActorSystems als ITimeProvider für testbare zeitbasierte Logik:
public static class SharedAkkaHostingExtensions
{
public static IServiceCollection AddAkkaWithTimeProvider(
this IServiceCollection services,
Action<AkkaConfigurationBuilder, IServiceProvider> configure)
{
// ITimeProvider mit dem Scheduler des ActorSystems registrieren
services.AddSingleton<ITimeProvider>(sp =>
sp.GetRequiredService<ActorSystem>().Scheduler);
return services.ConfigureAkka((builder, sp) =>
{
configure(builder, sp);
});
}
}
// In Ihrem Aktor ITimeProvider injizieren
public class SubscriptionActor : ReceiveActor
{
private readonly ITimeProvider _timeProvider;
public SubscriptionActor(ITimeProvider timeProvider)
{
_timeProvider = timeProvider;
// Verwenden Sie _timeProvider.GetUtcNow() anstelle von DateTime.UtcNow
// Dies ermöglicht es Tests, die Zeit zu steuern
}
}
Akka.Reminders-Integration
Für dauerhafte geplante Aufgaben, die Neustarts überstehen, verwenden Sie Akka-Reminders:
using Akka.Reminders;
using Akka.Reminders.Sql;
using Akka.Reminders.Sql.Configuration;
using Akka.Reminders.Storage;
public static class ReminderHostingExtensions
{
/// <summary>
/// Konfiguriert Akka-Reminders mit PostgreSQL-Speicher.
/// </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>
/// Konfiguriert Akka-Reminders mit In-Memory-Speicher für Tests.
/// </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)
}));
}
}
Benutzerdefinierter Reminder-Resolver für Child-Per-Entity
Leiten Sie Reminder-Callbacks an GenericChildPerEntityParent-Aktoren weiter:
using Akka.Actor;
using Akka.Hosting;
using Akka.Reminders;
/// <summary>
/// Löst Reminder-Ziele zu GenericChildPerEntityParent-Aktoren auf.
/// </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(
$"Unbekanntes Reminder-Schlüsselformat: {entry.Key}")
};
}
}
Singleton-Aktoren (Nicht Sharded)
Für Aktoren, die nur eine Instanz haben sollten:
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);
});
}
Marker-Typen für Registry
Wenn Sie Aktoren referenzieren müssen, die als Eltern registriert sind:
/// <summary>
/// Marker-Typ für ActorRegistry, um den Bestell-Manager abzurufen
/// (GenericChildPerEntityParent für OrderActors).
/// </summary>
public sealed class OrderManagerActor;
// Verwendung in der Erweiterungsmethode
registry.Register<OrderManagerActor>(parent);
// Verwendung in Controller/Service
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-Bereichsverwaltung in Aktoren
Aktoren haben keine automatischen DI-Bereiche. Im Gegensatz zu ASP.NET-Controllern (bei denen jede HTTP-Anfrage einen Bereich erstellt) sind Aktoren langlebig. Wenn Sie bereichsbezogene Dienste (wie DbContext) benötigen, injizieren Sie IServiceProvider und erstellen Sie Bereiche manuell.
Muster: Bereich Pro Nachricht
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)
{
// Bereich für diese Nachricht erstellen – nach der Verarbeitung freigegeben
using var scope = _serviceProvider.CreateScope();
// Bereichsbezogene Dienste innerhalb des Bereichs auflösen
var orderRepository = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
var paymentService = scope.ServiceProvider.GetRequiredService<IPaymentService>();
var emailComposer = scope.ServiceProvider.GetRequiredService<IOrderEmailComposer>();
// Arbeit mit bereichsbezogenen Diensten erledigen
var order = await orderRepository.GetByIdAsync(msg.OrderId);
var payment = await paymentService.ProcessAsync(order);
// DbContext-Änderungen werden beim Freigeben des Bereichs übernommen
}
}
Warum Dieses Muster
| Vorteil | Erklärung |
|---|---|
| Frischer DbContext pro Nachricht | Keine veraltete Entity-Nachverfolgung zwischen Nachrichten |
| Ordnungsgemäße Freigabe | Datenbankverbindungen werden nach jeder Nachricht freigegeben |
| Isolation | Fehler einer Nachricht korrumpieren nicht den Zustand einer anderen |
| Testbar | Mock-IServiceProvider kann in Tests injiziert werden |
Singleton-Dienste - Direkte Injektion
Für zustandslose, threadsichere Dienste direkt injizieren (kein Bereich erforderlich):
public sealed class NotificationActor : ReceiveActor
{
private readonly IEmailLinkGenerator _linkGenerator; // Singleton - OK!
private readonly IMjmlTemplateRenderer _renderer; // Singleton - OK!
public NotificationActor(
IEmailLinkGenerator linkGenerator,
IMjmlTemplateRenderer renderer)
{
_linkGenerator = linkGenerator;
_renderer = renderer;
Receive<SendNotification>(msg =>
{
var link = _linkGenerator.Generate(msg.TrackingId);
var html = _renderer.Render(msg.Template);
// Senden...
});
}
}