From 68e27d355b78e4092f3969fd91342ddbd2d94219 Mon Sep 17 00:00:00 2001 From: Jacob Viau Date: Thu, 12 Oct 2023 11:50:56 -0700 Subject: [PATCH 1/5] Further refine samples, add .http files and launch task --- .vscode/launch.json | 14 ++ .vscode/tasks.json | 33 +++- samples/AzureFunctionsApp/Entities/Counter.cs | 172 +++++++++++++++--- .../AzureFunctionsApp/Entities/Lifetime.cs | 38 ++-- samples/AzureFunctionsApp/Entities/User.cs | 18 +- .../AzureFunctionsApp/Entities/counters.http | 26 +++ .../AzureFunctionsApp/Entities/lifetimes.http | 15 ++ samples/AzureFunctionsApp/Entities/users.http | 19 ++ samples/AzureFunctionsApp/Greeting.cs | 9 +- 9 files changed, 283 insertions(+), 61 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 samples/AzureFunctionsApp/Entities/counters.http create mode 100644 samples/AzureFunctionsApp/Entities/lifetimes.http create mode 100644 samples/AzureFunctionsApp/Entities/users.http diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..fcaa00615 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "AzureFunctionsApp", + "type": "coreclr", + "request": "attach", + "processId": "${command:azureFunctions.pickProcess}" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 98ca606c0..f7dbaae02 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -9,17 +9,40 @@ "args": [ "build", "${workspaceRoot}/Microsoft.DurableTask.sln", - // Ask dotnet build to generate full paths for file names. "/property:GenerateFullPaths=true", - // Do not generate summary otherwise it leads to duplicate errors in Problems panel "/consoleloggerparameters:NoSummary" ], "label": "build", - "group": "build", - "presentation": { - "reveal": "silent" + "group": { + "kind": "build", + "isDefault": true }, "problemMatcher": "$msCompile" + }, + { + "label": "build (AzureFunctionsApp)", + "command": "dotnet", + "args": [ + "build", + "${workspaceFolder}/samples/AzureFunctionsApp", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "shell", + "group": { + "kind": "build" + }, + "problemMatcher": "$msCompile", + }, + { + "type": "func", + "dependsOn": "build (AzureFunctionsApp)", + "options": { + "cwd": "${workspaceFolder}/out/samples/bin/Debug/AzureFunctionsApp/net6.0" + }, + "command": "host start", + "isBackground": true, + "problemMatcher": "$func-dotnet-watch" } ] } diff --git a/samples/AzureFunctionsApp/Entities/Counter.cs b/samples/AzureFunctionsApp/Entities/Counter.cs index e1059b58e..af2bae46f 100644 --- a/samples/AzureFunctionsApp/Entities/Counter.cs +++ b/samples/AzureFunctionsApp/Entities/Counter.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Net; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.DurableTask; @@ -10,6 +11,20 @@ namespace AzureFunctionsApp.Entities; +/** +* The counter example shows the 3 different ways to dispatch to an entity. +* The mode query string is what controls this: +* mode=0 or mode=entity (default) - dispatch to "counter" entity +* mode=1 or mode=state - dispatch to "counter_state" entity +* mode=2 or mode=static - dispatch to "counter_alt" entity +* +* "counter" and "counter_alt" are the same entities, however they use +* two different functions to dispatch, and thus are different entities when +* persisted in the backend. +* +* See "counters.http" file for HTTP examples. +*/ + /// /// 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" @@ -32,6 +47,8 @@ public int Add(int input) public int Get() => this.State; + public void Reset() => this.State = 0; + [Function("Counter")] public Task DispatchAsync([EntityTrigger] TaskEntityDispatcher dispatcher) { @@ -67,12 +84,15 @@ public class StateCounter public int Get() => this.Value; + public void Reset() => this.Value = 0; + [Function("Counter_State")] public static Task DispatchAsync([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. + // "state object" is defined as any type which does not implement ITaskEntity. return dispatcher.DispatchAsync(); } } @@ -82,26 +102,136 @@ public static class CounterApis /// /// Usage: /// Add to : - /// POST /api/counter/{id}?value={value-to-add} - /// POST /api/counter/{id}?value={value-to-add}&mode=0 - /// POST /api/counter/{id}?value={value-to-add}&mode=entity + /// POST /api/counters/{id}/add/{value} + /// POST /api/counters/{id}/add/{value}?&mode=0 + /// POST /api/counters/{id}/add/{value}?&mode=entity /// /// Add to - /// POST /api/counter/{id}?value={value-to-add}&mode=1 - /// POST /api/counter/{id}?value={value-to-add}&mode=state + /// POST /api/counters/{id}/add/{value}?&mode=1 + /// POST /api/counters/{id}/add/{value}?&mode=state + /// + /// Add to , using the static method. + /// POST /api/counters/{id}/add/{value}?&mode=2 + /// POST /api/counters/{id}/add/{value}?&mode=static /// - /// - /// - /// - /// - [Function("StartCounter")] - public static async Task StartAsync( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "counter/{id}")] HttpRequestData request, + [Function("Counter_Add")] + public static async Task AddAsync( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "counters/{id}/add/{value}")] HttpRequestData request, + [DurableClient] DurableTaskClient client, + string id, + int value) + { + EntityInstanceId entityId = GetEntityId(request, id); + string instanceId = await client.ScheduleNewOrchestrationInstanceAsync( + "CounterOrchestration", new Payload(entityId, value)); + return client.CreateCheckStatusResponse(request, instanceId); + } + + /// + /// Usage: + /// Add to : + /// GET /api/counters/{id} + /// GET /api/counters/{id}?&mode=0 + /// GET /api/counters/{id}?&mode=entity + /// + /// Add to + /// GET /api/counters/{id}?&mode=1 + /// GET /api/counters/{id}?&mode=state + /// + /// Add to , using the static method. + /// GET /api/counters/{id}?&mode=2 + /// GET /api/counters/{id}?&mode=static + /// + [Function("Counter_Get")] + public static async Task GetAsync( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "counters/{id}")] HttpRequestData request, [DurableClient] DurableTaskClient client, string id) { - _ = int.TryParse(request.Query["value"], out int value); + EntityInstanceId entityId = GetEntityId(request, id); + // "counter_state" corresponds to StateCounter, which has a different state structure than the other modes. + // The following calls highlight how entity vs state dispatch changes the state structure of your entity. + object? entity = entityId.Name == "counter_state" + ? await client.Entities.GetEntityAsync(entityId) + : await client.Entities.GetEntityAsync(entityId); + if (entity is null) + { + return request.CreateResponse(HttpStatusCode.NotFound); + } + + // We serialize the entire entity to show the structural differences. + HttpResponseData response = request.CreateResponse(HttpStatusCode.OK); + await response.WriteAsJsonAsync(entity); + return response; + } + + /// + /// Usage: + /// Add to : + /// DELETE /api/counters/{id} + /// DELETE /api/counters/{id}?&mode=0 + /// DELETE /api/counters/{id}?&mode=entity + /// + /// Add to + /// DELETE /api/counters/{id}?&mode=1 + /// DELETE /api/counters/{id}?&mode=state + /// + /// Add to , using the static method. + /// DELETE /api/counters/{id}?&mode=2 + /// DELETE /api/counters/{id}?&mode=static + /// + [Function("Counter_Delete")] + public static async Task DeleteAsync( + [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "counters/{id}")] HttpRequestData request, + [DurableClient] DurableTaskClient client, + string id) + { + EntityInstanceId entityId = GetEntityId(request, id); + await client.Entities.SignalEntityAsync(entityId, "delete"); + return request.CreateResponse(HttpStatusCode.Accepted); + } + + /// + /// Usage: + /// Add to : + /// POST /api/counters/{id}/reset + /// POST /api/counters/{id}/reset?&mode=0 + /// POST /api/counters/{id}/reset?&mode=entity + /// + /// Add to + /// POST /api/counters/{id}/reset?&mode=1 + /// POST /api/counters/{id}/reset?&mode=state + /// + /// Add to , using the static method. + /// POST /api/counters/{id}/reset?&mode=2 + /// POST /api/counters/{id}/reset?&mode=static + /// + [Function("Counter_Reset")] + public static async Task ResetAsync( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "counters/{id}/reset")] HttpRequestData request, + [DurableClient] DurableTaskClient client, + string id) + { + EntityInstanceId entityId = GetEntityId(request, id); + await client.Entities.SignalEntityAsync(entityId, "reset"); + return request.CreateResponse(HttpStatusCode.Accepted); + } + + [Function("CounterOrchestration")] + public static async Task RunOrchestrationAsync( + [OrchestrationTrigger] TaskOrchestrationContext context, Payload input) + { + ILogger logger = context.CreateReplaySafeLogger(); + int result = await context.Entities.CallEntityAsync(input.Id, "add", input.Add); + logger.LogInformation("Counter value: {Value}", result); + return result; + } + + public record Payload(EntityInstanceId Id, int Add); + + static EntityInstanceId GetEntityId(HttpRequestData request, string key) + { // switch to Counter_State if ?mode=1 or ?mode=state is supplied. // or to Counter_Alt if ?mode=2 or ?mode=static is supplied. string name; @@ -125,20 +255,6 @@ public static async Task StartAsync( }; } - string instanceId = await client.ScheduleNewOrchestrationInstanceAsync( - "CounterOrchestration", new Payload(new EntityInstanceId(name, id), value)); - return client.CreateCheckStatusResponse(request, instanceId); - } - - [Function("CounterOrchestration")] - public static async Task RunOrchestrationAsync( - [OrchestrationTrigger] TaskOrchestrationContext context, Payload input) - { - ILogger logger = context.CreateReplaySafeLogger(); - int result = await context.Entities.CallEntityAsync(input.Id, "add", input.Add); - logger.LogInformation("Counter value: {Value}", result); - return result; + return new(name, key); } - - public record Payload(EntityInstanceId Id, int Add); } diff --git a/samples/AzureFunctionsApp/Entities/Lifetime.cs b/samples/AzureFunctionsApp/Entities/Lifetime.cs index c4bab30df..d7a33e409 100644 --- a/samples/AzureFunctionsApp/Entities/Lifetime.cs +++ b/samples/AzureFunctionsApp/Entities/Lifetime.cs @@ -13,7 +13,9 @@ namespace AzureFunctionsApp.Entities; /// /// Example showing the lifetime of an entity. An entity is initialized on the first operation it receives and then -/// is considered deleted when is null at the end of an operation. +/// is considered deleted when is null at the end of an operation. It +/// is also possible to design an entity which remains stateless by always returning null from +/// and never assigning a non-null state. /// public class Lifetime : TaskEntity { @@ -25,8 +27,10 @@ public Lifetime(ILogger 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'. + /// Optional property to override. When 'true', this will allow dispatching of operations to the + /// object if there is no matching method on the entity. Default is 'false'. + /// Stated differently: if this is true and there is no matching method for a given operation on the entity + /// type, then the operation will attempt to find a matching method on instead. /// protected override bool AllowStateDispatch => base.AllowStateDispatch; @@ -39,11 +43,15 @@ public Task DispatchAsync([EntityTrigger] TaskEntityDispatcher dispatcher) return dispatcher.DispatchAsync(this); } - public void Init() { } // no op just to init this entity. + public MyState Get() => this.State; + + public void Init() { } // no op just to initialize this entity. public void CustomDelete() { - // Deleting an entity is done by null-ing out the state. + // This method shows that entity deletion can be accomplished from any operation by nulling out the state. The + // operation does not have to be named "delete". The only requirement for deletion is that state is null + // when the operation returns. // The '!' in `null!;` is only needed because we are using C# explicit nullability. // This can be avoided by either: // 1) Declare TaskEntity instead. @@ -55,24 +63,24 @@ 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 (which will still delete it). To manually delete a value-type, you can declare - // it as nullable. Such as TaskEntity instead of TaskEntity. + // Since state deletion is determined by nulling out this.State, it means that value-types cannot be + // deleted except by the implicit delete (which will still delete it). To manually delete a value-type, the + // state can be declared as nullable. Such as TaskEntity instead of TaskEntity. this.State = null!; } protected override MyState InitializeState(TaskEntityOperation operation) { // This method allows for customizing the default state value for a new entity. - return new("Default", 10); + return new(Guid.NewGuid().ToString("N"), Random.Shared.Next(0, 1000)); } } public static class LifetimeApis { - [Function("GetLifetime")] + [Function("Lifetime_Get")] public static async Task GetAsync( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "state/{id}")] HttpRequestData request, + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "lifetimes/{id}")] HttpRequestData request, [DurableClient] DurableTaskClient client, string id) { @@ -89,9 +97,9 @@ public static async Task GetAsync( return response; } - [Function("InitLifetime")] + [Function("Lifetime_Init")] public static async Task InitAsync( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "state/{id}")] HttpRequestData request, + [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "lifetimes/{id}")] HttpRequestData request, [DurableClient] DurableTaskClient client, string id) { @@ -99,9 +107,9 @@ public static async Task InitAsync( return request.CreateResponse(HttpStatusCode.Accepted); } - [Function("DeleteLifetime")] + [Function("Lifetime_Delete")] public static async Task DeleteAsync( - [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "state/{id}")] HttpRequestData request, + [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "lifetimes/{id}")] HttpRequestData request, [DurableClient] DurableTaskClient client, string id) { diff --git a/samples/AzureFunctionsApp/Entities/User.cs b/samples/AzureFunctionsApp/Entities/User.cs index e819c12af..c66630a9c 100644 --- a/samples/AzureFunctionsApp/Entities/User.cs +++ b/samples/AzureFunctionsApp/Entities/User.cs @@ -82,11 +82,19 @@ protected override User InitializeState(TaskEntityOperation entityOperation) } } +/// +/// APIs: +/// Create User: PUT /api/users/{id}?name={name}&age={age} -- both name and age are required +/// Update User: PATCH /api/users/{id}?name={name}&age={age} -- either name or age can be updated +/// Get User: GET /api/users/{id} +/// Delete User: DELETE /api/users/{id} +/// Greet User: POST /api/users/{id}/greet?message={message} +/// public static class UserApis { [Function("PutUser")] public static async Task PutAsync( - [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "user/{id}")] HttpRequestData request, + [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "users/{id}")] HttpRequestData request, [DurableClient] DurableTaskClient client, string id) { @@ -111,7 +119,7 @@ public static async Task PutAsync( [Function("PatchUser")] public static async Task PatchAsync( - [HttpTrigger(AuthorizationLevel.Anonymous, "patch", Route = "user/{id}")] HttpRequestData request, + [HttpTrigger(AuthorizationLevel.Anonymous, "patch", Route = "users/{id}")] HttpRequestData request, [DurableClient] DurableTaskClient client, string id) { @@ -131,7 +139,7 @@ public static async Task PatchAsync( [Function("DeleteUser")] public static async Task DeleteAsync( - [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "user/{id}")] HttpRequestData request, + [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "users/{id}")] HttpRequestData request, [DurableClient] DurableTaskClient client, string id) { @@ -142,7 +150,7 @@ public static async Task DeleteAsync( [Function("GetUser")] public static async Task GetAsync( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "user/{id}")] HttpRequestData request, + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "users/{id}")] HttpRequestData request, [DurableClient] DurableTaskClient client, string id) { @@ -160,7 +168,7 @@ public static async Task GetAsync( [Function("GreetUser")] public static async Task GreetAsync( - [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "user/{id}/greet")] HttpRequestData request, + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "users/{id}/greet")] HttpRequestData request, [DurableClient] DurableTaskClient client, string id) { diff --git a/samples/AzureFunctionsApp/Entities/counters.http b/samples/AzureFunctionsApp/Entities/counters.http new file mode 100644 index 000000000..6209a1a5d --- /dev/null +++ b/samples/AzureFunctionsApp/Entities/counters.http @@ -0,0 +1,26 @@ +@host = http://localhost:7071/api +@mode = 1 + +// The counter example shows the 3 different ways to dispatch to an entity. +// The mode query string is what controls this: +// mode=0 or mode=entity (default) - dispatch to "counter" entity +// mode=1 or mode=state - dispatch to "counter_state" entity +// mode=2 or mode=static - dispatch to "counter_alt" entity +// +// "counter" and "counter_alt" are the same entities, however they use +// two different functions to dispatch, and thus are different entities when +// persisted in the backend. + +POST {{host}}/counters/1/add/10?mode={{mode}} + +### + +GET {{host}}/counters/1?mode={{mode}} + +### + +POST {{host}}/counters/1/reset?mode={{mode}} + +### + +DELETE {{host}}/counters/1?mode={{mode}} diff --git a/samples/AzureFunctionsApp/Entities/lifetimes.http b/samples/AzureFunctionsApp/Entities/lifetimes.http new file mode 100644 index 000000000..b1bbaba6e --- /dev/null +++ b/samples/AzureFunctionsApp/Entities/lifetimes.http @@ -0,0 +1,15 @@ +@host = http://localhost:7071/api + +PUT {{host}}/lifetimes/1 + +### + +GET {{host}}/lifetimes/1 + +### + +DELETE {{host}}/lifetimes/1 + +### + +DELETE {{host}}/lifetimes/1?custom=true diff --git a/samples/AzureFunctionsApp/Entities/users.http b/samples/AzureFunctionsApp/Entities/users.http new file mode 100644 index 000000000..53db72b5d --- /dev/null +++ b/samples/AzureFunctionsApp/Entities/users.http @@ -0,0 +1,19 @@ +@host = http://localhost:7071/api + +PUT {{host}}/users/1?name=John&Age=21 + +### + +PATCH {{host}}/users/1?Age=22 + +### + +GET {{host}}/users/1 + +### + +POST {{host}}/users/1/greet?message=custom greeting + +### + +DELETE {{host}}/users/1 \ No newline at end of file diff --git a/samples/AzureFunctionsApp/Greeting.cs b/samples/AzureFunctionsApp/Greeting.cs index 69cd5f555..a4c354c50 100644 --- a/samples/AzureFunctionsApp/Greeting.cs +++ b/samples/AzureFunctionsApp/Greeting.cs @@ -10,14 +10,7 @@ namespace AzureFunctionsApp; /// -/// An example of performing the fibonacci sequence in Durable Functions. While this is both a (naive) recursive -/// implementation of fibonacci and also not the best use of Durable, it does a good job at highlighting some patterns -/// that can be used in durable. Particularly: -/// 1. Sub orchestrations -/// 2. Orchestration flexibility - can be both a top level AND a sub orchestration -/// 3. Recursion can be performed with orchestrations! -/// 4. Control flow you are used to from regular C# programming works here as well! Particularly branching. -/// 5. Concurrency can be controlled like any other C# Task. +/// A simple greeting orchestration to demonstrate passing custom input and output data. /// public static class Greeting { From 8978500102e4692ea9da5e604550564eabae1f37 Mon Sep 17 00:00:00 2001 From: Jacob Viau Date: Thu, 12 Oct 2023 12:51:14 -0700 Subject: [PATCH 2/5] Add vscode recommended extensions --- .vscode/extensions.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..34f981804 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "humao.rest-client", + "ms-dotnettools.csharp", + "ms-azuretools.vscode-azurefunctions" + ] +} \ No newline at end of file From 9ff33598290af81bbecd7b2a98796510afd3ce1d Mon Sep 17 00:00:00 2001 From: Jacob Viau Date: Thu, 12 Oct 2023 13:01:22 -0700 Subject: [PATCH 3/5] Add newline at eof --- samples/AzureFunctionsApp/Entities/users.http | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/AzureFunctionsApp/Entities/users.http b/samples/AzureFunctionsApp/Entities/users.http index 53db72b5d..f6b017759 100644 --- a/samples/AzureFunctionsApp/Entities/users.http +++ b/samples/AzureFunctionsApp/Entities/users.http @@ -16,4 +16,4 @@ POST {{host}}/users/1/greet?message=custom greeting ### -DELETE {{host}}/users/1 \ No newline at end of file +DELETE {{host}}/users/1 From 268bde63e62f3c227ae430988fc7949c799e07a7 Mon Sep 17 00:00:00 2001 From: Jacob Viau Date: Thu, 12 Oct 2023 13:02:08 -0700 Subject: [PATCH 4/5] set mode to 0 --- samples/AzureFunctionsApp/Entities/counters.http | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/AzureFunctionsApp/Entities/counters.http b/samples/AzureFunctionsApp/Entities/counters.http index 6209a1a5d..eda209dc7 100644 --- a/samples/AzureFunctionsApp/Entities/counters.http +++ b/samples/AzureFunctionsApp/Entities/counters.http @@ -1,5 +1,5 @@ @host = http://localhost:7071/api -@mode = 1 +@mode = 0 // The counter example shows the 3 different ways to dispatch to an entity. // The mode query string is what controls this: From e787f20df1feaee077cf9217a7ad0338bd448009 Mon Sep 17 00:00:00 2001 From: Jacob Viau Date: Thu, 12 Oct 2023 13:06:06 -0700 Subject: [PATCH 5/5] Reword comments --- samples/AzureFunctionsApp/Entities/Lifetime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/AzureFunctionsApp/Entities/Lifetime.cs b/samples/AzureFunctionsApp/Entities/Lifetime.cs index d7a33e409..ed6c1b095 100644 --- a/samples/AzureFunctionsApp/Entities/Lifetime.cs +++ b/samples/AzureFunctionsApp/Entities/Lifetime.cs @@ -52,7 +52,7 @@ public void CustomDelete() // This method shows that entity deletion can be accomplished from any operation by nulling out the state. The // operation does not have to be named "delete". The only requirement for deletion is that state is null // when the operation returns. - // The '!' in `null!;` is only needed because we are using C# explicit nullability. + // The '!' in `null!;` is only needed because C# explicit nullability is enabled. // This can be avoided by either: // 1) Declare TaskEntity instead. // 2) Disable explicit nullability.