This sample demonstrates IEmbeddingGenerator wrapped with UseDurableExecution(), where each
GenerateAsync call dispatches as a separate Temporal activity. Two workflow variants show
sequential and parallel fan-out strategies for indexing a document corpus. If the worker crashes
mid-batch, completed embeddings replay from workflow history — no API calls are repeated.
UseDurableExecution()onEmbeddingGeneratorBuilder— middleware that detects workflow contextDocumentIndexingWorkflow— sequential per-chunk embedding; one activity per chunkParallelDocumentIndexingWorkflow— concurrent fan-out viaWorkflow.WhenAllAsync- Crash recovery: completed activities replay from history; only remaining chunks are re-run
DurableEmbeddingActivitiesis registered automatically byAddDurableAI()(when used withAddHostedTemporalWorker(...))
Sequential Parallel
────────── ────────
DocumentIndexingWorkflow ParallelDocumentIndexingWorkflow
foreach chunk tasks = chunks.Select(GenerateAsync).ToList()
await generator.GenerateAsync() await Workflow.WhenAllAsync(tasks)
└─ DurableEmbeddingActivities └─ N concurrent DurableEmbeddingActivities
└─ IEmbeddingGenerator └─ IEmbeddingGenerator (per activity)
└─ OpenAI API └─ OpenAI API
- One activity per chunk, not one per batch. This gives independent retry granularity: if chunk 3 fails on a rate-limit error, only chunk 3 is retried. Chunks 1 and 2 are replayed from history — no wasted API calls.
Workflow.WhenAllAsync, notTask.WhenAll. Inside a[Workflow]class,Task.WhenAllbypasses Temporal's customTaskSchedulerand breaks determinism during history replay.Workflow.WhenAllAsyncis the correct replacement.NullEmbeddingGeneratoras a workflow-side stub.DurableEmbeddingGeneratorrequires an inner generator in its constructor, butWorkflow.InWorkflow == trueprevents it from ever being called. A lightweightNullEmbeddingGeneratorsatisfies the constructor without pulling in API credentials on the workflow thread.- Parallel wall-clock time approaches
max(per-activity)notsum. The parallel demo schedules all N activities in one Temporal scheduling round, so total elapsed time scales with the slowest chunk rather than all chunks combined.
- .NET 10 SDK or later
- A local Temporal server:
temporal server start-dev - An OpenAI-compatible API key (
OPENAI_API_KEY) - Optional:
OPENAI_API_BASE_URL(defaults tohttps://api.openai.com/v1) andOPENAI_EMBEDDING_MODEL(defaults totext-embedding-3-small)
dotnet user-secrets set "OPENAI_API_KEY" "sk-..." --project samples/MEAI/DurableEmbeddings
dotnet user-secrets set "OPENAI_API_BASE_URL" "https://api.openai.com/v1" --project samples/MEAI/DurableEmbeddings
dotnet user-secrets set "OPENAI_EMBEDDING_MODEL" "text-embedding-3-small" --project samples/MEAI/DurableEmbeddingsdotnet run --project samples/MEAI/DurableEmbeddings/DurableEmbeddings.csproj Demo: Durable Document Indexing (RAG embedding pipeline)
Chunks to index: 3
Elapsed : ~Nms (sequential, varies)
Chunks indexed : 3
Vector dimension: 1536
Similarity (chunk 1 vs 2): 0.3241 (varies; lower = more distinct)
Demo: Parallel Document Indexing (fan-out embedding)
Chunks to index: 5
Elapsed : ~Nms (parallel, varies; approaches max(per-activity))
Chunks processed : 5
Vector dimension : 1536