Skip to content

Commit 07fc3f3

Browse files
Merge pull request #313366 from MicrosoftDocs/main
Auto Publish – main to live - 2026-03-18 22:03 UTC
2 parents bf0a3f4 + c9cc211 commit 07fc3f3

33 files changed

Lines changed: 853 additions & 341 deletions

articles/azure-functions/durable/durable-functions-dotnet-entities.md

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,65 @@ public class Counter : TaskEntity<int>
151151
152152
### Deleting entities in the isolated model
153153

154-
Deleting an entity in the isolated model is accomplished by setting the entity state to `null`, and this process depends on the entity implementation path used:
154+
Deleting an entity in the isolated model is accomplished by setting the entity state to `null`, and this process depends on the entity implementation path used.
155+
156+
#### Delete with ITaskEntity or function-based syntax
157+
158+
When deriving from `ITaskEntity` or using [function based syntax](#function-based-syntax), delete is accomplished by calling `TaskEntityOperation.State.SetState(null)`:
159+
160+
```csharp
161+
// Inside a function-based entity dispatch
162+
switch (operation.Name.ToLowerInvariant())
163+
{
164+
case "delete":
165+
operation.State.SetState(null);
166+
break;
167+
}
168+
```
169+
170+
#### Delete with TaskEntity\<TState\>
171+
172+
When deriving from `TaskEntity<TState>`, a delete operation is implicitly defined. However, it can be overridden by defining a method `Delete` on the entity. State can also be deleted from any operation via `this.State = null`.
173+
174+
- To delete by setting state to `null` requires `TState` to be nullable.
175+
- The implicitly defined delete operation deletes non-nullable `TState`.
176+
177+
The following example shows a `TaskEntity<int?>` with nullable state that overrides the default delete:
178+
179+
```csharp
180+
public class Counter : TaskEntity<int?>
181+
{
182+
public void Delete()
183+
{
184+
// Custom logic before deleting, such as logging
185+
this.State = null;
186+
}
187+
188+
[Function(nameof(Counter))]
189+
public static Task Run([EntityTrigger] TaskEntityDispatcher dispatcher)
190+
=> dispatcher.DispatchAsync<Counter>();
191+
}
192+
```
193+
194+
#### Delete with a POCO entity
195+
196+
When using a POCO as your state (not deriving from `TaskEntity<TState>`), a delete operation is implicitly defined. It's possible to override the delete operation by defining a method `Delete` on the POCO. However, there isn't a way to set state to `null` in the POCO route, so the implicitly defined delete operation is the only true delete.
197+
198+
```csharp
199+
public class Counter
200+
{
201+
public int Value { get; set; }
202+
203+
// The implicit delete operation handles state removal.
204+
// Defining a Delete method here overrides the implicit behavior,
205+
// but you cannot set state to null from within a POCO entity.
206+
207+
[Function(nameof(Counter))]
208+
public static Task Run([EntityTrigger] TaskEntityDispatcher dispatcher)
209+
=> dispatcher.DispatchAsync<Counter>();
210+
}
211+
```
155212

156-
- When deriving from `ITaskEntity` or using [function based syntax](#function-based-syntax), delete is accomplished by calling `TaskEntityOperation.State.SetState(null)`.
157-
- When deriving from `TaskEntity<TState>`, delete is implicitly defined. However, it can be overridden by defining a method `Delete` on the entity. State can also be deleted from any operation via `this.State = null`.
158-
- To delete by setting state to null requires `TState` to be nullable.
159-
- The implicitly defined delete operation deletes non-nullable `TState`.
160-
- When using a POCO as your state (not deriving from `TaskEntity<TState>`), delete is implicitly defined. It's possible to override the delete operation by defining a method `Delete` on the POCO. However, there isn't a way to set state to `null` in the POCO route, so the implicitly defined delete operation is the only true delete.
161213
::: zone-end
162214

163215
::: zone pivot="in-proc"

articles/azure-functions/durable/durable-functions-dotnet-isolated-overview.md

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,24 +53,66 @@ public static class MyFunctions
5353
}
5454
```
5555

56-
#### Class-based example
56+
#### Class-based example (with input/output)
5757

5858
```csharp
59-
[Function(nameof(MyOrchestration))]
60-
public static async Task<string> MyOrchestration(
61-
[OrchestrationTrigger] TaskOrchestrationContext context)
59+
using Microsoft.DurableTask;
60+
61+
[DurableTask]
62+
public class MyOrchestration : TaskOrchestrator<string, string>
63+
{
64+
public override async Task<string> RunAsync(TaskOrchestrationContext context, string input)
65+
{
66+
return await context.CallActivityAsync<string>(nameof(MyActivity), input);
67+
}
68+
}
69+
70+
[DurableTask]
71+
public class MyActivity : TaskActivity<string, string>
72+
{
73+
public override Task<string> RunAsync(TaskActivityContext context, string input)
74+
{
75+
return Task.FromResult($"Processed: {input}");
76+
}
77+
}
78+
```
79+
80+
#### Class-based example (no input/output, `ILogger` in activity)
81+
82+
```csharp
83+
using Microsoft.DurableTask;
84+
using Microsoft.Extensions.Logging;
85+
86+
[DurableTask]
87+
public class MaintenanceOrchestration : TaskOrchestrator<object?, object?>
6288
{
63-
string input = context.GetInput<string>()!;
64-
return await context.CallActivityAsync<string>(nameof(MyActivity), input);
89+
public override async Task<object?> RunAsync(TaskOrchestrationContext context, object? input)
90+
{
91+
await context.CallActivityAsync(nameof(WriteHeartbeatLogActivity), (object?)null);
92+
return null;
93+
}
6594
}
6695

67-
[Function(nameof(MyActivity))]
68-
public static string MyActivity([ActivityTrigger] string input)
96+
[DurableTask]
97+
public class WriteHeartbeatLogActivity : TaskActivity<object?, object?>
6998
{
70-
return $"Processed: {input}";
99+
private readonly ILogger<WriteHeartbeatLogActivity> _logger;
100+
101+
public WriteHeartbeatLogActivity(ILogger<WriteHeartbeatLogActivity> logger)
102+
{
103+
_logger = logger;
104+
}
105+
106+
public override Task<object?> RunAsync(TaskActivityContext context, object? input)
107+
{
108+
_logger.LogInformation("Heartbeat logged at {Timestamp}.", DateTimeOffset.UtcNow);
109+
return Task.FromResult<object?>(null);
110+
}
71111
}
72112
```
73113

114+
Use `object?` as the generic type argument for class-based orchestrations and activities that don't need functional input or output. This pattern lets you use dependency injection (for example, `ILogger<T>`) in activities while still using the class-based model.
115+
74116
## Durable entities
75117

76118
Durable entities are supported in the .NET isolated worker. For more information, see the [developer's guide](./durable-functions-dotnet-entities.md).

articles/azure-functions/durable/durable-functions-fan-in-fan-out.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,55 @@ Here is the code that implements the orchestrator function:
9292

9393
[!code-csharp[Main](~/samples-durable-functions/samples/precompiled/BackupSiteContent.cs?range=16-42)]
9494

95+
> [!NOTE]
96+
> The sample linked in the prerequisites (`samples/precompiled`) uses the in-process model. The following **Isolated model** sections show equivalent code for the .NET isolated worker model.
97+
9598
Notice the `await Task.WhenAll(tasks);` line. The code doesn't await the individual calls to `E2_CopyFileToBlob`, so they run in parallel. When the orchestrator passes the task array to `Task.WhenAll`, it returns a task that doesn't complete until all copy operations complete. If you're familiar with the Task Parallel Library (TPL) in .NET, this pattern is familiar. The difference is that these tasks could be running on multiple virtual machines concurrently, and the Durable Functions extension ensures that the end-to-end execution is resilient to process recycling.
9699

97100
After the orchestrator awaits `Task.WhenAll`, all function calls are complete and return values. Each call to `E2_CopyFileToBlob` returns the number of bytes uploaded. Calculate the total by adding the return values.
98101

102+
<br>
103+
104+
<details>
105+
<summary><b>Isolated model</b></summary>
106+
107+
```csharp
108+
using System.IO;
109+
using System.Linq;
110+
using System.Threading.Tasks;
111+
using Microsoft.Azure.Functions.Worker;
112+
using Microsoft.DurableTask;
113+
114+
namespace SampleApp;
115+
116+
public static class BackupSiteContent
117+
{
118+
[Function("E2_BackupSiteContent")]
119+
public static async Task<long> Run(
120+
[OrchestrationTrigger] TaskOrchestrationContext context)
121+
{
122+
string rootDirectory = context.GetInput<string>()?.Trim();
123+
if (string.IsNullOrEmpty(rootDirectory))
124+
{
125+
rootDirectory = Directory.GetParent(typeof(BackupSiteContent).Assembly.Location)!.FullName;
126+
}
127+
128+
string[] files = await context.CallActivityAsync<string[]>("E2_GetFileList", rootDirectory);
129+
130+
Task<long>[] tasks = files
131+
.Select(file => context.CallActivityAsync<long>("E2_CopyFileToBlob", file))
132+
.ToArray();
133+
134+
long[] results = await Task.WhenAll(tasks);
135+
return results.Sum();
136+
}
137+
}
138+
```
139+
140+
</details>
141+
142+
<br>
143+
99144
# [JavaScript](#tab/javascript)
100145

101146
<details>
@@ -321,6 +366,40 @@ The helper activity functions are regular functions that use the `activityTrigge
321366

322367
[!code-csharp[Main](~/samples-durable-functions/samples/precompiled/BackupSiteContent.cs?range=44-54)]
323368

369+
<br>
370+
371+
<details>
372+
<summary><b>Isolated model</b></summary>
373+
374+
```csharp
375+
using System.IO;
376+
using Microsoft.Azure.Functions.Worker;
377+
using Microsoft.Extensions.Logging;
378+
379+
namespace SampleApp;
380+
381+
public static class BackupSiteContent
382+
{
383+
[Function("E2_GetFileList")]
384+
public static string[] GetFileList(
385+
[ActivityTrigger] string rootDirectory,
386+
FunctionContext executionContext)
387+
{
388+
ILogger logger = executionContext.GetLogger("E2_GetFileList");
389+
logger.LogInformation("Searching for files under '{RootDirectory}'...", rootDirectory);
390+
391+
string[] files = Directory.GetFiles(rootDirectory, "*", SearchOption.AllDirectories);
392+
logger.LogInformation("Found {FileCount} file(s) under {RootDirectory}.", files.Length, rootDirectory);
393+
394+
return files;
395+
}
396+
}
397+
```
398+
399+
</details>
400+
401+
<br>
402+
324403
# [JavaScript](#tab/javascript)
325404

326405
<details>
@@ -385,6 +464,60 @@ Java sample coming soon.
385464
386465
The function uses Azure Functions binding features like the [`Binder` parameter](../functions-dotnet-class-library.md#binding-at-runtime). You don't need those details for this walkthrough.
387466

467+
<br>
468+
469+
<details>
470+
<summary><b>Isolated model</b></summary>
471+
472+
```csharp
473+
using System;
474+
using System.IO;
475+
using System.Threading.Tasks;
476+
using Azure.Storage.Blobs;
477+
using Microsoft.Azure.Functions.Worker;
478+
using Microsoft.Extensions.Logging;
479+
480+
namespace SampleApp;
481+
482+
public static class BackupSiteContent
483+
{
484+
[Function("E2_CopyFileToBlob")]
485+
public static async Task<long> CopyFileToBlob(
486+
[ActivityTrigger] string filePath,
487+
FunctionContext executionContext)
488+
{
489+
ILogger logger = executionContext.GetLogger("E2_CopyFileToBlob");
490+
long byteCount = new FileInfo(filePath).Length;
491+
492+
string blobPath = filePath
493+
.Substring(Path.GetPathRoot(filePath)!.Length)
494+
.Replace('\\', '/');
495+
string outputLocation = $"backups/{blobPath}";
496+
497+
string? connectionString = Environment.GetEnvironmentVariable("AzureWebJobsStorage");
498+
if (string.IsNullOrEmpty(connectionString))
499+
{
500+
throw new InvalidOperationException("AzureWebJobsStorage is not configured.");
501+
}
502+
503+
BlobContainerClient containerClient = new(connectionString, "backups");
504+
await containerClient.CreateIfNotExistsAsync();
505+
BlobClient blobClient = containerClient.GetBlobClient(blobPath);
506+
507+
logger.LogInformation("Copying '{FilePath}' to '{OutputLocation}'. Total bytes = {ByteCount}.", filePath, outputLocation, byteCount);
508+
509+
await using Stream source = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
510+
await blobClient.UploadAsync(source, overwrite: true);
511+
512+
return byteCount;
513+
}
514+
}
515+
```
516+
517+
</details>
518+
519+
<br>
520+
388521
# [JavaScript](#tab/javascript)
389522

390523
<details>

0 commit comments

Comments
 (0)