mirror of https://github.com/dapr/quickstarts.git
210 lines
8.5 KiB
Markdown
210 lines
8.5 KiB
Markdown
# Dapr Actors (Dapr SDK)
|
|
|
|
Let's take a look at the Dapr [Actors building block](https://docs.dapr.io/developing-applications/building-blocks/actors/actors-overview/). In this Quickstart, you will run a SmartDevice.Service microservice and a simple console client to demonstrate the stateful object patterns in Dapr Actors.
|
|
1. Using a SmartDevice.Service microservice, developers can host two SmartDetectorActor smoke alarm objects, and a third ControllerActor object that command and controls the smart devices.
|
|
2. Using a SmartDevice.Client console app, developers have a client app to interact with each actor, or the controller to perform actions in aggregate.
|
|
3. The SmartDevice.Interfaces contains the shared interfaces and data types used by both service and client apps
|
|
|
|
**Note:** This example leverages the Dapr client SDK.
|
|
|
|
|
|
### Step 1: Pre-requisites
|
|
|
|
For this example, you will need:
|
|
|
|
- [Dapr CLI and initialized environment](https://docs.dapr.io/getting-started).
|
|
- [.NET 8 SDK](https://dotnet.microsoft.com/download).
|
|
- Docker Desktop
|
|
|
|
### Step 2: Set up the environment
|
|
|
|
Clone the [sample provided in the Quickstarts repo](https://github.com/dapr/quickstarts/tree/master/actors).
|
|
|
|
```bash
|
|
git clone https://github.com/dapr/quickstarts.git
|
|
```
|
|
|
|
### Step 3: Run the service app
|
|
|
|
In a new terminal window, navigate to the `actors/csharp/sdk/service` directory and restore dependencies:
|
|
|
|
|
|
Run the `SmartDevice.Service`, which will start service itself and the Dapr sidecar:
|
|
|
|
<!-- STEP
|
|
name: Run actor service
|
|
expected_stdout_lines:
|
|
- "Request finished HTTP/1.1 GET http://127.0.0.1:5001/dapr/config - 200"
|
|
expected_stderr_lines:
|
|
working_dir: .
|
|
output_match_mode: substring
|
|
background: true
|
|
sleep: 30
|
|
-->
|
|
```bash
|
|
cd service
|
|
dapr run --app-id actorservice --app-port 5001 --app-protocol http --dapr-http-port 56001 --resources-path ../../../resources -- dotnet run --urls=http://localhost:5001/
|
|
```
|
|
<!-- END_STEP -->
|
|
|
|
Expected output:
|
|
|
|
```bash
|
|
== APP == info: Microsoft.Hosting.Lifetime[14]
|
|
== APP == Now listening on: http://localhost:5001
|
|
== APP == info: Microsoft.Hosting.Lifetime[0]
|
|
== APP == Application started. Press Ctrl+C to shut down.
|
|
== APP == info: Microsoft.Hosting.Lifetime[0]
|
|
== APP == Hosting environment: Production
|
|
== APP == info: Microsoft.Hosting.Lifetime[0]
|
|
== APP == info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
|
|
== APP == Request starting HTTP/1.1 GET http://127.0.0.1:5001/dapr/config - application/json -
|
|
== APP == warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
|
|
== APP == Failed to determine the https port for redirect.
|
|
== APP == info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
|
|
== APP == Executing endpoint 'Dapr Actors Config'
|
|
== APP == info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
|
|
== APP == Executed endpoint 'Dapr Actors Config'
|
|
== APP == info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
|
|
== APP == Request finished HTTP/1.1 GET http://127.0.0.1:5001/dapr/config - 200 - application/json 30.6175ms
|
|
```
|
|
|
|
### Step 4: Run the client app
|
|
|
|
In a new terminal instance, navigate to the `actors/csharp/sdk/client` directory and install the dependencies:
|
|
|
|
Then run the client app:
|
|
<!-- STEP
|
|
name: Run actor client
|
|
expected_stdout_lines:
|
|
- "Device 2 state: Location: Second Floor, Status: Ready"
|
|
expected_stderr_lines:
|
|
working_dir: .
|
|
output_match_mode: substring
|
|
background: true
|
|
sleep: 60
|
|
-->
|
|
```bash
|
|
cd client
|
|
dapr run --app-id actorclient -- dotnet run
|
|
```
|
|
<!-- END_STEP -->
|
|
|
|
Expected output:
|
|
|
|
```bash
|
|
== APP == Startup up...
|
|
== APP == Calling SetDataAsync on SmokeDetectorActor:1...
|
|
== APP == Got response: Success
|
|
== APP == Calling GetDataAsync on SmokeDetectorActor:1...
|
|
== APP == Device 1 state: Location: First Floor, Status: Ready
|
|
== APP == Calling SetDataAsync on SmokeDetectorActor:2...
|
|
== APP == Got response: Success
|
|
== APP == Calling GetDataAsync on SmokeDetectorActor:2...
|
|
== APP == Device 2 state: Location: Second Floor, Status: Ready
|
|
== APP == Registering the IDs of both Devices...
|
|
== APP == Registered devices: 1, 2
|
|
== APP == Detecting smoke on Device 1...
|
|
== APP == Device 1 state: Location: First Floor, Status: Alarm
|
|
== APP == Device 2 state: Location: Second Floor, Status: Alarm
|
|
== APP == Sleeping for 16 seconds before checking status again to see reminders fire and clear alarms
|
|
== APP == Device 1 state: Location: First Floor, Status: Ready
|
|
== APP == Device 2 state: Location: Second Floor, Status: Ready
|
|
```
|
|
|
|
### Cleanup
|
|
|
|
<!-- STEP
|
|
expected_stdout_lines:
|
|
- '✅ app stopped successfully: actorservice'
|
|
expected_stderr_lines:
|
|
name: Shutdown dapr
|
|
-->
|
|
|
|
```bash
|
|
dapr stop --app-id actorservice
|
|
(lsof -iTCP -sTCP:LISTEN -P | grep :5001) | awk '{print $2}' | xargs kill
|
|
```
|
|
|
|
<!-- END_STEP -->
|
|
|
|
### What happened
|
|
|
|
When you ran the client app:
|
|
|
|
1. Two `SmartDetectorActor` actors are created and initialized with Id, Location, and Status="Ready"
|
|
2. The `DetectSmokeAsync` method of `SmartDetectorActor` 1 is called.
|
|
3. The `TriggerAlarmForAllDetectors` method of `ControllerActor` is called.
|
|
4. The `SoundAlarm` methods of `SmartDetectorActor` 1 and 2 are called.
|
|
5. The `ControllerActor` also creates a reminder to `ClearAlarm` after 15 seconds using `RegisterReminderAsync`
|
|
|
|
|
|
Looking at the code, `SmartDetectorActor` objects are created in the client application and initialized with object state with `ActorProxy.Create<ISmartDevice>(actorId, actorType)` and then `proxySmartDevice.SetDataAsync(data)`. These objects are re-entrant and will hold on to the state as shown by `proxySmartDevice.GetDataAsync()`.
|
|
|
|
```cs
|
|
// Actor Ids and types
|
|
var deviceId1 = "1";
|
|
var deviceId2 = "2";
|
|
var smokeDetectorActorType = "SmokeDetectorActor";
|
|
var controllerActorType = "ControllerActor";
|
|
|
|
Console.WriteLine("Startup up...");
|
|
|
|
// An ActorId uniquely identifies an actor instance
|
|
var deviceActorId1 = new ActorId(deviceId1);
|
|
|
|
// Create the local proxy by using the same interface that the service implements.
|
|
// You need to provide the type and id so the actor can be located.
|
|
// If the actor matching this id does not exist, it will be created
|
|
var proxySmartDevice1 = ActorProxy.Create<ISmartDevice>(deviceActorId1, smokeDetectorActorType);
|
|
|
|
// Create a new instance of the data class that will be stored in the actor
|
|
var deviceData1 = new SmartDeviceData(){
|
|
Location = "First Floor",
|
|
Status = "Ready",
|
|
};
|
|
|
|
// Now you can use the actor interface to call the actor's methods.
|
|
Console.WriteLine($"Calling SetDataAsync on {smokeDetectorActorType}:{deviceActorId1}...");
|
|
var setDataResponse1 = await proxySmartDevice1.SetDataAsync(deviceData1);
|
|
Console.WriteLine($"Got response: {setDataResponse1}");
|
|
```
|
|
|
|
The `ControllerActor` object is used to keep track of the devices and trigger the alarm for all of them.
|
|
|
|
```csharp
|
|
var controllerActorId = new ActorId("controller");
|
|
var proxyController = ActorProxy.Create<IController>(controllerActorId, controllerActorType);
|
|
|
|
Console.WriteLine($"Registering the IDs of both Devices...");
|
|
await proxyController.RegisterDeviceIdsAsync(new string[]{deviceId1, deviceId2});
|
|
var deviceIds = await proxyController.ListRegisteredDeviceIdsAsync();
|
|
Console.WriteLine($"Registered devices: {string.Join(", " , deviceIds)}");
|
|
```
|
|
|
|
The `ControllerActor` internally triggers all alarms when smoke is detected, and then sets a reminder to clear all alarm states after 15 seconds.
|
|
|
|
```cs
|
|
public async Task TriggerAlarmForAllDetectors()
|
|
{
|
|
var deviceIds = await ListRegisteredDeviceIdsAsync();
|
|
foreach (var deviceId in deviceIds)
|
|
{
|
|
// Sound the alarm on all devices
|
|
var actorId = new ActorId(deviceId);
|
|
var proxySmartDevice = ProxyFactory.CreateActorProxy<ISmartDevice>(actorId, "SmokeDetectorActor");
|
|
await proxySmartDevice.SoundAlarm();
|
|
}
|
|
|
|
// Register a reminder to refresh and clear alarm state every 15 seconds
|
|
await this.RegisterReminderAsync("AlarmRefreshReminder", null, TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(15));
|
|
}
|
|
```
|
|
|
|
Additionally look at:
|
|
|
|
- `SmartDevice.Service/SmartDetectorActor.cs` which contains the implementation of the the smart device actor actions
|
|
- `SmartDevice.Service/ControllerActor.cs` which contains the implementation of the controller actor that manages all devices
|
|
- `SmartDevice.Interfaces/ISmartDevice` which contains the required actions and shared data types for each SmartDetectorActor
|
|
- `SmartDevice.Interfaces/IController` which contains the actions a controller can perform across all devices
|