Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions samples/isolated-entities/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Build output
bin/
obj/

# User-specific files
*.user
*.suo

# Azure Functions local settings
local.settings.json

# Visual Studio cache
.vs/
14 changes: 14 additions & 0 deletions samples/isolated-entities/ChargePayment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
using InventorySample;

public static class ChargePayment
{
[Function(nameof(ChargePayment))]
public static Task<bool> Run([ActivityTrigger] OrderRequest order)
{
// Simulate payment processing — always succeed for now
Console.WriteLine($"Charging {order.Amount:C} for Order {order.OrderId}");
return Task.FromResult(true);
}
}
77 changes: 77 additions & 0 deletions samples/isolated-entities/InventoryEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Entities;

public static class InventoryEndpoints
{
// POST /orders
[Function("StartOrder")]
public static async Task<HttpResponseData> StartOrder(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "orders")] HttpRequestData req,
[DurableClient] DurableTaskClient client)
{
var order = await JsonSerializer.DeserializeAsync<OrderRequest>(req.Body, SerializerOptions());
if (order is null)
{
var badRes = req.CreateResponse(HttpStatusCode.BadRequest);
await badRes.WriteStringAsync("Invalid order payload.");
return badRes;
}

await client.ScheduleNewOrchestrationInstanceAsync("OrderOrchestrator", order);

var res = req.CreateResponse(HttpStatusCode.Accepted);
await res.WriteStringAsync($"Order {order.OrderId} started.");
return res;
}

// POST /inventory/setup
[Function("SetupInventory")]
public static async Task<HttpResponseData> SetupInventory(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "inventory/setup")] HttpRequestData req,
[DurableClient] DurableTaskClient client)
{
var inventory = await JsonSerializer.DeserializeAsync<Dictionary<string, int>>(req.Body, SerializerOptions());
if (inventory is null)
{
var badRes = req.CreateResponse(HttpStatusCode.BadRequest);
await badRes.WriteStringAsync("Invalid inventory payload.");
return badRes;
}

foreach (var (sku, qty) in inventory)
{
await client.Entities.SignalEntityAsync(
new EntityInstanceId("InventoryEntity", "store"),
"AddOrUpdate",
new Item(sku, qty));
}

var res = req.CreateResponse(HttpStatusCode.OK);
await res.WriteStringAsync("Inventory updated.");
return res;
}

// GET /inventory
[Function("GetInventory")]
public static async Task<HttpResponseData> GetInventory(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "inventory")] HttpRequestData req,
[DurableClient] DurableTaskClient client)
{
string instanceId = $"query-inventory-{Guid.NewGuid()}";

await client.ScheduleNewOrchestrationInstanceAsync("QueryInventory", instanceId);

return await client.CreateCheckStatusResponseAsync(req, instanceId);
}

private static JsonSerializerOptions SerializerOptions() => new()
{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
}
50 changes: 50 additions & 0 deletions samples/isolated-entities/InventoryEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Microsoft.DurableTask.Entities;
using Microsoft.Azure.Functions.Worker;
using InventorySample;

public class InventoryEntity
{
public Dictionary<string, int> Stock { get; set; } = new();

[Function(nameof(InventoryEntity))]
public static Task HandleAsync([EntityTrigger] TaskEntityDispatcher dispatcher)
=> dispatcher.DispatchAsync<InventoryEntity>();

public List<Item> GetAll()
{
return Stock.Select(item => new Item(item.Key, item.Value)).ToList();
}

public void AddOrUpdate(Item update)
{
if (!Stock.ContainsKey(update.Sku))
Stock[update.Sku] = 0;

Stock[update.Sku] += update.Qty;
}

public bool TryRemoveMany(List<OrderLine> items)
{
foreach (var item in items)
if (!Stock.ContainsKey(item.Sku) || Stock[item.Sku] < item.Qty)
return false;

foreach (var item in items)
Stock[item.Sku] -= item.Qty;

return true;
}

public void AddMany(List<OrderLine> items)
{
foreach (var item in items)
{
if (!Stock.ContainsKey(item.Sku))
Stock[item.Sku] = 0;

Stock[item.Sku] += item.Qty;
}
}
}

public record Item(string Sku, int Qty);
22 changes: 22 additions & 0 deletions samples/isolated-entities/InventorySample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" VersionOverride="2.23.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker" VersionOverride="2.1.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" VersionOverride="2.0.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" VersionOverride="1.6.1" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" VersionOverride="3.3.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" VersionOverride="2.0.2" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" VersionOverride="2.0.5" />
</ItemGroup>

</Project>
40 changes: 40 additions & 0 deletions samples/isolated-entities/OrderOrchestrator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using InventorySample;
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Entities;

