Skip to content

.NET Actor Public Interface

A relocation including an Actor bound to a session keeps the same ObjectGeneration and updates the bound-session current Actor location snapshot to the target MeshName/NodeRid. Since relocation itself isn't a physical/logical disconnect, it doesn't run the Actor disconnect callback.

.NET per-language interface table of contents

1. Actor

ActorId is a logical ID unique across the whole Location Store transaction domain. Its UTF-8 encoded size is 1..255 bytes, it's compared as a case-sensitive value comparison, and it isn't normalized. A regular Actor message only takes ActorId and resolves current authority. ActorRef is the immutable location snapshot used to change an specified incarnation or bind to a session.

public readonly record struct ActorRef(
 string ActorId,
 ulong ObjectGeneration,
 string MeshName,
 RoutingId NodeRid);

public interface IZLinkActor
{
 IZLinkActorContext Context { get; }
 void Configure() { }
 ValueTask OnJoinCompletedAsync(
 ZLinkActorJoinCompletion completion,
 CancellationToken cancellationToken)
 {
 return ValueTask.CompletedTask;
 }
}

public interface IZLinkActorContext
{
 string ActorId { get; }
 ulong ObjectGeneration { get; }
 string MeshName { get; }
 string? SpotId { get; }
 IZLinkBoundSession BoundSession { get; }
 IZLinkActorJoinSpotCall JoinSpot(string spotId);
 IZLinkActorJoinSpotCall JoinSpot(
 string spotId,
 ZLinkMessage request);
 IZLinkActorJoinSpotCall JoinSpot<TRequest>(
 string spotId,
 TRequest request)
 {
 return JoinSpot(spotId, ZLinkMessage.From(request));
 }
 IZLinkActorJoinEntrySpotCall JoinEntrySpot();
 IZLinkActorJoinEntrySpotCall JoinEntrySpot(
 ZLinkMessage request);
 IZLinkActorJoinEntrySpotCall JoinEntrySpot<TRequest>(
 TRequest request)
 {
 return JoinEntrySpot(ZLinkMessage.From(request));
 }
}

public interface IZLinkActorHandlerRegistry
{
 void AddHandler<THandler>()
 where THandler : class;
 void AddHandler<THandler>(string packetName)
 where THandler : class;
 void AddActorPacket<THandler, TActor>()
 where THandler : class
 where TActor : IZLinkActor;
 void AddActorPacket<THandler, TActor>(string packetName)
 where THandler : class
 where TActor : IZLinkActor;
}

public interface IZLinkActorFactory
{
 ValueTask<IZLinkActor> CreateAsync(
 IZLinkActorContext context,
 CancellationToken cancellationToken = default);
}

public interface IZLinkActorFactory<TActor>
 : IZLinkActorFactory
 where TActor : class, IZLinkActor
{
 new ValueTask<TActor> CreateAsync(
 IZLinkActorContext context,
 CancellationToken cancellationToken = default);
}

public interface IZLinkActorRelocationAdapter<TActor>
 where TActor : class, IZLinkActor
{
 ValueTask<byte[]> CaptureAsync(
 TActor actor,
 CancellationToken cancellationToken);
 ValueTask RestoreAsync(
 TActor actor,
 ReadOnlyMemory<byte> payload,
 CancellationToken cancellationToken);
}

public interface IZLinkActorClient
{
 IZLinkActorSendCall SendToActor<TMessage>(
 string actorId,
 TMessage message);
 IZLinkActorRequestCall RequestToActor<TRequest>(
 string actorId,
 TRequest request);
}

public interface IZLinkActorManager
{
 IZLinkActorCreateCall Create(
 string actorId,
 string actorType);
 IZLinkActorGetOrCreateCall GetOrCreate(
 string actorId,
 string actorType);
 ValueTask<ActorRef?> FindAsync(
 string actorId,
 CancellationToken cancellationToken = default);
 ValueTask<SpotRef?> FindSpotAsync(
 string actorId,
 CancellationToken cancellationToken = default);
 ValueTask<bool> DestroyAsync(
 ActorRef actor,
 CancellationToken cancellationToken = default);
}

public abstract record ZLinkActorCreateResult
{
 private protected ZLinkActorCreateResult() { }

 public sealed record Existing(ActorRef Actor)
 : ZLinkActorCreateResult;

 public sealed record Created(
 ActorRef Actor,
 ZLinkMessage? Reply)
 : ZLinkActorCreateResult;

 public sealed record Rejected(ZLinkMessage? Reply)
 : ZLinkActorCreateResult;
}

public interface IZLinkActorCreateCall
{
 IZLinkActorCreateCall InMesh(string meshName);
 IZLinkActorCreateCall Request(ZLinkMessage request);
 IZLinkActorCreateCall Request<TRequest>(TRequest request);
 IZLinkActorCreateCall Timeout(TimeSpan timeout);
 ValueTask<ZLinkActorCreateResult> Async(
 CancellationToken cancellationToken = default);
 ValueTask<ZLinkActorCreateResult> Yield(
 CancellationToken cancellationToken = default);
}

