Getting Started with Virtual Actors (Grains) in .NET Using Proto.Actor
In the previous article, I discussed setting up a simple actor. This article focuses on Virtual Actors, a concept that builds on the traditional actor model by introducing automated lifecycle management and simplified communication. Virtual Actor The Virtual Actor (or grain) model, pioneered by Microsoft's Orleans framework, abstracts away manual actor lifecycle management. Unlike traditional actors that require explicit creation and reference via a PID, virtual actors are identified by a unique key. The framework automatically creates, activates, or reactivates them as needed. This abstraction simplifies scalability in distributed systems by decoupling actor identity from physical location or state. Key differences from classic actors: Lifecycle Management: The framework (e.g., Orleans or Proto.Actor) handles activation/deactivation. Addressing: Communication uses logical identifiers instead of PIDs. State Persistence: Virtual actors often integrate state management layers for fault tolerance. Requirement .NET 6+ Install these packages Proto.Actor - Core library for actor model implementation Proto.Remote Proto.Cluster - Enables distributed virtual actor clusters via gRPC. Proto.Cluster.CodeGen - Generates grain interfaces from Protobuf definitions. Proto.Cluster.TestProvider Grpc.Tools - For gPRC support Microsoft.Extensions.Hosting Define the Virtual Actor (Grain) Proto.Actor uses Protocol Buffers to define actor interfaces. Create a Greeting.proto file: syntax = "proto3"; option csharp_namespace = "VirtualActor"; import "google/protobuf/empty.proto"; message SayHelloRequest { string name = 1; } service GreetingGrain { rpc SayHello(SayHelloRequest) returns (google.protobuf.Empty); } This defines a GreetingGrain service with a SayHello method. The .proto file generates: Request/response classes (e.g., SayHelloRequest). A base class (GreetingGrainBase) for your actor logic. Update your .csproj to enable code generation: None Implement the Actor Create a GreetingActor class that inherits from the generated GreetingGrainBase: public class GreetingActor( IContext context, ClusterIdentity clusterIdentity, ILogger logger ) : GreetingGrainBase(context) { private int _invocationCount = 0; public override Task SayHello(SayHelloRequest request) { logger.LogInformation( "Hello {Name} (Cluster ID: {ClusterId} | Invocation Count: {Count})", request.Name, clusterIdentity.Identity, _invocationCount++ ); return Task.CompletedTask; } } Key details: State Management: _invocationCount tracks method calls (thread-safe due to actor concurrency guarantees). Dependencies: Injected via ActivatorUtilities (e.g., ILogger). Registering the Actor System The cluster configuration defines: Cluster membership via TestProvider (for development). Actor activation rules using PartitionIdentityLookup. Configure the Actor System Set up the actor system, remoting, and clustering: // Create the actor system configuration var actorSystemConfig = Proto.ActorSystemConfig.Setup(); // The remote configuration var remoteConfig = GrpcNetRemoteConfig.BindToLocalhost(); // The cluster configuration var clusterConfig = ClusterConfig .Setup( clusterName: "VirtualActor", clusterProvider: new TestProvider(new TestProviderOptions(), new InMemAgent() ), identityLookup: new PartitionIdentityLookup() ) .WithClusterKind( kind: GreetingGrainActor.Kind, prop: Props.FromProducer(() => new GreetingGrainActor((context, clusterIdentity) => ActivatorUtilities.CreateInstance(provider, context, clusterIdentity))) ); return new ActorSystem(actorSystemConfig) .WithServiceProvider(provider) .WithRemote(remoteConfig) .WithCluster(clusterConfig); Components Explained: TestProvider: Simulates cluster membership (replace with a production provider like Consul in real deployments). PartitionIdentityLookup: Distributes actors evenly across cluster nodes. Manage Cluster Lifecycle Integrate the actor system with .NET’s hosted services: public class ActorSystemClusterHostedService(ActorSystem actorSystem) : IHostedService { public async Task StartAsync(CancellationToken cancellationToken) { await actorSystem.Cluster().StartMemberAsync(); } public async Task StopAsync(CancellationToken cancellationToken) { await actorSystem.Cluster().ShutdownAsync(); } } Register the service in Program.cs: services.AddHostedService(); Interact with Virtual Actors Use the GetGreetingGrain method to reference an actor by ID: var actor = actorSystem.Cluster().GetGreetingGrain(fromName); await actor.SayHello(new SayHelloRequest { Name = toName }, CancellationToke