public static class OrderOrchestrator
{
[Function(nameof(OrderOrchestrator))]
public static async Task<string> Run(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var input = context.GetInput<OrderRequest>()!;
var entityId = new EntityInstanceId(nameof(InventoryEntity), "store");

var stockOk = await context.Entities.CallEntityAsync<bool>(
entityId, "TryRemoveMany", input.Items);

if (!stockOk)
return $"Order {input.OrderId} failed: not enough stock.";

var paid = await context.CallActivityAsync<bool>("ChargePayment", input);
if (!paid)
{
await context.Entities.CallEntityAsync<object>(entityId, "AddMany", input.Items);
return $"Order {input.OrderId} failed: payment declined.";
}

await context.CallActivityAsync("SendEmail", input.Email);
return $"Order {input.OrderId} succeeded.";
}
}

public record OrderRequest(
string OrderId,
List<OrderLine> Items,
string Email,
decimal Amount
);

public record OrderLine(string Sku, int Qty);
14 changes: 14 additions & 0 deletions samples/isolated-entities/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = FunctionsApplication.CreateBuilder(args);

builder.ConfigureFunctionsWebApplication();

builder.Services
.AddApplicationInsightsTelemetryWorkerService()
.ConfigureFunctionsApplicationInsights();

builder.Build().Run();
11 changes: 11 additions & 0 deletions samples/isolated-entities/Properties/serviceDependencies.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"dependencies": {
"appInsights1": {
"type": "appInsights"
},
"storage1": {
"type": "storage",
"connectionId": "AzureWebJobsStorage"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"dependencies": {
"appInsights1": {
"type": "appInsights.sdk"
},
"storage1": {
"type": "storage.emulator",
"connectionId": "AzureWebJobsStorage"
}
}
}
18 changes: 18 additions & 0 deletions samples/isolated-entities/QueryInventory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Text.Json;
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Entities;

public static class QueryInventory
{
[Function("QueryInventory")]
public static async Task GetInventory(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var stock = await context.Entities.CallEntityAsync<List<Item>>(
new EntityInstanceId("InventoryEntity", "store"),
"GetAll");

Console.WriteLine(JsonSerializer.Serialize(stock));
}
}
84 changes: 84 additions & 0 deletions samples/isolated-entities/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Inventory Management Sample - Azure Functions Durable Entities

A sample inventory management system built with Azure Functions using Durable Entities and the isolated worker model.

## Overview

This sample demonstrates how to use Azure Durable Entities to manage inventory state in a distributed system. It implements a simple e-commerce scenario where orders are processed, inventory is checked, payments are charged, and confirmation emails are sent.

## Prerequisites

- .NET 8.0
- Azure Functions Core Tools
- Azure Storage Account (for local development)

## Running Locally

1. Clone the repository
2. Navigate to the project directory
3. Ensure you have a `local.settings.json` file with Azure Storage configuration
4. Run the function app:
```
func start
```
5. The function app will start on `http://localhost:7071`

## API Endpoints & Usage

### 1. Setup Inventory First
Before processing orders, you need to set up your inventory:

```bash
curl -X POST http://localhost:7071/api/inventory/setup \
-H "Content-Type: application/json" \
-d '{
"LAPTOP": 10,
"MOUSE": 50,
"KEYBOARD": 25
}'
```

**Response:** `Inventory updated.`

### 2. Start an Order
Process a new order:

```bash
curl -X POST http://localhost:7071/api/orders \
-H "Content-Type: application/json" \
-d '{
"orderId": "ORD-001",
"items": [
{ "sku": "LAPTOP", "qty": 1 },
{ "sku": "MOUSE", "qty": 2 }
],
"email": "[email protected]",
"amount": 599.99
}'
```

**Response:** `Order ORD-001 started.`

### 3. Check Inventory Levels
Query current inventory:

```bash
curl -X GET http://localhost:7071/api/inventory
```

**Response:** Prints the inventory to the console.

## How It Works

1. **Order Submission**: Orders are submitted via HTTP and processed by the `OrderOrchestrator`
2. **Inventory Check**: The orchestrator calls the `InventoryEntity` to check and reserve stock
3. **Payment Processing**: If stock is available, payment is processed via the `ChargePayment` activity
4. **Email Notification**: On successful payment, a confirmation email is sent via the `SendEmail` activity
5. **Rollback**: If payment fails, reserved inventory is released

## Architecture

- **InventoryEntity**: Durable entity managing product stock levels
- **OrderOrchestrator**: Orchestrates the order processing workflow
- **InventoryEndpoints**: HTTP triggers for API endpoints
- **Activities**: ChargePayment and SendEmail activity functions
13 changes: 13 additions & 0 deletions samples/isolated-entities/SendEmail.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;

public static class SendEmail
{
[Function(nameof(SendEmail))]
public static Task Run([ActivityTrigger] string email)
{
// Simulate sending an email
Console.WriteLine($"Order confirmation email sent to {email}");
return Task.CompletedTask;
}
}
Loading
Loading