diff --git a/all.sln b/all.sln
index 228047852..ffdfd5130 100644
--- a/all.sln
+++ b/all.sln
@@ -58,6 +58,9 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControllerSample", "examples\AspNetCore\ControllerSample\ControllerSample.csproj", "{3160CC92-1D6E-42CB-AE89-9401C8CEC5CB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Actor", "Actor", "{02374BD0-BF0B-40F8-A04A-C4C4D61D4992}"
+ ProjectSection(SolutionItems) = preProject
+ examples\Actor\README.md = examples\Actor\README.md
+ EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IDemoActor", "examples\Actor\IDemoActor\IDemoActor.csproj", "{7957E852-1291-4FAA-9034-FB66CE817FF1}"
EndProject
@@ -118,12 +121,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.E2E.Test.Actors.Genera
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cryptography", "examples\Client\Cryptography\Cryptography.csproj", "{C74FBA78-13E8-407F-A173-4555AEE41FF3}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoActor.UnitTest", "examples\Actor\DemoActor.UnitTest\DemoActor.UnitTest.csproj", "{67EB5D79-B31D-4CDB-8AA2-BC50AB82828A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {67EB5D79-B31D-4CDB-8AA2-BC50AB82828A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {67EB5D79-B31D-4CDB-8AA2-BC50AB82828A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {67EB5D79-B31D-4CDB-8AA2-BC50AB82828A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {67EB5D79-B31D-4CDB-8AA2-BC50AB82828A}.Release|Any CPU.Build.0 = Release|Any CPU
{C2DB4B64-B7C3-4FED-8753-C040F677C69A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C2DB4B64-B7C3-4FED-8753-C040F677C69A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2DB4B64-B7C3-4FED-8753-C040F677C69A}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -295,6 +304,7 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
+ {67EB5D79-B31D-4CDB-8AA2-BC50AB82828A} = {02374BD0-BF0B-40F8-A04A-C4C4D61D4992}
{C2DB4B64-B7C3-4FED-8753-C040F677C69A} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{41BF4392-54BD-4FE7-A3EB-CD045F88CA9A} = {DD020B34-460F-455F-8D17-CF4A949F100B}
{B9C12532-0969-4DAC-A2F8-CA9208D7A901} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
diff --git a/examples/Actor/ActorClient/Program.cs b/examples/Actor/ActorClient/Program.cs
index bae5d2ec2..7d657a059 100644
--- a/examples/Actor/ActorClient/Program.cs
+++ b/examples/Actor/ActorClient/Program.cs
@@ -32,10 +32,14 @@ public class Program
/// A representing the asynchronous operation.
public static async Task Main(string[] args)
{
- var data = new MyData()
+ var data = new MyDataWithTTL()
{
- PropertyA = "ValueA",
- PropertyB = "ValueB",
+ MyData = new MyData
+ {
+ PropertyA = "ValueA",
+ PropertyB = "ValueB",
+ },
+ TTL = TimeSpan.FromMinutes(10),
};
// Create an actor Id.
@@ -46,7 +50,7 @@ public static async Task Main(string[] args)
var proxy = ActorProxy.Create(actorId, "DemoActor");
Console.WriteLine("Making call using actor proxy to save data.");
- await proxy.SaveData(data, TimeSpan.FromMinutes(10));
+ await proxy.SaveData(data);
Console.WriteLine("Making call using actor proxy to get data.");
var receivedData = await proxy.GetData();
Console.WriteLine($"Received data is {receivedData}.");
@@ -103,7 +107,7 @@ public static async Task Main(string[] args)
await proxy.UnregisterTimer();
Console.WriteLine("Deregistering reminder. Reminders are durable and would not stop until an explicit deregistration or the actor is deleted.");
await proxy.UnregisterReminder();
-
+
Console.WriteLine("Registering reminder with repetitions - The reminder will repeat 3 times.");
await proxy.RegisterReminderWithRepetitions(3);
Console.WriteLine("Waiting so the reminder can be triggered");
diff --git a/examples/Actor/DemoActor.UnitTest/DemoActor.UnitTest.csproj b/examples/Actor/DemoActor.UnitTest/DemoActor.UnitTest.csproj
new file mode 100644
index 000000000..5c8318bdb
--- /dev/null
+++ b/examples/Actor/DemoActor.UnitTest/DemoActor.UnitTest.csproj
@@ -0,0 +1,36 @@
+
+
+
+ net6;net7;net8
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/Actor/DemoActor.UnitTest/DemoActorTests.cs b/examples/Actor/DemoActor.UnitTest/DemoActorTests.cs
new file mode 100644
index 000000000..8f5a92c63
--- /dev/null
+++ b/examples/Actor/DemoActor.UnitTest/DemoActorTests.cs
@@ -0,0 +1,91 @@
+using Dapr.Actors.Runtime;
+using DaprDemoActor;
+using IDemoActorInterface;
+using Moq;
+
+namespace DemoActor.UnitTest
+{
+ public class DemoActorTests
+ {
+ [Fact]
+ public async Task SaveData_CorrectlyPersistDataWithGiveTTL()
+ {
+ // arrange
+ // Name of the state to be saved in the actor
+ var actorStateName = "my_data";
+ // Create a mock actor state manager to simulate the actor state
+ var mockStateManager = new Mock(MockBehavior.Strict);
+ // Prepare other dependencies
+ var bankService = new BankService();
+ // Create an actor host for testing
+ var host = ActorHost.CreateForTest();
+ // Create an actor instance with the mock state manager and its dependencies
+ var storageActor = new DaprDemoActor.DemoActor(host, bankService, mockStateManager.Object);
+ // Prepare test data to be saved
+ var data = new MyDataWithTTL
+ {
+ MyData = new MyData
+ {
+ PropertyA = "PropA",
+ PropertyB = "PropB",
+ },
+ TTL = TimeSpan.FromSeconds(10)
+ };
+ // Setup the mock state manager to enable the actor to save the state with the SetStateAsync method, and return
+ // a completed task when the state is saved, so that the actor can continue with the test.
+ // When MockBehavior.Strict is used, the test will fail if the actor does not call SetStateAsync or
+ // calls other methods on the state manager.
+ mockStateManager
+ .Setup(x => x.SetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns(Task.CompletedTask);
+
+ // act
+ await storageActor.SaveData(data);
+
+ // assert
+ // Verify that the state manager is called with the correct state name and data, only one time.
+ mockStateManager.Verify(x => x.SetStateAsync(
+ actorStateName,
+ It.Is(x => x.PropertyA == "PropA" && x.PropertyB == "PropB"),
+ It.Is(x => x.TotalSeconds == 10),
+ It.IsAny()),
+ Times.Once);
+ }
+
+ [Fact]
+ public async Task GetData_CorrectlyRetrieveData()
+ {
+ // arrange
+ // Name of the state to be saved in the actor
+ var actorStateName = "my_data";
+ // Create a mock actor state manager to simulate the actor state
+ var mockStateManager = new Mock(MockBehavior.Strict);
+ // Prepare other dependencies
+ var bankService = new BankService();
+ // Create an actor host for testing
+ var host = ActorHost.CreateForTest();
+ // Create an actor instance with the mock state manager and its dependencies
+ var storageActor = new DaprDemoActor.DemoActor(host, bankService, mockStateManager.Object);
+ // Prepare prepare the state to be returned by the state manager
+ var state = new MyData
+ {
+ PropertyA = "PropA",
+ PropertyB = "PropB",
+ };
+ // Setup the mock state manager to return the state when the actor calls GetStateAsync.
+ mockStateManager
+ .Setup(x => x.GetStateAsync(actorStateName, It.IsAny()))
+ .Returns(Task.FromResult(state));
+
+ // act
+ var result = await storageActor.GetData();
+
+ // assert
+ // Verify that the state manager is called with the correct state name, only one time.
+ mockStateManager.Verify(x => x.GetStateAsync(actorStateName, It.IsAny()), Times.Once);
+ // Verify that the actor returns the correct data.
+ Assert.Equal("PropA", result.PropertyA);
+ Assert.Equal("PropB", result.PropertyB);
+ }
+ }
+}
diff --git a/examples/Actor/DemoActor/DemoActor.cs b/examples/Actor/DemoActor/DemoActor.cs
index da780d517..a9b2b35e8 100644
--- a/examples/Actor/DemoActor/DemoActor.cs
+++ b/examples/Actor/DemoActor/DemoActor.cs
@@ -33,58 +33,83 @@ public class DemoActor : Actor, IDemoActor, IBankActor, IRemindable
private readonly BankService bank;
- public DemoActor(ActorHost host, BankService bank)
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// ActorHost.
+ /// BankService.
+ /// ActorStateManager used in UnitTests.
+ public DemoActor(
+ ActorHost host,
+ BankService bank,
+ IActorStateManager actorStateManager = null)
: base(host)
{
// BankService is provided by dependency injection.
// See Program.cs
this.bank = bank;
+
+ // Assign ActorStateManager when passed as parameter.
+ // This is used in UnitTests.
+ if (actorStateManager != null)
+ {
+ this.StateManager = actorStateManager;
+ }
}
- public async Task SaveData(MyData data, TimeSpan ttl)
+ ///
+ public async Task SaveData(MyDataWithTTL data)
{
Console.WriteLine($"This is Actor id {this.Id} with data {data}.");
// Set State using StateManager, state is saved after the method execution.
- await this.StateManager.SetStateAsync(StateName, data, ttl);
+ await this.StateManager.SetStateAsync(StateName, data.MyData, data.TTL);
}
+ ///
public Task GetData()
{
// Get state using StateManager.
return this.StateManager.GetStateAsync(StateName);
}
+ ///
public Task TestThrowException()
{
throw new NotImplementedException();
}
+ ///
public Task TestNoArgumentNoReturnType()
{
return Task.CompletedTask;
}
+ ///
public async Task RegisterReminder()
{
await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
}
+ ///
public async Task RegisterReminderWithTtl(TimeSpan ttl)
{
await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5), ttl);
}
-
+
+ ///
public async Task RegisterReminderWithRepetitions(int repetitions)
{
await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1), repetitions);
}
-
+
+ ///
public async Task RegisterReminderWithTtlAndRepetitions(TimeSpan ttl, int repetitions)
{
await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1), repetitions, ttl);
}
+ ///
public async Task GetReminder()
{
var reminder = await this.GetReminderAsync("TestReminder");
@@ -98,12 +123,14 @@ public async Task GetReminder()
}
: null;
}
-
+
+ ///
public Task UnregisterReminder()
{
return this.UnregisterReminderAsync("TestReminder");
}
+ ///
public async Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period)
{
// This method is invoked when an actor reminder is fired.
@@ -131,6 +158,7 @@ public Task RegisterTimer()
return this.RegisterTimerAsync("TestTimer", nameof(this.TimerCallback), serializedTimerParams, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3));
}
+ ///
public Task RegisterTimerWithTtl(TimeSpan ttl)
{
var timerParams = new TimerParams
@@ -143,6 +171,7 @@ public Task RegisterTimerWithTtl(TimeSpan ttl)
return this.RegisterTimerAsync("TestTimer", nameof(this.TimerCallback), serializedTimerParams, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3), ttl);
}
+ ///
public Task UnregisterTimer()
{
return this.UnregisterTimerAsync("TestTimer");
@@ -179,6 +208,7 @@ public async Task TimerCallback(byte[] data)
Console.WriteLine("Timer parameter2: " + timerParams.StringParam);
}
+ ///
public async Task GetAccountBalance()
{
var starting = new AccountBalance()
@@ -191,6 +221,7 @@ public async Task GetAccountBalance()
return balance;
}
+ ///
public async Task Withdraw(WithdrawRequest withdraw)
{
var starting = new AccountBalance()
diff --git a/examples/Actor/DemoActor/Program.cs b/examples/Actor/DemoActor/Program.cs
index a56681fdb..5915a6d7d 100644
--- a/examples/Actor/DemoActor/Program.cs
+++ b/examples/Actor/DemoActor/Program.cs
@@ -1,4 +1,4 @@
-// ------------------------------------------------------------------------
+// ------------------------------------------------------------------------
// Copyright 2021 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -11,23 +11,31 @@
// limitations under the License.
// ------------------------------------------------------------------------
-namespace DaprDemoActor
+using DaprDemoActor;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddSingleton();
+builder.Services.AddActors(options =>
{
- using Microsoft.AspNetCore.Hosting;
- using Microsoft.Extensions.Hosting;
+ options.Actors.RegisterActor();
+});
- public class Program
- {
- public static void Main(string[] args)
- {
- CreateHostBuilder(args).Build().Run();
- }
+var app = builder.Build();
- public static IHostBuilder CreateHostBuilder(string[] args) =>
- Host.CreateDefaultBuilder(args)
- .ConfigureWebHostDefaults(webBuilder =>
- {
- webBuilder.UseStartup();
- });
- }
+if (app.Environment.IsDevelopment())
+{
+ app.UseDeveloperExceptionPage();
}
+else
+{
+ app.UseHsts();
+}
+
+app.UseRouting();
+app.MapActorsHandlers();
+
+await app.RunAsync();
diff --git a/examples/Actor/DemoActor/Startup.cs b/examples/Actor/DemoActor/Startup.cs
deleted file mode 100644
index da2b9e764..000000000
--- a/examples/Actor/DemoActor/Startup.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-// ------------------------------------------------------------------------
-// Copyright 2021 The Dapr Authors
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-// ------------------------------------------------------------------------
-
-namespace DaprDemoActor
-{
- using Microsoft.AspNetCore.Builder;
- using Microsoft.AspNetCore.Hosting;
- using Microsoft.Extensions.Configuration;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.Extensions.Hosting;
-
- public class Startup
- {
- public Startup(IConfiguration configuration)
- {
- this.Configuration = configuration;
- }
-
- public IConfiguration Configuration { get; }
-
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddSingleton();
- services.AddActors(options =>
- {
- options.Actors.RegisterActor();
- });
- }
-
- public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
- {
- if (env.IsDevelopment())
- {
- app.UseDeveloperExceptionPage();
- }
- else
- {
- app.UseHsts();
- }
-
- app.UseRouting();
-
- app.UseEndpoints(endpoints =>
- {
- endpoints.MapActorsHandlers();
- });
- }
- }
-}
diff --git a/examples/Actor/DemoActor/previewConfig.yaml b/examples/Actor/DemoActor/previewConfig.yaml
new file mode 100644
index 000000000..cf8869c8f
--- /dev/null
+++ b/examples/Actor/DemoActor/previewConfig.yaml
@@ -0,0 +1,8 @@
+apiVersion: dapr.io/v1alpha1
+kind: Configuration
+metadata:
+ name: featureconfig
+spec:
+ features:
+ - name: ActorStateTTL
+ enabled: true
\ No newline at end of file
diff --git a/examples/Actor/IDemoActor/IDemoActor.cs b/examples/Actor/IDemoActor/IDemoActor.cs
index 25ce09370..8cb1bec4f 100644
--- a/examples/Actor/IDemoActor/IDemoActor.cs
+++ b/examples/Actor/IDemoActor/IDemoActor.cs
@@ -16,7 +16,6 @@ namespace IDemoActorInterface
using System;
using System.Threading.Tasks;
using Dapr.Actors;
- using Dapr.Actors.Runtime;
///
/// Interface for Actor method.
@@ -26,10 +25,9 @@ public interface IDemoActor : IActor
///
/// Method to save data.
///
- /// DAta to save.
- /// TTL of state key.
+ /// Data to save with its TTL.
/// A task that represents the asynchronous save operation.
- Task SaveData(MyData data, TimeSpan ttl);
+ Task SaveData(MyDataWithTTL data);
///
/// Method to get data.
@@ -80,14 +78,14 @@ public interface IDemoActor : IActor
/// Optional TimeSpan that dictates when the timer expires.
/// A task that represents the asynchronous save operation.
Task RegisterTimerWithTtl(TimeSpan ttl);
-
+
///
/// Registers a reminder with repetitions.
///
/// The number of repetitions for which the reminder should be invoked.
/// A task that represents the asynchronous save operation.
Task RegisterReminderWithRepetitions(int repetitions);
-
+
///
/// Registers a reminder with ttl and repetitions.
///
@@ -134,6 +132,25 @@ public override string ToString()
}
}
+ ///
+ /// Variant of MyData with TTL.
+ ///
+ public class MyDataWithTTL
+ {
+ ///
+ /// Gets or sets the MyData value.
+ ///
+ public MyData MyData { get; set; }
+
+ ///
+ /// Duration for which the state is valid.
+ ///
+ public TimeSpan TTL { get; set; }
+ }
+
+ ///
+ /// Object to hold reminder data.
+ ///
public class ActorReminderData
{
public string Name { get; set; }
diff --git a/examples/Actor/README.md b/examples/Actor/README.md
index a7bb46c03..bc205badb 100644
--- a/examples/Actor/README.md
+++ b/examples/Actor/README.md
@@ -4,7 +4,7 @@ The Actor example shows how to create a virtual actor (`DemoActor`) and invoke i
## Prerequisites
-- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed
+- [.NET 6+](https://dotnet.microsoft.com/download) installed
- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/)
- [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/)
- [Dapr .NET SDK](https://github.com/dapr/dotnet-sdk/)
@@ -22,7 +22,7 @@ The Actor example shows how to create a virtual actor (`DemoActor`) and invoke i
To run the actor service locally run this command in `DemoActor` directory:
```sh
- dapr run --dapr-http-port 3500 --app-id demo_actor --app-port 5010 dotnet run
+dapr run --dapr-http-port 3500 --app-id demo_actor --app-port 5010 --config ./previewConfig.yaml dotnet run
```
The `DemoActor` service will listen on port `5010` for HTTP.
@@ -54,15 +54,14 @@ Following curl call will save data for actor id "abc"
On Linux, MacOS:
-```sh
-curl -X POST http://127.0.0.1:3500/v1.0/actors/DemoActor/abc/method/SaveData -d '{ "PropertyA": "ValueA", "PropertyB": "ValueB" }'
+``` bash
+curl -X POST http://127.0.0.1:3500/v1.0/actors/DemoActor/abc/method/SaveData -d '{ "mydata": {"PropertyA": "ValueA", "PropertyB": "ValueB" }, "ttl": "00:10:00" }'
```
On Windows:
-```sh
-curl -X POST http://127.0.0.1:3500/v1.0/actors/DemoActor/abc/method/SaveData -d "{ \"PropertyA\": \"ValueA\", \"PropertyB\": \"ValueB\" }"
-
+``` powershell
+Invoke-WebRequest -Method POST -Uri http://127.0.0.1:3500/v1.0/actors/DemoActor/abc/method/SaveData -ContentType "application/json" -Body '{ "mydata": {"PropertyA": "ValueA", "PropertyB": "ValueB" }, "ttl": "00:10:00" }'
```
**Get Data**
@@ -71,14 +70,14 @@ Following curl call will get data for actor id "abc"
On Linux, MacOS:
-```sh
+``` bash
curl -X POST http://127.0.0.1:3500/v1.0/actors/DemoActor/abc/method/GetData
```
On Windows:
-```sh
-curl -X POST http://127.0.0.1:3500/v1.0/actors/DemoActor/abc/method/GetData
+``` powershell
+Invoke-WebRequest -Method POST -Uri http://127.0.0.1:3500/v1.0/actors/DemoActor/abc/method/GetData
```
### Build and push Docker image
diff --git a/properties/IsExternalInit.cs b/properties/IsExternalInit.cs
index 34357c39a..28e38a0c8 100644
--- a/properties/IsExternalInit.cs
+++ b/properties/IsExternalInit.cs
@@ -13,5 +13,5 @@ namespace System.Runtime.CompilerServices
internal static class IsExternalInit
{
}
-
+
}