Example of running and using virtual actors in the .NET SDK

Try out .NET Dapr virtual actors with this example

The Dapr actor package allows you to interact with Dapr virtual actors from a .NET application.

Prerequisites

Overview

This document describes how to create an Actor (MyActor) and invoke its methods on the client application.

  1. MyActor --- MyActor.Interfaces
  2. |
  3. +- MyActorService
  4. |
  5. +- MyActorClient
  • The interface project(\MyActor\MyActor.Interfaces). This project contains the interface definition for the actor. Actor interfaces can be defined in any project with any name. The interface defines the actor contract that is shared by the actor implementation and the clients calling the actor. Because client projects may depend on it, it typically makes sense to define it in an assembly that is separate from the actor implementation.

  • The actor service project(\MyActor\MyActorService). This project implements ASP.Net Core web service that is going to host the actor. It contains the implementation of the actor, MyActor.cs. An actor implementation is a class that derives from the base type Actor and implements the interfaces defined in the MyActor.Interfaces project. An actor class must also implement a constructor that accepts an ActorService instance and an ActorId and passes them to the base Actor class.

  • The actor client project(\MyActor\MyActorClient) This project contains the implementation of the actor client which calls MyActor’s method defined in Actor Interfaces.

Step 0: Prepare

Since we’ll be creating 3 projects, choose an empty directory to start from, and open it in your terminal of choice.

Step 1: Create actor interfaces

Actor interface defines the actor contract that is shared by the actor implementation and the clients calling the actor.

Actor interface is defined with the below requirements:

  • Actor interface must inherit Dapr.Actors.IActor interface
  • The return type of Actor method must be Task or Task<object>
  • Actor method can have one argument at a maximum

Create interface project and add dependencies

  1. # Create Actor Interfaces
  2. dotnet new classlib -o MyActor.Interfaces
  3. cd MyActor.Interfaces
  4. # Add Dapr.Actors nuget package. Please use the latest package version from nuget.org
  5. dotnet add package Dapr.Actors -v 1.0.0
  6. cd ..

Implement IMyActor interface

Define IMyActor interface and MyData data object. Paste the following code into MyActor.cs in the MyActor.Interfaces project.

  1. using Dapr.Actors;
  2. using System.Threading.Tasks;
  3. namespace MyActor.Interfaces
  4. {
  5. public interface IMyActor : IActor
  6. {
  7. Task<string> SetDataAsync(MyData data);
  8. Task<MyData> GetDataAsync();
  9. Task RegisterReminder();
  10. Task UnregisterReminder();
  11. Task RegisterTimer();
  12. Task UnregisterTimer();
  13. }
  14. public class MyData
  15. {
  16. public string PropertyA { get; set; }
  17. public string PropertyB { get; set; }
  18. public override string ToString()
  19. {
  20. var propAValue = this.PropertyA == null ? "null" : this.PropertyA;
  21. var propBValue = this.PropertyB == null ? "null" : this.PropertyB;
  22. return $"PropertyA: {propAValue}, PropertyB: {propBValue}";
  23. }
  24. }
  25. }

Step 2: Create actor service

Dapr uses ASP.NET web service to host Actor service. This section will implement IMyActor actor interface and register Actor to Dapr Runtime.

Create actor service project and add dependencies

  1. # Create ASP.Net Web service to host Dapr actor
  2. dotnet new web -o MyActorService
  3. cd MyActorService
  4. # Add Dapr.Actors.AspNetCore nuget package. Please use the latest package version from nuget.org
  5. dotnet add package Dapr.Actors.AspNetCore -v 1.0.0
  6. # Add Actor Interface reference
  7. dotnet add reference ../MyActor.Interfaces/MyActor.Interfaces.csproj
  8. cd ..

Add actor implementation

Implement IMyActor interface and derive from Dapr.Actors.Actor class. Following example shows how to use Actor Reminders as well. For Actors to use Reminders, it must derive from IRemindable. If you don’t intend to use Reminder feature, you can skip implementing IRemindable and reminder specific methods which are shown in the code below.