In the previous article, I discussed setting up a simple actor. This article focuses on Virtual Actors, a concept that builds on the traditional actor model by introducing automated lifecycle management and simplified communication.
Virtual Actor
The Virtual Actor (or grain
) model, pioneered by Microsoft's Orleans framework, abstracts away manual actor lifecycle management. Unlike traditional actors that require explicit creation and reference via a PID, virtual actors are identified by a unique key. The framework automatically creates, activates, or reactivates them as needed. This abstraction simplifies scalability in distributed systems by decoupling actor identity from physical location or state.
Key differences from classic actors:
- Lifecycle Management: The framework (e.g., Orleans or Proto.Actor) handles activation/deactivation.
- Addressing: Communication uses logical identifiers instead of PIDs.
- State Persistence: Virtual actors often integrate state management layers for fault tolerance.
Requirement
- .NET 6+
- Install these packages
- Proto.Actor - Core library for actor model implementation
- Proto.Remote
- Proto.Cluster - Enables distributed virtual actor clusters via gRPC.
- Proto.Cluster.CodeGen - Generates grain interfaces from Protobuf definitions.
- Proto.Cluster.TestProvider
- Grpc.Tools - For gPRC support
- Microsoft.Extensions.Hosting
Define the Virtual Actor (Grain)
Proto.Actor
uses Protocol Buffers to define actor interfaces. Create a Greeting.proto
file:
syntax = "proto3";
option csharp_namespace = "VirtualActor";
import "google/protobuf/empty.proto";
message SayHelloRequest {
string name = 1;
}
service GreetingGrain {
rpc SayHello(SayHelloRequest) returns (google.protobuf.Empty);
}
This defines a GreetingGrain
service with a SayHello
method. The .proto
file generates:
- Request/response classes (e.g.,
SayHelloRequest
). - A base class (
GreetingGrainBase
) for your actor logic.
Update your .csproj
to enable code generation:
Include="Greeting.proto">
None
Include="Greeting.proto" />
Implement the Actor
Create a GreetingActor
class that inherits from the generated GreetingGrainBase
:
public class GreetingActor(
IContext context,
ClusterIdentity clusterIdentity,
ILogger<GreetingActor> logger
) : GreetingGrainBase(context)
{
private int _invocationCount = 0;
public override Task SayHello(SayHelloRequest request)
{
logger.LogInformation(
"Hello {Name} (Cluster ID: {ClusterId} | Invocation Count: {Count})",
request.Name,
clusterIdentity.Identity,
_invocationCount++
);
return Task.CompletedTask;
}
}
Key details:
- State Management: _invocationCount tracks method calls (thread-safe due to actor concurrency guarantees).
-
Dependencies: Injected via
ActivatorUtilities
(e.g.,ILogger
).
Registering the Actor System
The cluster configuration defines:
- Cluster membership via
TestProvider
(for development). - Actor activation rules using
PartitionIdentityLookup
.
Configure the Actor System
Set up the actor system, remoting, and clustering:
// Create the actor system configuration
var actorSystemConfig = Proto.ActorSystemConfig.Setup();
// The remote configuration
var remoteConfig = GrpcNetRemoteConfig.BindToLocalhost();
// The cluster configuration
var clusterConfig = ClusterConfig
.Setup(
clusterName: "VirtualActor",
clusterProvider: new TestProvider(new TestProviderOptions(), new InMemAgent() ),
identityLookup: new PartitionIdentityLookup()
)
.WithClusterKind(
kind: GreetingGrainActor.Kind,
prop: Props.FromProducer(() => new GreetingGrainActor((context, clusterIdentity) => ActivatorUtilities.CreateInstance<GreetingActor>(provider, context, clusterIdentity)))
);
return new ActorSystem(actorSystemConfig)
.WithServiceProvider(provider)
.WithRemote(remoteConfig)
.WithCluster(clusterConfig);
Components Explained:
- TestProvider: Simulates cluster membership (replace with a production provider like Consul in real deployments).
- PartitionIdentityLookup: Distributes actors evenly across cluster nodes.
Manage Cluster Lifecycle
Integrate the actor system with .NET’s hosted services:
public class ActorSystemClusterHostedService(ActorSystem actorSystem) : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
await actorSystem.Cluster().StartMemberAsync();
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await actorSystem.Cluster().ShutdownAsync();
}
}
Register the service in Program.cs
:
services.AddHostedService<ActorSystemClusterHostedService>();
Interact with Virtual Actors
Use the GetGreetingGrain
method to reference an actor by ID:
var actor = actorSystem.Cluster().GetGreetingGrain(fromName);
await actor.SayHello(new SayHelloRequest { Name = toName }, CancellationToken.None);
Example Workflow:
while (true)
{
Console.Write("Your name (or 'q' to quit): ");
var fromName = Console.ReadLine();
if (fromName == "q")
{
break;
}
Console.Write("Recipient name: ");
var toName = Console.ReadLine();
if (toName == "q")
{
break;
}
// Call the virtual actor
await actor.SayHello(new SayHelloRequest { Name = toName });
}
Key Benefits of Virtual Actors
- Simplified Concurrency: Actors process messages sequentially, avoiding race conditions.
- Elastic Scalability: Add/remove nodes without reconfiguring actors.
- Resilience: Automatic reactivation ensures "always-on" behavior.
Conclusion
Virtual Actors (or Grains) revolutionize distributed system development by abstracting complexity while retaining the actor model’s core strengths. With Proto.Actor, .NET developers can:
- Focus on Business Logic: Forget manual actor lifecycle management—let the framework handle activation, scaling, and recovery.
- Build Resilient Systems: Automatic reactivation and state management ensure fault tolerance, even in dynamic environments.
- Scale Effortlessly: Location transparency and elastic clustering make it simple to distribute workloads across nodes.
For production deployments, consider:
- Replacing
TestProvider
with Kubernetes/Azure-based cluster management. - Adding persistent state storage (e.g.,
Redis
,PostgreSQL
). - Implementing monitoring and health checks.
Reference
- Full code repository: [GitHub])(https://github.com/lillo42/proto-actor-sample)
- Proto.Actor Docs: Getting Started With Grains / Virtual Actors (.NET)