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 { } - + }