Paste the following code into MyActor.cs in the MyActorService project:

  1. using Dapr.Actors;
  2. using Dapr.Actors.Runtime;
  3. using MyActor.Interfaces;
  4. using System;
  5. using System.Threading.Tasks;
  6. namespace MyActorService
  7. {
  8. internal class MyActor : Actor, IMyActor, IRemindable
  9. {
  10. // The constructor must accept ActorHost as a parameter, and can also accept additional
  11. // parameters that will be retrieved from the dependency injection container
  12. //
  13. /// <summary>
  14. /// Initializes a new instance of MyActor
  15. /// </summary>
  16. /// <param name="host">The Dapr.Actors.Runtime.ActorHost that will host this actor instance.</param>
  17. public MyActor(ActorHost host)
  18. : base(host)
  19. {
  20. }
  21. /// <summary>
  22. /// This method is called whenever an actor is activated.
  23. /// An actor is activated the first time any of its methods are invoked.
  24. /// </summary>
  25. protected override Task OnActivateAsync()
  26. {
  27. // Provides opportunity to perform some optional setup.
  28. Console.WriteLine($"Activating actor id: {this.Id}");
  29. return Task.CompletedTask;
  30. }
  31. /// <summary>
  32. /// This method is called whenever an actor is deactivated after a period of inactivity.
  33. /// </summary>
  34. protected override Task OnDeactivateAsync()
  35. {
  36. // Provides Opporunity to perform optional cleanup.
  37. Console.WriteLine($"Deactivating actor id: {this.Id}");
  38. return Task.CompletedTask;
  39. }
  40. /// <summary>
  41. /// Set MyData into actor's private state store
  42. /// </summary>
  43. /// <param name="data">the user-defined MyData which will be stored into state store as "my_data" state</param>
  44. public async Task<string> SetDataAsync(MyData data)
  45. {
  46. // Data is saved to configured state store implicitly after each method execution by Actor's runtime.
  47. // Data can also be saved explicitly by calling this.StateManager.SaveStateAsync();
  48. // State to be saved must be DataContract serializable.
  49. await this.StateManager.SetStateAsync<MyData>(
  50. "my_data", // state name
  51. data); // data saved for the named state "my_data"
  52. return "Success";
  53. }
  54. /// <summary>
  55. /// Get MyData from actor's private state store
  56. /// </summary>
  57. /// <return>the user-defined MyData which is stored into state store as "my_data" state</return>
  58. public Task<MyData> GetDataAsync()
  59. {
  60. // Gets state from the state store.
  61. return this.StateManager.GetStateAsync<MyData>("my_data");
  62. }
  63. /// <summary>
  64. /// Register MyReminder reminder with the actor
  65. /// </summary>
  66. public async Task RegisterReminder()
  67. {
  68. await this.RegisterReminderAsync(
  69. "MyReminder", // The name of the reminder
  70. null, // User state passed to IRemindable.ReceiveReminderAsync()
  71. TimeSpan.FromSeconds(5), // Time to delay before invoking the reminder for the first time
  72. TimeSpan.FromSeconds(5)); // Time interval between reminder invocations after the first invocation
  73. }
  74. /// <summary>
  75. /// Unregister MyReminder reminder with the actor
  76. /// </summary>
  77. public Task UnregisterReminder()
  78. {
  79. Console.WriteLine("Unregistering MyReminder...");
  80. return this.UnregisterReminderAsync("MyReminder");
  81. }
  82. // <summary>
  83. // Implement IRemindeable.ReceiveReminderAsync() which is call back invoked when an actor reminder is triggered.
  84. // </summary>
  85. public Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period)
  86. {
  87. Console.WriteLine("ReceiveReminderAsync is called!");
  88. return Task.CompletedTask;
  89. }
  90. /// <summary>
  91. /// Register MyTimer timer with the actor
  92. /// </summary>
  93. public Task RegisterTimer()
  94. {
  95. return this.RegisterTimerAsync(
  96. "MyTimer", // The name of the timer
  97. nameof(this.OnTimerCallBack), // Timer callback
  98. null, // User state passed to OnTimerCallback()
  99. TimeSpan.FromSeconds(5), // Time to delay before the async callback is first invoked
  100. TimeSpan.FromSeconds(5)); // Time interval between invocations of the async callback
  101. }
  102. /// <summary>
  103. /// Unregister MyTimer timer with the actor
  104. /// </summary>
  105. public Task UnregisterTimer()
  106. {
  107. Console.WriteLine("Unregistering MyTimer...");
  108. return this.UnregisterTimerAsync("MyTimer");
  109. }
  110. /// <summary>
  111. /// Timer callback once timer is expired
  112. /// </summary>
  113. private Task OnTimerCallBack(byte[] data)
  114. {
  115. Console.WriteLine("OnTimerCallBack is called!");
  116. return Task.CompletedTask;
  117. }
  118. }
  119. }

Register actor runtime with ASP.NET Core startup

The Actor runtime is configured through ASP.NET Core Startup.cs.

The runtime uses the ASP.NET Core dependency injection system to register actor types and essential services. This integration is provided through the AddActors(...) method call in ConfigureServices(...). Use the delegate passed to AddActors(...) to register actor types and configure actor runtime settings. You can register additional types for dependency injection inside ConfigureServices(...). These will be available to be injected into the constructors of your Actor types.

Actors are implemented via HTTP calls with the Dapr runtime. This functionality is part of the application’s HTTP processing pipeline and is registered inside UseEndpoints(...) inside Configure(...).

