Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Actors .NET Quickstart #804

Merged
merged 46 commits into from
May 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
7eaaea2
Initial checkin of starter app
Feb 1, 2023
0ebfd9e
changing Record to Class given Actors cannot serialize records
Feb 1, 2023
8710e18
Updating to use custom statestore and port 5001
Feb 1, 2023
c8751f5
Modernized to 100% minimal API code; no Startup.cs needed.
Feb 1, 2023
085d783
Mocked Controller actor and chose better names
Feb 1, 2023
96d03bb
Fixed ControllerActor activiation
Feb 2, 2023
fb7852a
Readme cleanup
Feb 2, 2023
568d1aa
removed dead legacy usings/code
Feb 2, 2023
5f92989
multitarget inferfaces to .net 6;7
Feb 2, 2023
eec8993
Style linting
Feb 2, 2023
9f27d82
readme improvement
Feb 2, 2023
406a867
new structure of README
marcduiker Feb 3, 2023
ec26ee1
Rewrite first section to better align with existing quickstarts
marcduiker Feb 6, 2023
d5ca290
Add examples of using the client app.
marcduiker Feb 6, 2023
0e275e9
Align README with new structure
marcduiker Feb 15, 2023
14f14c1
Updated readme for format and simplicity. Todos are done.
Mar 3, 2023
4587d89
tweak to readme to make more language independent
Mar 3, 2023
a9ae13f
fixing up folder names in readme
Mar 3, 2023
4a1c1bf
Merge branch 'dapr:master' into feature/actors
paulyuk Mar 4, 2023
568f5d0
Update folders and csproj to match other building blocks
marcduiker Mar 6, 2023
8db5898
Fix capitalization in namespaces and remove unused reminders
marcduiker Mar 6, 2023
5cd415d
Simplifying SmartDeviceData
marcduiker Mar 6, 2023
cdad270
Change to signal controller and sound the alarm
marcduiker Mar 6, 2023
dadb2a8
Set Status to Alarm
marcduiker Mar 6, 2023
849ea9c
Change naming to SmokeDetectorActor
marcduiker Mar 6, 2023
bc16b76
Fix actor name and remove console logging in actors
marcduiker Mar 6, 2023
48d8fb3
Fix type
marcduiker Mar 6, 2023
e24a945
Upgrade to 1.10
marcduiker Mar 6, 2023
838fca3
Remove battery and temp functionality
marcduiker Mar 8, 2023
9e6e53b
Use explicit device1 and 2 variables in the client to avoid confusion
marcduiker Mar 10, 2023
4cdf380
Update README with new code
marcduiker Mar 10, 2023
41c9201
Merge branch 'dapr:master' into feature/actors
paulyuk May 22, 2023
a3b93d8
staging a reminders feature
May 22, 2023
702bbcd
Implemented reminders to clear alarm state after 15 seconds
May 22, 2023
ec65c5c
Update actors/csharp/sdk/README.md
paulyuk May 22, 2023
8cfe12c
updated expected output in readme with reminder
May 22, 2023
ff144fe
Merge branch 'feature/actors' of github.com:paulyuk/quickstarts into …
May 22, 2023
9487308
Updating main quickstarts readme to include Actors
May 22, 2023
4a55409
Removing In-development label from readme
May 22, 2023
bc3a760
updating MD per https://github.com/jgm/pandoc/issues/2117
May 22, 2023
8c77a89
Update actors/csharp/sdk/README.md
paulyuk May 22, 2023
442909f
Update actors/csharp/sdk/service/SmokeDetectorActor.cs
paulyuk May 22, 2023
4c22289
Update actors/csharp/sdk/service/SmokeDetectorActor.cs
paulyuk May 22, 2023
c055b54
Update actors/csharp/sdk/README.md
paulyuk May 22, 2023
c0caecf
Update actors/csharp/sdk/service/ControllerActor.cs
paulyuk May 22, 2023
a34f6bb
Update actors/csharp/sdk/service/SmokeDetectorActor.cs
paulyuk May 22, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Pick a building block API (for example, pub-sub, state management) and rapidly t
| [State Management](./state_management/) | Store a service's data as key/value pairs in supported state stores |
| [Bindings](./bindings/) | Work with external systems using input bindings to respond to events and output bindings to call operations|
| [Secrets Management](./secrets_management/) | Securely fetch secrets |
| Actors | Coming soon... |
| [Actors](./actors) | Create stateful, long running objects with identity |
| [Configuration](./configuration) | Get configuration items as key/value pairs or subscribe to changes whenever a configuration item changes |
| [Resiliency](./resiliency) | Define and apply fault-tolerant policies (retries/back-offs, timeouts and circuit breakers) to your Dapr API requests |
| [Workflow](./workflows) | Dapr Workflow enables you to create long running, fault-tolerant, stateful applications. |
Expand Down
170 changes: 170 additions & 0 deletions actors/csharp/sdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# 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 SmartDectectorActor 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 7 SDK](https://dotnet.microsoft.com/download).
- [Docker Desktop](https://www.docker.com/products/docker-desktop)

### Step 2: Set up the environment

Clone the [sample provided in the Quickstarts repo](https://github.com/dapr/quickstarts/tree/master/workflows).

```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:

```bash
cd actors/csharp/sdk/service
dotnet build
```

Run the `SmartDevice.Service`, which will start service itself and the Dapr sidecar:

```bash
dapr run --app-id actorservice --app-port 5001 --dapr-http-port 3500 --resources-path ../../../resources -- dotnet run --urls=http://localhost:5001/
```

Expected output:

```bash
== APP == info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
== APP == Request starting HTTP/1.1 GET http://127.0.0.1:5001/healthz - -
== APP == info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
== APP == Executing endpoint 'Dapr Actors Health Check'
== APP == info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
== APP == Executed endpoint 'Dapr Actors Health Check'
== APP == info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
== APP == Request finished HTTP/1.1 GET http://127.0.0.1:5001/healthz - - - 200 - text/plain 5.2599ms
```

### Step 4: Run the client app

In a new terminal instance, navigate to the `actors/csharp/sdk/client` directory and install the dependencies:

```bash
cd ./actors/csharp/sdk/client
dotnet build
```

Run the `SmartDevice.Client` app:

```bash
dapr run --app-id actorclient -- dotnet run
```

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
```

### 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 durable reminder to call `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 hold 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
85 changes: 85 additions & 0 deletions actors/csharp/sdk/client/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using Dapr.Actors;
using Dapr.Actors.Client;
using SmartDevice.Interfaces;

namespace SmartDevice;

class Program
{
static async Task Main(string[] args)
{
// 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 a new instance of the data class that will be stored in the actor
var deviceData1 = new SmartDeviceData(){
Location = "First Floor",
Status = "Ready",
};

// 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);

// 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}");

Console.WriteLine($"Calling GetDataAsync on {smokeDetectorActorType}:{deviceActorId1}...");
var storedDeviceData1 = await proxySmartDevice1.GetDataAsync();
Console.WriteLine($"Device 1 state: {storedDeviceData1}");

// Create a second actor for second device
var deviceActorId2 = new ActorId(deviceId2);
var deviceData2 = new SmartDeviceData(){
Location = "Second Floor",
Status = "Ready",
};
var proxySmartDevice2 = ActorProxy.Create<ISmartDevice>(deviceActorId2, smokeDetectorActorType);
Console.WriteLine($"Calling SetDataAsync on {smokeDetectorActorType}:{deviceActorId2}...");
var setDataResponse2 = await proxySmartDevice2.SetDataAsync(deviceData2);
Console.WriteLine($"Got response: {setDataResponse2}");
Console.WriteLine($"Calling GetDataAsync on {smokeDetectorActorType}:{deviceActorId2}...");
var storedDeviceData2 = await proxySmartDevice2.GetDataAsync();
Console.WriteLine($"Device 2 state: {storedDeviceData2}");

// Use the controller actor to register the device ids.
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)}");

// Smoke is detected on device 1 that triggers an alarm on all devices.
Console.WriteLine($"Detecting smoke on Device 1...");
proxySmartDevice1 = ActorProxy.Create<ISmartDevice>(deviceActorId1, smokeDetectorActorType);
await proxySmartDevice1.DetectSmokeAsync();

// Get the state of both devices.
storedDeviceData1 = await proxySmartDevice1.GetDataAsync();
Console.WriteLine($"Device 1 state: {storedDeviceData1}");
storedDeviceData2 = await proxySmartDevice2.GetDataAsync();
Console.WriteLine($"Device 2 state: {storedDeviceData2}");

// Sleep for 35 seconds and observe reminders have cleared alarm state
Console.WriteLine("Sleeping for 16 seconds before checking status again to see reminders fire and clear alarms");
await Task.Delay(16000);

storedDeviceData1 = await proxySmartDevice1.GetDataAsync();
Console.WriteLine($"Device 1 state: {storedDeviceData1}");
storedDeviceData2 = await proxySmartDevice2.GetDataAsync();
Console.WriteLine($"Device 2 state: {storedDeviceData2}");
}
}
18 changes: 18 additions & 0 deletions actors/csharp/sdk/client/SmartDevice.Client.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Dapr.Actors" Version="1.10.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\interfaces\SmartDevice.Interfaces.csproj" />
</ItemGroup>

