diff --git a/nuget.config b/nuget.config index 829072cb..6b515b73 100644 --- a/nuget.config +++ b/nuget.config @@ -2,6 +2,8 @@ + + diff --git a/samples/AzureFunctionsApp/AzureFunctionsApp.csproj b/samples/AzureFunctionsApp/AzureFunctionsApp.csproj index c2e463ef..269e6752 100644 --- a/samples/AzureFunctionsApp/AzureFunctionsApp.csproj +++ b/samples/AzureFunctionsApp/AzureFunctionsApp.csproj @@ -6,10 +6,10 @@ Exe enable - + - - + + diff --git a/samples/AzureFunctionsApp/Entities/Counter.Entity.cs b/samples/AzureFunctionsApp/Entities/Counter.Entity.cs new file mode 100644 index 00000000..e6a0cea5 --- /dev/null +++ b/samples/AzureFunctionsApp/Entities/Counter.Entity.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Entities; +using Microsoft.Extensions.Logging; + +namespace AzureFunctionsApp.Entities.Entity; + +/// +/// Example on how to dispatch to an entity which directly implements TaskEntity. Using TaskEntity gives +/// the added benefit of being able to use DI. When using TaskEntity, state is deserialized to the "State" +/// property. No other properties on this type will be serialized/deserialized. +/// +public class Counter : TaskEntity +{ + readonly ILogger logger; + + public Counter(ILogger logger) + { + this.logger = logger; + } + + public int Add(int input) + { + this.logger.LogInformation("Adding {Input} to {State}", input, this.State); + return this.State += input; + } + + public int OperationWithContext(int input, TaskEntityContext context) + { + // Get access to TaskEntityContext by adding it as a parameter. Can be with or without an input parameter. + // Order does not matter. + context.StartOrchestration("SomeOrchestration", "SomeInput"); + + // When using TaskEntity, the TaskEntityContext can also be accessed via this.Context. + this.Context.StartOrchestration("SomeOrchestration", "SomeInput"); + return this.Add(input); + } + + public int Get() => this.State; + + [Function("Counter2")] + public Task RunEntityAsync([EntityTrigger] TaskEntityDispatcher dispatcher) + { + // Can dispatch to a TaskEntity by passing a instance. + return dispatcher.DispatchAsync(this); + } + + [Function("Counter3")] + public static Task RunEntityStaticAsync([EntityTrigger] TaskEntityDispatcher dispatcher) + { + // Can also dispatch to a TaskEntity by using a static method. + return dispatcher.DispatchAsync(); + } + + [Function("StartCounter2")] + public static async Task StartAsync( + [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData request, + [DurableClient] DurableTaskClient client) + { + Payload? payload = await request.ReadFromJsonAsync(); + string id = await client.ScheduleNewOrchestrationInstanceAsync("CounterOrchestration2", payload); + return client.CreateCheckStatusResponse(request, id); + } + + [Function("CounterOrchestration2")] + public static async Task RunOrchestrationAsync( + [OrchestrationTrigger] TaskOrchestrationContext context, Payload input) + { + ILogger logger = context.CreateReplaySafeLogger(); + int result = await context.Entities.CallEntityAsync( + new EntityInstanceId("Counter2", input.Key), "add", input.Add); + + logger.LogInformation("Counter value: {Value}", result); + return result; + } + + protected override int InitializeState() + { + // Optional method to override to customize initialization of state for a new instance. + return base.InitializeState(); + } + + public record Payload(string Key, int Add); +} diff --git a/samples/AzureFunctionsApp/Entities/Counter.State.cs b/samples/AzureFunctionsApp/Entities/Counter.State.cs new file mode 100644 index 00000000..d286ac84 --- /dev/null +++ b/samples/AzureFunctionsApp/Entities/Counter.State.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Entities; +using Microsoft.Extensions.Logging; + +namespace AzureFunctionsApp.Entities.State; + +/// +/// Example on how to dispatch to a POCO as the entity implementation. When using POCO, the entire object is serialized +/// and deserialized. +/// +public class Counter +{ + public int Value { get; set; } + + public int Add(int input) => this.Value += input; + + public int OperationWithContext(int input, TaskEntityContext context) + { + // Get access to TaskEntityContext by adding it as a parameter. Can be with or without an input parameter. + // Order does not matter. + context.StartOrchestration("SomeOrchestration", "SomeInput"); + return this.Add(input); + } + + public int Get() => this.Value; + + [Function("Counter1")] + public static Task RunEntityAsync([EntityTrigger] TaskEntityDispatcher dispatcher) + { + // Using the dispatch to a state object will deserialize the state directly to that instance and dispatch to an + // appropriate method. + // Can only dispatch to a state object via generic argument. + return dispatcher.DispatchAsync(); + } + + [Function("StartCounter1")] + public static async Task StartAsync( + [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData request, + [DurableClient] DurableTaskClient client) + { + Payload? payload = await request.ReadFromJsonAsync(); + string id = await client.ScheduleNewOrchestrationInstanceAsync("CounterOrchestration1", payload); + return client.CreateCheckStatusResponse(request, id); + } + + [Function("CounterOrchestration1")] + public static async Task RunOrchestrationAsync( + [OrchestrationTrigger] TaskOrchestrationContext context, Payload input) + { + ILogger logger = context.CreateReplaySafeLogger(); + int result = await context.Entities.CallEntityAsync( + new EntityInstanceId("Counter1", input.Key), "add", input.Add); + + logger.LogInformation("Counter value: {Value}", result); + return result; + } + + public record Payload(string Key, int Add); +} diff --git a/samples/AzureFunctionsApp/Entities/StateManagement.cs b/samples/AzureFunctionsApp/Entities/StateManagement.cs new file mode 100644 index 00000000..b590af4d --- /dev/null +++ b/samples/AzureFunctionsApp/Entities/StateManagement.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Entities; +using Microsoft.Extensions.Logging; + +namespace AzureFunctionsApp.Entities; + +public class StateManagement : TaskEntity +{ + readonly ILogger logger; + + public StateManagement(ILogger logger) + { + this.logger = logger; + } + + /// + /// Optional property to override. When 'true', this will allow dispatching of operations to the TState object if + /// there is no matching method on the entity. Default is 'false'. + /// + protected override bool AllowStateDispatch => base.AllowStateDispatch; + + public MyState Get() => this.State; + + public void CustomDelete() + { + // Deleting an entity is done by null-ing out the state. + // The '!' in `null!;` is only needed because we are using C# explicit nullability. + // This can be avoided by either: + // 1) Declare TaskEntity instead. + // 2) Disable explicit nullability. + this.State = null!; + } + + public void Delete() + { + // Entities have an implicit 'delete' operation when there is no matching 'delete' method. By explicitly adding + // a 'Delete' method, it will override the implicit 'delete' operation. + // Since state deletion is determined by nulling out this.State, it means that value-types cannot be deleted + // except by the implicit delete (this will still delete it). To delete a value-type, you can declare it as + // nullable such as TaskEntity instead of TaskEntity. + this.State = null!; + } + + protected override MyState InitializeState() + { + // This method allows for customizing the default state value for a new entity. + return new("Default", 10); + } +} + + +public record MyState(string PropA, int PropB);