Paste the following code into Startup.cs in the MyActorService project:

  1. using Microsoft.AspNetCore.Builder;
  2. using Microsoft.AspNetCore.Hosting;
  3. using Microsoft.Extensions.DependencyInjection;
  4. using Microsoft.Extensions.Hosting;
  5. namespace MyActorService
  6. {
  7. public class Startup
  8. {
  9. public void ConfigureServices(IServiceCollection services)
  10. {
  11. services.AddActors(options =>
  12. {
  13. // Register actor types and configure actor settings
  14. options.Actors.RegisterActor<MyActor>();
  15. });
  16. }
  17. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  18. {
  19. if (env.IsDevelopment())
  20. {
  21. app.UseDeveloperExceptionPage();
  22. }
  23. else
  24. {
  25. // By default, ASP.Net Core uses port 5000 for HTTP. The HTTP
  26. // redirection will interfere with the Dapr runtime. You can
  27. // move this out of the else block if you use port 5001 in this
  28. // example, and developer tooling (such as the VSCode extension).
  29. app.UseHttpsRedirection();
  30. }
  31. app.UseRouting();
  32. app.UseEndpoints(endpoints =>
  33. {
  34. // Register actors handlers that interface with the Dapr runtime.
  35. endpoints.MapActorsHandlers();
  36. });
  37. }
  38. }
  39. }

Step 3: Add a client

Create a simple console app to call the actor service. Dapr SDK provides Actor Proxy client to invoke actor methods defined in Actor Interface.

Create actor client project and add dependencies

  1. # Create Actor's Client
  2. dotnet new console -o MyActorClient
  3. cd MyActorClient
  4. # Add Dapr.Actors nuget package. Please use the latest package version from nuget.org
  5. dotnet add package Dapr.Actors -v 1.0.0
  6. # Add Actor Interface reference
  7. dotnet add reference ../MyActor.Interfaces/MyActor.Interfaces.csproj
  8. cd ..

Invoke actor methods with strongly-typed client

You can use ActorProxy.Create<IMyActor>(..) to create a strongly-typed client and invoke methods on the actor.

Paste the following code into Program.cs in the MyActorClient project:

  1. using System;
  2. using System.Threading.Tasks;
  3. using Dapr.Actors;
  4. using Dapr.Actors.Client;
  5. using MyActor.Interfaces;
  6. namespace MyActorClient
  7. {
  8. class Program
  9. {
  10. static async Task MainAsync(string[] args)
  11. {
  12. Console.WriteLine("Startup up...");
  13. // Registered Actor Type in Actor Service
  14. var actorType = "MyActor";
  15. // An ActorId uniquely identifies an actor instance
  16. // If the actor matching this id does not exist, it will be created
  17. var actorId = new ActorId("1");
  18. // Create the local proxy by using the same interface that the service implements.
  19. //
  20. // You need to provide the type and id so the actor can be located.
  21. var proxy = ActorProxy.Create<IMyActor>(actorId, actorType);
  22. // Now you can use the actor interface to call the actor's methods.
  23. Console.WriteLine($"Calling SetDataAsync on {actorType}:{actorId}...");
  24. var response = await proxy.SetDataAsync(new MyData()
  25. {
  26. PropertyA = "ValueA",
  27. PropertyB = "ValueB",
  28. });
  29. Console.WriteLine($"Got response: {response}");
  30. Console.WriteLine($"Calling GetDataAsync on {actorType}:{actorId}...");
  31. var savedData = await proxy.GetDataAsync();
  32. Console.WriteLine($"Got response: {response}");
  33. }
  34. }
  35. }

Running the code

The projects that you’ve created can now to test the sample.

  1. Run MyActorService

    Since MyActorService is hosting actors, it needs to be run with the Dapr CLI.

    1. cd MyActorService
    2. dapr run --app-id myapp --app-port 5000 --dapr-http-port 3500 -- dotnet run

    You will see commandline output from both daprd and MyActorService in this terminal. You should see something like the following, which indicates that the application started successfully.

    1. ...
    2. ℹ️ Updating metadata for app command: dotnet run
    3. You're up and running! Both Dapr and your app logs will appear here.
    4. == APP == info: Microsoft.Hosting.Lifetime[0]
    5. == APP == Now listening on: https://localhost:5001
    6. == APP == info: Microsoft.Hosting.Lifetime[0]
    7. == APP == Now listening on: http://localhost:5000
    8. == APP == info: Microsoft.Hosting.Lifetime[0]
    9. == APP == Application started. Press Ctrl+C to shut down.
    10. == APP == info: Microsoft.Hosting.Lifetime[0]
    11. == APP == Hosting environment: Development
    12. == APP == info: Microsoft.Hosting.Lifetime[0]
    13. == APP == Content root path: /Users/ryan/actortest/MyActorService
  2. Run MyActorClient

    MyActorClient is acting as the client, and it can be run normally with dotnet run.

    Open a new terminal an navigate to the MyActorClient directory. Then run the project with:

    1. dotnet run

    You should see commandline output like:

    1. Startup up...
    2. Calling SetDataAsync on MyActor:1...
    3. Got response: Success
    4. Calling GetDataAsync on MyActor:1...
    5. Got response: Success

💡 This sample relies on a few assumptions. The default listening port for an ASP.NET Core web project is 5000, which is being passed to dapr run as --app-port 5000. The default HTTP port for the Dapr sidecar is 3500. We’re telling the sidecar for MyActorService to use 3500 so that MyActorClient can rely on the default value.

Now you have successfully created an actor service and client. See the related links section to learn more.