</Project>
14 changes: 14 additions & 0 deletions actors/csharp/sdk/interfaces/IController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Dapr.Actors;

namespace SmartDevice.Interfaces;
public interface IController : IActor
{
Task RegisterDeviceIdsAsync(string[] deviceIds);
Task<string[]> ListRegisteredDeviceIdsAsync();
Task TriggerAlarmForAllDetectors();
}

public class ControllerData
{
public string[] DeviceIds { get; set; } = default!;
}
22 changes: 22 additions & 0 deletions actors/csharp/sdk/interfaces/ISmartDevice.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Dapr.Actors;

namespace SmartDevice.Interfaces;
public interface ISmartDevice : IActor
{
Task<string> SetDataAsync(SmartDeviceData device);
Task<SmartDeviceData> GetDataAsync();
Task DetectSmokeAsync();
Task SoundAlarm();
Task ClearAlarm();
}

public class SmartDeviceData
{
public string Status { get; set; } = default!;
public string Location { get; set; } = default!;

public override string ToString()
{
return $"Location: {this.Location}, Status: {this.Status}";
}
}
13 changes: 13 additions & 0 deletions actors/csharp/sdk/interfaces/SmartDevice.Interfaces.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6;net7</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Dapr.Actors" Version="1.10.0" />
</ItemGroup>

</Project>
2 changes: 2 additions & 0 deletions actors/csharp/sdk/makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include ../../../docker.mk
include ../../../validate.mk
Loading
Loading