public interface IZLinkActorGetOrCreateCall
{
 IZLinkActorGetOrCreateCall InMesh(string meshName);
 IZLinkActorGetOrCreateCall Request(ZLinkMessage request);
 IZLinkActorGetOrCreateCall Request<TRequest>(TRequest request);
 IZLinkActorGetOrCreateCall Timeout(TimeSpan timeout);
 ValueTask<ZLinkActorCreateResult> Async(
 CancellationToken cancellationToken = default);
 ValueTask<ZLinkActorCreateResult> Yield(
 CancellationToken cancellationToken = default);
}

public interface IZLinkActorSendCall : IZLinkMetadataCall<IZLinkActorSendCall>
{
 ValueTask Async();
}

public interface IZLinkActorRequestCall : IZLinkMetadataCall<IZLinkActorRequestCall>
{
 IZLinkActorRequestCall Timeout(TimeSpan timeout);
 ValueTask<TReply> Async<TReply>(
 CancellationToken cancellationToken = default);
 ValueTask<TReply> Yield<TReply>(
 CancellationToken cancellationToken = default);
}

public interface IZLinkActorDeferredJoinCall
{
 void Defer();
}

public interface IZLinkActorJoinSpotCall : IZLinkActorDeferredJoinCall
{
 IZLinkActorJoinSpotCall Timeout(TimeSpan timeout);
}

public interface IZLinkActorJoinEntrySpotCall : IZLinkActorDeferredJoinCall
{
 IZLinkActorJoinEntrySpotCall Timeout(TimeSpan timeout);
}

public readonly record struct ZLinkActorJoinOperationId(ulong High, ulong Low);

public abstract record ZLinkActorJoinCompletion
{
 private protected ZLinkActorJoinCompletion() { }
 public sealed record Accepted(
 ZLinkActorJoinOperationId OperationId,
 ActorRef Actor,
 ZLinkMessage? Reply) : ZLinkActorJoinCompletion;
 public sealed record Rejected(
 ZLinkActorJoinOperationId OperationId,
 ZLinkMessage? Reply) : ZLinkActorJoinCompletion;
 public sealed record Failed(
 ZLinkActorJoinOperationId OperationId,
 ZLinkFrameworkErrorKind Kind) : ZLinkActorJoinCompletion;
}

The Actor packet handler is registered on the registry the Spot owns. The handler's context and generic parameter are defined by the Spot Interface. SpotId == null means the Entry Spot stage, and a value present means it's a member of that user Spot. A separate boolean representing the same state isn't provided.

The Actor Join call provides only a resultless synchronous Defer(), without Async(...) or Yield(...). Handler turn and execution gate defines gate and claim handling when an Actor request yields.

Handler turn and execution gate defines Defer() registration and barrier activation. .NET delivers the result through OnJoinCompletedAsync(...).

Actor Join completion owns the Operation ID purpose and lifetime.

The overload with no request fixes an empty ZLinkMessage. The default timeout is 5 seconds, and an explicit value is a finite 1..int.MaxValue ms rounded up to milliseconds. Defer() fixes a monotonic absolute deadline.

PreserveStateWith<TAdapter>() uses IZLinkActorRelocationAdapter<TActor>; Location runtime defines Actor relocation policy and payload transfer.

Location runtime defines which Actor moves invoke the adapter.

Location runtime defines queue cutover and Ready admission.

CaptureAsync(...) and RestoreAsync(...) use ReadOnlyMemory<byte> and byte[] in .NET; Location runtime and Failure and failover policy define retry, failure, and completion authority.

Host relocation defines the pre-relocation drain and deadline result.

The order of lifecycle callbacks run during Entry Spot maintenance and a regular join, sealed retry after a callback failure, and callback omission for a whole User Spot aggregate move are determined by the Spot Interface. The Actor relocation adapter doesn't substitute for this lifecycle callback. There's no public phase API that controls this order.

Actor model defines creation reservation and the Ready barrier.

Actor factory options and relocation policy are registered together in the AddActorFactory<TActor,TFactory>(...) configure callback of Topology Configuration. The callback must select exactly one policy.

The Actor Model §6.2 owns the single-use rules, duplicate-option handling, and terminal re-invocation errors of Create and GetOrCreate calls.

Actor model defines the create deadline and Mesh selection. .NET maps the selection failures to NotConfigured, InvalidOperation, and NotFound.

Actor model, Object lifecycle, and Framework API define Create/GetOrCreate outcomes and terminal replay. .NET projects AlreadyExists, TypeMismatch, Existing, and DeadlineExceeded.

The creation request and semantic terminal envelope are each at most 1 MiB. The creation request records an immutable reference and hash before reservation. The factory must be retry-safe for the same ID, ObjectGeneration, and creation attempt.

FindAsync(actorId) only returns the current Ready ActorRef. FindSpotAsync(actorId) only returns the SpotRef of the current User Spot membership. A separate Actor directory and public handle/resolver aren't provided. DestroyAsync(actorRef) only closes the incarnation. If that incarnation doesn't exist, false; if the generation differs, InvalidOperation; if in pre-commit seal, Unavailable — it doesn't find the current ref and hidden-retry.

ActorRef.ObjectGeneration is 1..long.MaxValue. MeshName and NodeRid are the route snapshot at query time, and aren't included in logical identity. Even after relocation, ActorId and ObjectGeneration are kept, and a ref with the new location is issued. Regular messaging doesn't fix the ref route.