├── DurableECommerceWorkflow ├── host.json ├── Models │ ├── OrderResult.cs │ ├── ApprovalResult.cs │ ├── OrderEntity.cs │ └── Order.cs ├── Properties │ ├── launchSettings.json │ ├── serviceDependencies.json │ └── serviceDependencies.local.json ├── Functions │ ├── BlobClientExtensions.cs │ ├── ApproveOrderFunctions.cs │ ├── CreateOrderFunctions.cs │ ├── OrchestratorFunctions.cs │ ├── OrderStatusFunctions.cs │ ├── ExampleOrchestratorFunctions.cs │ └── ActivityFunctions.cs ├── DurableECommerceWorkflow.csproj └── .gitignore ├── DurableECommerceWeb ├── wwwroot │ ├── images │ │ ├── cakes.jpg │ │ ├── strat.jpg │ │ └── football.jpg │ ├── scripts │ │ ├── admin.js │ │ ├── orderStatus.js │ │ └── index.js │ ├── admin.html │ ├── orderStatus.html │ └── index.html ├── DurableECommerceWeb.csproj ├── Program.cs ├── Properties │ └── launchSettings.json └── Startup.cs ├── DurableECommerceWorkflowIsolated ├── Models │ ├── OrderResult.cs │ ├── ApprovalResult.cs │ ├── OrderEntity.cs │ └── Order.cs ├── Properties │ ├── launchSettings.json │ ├── serviceDependencies.json │ └── serviceDependencies.local.json ├── host.json ├── Extensions │ ├── BlobClientExtensions.cs │ └── SendGridClientExtensions.cs ├── Program.cs ├── DurableECommerceWorkflowIsolated.csproj ├── ApiFunctions │ ├── CreateOrderFunctions.cs │ ├── ApproveOrderFunctions.cs │ └── OrderStatusFunctions.cs ├── Functions │ ├── OrchestratorFunctions.cs │ ├── ExampleOrchestratorFunctions.cs │ └── ActivityFunctions.cs └── .gitignore ├── DurableECommerceTests ├── Properties │ └── launchSettings.json ├── DurableECommerceTests.csproj ├── ActivityFunctionTests.cs └── OrchestratorFunctionTests.cs ├── DurableECommerceWorkflow.sln.DotSettings ├── .gitattributes ├── DurableECommerceWorkflow.sln ├── README.md └── .gitignore /DurableECommerceWorkflow/host.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /DurableECommerceWeb/wwwroot/images/cakes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markheath/durable-functions-ecommerce-sample/HEAD/DurableECommerceWeb/wwwroot/images/cakes.jpg -------------------------------------------------------------------------------- /DurableECommerceWeb/wwwroot/images/strat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markheath/durable-functions-ecommerce-sample/HEAD/DurableECommerceWeb/wwwroot/images/strat.jpg -------------------------------------------------------------------------------- /DurableECommerceWeb/wwwroot/images/football.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markheath/durable-functions-ecommerce-sample/HEAD/DurableECommerceWeb/wwwroot/images/football.jpg -------------------------------------------------------------------------------- /DurableECommerceWeb/DurableECommerceWeb.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DurableECommerceWorkflow/Models/OrderResult.cs: -------------------------------------------------------------------------------- 1 | namespace DurableECommerceWorkflow.Models; 2 | 3 | public class OrderResult 4 | { 5 | public string Status { get; set; } 6 | public string[] Downloads { get; set; } 7 | } -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/Models/OrderResult.cs: -------------------------------------------------------------------------------- 1 | namespace DurableECommerceWorkflowIsolated.Models; 2 | 3 | public class OrderResult 4 | { 5 | public string? Status { get; set; } 6 | public string[]? Downloads { get; set; } 7 | } -------------------------------------------------------------------------------- /DurableECommerceWorkflow/Models/ApprovalResult.cs: -------------------------------------------------------------------------------- 1 | namespace DurableECommerceWorkflow.Models; 2 | 3 | public class ApprovalResult 4 | { 5 | public string OrchestrationId { get; set; } 6 | public bool Approved { get; set; } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /DurableECommerceWorkflow/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "DurableECommerceWorkflowInProc": { 4 | "commandName": "Project", 5 | "commandLineArgs": "--port 7071", 6 | "launchBrowser": false 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/Models/ApprovalResult.cs: -------------------------------------------------------------------------------- 1 | namespace DurableECommerceWorkflowIsolated.Models; 2 | 3 | public class ApprovalResult 4 | { 5 | public string? OrchestrationId { get; set; } 6 | public bool Approved { get; set; } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "DurableECommerceWorkflowIsolated": { 4 | "commandName": "Project", 5 | "commandLineArgs": "--port 7071", 6 | "launchBrowser": false 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /DurableECommerceWorkflow/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights" 5 | }, 6 | "storage1": { 7 | "type": "storage", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights" 5 | }, 6 | "storage1": { 7 | "type": "storage", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /DurableECommerceWorkflow/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights.sdk" 5 | }, 6 | "storage1": { 7 | "type": "storage.emulator", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appInsights1": { 4 | "type": "appInsights.sdk" 5 | }, 6 | "storage1": { 7 | "type": "storage.emulator", 8 | "connectionId": "AzureWebJobsStorage" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /DurableECommerceTests/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "DurableECommerceTests": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "https://localhost:55912;http://localhost:55913" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /DurableECommerceWeb/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace DurableECommerceWeb; 5 | 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateWebHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 14 | WebHost.CreateDefaultBuilder(args) 15 | .UseStartup(); 16 | } 17 | -------------------------------------------------------------------------------- /DurableECommerceWorkflow.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> -------------------------------------------------------------------------------- /DurableECommerceWorkflow/Models/OrderEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DurableECommerceWorkflow.Models; 4 | 5 | // for the purposes of storing in table storage 6 | public class OrderEntity 7 | { 8 | public const string TableName = "Orders"; 9 | public const string OrderPartitionKey = "ORDER"; 10 | 11 | public string PartitionKey { get; set; } = OrderPartitionKey; 12 | public string RowKey { get; set; } 13 | public string OrchestrationId { get; set; } 14 | public string Items { get; set; } 15 | public string Email { get; set; } 16 | public DateTime OrderDate { get; set; } 17 | public decimal Amount { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/Extensions/BlobClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using Azure.Storage.Blobs; 2 | 3 | namespace DurableECommerceWorkflowIsolated.Extensions; 4 | 5 | internal static class BlobClientExtensions 6 | { 7 | public static Task UploadTextAsync(this BlobClient client, string text) 8 | { 9 | var content = new BinaryData(text); 10 | return client.UploadAsync(content, true); // support overwrite as we use this to update blobs 11 | } 12 | 13 | public async static Task DownloadTextAsync(this BlobClient client) 14 | { 15 | var res = await client.DownloadContentAsync(); 16 | return res.Value.Content.ToString(); 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /DurableECommerceTests/DurableECommerceTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /DurableECommerceWorkflow/Functions/BlobClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Azure.Storage.Blobs; 4 | 5 | namespace DurableECommerceWorkflow.Functions; 6 | 7 | internal static class BlobClientExtensions 8 | { 9 | public static Task UploadTextAsync(this BlobClient client, string text) 10 | { 11 | var content = new BinaryData(text); 12 | return client.UploadAsync(content, true); // support overwrite as we use this to update blobs 13 | } 14 | 15 | public async static Task DownloadTextAsync(this BlobClient client) 16 | { 17 | var res = await client.DownloadContentAsync(); 18 | return res.Value.Content.ToString(); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /DurableECommerceWeb/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:54045", 7 | "sslPort": 44358 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "DurableECommerceWeb": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/Models/OrderEntity.cs: -------------------------------------------------------------------------------- 1 | using Azure; 2 | using Azure.Data.Tables; 3 | 4 | namespace DurableECommerceWorkflowIsolated.Models; 5 | 6 | // for the purposes of storing in table storage 7 | public class OrderEntity : ITableEntity 8 | { 9 | public const string TableName = "Orders"; 10 | public const string OrderPartitionKey = "ORDER"; 11 | 12 | public string PartitionKey { get; set; } = OrderPartitionKey; 13 | public string? RowKey { get; set; } 14 | public string OrchestrationId { get; set; } = String.Empty; 15 | public string? Items { get; set; } 16 | public string? Email { get; set; } 17 | public DateTime OrderDate { get; set; } 18 | public decimal Amount { get; set; } 19 | public DateTimeOffset? Timestamp { get; set; } 20 | public ETag ETag { get; set; } 21 | } 22 | -------------------------------------------------------------------------------- /DurableECommerceWorkflow/Models/Order.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace DurableECommerceWorkflow.Models; 5 | 6 | public class Order 7 | { 8 | public string Id { get; set; } = Guid.NewGuid().ToString("N"); 9 | public DateTime Date { get; set; } = DateTime.Now; 10 | public string PurchaserEmail { get; set; } 11 | public OrderItem[] Items { get; set; } 12 | public string OrchestrationId { get; set; } 13 | } 14 | 15 | public static class OrderExtensions 16 | { 17 | public static int ItemCount(this Order order) 18 | { 19 | return order.Items.Length; 20 | } 21 | 22 | public static decimal Total(this Order order) 23 | { 24 | return order.Items.Sum(i => i.Amount); 25 | } 26 | } 27 | 28 | public class OrderItem 29 | { 30 | public string ProductId { get; set; } 31 | public decimal Amount { get; set; } 32 | 33 | } 34 | 35 | -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/Extensions/SendGridClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using SendGrid; 3 | using SendGrid.Helpers.Mail; 4 | 5 | namespace DurableECommerceWorkflowIsolated.Extensions; 6 | 7 | static class SendGridClientExtensions 8 | { 9 | public static async Task PostAsync(this ISendGridClient sender, SendGridMessage message, ILogger log) 10 | { 11 | var sendGridKey = Environment.GetEnvironmentVariable("SendGridKey"); 12 | // don't actually try to send SendGrid emails if we are just using example or missing email addresses 13 | var testMode = string.IsNullOrEmpty(sendGridKey) || sendGridKey == "TEST" || message.Personalizations.SelectMany(p => p.Tos.Select(t => t.Email)) 14 | .Any(e => string.IsNullOrEmpty(e) || e.Contains("@example") || e.Contains("@email")); 15 | if (testMode) 16 | { 17 | log.LogWarning($"Sending email with body {message.HtmlContent}"); 18 | } 19 | else 20 | { 21 | await sender.SendEmailAsync(message); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/Models/Order.cs: -------------------------------------------------------------------------------- 1 | namespace DurableECommerceWorkflowIsolated.Models; 2 | 3 | public class Order 4 | { 5 | public string Id { get; set; } = Guid.NewGuid().ToString("N"); 6 | public DateTime Date { get; set; } = DateTime.Now; 7 | public string PurchaserEmail { get; set; } = String.Empty; 8 | public OrderItem[] Items { get; set; } = Array.Empty(); 9 | public string OrchestrationId { get; set; } = String.Empty; 10 | } 11 | 12 | public record PdfInfo(string OrderId, string ProductId, string PurchaserEmail); 13 | public record ConfirmationInfo(Order Order, string[] Files); 14 | 15 | public static class OrderExtensions 16 | { 17 | public static int ItemCount(this Order order) 18 | { 19 | return order.Items?.Length ?? 0; 20 | } 21 | 22 | public static decimal Total(this Order order) 23 | { 24 | return order.Items?.Sum(i => i.Amount) ?? 0; 25 | } 26 | } 27 | 28 | public class OrderItem 29 | { 30 | public string? ProductId { get; set; } 31 | public decimal Amount { get; set; } 32 | 33 | } 34 | 35 | -------------------------------------------------------------------------------- /DurableECommerceWorkflow/DurableECommerceWorkflow.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | v4 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | 17 | 18 | PreserveNewest 19 | Never 20 | 21 | 22 | -------------------------------------------------------------------------------- /DurableECommerceWeb/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | namespace DurableECommerceWeb; 8 | 9 | public class Startup 10 | { 11 | private readonly IWebHostEnvironment environment; 12 | 13 | public Startup(IConfiguration configuration, IWebHostEnvironment environment) 14 | { 15 | this.environment = environment; 16 | } 17 | 18 | // This method gets called by the runtime. Use this method to add services to the container. 19 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 20 | public void ConfigureServices(IServiceCollection services) 21 | { 22 | //services.AddMvc(); 23 | } 24 | 25 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 26 | public void Configure(IApplicationBuilder app) 27 | { 28 | if (environment.IsDevelopment()) 29 | { 30 | app.UseDeveloperExceptionPage(); 31 | } 32 | app.UseDefaultFiles(); 33 | app.UseStaticFiles(); 34 | //app.UseMvc(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using Microsoft.Extensions.Azure; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using SendGrid.Extensions.DependencyInjection; 7 | 8 | var sendGridKey = Environment.GetEnvironmentVariable("SendGridKey"); 9 | var host = new HostBuilder() 10 | .ConfigureFunctionsWorkerDefaults() // supposedly setting camelCase 11 | .ConfigureServices(services => 12 | { 13 | services.Configure(options => 14 | { 15 | // options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; // https://stackoverflow.com/questions/60574428/how-to-configure-a-default-jsonserializeroptions-system-text-json-to-be-used-b 16 | options.Converters.Add(new JsonStringEnumConverter()); 17 | }); 18 | services.AddSendGrid(options => options.ApiKey = sendGridKey); 19 | services.AddAzureClients(clientBuilder => 20 | { 21 | clientBuilder.AddBlobServiceClient(Environment.GetEnvironmentVariable("AzureWebJobsStorage")); 22 | clientBuilder.AddTableServiceClient(Environment.GetEnvironmentVariable("AzureWebJobsStorage")); 23 | // clientBuilder.UseCredential(new DefaultAzureCredential()); 24 | }); 25 | 26 | }) 27 | .Build(); 28 | 29 | host.Run(); -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/DurableECommerceWorkflowIsolated.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net7.0 4 | v4 5 | Exe 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | PreserveNewest 30 | Never 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /DurableECommerceTests/ActivityFunctionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using DurableECommerceWorkflow.Functions; 5 | using DurableECommerceWorkflow.Models; 6 | using Microsoft.Azure.WebJobs; 7 | using Microsoft.Extensions.Logging; 8 | using Moq; 9 | using NUnit.Framework; 10 | 11 | namespace DurableECommerceTests; 12 | 13 | public class ActivityFunctionTests 14 | { 15 | private ILogger mockLogger; 16 | 17 | [SetUp] 18 | public void Setup() 19 | { 20 | mockLogger = Mock.Of(); 21 | } 22 | 23 | [Test] 24 | public async Task CanSaveOrderToDatabase() 25 | { 26 | var order = CreateTestOrder(); 27 | var collector = new Mock>(); 28 | OrderEntity entity = null; 29 | collector.Setup(c => c.AddAsync(It.IsAny(), It.IsAny())) 30 | .Callback((OrderEntity oe, CancellationToken ct) => entity = oe) 31 | .Returns(Task.CompletedTask); 32 | 33 | await ActivityFunctions.SaveOrderToDatabase(order, collector.Object, mockLogger); 34 | 35 | Assert.That(entity, Is.Not.Null); 36 | Assert.That(entity.OrchestrationId, Is.EqualTo(order.OrchestrationId)); 37 | Assert.That(entity.OrderDate, Is.EqualTo(order.Date)); 38 | Assert.That(entity.Amount, Is.EqualTo(order.Total())); 39 | Assert.That(entity.Email, Is.EqualTo(order.PurchaserEmail)); 40 | Assert.That(entity.RowKey, Is.EqualTo(order.Id)); 41 | Assert.That(entity.PartitionKey, Is.EqualTo(OrderEntity.OrderPartitionKey)); 42 | } 43 | 44 | private static Order CreateTestOrder() 45 | { 46 | var order = new Order 47 | { 48 | Id = "102030", 49 | OrchestrationId = "100200", 50 | Items = new [] 51 | { 52 | new OrderItem {ProductId = "Prod1", Amount = 1234 } 53 | }, 54 | Date = DateTime.Now, 55 | PurchaserEmail = "test@example.com" 56 | }; 57 | return order; 58 | } 59 | // ActivityFunctions.CreatePersonalizedPdf - can't easily mock a CloudBlobContainer 60 | 61 | } -------------------------------------------------------------------------------- /DurableECommerceWorkflow/Functions/ApproveOrderFunctions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using DurableECommerceWorkflow.Models; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Azure.WebJobs; 7 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 8 | using Microsoft.Azure.WebJobs.Extensions.Http; 9 | using Microsoft.Extensions.Logging; 10 | using Newtonsoft.Json; 11 | 12 | namespace DurableECommerceWorkflow.Functions; 13 | 14 | public static class ApproveOrderFunctions 15 | { 16 | [FunctionName(nameof(ApproveOrderById))] 17 | public static async Task ApproveOrderById( 18 | [HttpTrigger(AuthorizationLevel.Anonymous, 19 | "post", Route = "approve/{id}")]HttpRequest req, 20 | [DurableClient] IDurableOrchestrationClient client, 21 | [Table(OrderEntity.TableName, OrderEntity.OrderPartitionKey, "{id}", Connection = "AzureWebJobsStorage")] OrderEntity order, 22 | ILogger log, string id) 23 | { 24 | log.LogInformation($"Setting approval status of order {id}"); 25 | 26 | if (order == null) 27 | { 28 | return new NotFoundResult(); 29 | } 30 | 31 | var body = await req.ReadAsStringAsync(); // should be "Approved" or "Rejected" 32 | var status = JsonConvert.DeserializeObject(body); 33 | await client.RaiseEventAsync(order.OrchestrationId, "OrderApprovalResult", status); 34 | 35 | return new OkResult(); 36 | } 37 | 38 | [FunctionName(nameof(ApproveOrder))] 39 | public static async Task ApproveOrder( 40 | [HttpTrigger(AuthorizationLevel.Anonymous, 41 | "post", Route = null)]HttpRequest req, 42 | [DurableClient] IDurableOrchestrationClient client, 43 | ILogger log) 44 | { 45 | log.LogInformation("Received an approval result."); 46 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 47 | var approvalResult = JsonConvert.DeserializeObject(requestBody); 48 | await client.RaiseEventAsync(approvalResult.OrchestrationId, "OrderApprovalResult", approvalResult.Approved ? "Approved" : "Rejected"); 49 | log.LogInformation($"Approval Result for {approvalResult.OrchestrationId} is {approvalResult.Approved}"); 50 | return new OkResult(); 51 | } 52 | } -------------------------------------------------------------------------------- /DurableECommerceWorkflow/Functions/CreateOrderFunctions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using DurableECommerceWorkflow.Models; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Azure.WebJobs; 8 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 9 | using Microsoft.Azure.WebJobs.Extensions.Http; 10 | using Microsoft.Extensions.Logging; 11 | using Newtonsoft.Json; 12 | 13 | namespace DurableECommerceWorkflow.Functions; 14 | 15 | public static class CreateOrderFunctions 16 | { 17 | [FunctionName(nameof(CreateOrder))] 18 | public static async Task CreateOrder( 19 | [HttpTrigger(AuthorizationLevel.Anonymous, 20 | "post", Route = null)]HttpRequest req, 21 | [DurableClient] IDurableOrchestrationClient client, 22 | ILogger log) 23 | { 24 | log.LogInformation("Received a new order from website."); 25 | 26 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 27 | var order = JsonConvert.DeserializeObject(requestBody); 28 | 29 | // use shorter order ids for the webpage 30 | var r = new Random(); 31 | order.Id = r.Next(10000, 100000).ToString(); 32 | 33 | log.LogWarning($"Order {order.Id} for {order.ItemCount()} items, total {order.Total()}"); 34 | 35 | var orchestrationId = await client.StartNewAsync("O_ProcessOrder", order); 36 | return new OkObjectResult(new { order.Id }); 37 | } 38 | 39 | [FunctionName(nameof(NewPurchaseWebhook))] 40 | public static async Task NewPurchaseWebhook( 41 | [HttpTrigger(AuthorizationLevel.Anonymous, 42 | "post", Route = null)]HttpRequest req, 43 | [DurableClient] IDurableOrchestrationClient client, 44 | ILogger log) 45 | { 46 | log.LogInformation("Received an order webhook."); 47 | 48 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 49 | var order = JsonConvert.DeserializeObject(requestBody); 50 | log.LogInformation($"Order is from {order.PurchaserEmail} for {order.ItemCount()} items, total {order.Total()}"); 51 | 52 | var orchestrationId = await client.StartNewAsync("O_ProcessOrder", order); 53 | var statusUris = client.CreateHttpManagementPayload(orchestrationId); 54 | return new OkObjectResult(statusUris); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /DurableECommerceWeb/wwwroot/scripts/admin.js: -------------------------------------------------------------------------------- 1 | const baseUrl = 'http://localhost:7071/api' 2 | var postData = (url, data) => { 3 | return fetch(url, 4 | { 5 | method: "POST", 6 | headers: { 7 | "Content-Type": "application/json; charset=utf-8" 8 | }, 9 | body: JSON.stringify(data) 10 | }); 11 | }; 12 | 13 | Vue.filter('formatDate', 14 | function (value) { 15 | var d = new Date(value); 16 | return d.toLocaleDateString(); 17 | }); 18 | 19 | Vue.filter('formatOrderItems', 20 | function (value) { 21 | var x = ""; 22 | for (var i = 0; i < value.length; i++) { 23 | x += value[i].productId + ", "; 24 | } 25 | return x.substring(0,x.length-2); 26 | }); 27 | 28 | Vue.filter('formatRuntimeStatus', 29 | function (value) { 30 | return ["Running", 31 | "Completed", 32 | "ContinuedAsNew", 33 | "Failed", 34 | "Canceled", 35 | "Terminated", 36 | "Pending"][value]; 37 | }); 38 | 39 | var app = new Vue({ 40 | el: '#app', 41 | data: { 42 | orders: null, 43 | errorMessage: null 44 | }, 45 | mounted: function () { 46 | this.getOrderStatuses(); 47 | }, 48 | methods: { 49 | getOrderStatuses: function () { 50 | this.errorMessage = null; 51 | fetch(`${baseUrl}/getallorders/`) 52 | .then(response => response.json()) 53 | .then(json => { 54 | this.orders = json; 55 | }) 56 | .catch(err => { 57 | this.errorMessage = `failed to get orders (${err})`; 58 | }); 59 | }, 60 | approve: function (order, status) { 61 | postData(`${baseUrl}/approve/${order.input.id}`, status) 62 | .then(_ => order.customStatus = ''); 63 | }, 64 | deleteOrder: function (order) { 65 | fetch(`${baseUrl}/order/${order.input.id}`, { method: 'DELETE' }) 66 | .then(_ => { 67 | var index = this.orders.indexOf(order); 68 | if (index > -1) { 69 | this.orders.splice(index, 1); 70 | } 71 | }) 72 | .catch(err => { 73 | this.errorMessage = `failed to delete order (${err})`; 74 | }); 75 | } 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /DurableECommerceWeb/wwwroot/scripts/orderStatus.js: -------------------------------------------------------------------------------- 1 | const baseUrl = 'http://localhost:7071/api' 2 | // get a query string parameter https://stackoverflow.com/a/901144/7532 3 | function getParameterByName(name, url) { 4 | if (!url) url = window.location.href; 5 | name = name.replace(/[\[\]]/g, '\\$&'); 6 | var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), 7 | results = regex.exec(url); 8 | if (!results) return null; 9 | if (!results[2]) return ''; 10 | return decodeURIComponent(results[2].replace(/\+/g, ' ')); 11 | } 12 | 13 | function getOrderId() { 14 | var id = getParameterByName('id'); 15 | if (id) return id; 16 | var pathArray = window.location.pathname.split('/'); 17 | return pathArray[pathArray.length - 1]; 18 | } 19 | 20 | Vue.filter('formatDateTime', 21 | function (value) { 22 | var d = new Date(value); 23 | return d.toLocaleString(); 24 | }); 25 | 26 | Vue.filter('formatRuntimeStatus', 27 | function (value) { 28 | return ["Running", "Completed", "ContinuedAsNew", "Failed", "Canceled", "Terminated", "Pending"][value]; 29 | }); 30 | 31 | Vue.component('order-status', 32 | { 33 | props: ['orderStatus'], 34 | template: '#orderStatusTemplate' 35 | }); 36 | 37 | var postData = (url, data) => 38 | fetch(url, 39 | { 40 | method: "POST", 41 | headers: { 42 | "Content-Type": "application/json; charset=utf-8" 43 | }, 44 | body: JSON.stringify(data) 45 | }); 46 | 47 | var app = new Vue({ 48 | el: '#app', 49 | data: { 50 | orderId: null, 51 | orderStatus: null, 52 | errorMessage: null 53 | }, 54 | mounted: function () { 55 | this.orderId = getOrderId(); 56 | this.checkStatus(); 57 | }, 58 | 59 | methods: { 60 | checkStatus: function (event) { 61 | if (event) event.preventDefault(); 62 | this.errorMessage = null; 63 | this.orderStatus = null; 64 | fetch(`${baseUrl}/orderstatus/${this.orderId}`) 65 | .then(response => { 66 | if (response.status === 404) { 67 | this.errorMessage = `Order ${this.orderId} not found`; 68 | } else { 69 | response.json().then(json => this.orderStatus = json); 70 | } 71 | } 72 | ) 73 | .catch(err => console.error(err)); 74 | } 75 | } 76 | }); -------------------------------------------------------------------------------- /DurableECommerceWeb/wwwroot/scripts/index.js: -------------------------------------------------------------------------------- 1 | const baseUrl = 'http://localhost:7071/api' 2 | var postData = function (url, data) { 3 | return fetch(url, 4 | { 5 | method: "POST", 6 | headers: { 7 | "Content-Type": "application/json; charset=utf-8" 8 | }, 9 | body: JSON.stringify(data) 10 | }) 11 | .then(response => response.json()); 12 | }; 13 | 14 | var app = new Vue({ 15 | el: '#app', 16 | data: { 17 | products: [ 18 | { 19 | id: 'performance-tuning', 20 | name: 'Performance Tuning', 21 | price: 2000, 22 | description: 'Make things go faster', 23 | image: 'images/strat.jpg' 24 | }, 25 | { 26 | id: 'achieving-your-goals', 27 | name: 'Achieving Your Goals', 28 | price: 400, 29 | description: 'Make your dreams come true', 30 | image: 'images/football.jpg' 31 | }, 32 | { 33 | id: 'cake-driven-development', 34 | name: 'Cake Driven Development', 35 | price: 50, 36 | description: 'Make your code tastier', 37 | image: 'images/cakes.jpg' 38 | } 39 | ], 40 | orderId: null, 41 | cart: [], 42 | email: 'durable-funcs-customer@mailinator.com' 43 | }, 44 | methods: { 45 | onOrderCreated: function (orderInfo) { 46 | $('#shoppingCart').modal('hide'); 47 | this.orderId = orderInfo.id; 48 | this.cart.length = 0; 49 | }, 50 | addToCart: function(product) { 51 | if (this.cart.indexOf(product) === -1) 52 | this.cart.push(product); 53 | }, 54 | removeFromCart: function (product) { 55 | var index = this.cart.indexOf(product); 56 | if (index > -1) { 57 | this.cart.splice(index, 1); 58 | } 59 | }, 60 | buy: function () { 61 | var items = []; 62 | for (var i = 0; i < this.cart.length; i++) { 63 | items.push({ 64 | ProductId: this.cart[i].id, 65 | Amount: this.cart[i].price 66 | }); 67 | } 68 | postData(`${baseUrl}/CreateOrder`, 69 | { 70 | Items: items, 71 | PurchaserEmail: this.email 72 | }) 73 | .then(data => this.onOrderCreated(data)) 74 | .catch(error => console.error(error)); 75 | } 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /DurableECommerceWeb/wwwroot/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Durable Functions Shop Admin Console 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

Admin Console

18 |
19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 47 | 48 | 49 |
Order IdCreatedProductStatus
{{order.input.id}}{{order.createdTime | formatDate}}{{order.input.items | formatOrderItems}}{{order.runtimeStatus | formatRuntimeStatus}} 39 | 40 | Approve 41 | Reject 42 | 43 | 44 | 45 | 46 |
50 |
51 |
52 |
53 |
54 | {{errorMessage}} 55 |
56 |
57 |
58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /DurableECommerceWorkflow/Functions/OrchestratorFunctions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using DurableECommerceWorkflow.Models; 6 | using Microsoft.Azure.WebJobs; 7 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace DurableECommerceWorkflow.Functions; 11 | 12 | public static class OrchestratorFunctions 13 | { 14 | [FunctionName("O_ProcessOrder")] 15 | public static async Task ProcessOrder( 16 | [OrchestrationTrigger] IDurableOrchestrationContext ctx, 17 | ILogger log) 18 | { 19 | var order = ctx.GetInput(); 20 | order.OrchestrationId = ctx.InstanceId; 21 | 22 | if (!ctx.IsReplaying) 23 | log.LogInformation($"Processing order #{order.Id}"); 24 | 25 | var total = order.Items.Sum(i => i.Amount); 26 | 27 | await ctx.CallActivityAsync("A_SaveOrderToDatabase", order); 28 | 29 | if (total > 1000) 30 | { 31 | if (!ctx.IsReplaying) 32 | log.LogWarning($"Need approval for {ctx.InstanceId}"); 33 | 34 | ctx.SetCustomStatus("Needs approval"); 35 | await ctx.CallActivityAsync("A_RequestOrderApproval", order); 36 | 37 | var approvalResult = await ctx.WaitForExternalEvent("OrderApprovalResult", TimeSpan.FromSeconds(180), null); 38 | ctx.SetCustomStatus(""); // clear the needs approval flag 39 | 40 | if (approvalResult != "Approved") 41 | { 42 | // timed out or got a rejected 43 | if (!ctx.IsReplaying) 44 | log.LogWarning($"Not approved [{approvalResult}]"); 45 | await ctx.CallActivityAsync("A_SendNotApprovedEmail", order); 46 | return new OrderResult { Status = "NotApproved" }; 47 | } 48 | 49 | } 50 | 51 | string[] downloads = null; 52 | try 53 | { 54 | // create files in parallel 55 | var tasks = new List>(); 56 | foreach (var item in order.Items) 57 | { 58 | tasks.Add(ctx.CallActivityAsync("A_CreatePersonalizedPdf", (order, item))); 59 | } 60 | 61 | downloads = await Task.WhenAll(tasks); 62 | 63 | } 64 | catch (Exception ex) 65 | { 66 | if (!ctx.IsReplaying) 67 | log.LogError($"Failed to create files", ex); 68 | } 69 | 70 | if (downloads != null) 71 | { 72 | await ctx.CallActivityWithRetryAsync("A_SendOrderConfirmationEmail", 73 | new RetryOptions(TimeSpan.FromSeconds(30), 3), 74 | (order, downloads)); 75 | return new OrderResult { Status = "Success", Downloads = downloads }; 76 | } 77 | await ctx.CallActivityWithRetryAsync("A_SendProblemEmail", 78 | new RetryOptions(TimeSpan.FromSeconds(30), 3), 79 | order); 80 | return new OrderResult { Status = "Problem" }; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /DurableECommerceWorkflow.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33122.133 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DurableECommerceWorkflow", "DurableECommerceWorkflow\DurableECommerceWorkflow.csproj", "{E986C168-BA17-4412-897A-0AFC9DA77CD5}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{51D25259-D7B2-4C38-8EF3-42A4BE480B9D}" 9 | ProjectSection(SolutionItems) = preProject 10 | .gitignore = .gitignore 11 | README.md = README.md 12 | EndProjectSection 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DurableECommerceWeb", "DurableECommerceWeb\DurableECommerceWeb.csproj", "{484DA20F-1F96-4CC7-A83F-5010C4D6AE66}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DurableECommerceTests", "DurableECommerceTests\DurableECommerceTests.csproj", "{EE2EF1B7-7CA1-46A5-A8FD-A8CB66825566}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DurableECommerceWorkflowIsolated", "DurableECommerceWorkflowIsolated\DurableECommerceWorkflowIsolated.csproj", "{94715BA2-A99C-476E-878F-73769E5DF7E7}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {E986C168-BA17-4412-897A-0AFC9DA77CD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {E986C168-BA17-4412-897A-0AFC9DA77CD5}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {E986C168-BA17-4412-897A-0AFC9DA77CD5}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {E986C168-BA17-4412-897A-0AFC9DA77CD5}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {484DA20F-1F96-4CC7-A83F-5010C4D6AE66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {484DA20F-1F96-4CC7-A83F-5010C4D6AE66}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {484DA20F-1F96-4CC7-A83F-5010C4D6AE66}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {484DA20F-1F96-4CC7-A83F-5010C4D6AE66}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {EE2EF1B7-7CA1-46A5-A8FD-A8CB66825566}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {EE2EF1B7-7CA1-46A5-A8FD-A8CB66825566}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {EE2EF1B7-7CA1-46A5-A8FD-A8CB66825566}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {EE2EF1B7-7CA1-46A5-A8FD-A8CB66825566}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {94715BA2-A99C-476E-878F-73769E5DF7E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {94715BA2-A99C-476E-878F-73769E5DF7E7}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {94715BA2-A99C-476E-878F-73769E5DF7E7}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {94715BA2-A99C-476E-878F-73769E5DF7E7}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {8F935D4B-2AB5-4EDB-9819-861DA86F4138} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/ApiFunctions/CreateOrderFunctions.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | using Azure.Core.Serialization; 5 | using DurableECommerceWorkflowIsolated.Models; 6 | using Microsoft.Azure.Functions.Worker; 7 | using Microsoft.Azure.Functions.Worker.Http; 8 | using Microsoft.DurableTask.Client; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace DurableECommerceWorkflowIsolated.ApiFunctions; 12 | 13 | public static class CreateOrderFunctions 14 | { 15 | 16 | internal static readonly ObjectSerializer serializer = new JsonObjectSerializer(new JsonSerializerOptions 17 | { 18 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 19 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull 20 | }); 21 | 22 | [Function(nameof(CreateOrder))] 23 | public static async Task CreateOrder( 24 | [HttpTrigger(AuthorizationLevel.Anonymous, 25 | "post", Route = null)]HttpRequestData req, 26 | [DurableClient] DurableTaskClient durableTaskClient, 27 | FunctionContext context) 28 | { 29 | var log = context.GetLogger(nameof(CreateOrder)); 30 | log.LogInformation("Received a new order from website."); 31 | 32 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 33 | var order = JsonSerializer.Deserialize(requestBody); 34 | if (order == null) throw new InvalidOperationException("Failed to deserialize order from request body"); 35 | 36 | // use shorter order ids for the webpage 37 | var r = new Random(); 38 | order.Id = r.Next(10000, 100000).ToString(); 39 | 40 | log.LogWarning($"Order {order.Id} for {order.ItemCount()} items, total {order.Total()}"); 41 | 42 | var orchestrationId = await durableTaskClient.ScheduleNewOrchestrationInstanceAsync("O_ProcessOrder", order); 43 | var resp = req.CreateResponse(HttpStatusCode.OK); 44 | await resp.WriteAsJsonAsync(new { order.Id }, serializer); 45 | return resp; 46 | } 47 | 48 | [Function(nameof(NewPurchaseWebhook))] 49 | public static async Task NewPurchaseWebhook( 50 | [HttpTrigger(AuthorizationLevel.Anonymous, 51 | "post", Route = null)]HttpRequestData req, 52 | [DurableClient] DurableTaskClient durableTaskClient, 53 | FunctionContext context) 54 | { 55 | var log = context.GetLogger(nameof(NewPurchaseWebhook)); 56 | log.LogInformation("Received an order webhook."); 57 | 58 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 59 | var order = JsonSerializer.Deserialize(requestBody); 60 | if (order == null) throw new InvalidOperationException("Failed to deserialize order from request body"); 61 | log.LogInformation($"Order is from {order.PurchaserEmail} for {order.ItemCount()} items, total {order.Total()}"); 62 | 63 | var orchestrationId = await durableTaskClient.ScheduleNewOrchestrationInstanceAsync("O_ProcessOrder", order); 64 | return durableTaskClient.CreateCheckStatusResponse(req, orchestrationId); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/ApiFunctions/ApproveOrderFunctions.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Text.Json; 3 | using Azure.Data.Tables; 4 | using DurableECommerceWorkflowIsolated.Models; 5 | using Microsoft.Azure.Functions.Worker; 6 | using Microsoft.Azure.Functions.Worker.Http; 7 | using Microsoft.DurableTask.Client; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace DurableECommerceWorkflowIsolated.ApiFunctions; 12 | 13 | public static class ApproveOrderFunctions 14 | { 15 | [Function(nameof(ApproveOrderById))] 16 | public static async Task ApproveOrderById( 17 | [HttpTrigger(AuthorizationLevel.Anonymous, 18 | "post", Route = "approve/{id}")]HttpRequestData req, 19 | [DurableClient] DurableTaskClient durableTaskClient, 20 | //[TableInput(OrderEntity.TableName, OrderEntity.OrderPartitionKey, "{id}", Connection = "AzureWebJobsStorage")] OrderEntity order, 21 | FunctionContext functionContext, string id) 22 | { 23 | var log = functionContext.GetLogger(nameof(ApproveOrderById)); 24 | log.LogInformation($"Setting approval status of order {id}"); 25 | 26 | var tableServiceClient = functionContext.InstanceServices.GetRequiredService(); 27 | var tableClient = tableServiceClient.GetTableClient(OrderEntity.TableName); 28 | var orderResp = await tableClient.GetEntityIfExistsAsync(OrderEntity.OrderPartitionKey, id); 29 | 30 | if (!orderResp.HasValue) 31 | { 32 | log.LogWarning($"Cannot find order {id}"); 33 | return req.CreateResponse(HttpStatusCode.NotFound); 34 | } 35 | var order = orderResp.Value; 36 | 37 | var body = await req.ReadAsStringAsync(); // should be "Approved" or "Rejected" 38 | if (body == null) throw new InvalidOperationException("No approval status supplied"); 39 | var status = JsonSerializer.Deserialize(body); 40 | await durableTaskClient.RaiseEventAsync(order.OrchestrationId, "OrderApprovalResult", status); 41 | 42 | return req.CreateResponse(HttpStatusCode.OK); 43 | } 44 | 45 | [Function(nameof(ApproveOrder))] 46 | public static async Task ApproveOrder( 47 | [HttpTrigger(AuthorizationLevel.Anonymous, 48 | "post", Route = null)]HttpRequestData req, 49 | [DurableClient] DurableTaskClient client, 50 | FunctionContext functionContext) 51 | { 52 | var log = functionContext.GetLogger(nameof(ApproveOrder)); 53 | log.LogInformation("Received an approval result."); 54 | ApprovalResult approvalResult = await GetApprovalResult(req); 55 | await client.RaiseEventAsync(approvalResult.OrchestrationId!, "OrderApprovalResult", approvalResult.Approved ? "Approved" : "Rejected"); 56 | log.LogInformation($"Approval Result for {approvalResult.OrchestrationId} is {approvalResult.Approved}"); 57 | return req.CreateResponse(HttpStatusCode.OK); 58 | } 59 | 60 | private static async Task GetApprovalResult(HttpRequestData req) 61 | { 62 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 63 | var approvalResult = JsonSerializer.Deserialize(requestBody); 64 | if (approvalResult == null || approvalResult.OrchestrationId == null) throw new InvalidOperationException("Invalid approval"); 65 | return approvalResult; 66 | } 67 | } -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/Functions/OrchestratorFunctions.cs: -------------------------------------------------------------------------------- 1 | using DurableECommerceWorkflowIsolated.Models; 2 | using Microsoft.Azure.Functions.Worker; 3 | using Microsoft.DurableTask; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace DurableECommerceWorkflowIsolated.Functions; 7 | 8 | public static class OrchestratorFunctions 9 | { 10 | [Function("O_ProcessOrder")] 11 | public static async Task ProcessOrder( 12 | [OrchestrationTrigger] TaskOrchestrationContext ctx, 13 | FunctionContext functionContext) 14 | { 15 | var log = ctx.CreateReplaySafeLogger(nameof(ProcessOrder)); 16 | 17 | var order = ctx.GetInput(); 18 | order.OrchestrationId = ctx.InstanceId; 19 | 20 | if (!ctx.IsReplaying) 21 | log.LogInformation($"Processing order #{order.Id}"); 22 | 23 | await ctx.CallActivityAsync("A_SaveOrderToDatabase", order); 24 | 25 | var total = order.Items.Sum(i => i.Amount); 26 | if (total > 1000) 27 | { 28 | log.LogWarning($"Need approval for {ctx.InstanceId}"); 29 | 30 | ctx.SetCustomStatus("Needs approval"); 31 | await ctx.CallActivityAsync("A_RequestOrderApproval", order); 32 | 33 | string approvalResult; 34 | try 35 | { 36 | approvalResult = await ctx.WaitForExternalEvent("OrderApprovalResult", TimeSpan.FromSeconds(180)); 37 | } 38 | catch (TaskCanceledException) // not a TimeoutException as you might expect 39 | { 40 | log.LogWarning($"Timed out waiting for approval"); 41 | approvalResult = "Timed out"; 42 | } 43 | catch(Exception e) 44 | { 45 | log.LogError(e, $"Something else"); 46 | approvalResult = "Timed out 2"; 47 | 48 | } 49 | 50 | ctx.SetCustomStatus(""); // clear the needs approval flag 51 | if (approvalResult != "Approved") 52 | { 53 | // timed out or got a rejected 54 | log.LogWarning($"Not approved [{approvalResult}]"); 55 | await ctx.CallActivityAsync("A_SendNotApprovedEmail", order); 56 | return new OrderResult { Status = "NotApproved" }; 57 | } 58 | 59 | } 60 | 61 | string[]? downloads = null; 62 | try 63 | { 64 | // create files in parallel 65 | var tasks = new List>(); 66 | foreach (var item in order.Items) 67 | { 68 | tasks.Add(ctx.CallActivityAsync("A_CreatePersonalizedPdf", new PdfInfo(order.Id,item.ProductId,order.PurchaserEmail))); 69 | } 70 | 71 | downloads = await Task.WhenAll(tasks); 72 | 73 | } 74 | catch (Exception ex) 75 | { 76 | if (!ctx.IsReplaying) 77 | log.LogError($"Failed to create files", ex); 78 | } 79 | 80 | if (downloads != null) 81 | { 82 | await ctx.CallActivityAsync("A_SendOrderConfirmationEmail", 83 | 84 | new ConfirmationInfo(order, downloads), 85 | TaskOptions.FromRetryPolicy(new RetryPolicy(3, TimeSpan.FromSeconds(30)))); 86 | return new OrderResult { Status = "Success", Downloads = downloads }; 87 | } 88 | await ctx.CallActivityAsync("A_SendProblemEmail", 89 | order, 90 | TaskOptions.FromRetryPolicy(new RetryPolicy(3, TimeSpan.FromSeconds(30)))); 91 | return new OrderResult { Status = "Problem" }; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /DurableECommerceWeb/wwwroot/orderStatus.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Durable Functions Shop 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |

Order Status

17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 |
25 |
26 |
27 |
28 | {{errorMessage}} 29 |
30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 | 38 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /DurableECommerceWorkflow/Functions/OrderStatusFunctions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using DurableECommerceWorkflow.Models; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Azure.WebJobs; 8 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 9 | using Microsoft.Azure.WebJobs.Extensions.Http; 10 | using Microsoft.Extensions.Logging; 11 | 12 | namespace DurableECommerceWorkflow.Functions 13 | { 14 | public static class OrderStatusFunctions 15 | { 16 | [FunctionName("GetOrderStatus")] 17 | public static async Task GetOrderStatus( 18 | [HttpTrigger(AuthorizationLevel.Anonymous, 19 | "get", Route = "orderstatus/{id}")]HttpRequest req, 20 | [DurableClient] IDurableOrchestrationClient client, 21 | [Table(OrderEntity.TableName, OrderEntity.OrderPartitionKey, "{id}", Connection = "AzureWebJobsStorage")] OrderEntity order, 22 | ILogger log, string id) 23 | { 24 | log.LogInformation($"Checking status of order {id}"); 25 | 26 | if (order == null) 27 | { 28 | return new NotFoundResult(); 29 | } 30 | var status = await client.GetStatusAsync(order.OrchestrationId); 31 | 32 | var statusObj = new 33 | { 34 | status.InstanceId, 35 | status.CreatedTime, 36 | status.CustomStatus, 37 | status.Output, 38 | status.LastUpdatedTime, 39 | status.RuntimeStatus, 40 | order.Items, 41 | order.Amount, 42 | PurchaserEmail = order.Email 43 | }; 44 | 45 | return new OkObjectResult(statusObj); 46 | } 47 | 48 | [FunctionName("DeleteOrder")] 49 | public static async Task DeleteOrder( 50 | [HttpTrigger(AuthorizationLevel.Anonymous, 51 | "delete", Route = "order/{id}")]HttpRequest req, 52 | [DurableClient] IDurableOrchestrationClient client, 53 | [Table(OrderEntity.TableName, OrderEntity.OrderPartitionKey, "{id}", Connection = "AzureWebJobsStorage")] OrderEntity order, 54 | ILogger log, string id) 55 | { 56 | 57 | if (order == null) 58 | { 59 | log.LogWarning($"Cannot find order {id}"); 60 | 61 | return new NotFoundResult(); 62 | } 63 | log.LogInformation($"Deleting order {id}"); 64 | 65 | var status = await client.GetStatusAsync(order.OrchestrationId); 66 | if (status.RuntimeStatus == OrchestrationRuntimeStatus.Running) 67 | { 68 | log.LogWarning($"Cannot find order {id}"); 69 | 70 | return new BadRequestResult(); 71 | 72 | } 73 | await client.PurgeInstanceHistoryAsync(order.OrchestrationId); 74 | 75 | return new OkResult(); 76 | } 77 | 78 | [FunctionName("GetAllOrders")] 79 | public static async Task GetAllOrders( 80 | [HttpTrigger(AuthorizationLevel.Anonymous, 81 | "get", Route = null)]HttpRequest req, 82 | [DurableClient] IDurableOrchestrationClient client, 83 | ILogger log) 84 | { 85 | log.LogInformation("getting all orders."); 86 | // just get orders in the last couple of hours to keep manage screen simple 87 | // interested in orders of all statuses 88 | // ListInstancesAsync instead? 89 | var statuses = await client.GetStatusAsync(DateTime.Today.AddHours(-2.0), null, 90 | Enum.GetValues(typeof(OrchestrationRuntimeStatus)).Cast() 91 | ); 92 | 93 | return new OkObjectResult(statuses); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /DurableECommerceWorkflow/Functions/ExampleOrchestratorFunctions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using DurableECommerceWorkflow.Models; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace DurableECommerceWorkflow.Functions; 9 | 10 | static class ExampleOrchestratorFunctions 11 | { 12 | 13 | [FunctionName("O_ProcessOrder_V3")] 14 | public static async Task ProcessOrderV3( 15 | [OrchestrationTrigger] IDurableOrchestrationContext ctx, 16 | ILogger log) 17 | { 18 | var order = ctx.GetInput(); 19 | 20 | if (!ctx.IsReplaying) 21 | log.LogInformation($"Processing order {order.Id}"); 22 | 23 | await ctx.CallActivityAsync("A_SaveOrderToDatabase", order); 24 | 25 | string pdfLocation = null; 26 | string videoLocation = null; 27 | try 28 | { 29 | // create files in parallel 30 | var pdfTask = ctx.CallActivityAsync("A_CreatePersonalizedPdf", order); 31 | var videoTask = ctx.CallActivityAsync("A_CreateWatermarkedVideo", order); 32 | await Task.WhenAll(pdfTask, videoTask); 33 | pdfLocation = pdfTask.Result; 34 | videoLocation = videoTask.Result; 35 | } 36 | catch (Exception ex) 37 | { 38 | if (!ctx.IsReplaying) 39 | log.LogError($"Failed to create files", ex); 40 | } 41 | 42 | if (pdfLocation != null && videoLocation != null) 43 | { 44 | await ctx.CallActivityWithRetryAsync("A_SendOrderConfirmationEmail", 45 | new RetryOptions(TimeSpan.FromSeconds(30), 3), 46 | (order, pdfLocation, videoLocation)); 47 | return "Order processed successfully"; 48 | } 49 | await ctx.CallActivityWithRetryAsync("A_SendProblemEmail", 50 | new RetryOptions(TimeSpan.FromSeconds(30), 3), 51 | order); 52 | return "There was a problem processing this order"; 53 | } 54 | 55 | 56 | [FunctionName("O_ProcessOrder_V2")] 57 | public static async Task ProcessOrderV2( 58 | [OrchestrationTrigger] IDurableOrchestrationContext ctx, 59 | ILogger log) 60 | { 61 | var order = ctx.GetInput(); 62 | 63 | if (!ctx.IsReplaying) 64 | log.LogInformation($"Processing order {order.Id}"); 65 | 66 | await ctx.CallActivityAsync("A_SaveOrderToDatabase", order); 67 | 68 | // create files in parallel 69 | var pdfTask = ctx.CallActivityAsync("A_CreatePersonalizedPdf", order); 70 | var videoTask = ctx.CallActivityAsync("A_CreateWatermarkedVideo", order); 71 | await Task.WhenAll(pdfTask, videoTask); 72 | 73 | var pdfLocation = pdfTask.Result; 74 | var videoLocation = videoTask.Result; 75 | 76 | await ctx.CallActivityAsync("A_SendOrderConfirmationEmail", (order, pdfLocation, videoLocation)); 77 | 78 | return "Order processed successfully"; 79 | } 80 | 81 | // basic orchestrator, calls all functions in sequence 82 | [FunctionName("O_ProcessOrder_V1")] 83 | public static async Task ProcessOrderV1( 84 | [OrchestrationTrigger] IDurableOrchestrationContext ctx, 85 | ILogger log) 86 | { 87 | var order = ctx.GetInput(); 88 | 89 | if (!ctx.IsReplaying) 90 | log.LogInformation($"Processing order {order.Id}"); 91 | 92 | await ctx.CallActivityAsync("A_SaveOrderToDatabase", order); 93 | var pdfLocation = await ctx.CallActivityAsync("A_CreatePersonalizedPdf", order); 94 | var videoLocation = await ctx.CallActivityAsync("A_CreateWatermarkedVideo", order); 95 | await ctx.CallActivityAsync("A_SendOrderConfirmationEmail", (order, pdfLocation, videoLocation)); 96 | 97 | return "Order processed successfully"; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Durable Functions E-Commerce Sample 2 | 3 | In this sample application, when an order webhook is received (by the `NewPurchaseWebhook` function), we initiate an order processing workflow. 4 | 5 | - First, it stores a new row in our "database" (using Azure Table storage for simplicity). 6 | - Second, it requests approval if the value of the order is greater than a certain amount. This involves sending an email to an administrator and them using the "management" web-page to approve or reject the order. 7 | - Then in parallel it creates a 'PDF' (actually just text file) for each item ordered in a blob storage account and generates SAS tokens to download them. 8 | - Finally, it sends out an email to the purchaser containing the download SAS tokens. 9 | - (note you need your own SendGrid API key to actually send emails - use a key of "TEST" to avoid attempting to send emails) 10 | 11 | ### Local Application Settings 12 | 13 | To run the application locally, you'll need to set up your `local.settings.json` file, which is not checked into source control. An example is shown below: 14 | 15 | ```javascript 16 | { 17 | "IsEncrypted": false, 18 | "Values": { 19 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 20 | "SendGridKey": "<>", 21 | "ApproverEmail": "durable-funcs-approver@mailinator.com", 22 | "SenderEmail": "any@example.email", 23 | "Host": "http://localhost:7071", 24 | "FUNCTIONS_WORKER_RUNTIME": "dotnet", 25 | }, 26 | "Host": { 27 | "CORS": "*" 28 | } 29 | } 30 | ``` 31 | 32 | Using mailinator for testing and demos: 33 | - https://www.mailinator.com/v4/public/inboxes.jsp?to=durable-funcs-approver 34 | - https://www.mailinator.com/v4/public/inboxes.jsp?to=durable-funcs-customer 35 | 36 | ### Running in the cloud 37 | - You need to run the static website (can run anywhere - Azure App Service is probably easiest). 38 | - You need to run the 39 | - You will need to update `baseUrl` in the JavaScript files to point to the fucntion app 40 | - You will need the Azure Function app to accept CORS requests from the website 41 | - You will need to set up Application Settings for the function app - `SendGridKey`, `ApproverEmail`, `SenderEmail` 42 | 43 | ### Testing from the Web UI 44 | 45 | Set both `DurableECommerceWeb` and `DurableECommerceWorkflow` projects as startup projects 46 | 47 | Visiting `https://localhost:5001/` will take you to a page where you can add items to your shopping cart and create an order. 48 | 49 | By visiting `https://localhost:5001/orderStatus.html?id=` you can view the current status of the order. 50 | 51 | And visiting `https://localhost:5001/admin` takes you to an order management dashboard that lets administrators approve or reject orders for large amounts, as well as purge order history for completed orders. 52 | 53 | ### Testing from PowerShell 54 | 55 | Calling the starter function from PowerShell: 56 | 57 | ```powershell 58 | $orderInfo = "{ items: [{productId: 'azure-functions', amount:24}], purchaserEmail:'your@email.com' }" 59 | $webhookUri = "http://localhost:7071/api/NewPurchaseWebhook" 60 | $statusUris = Invoke-RestMethod -Method Post -Body $orderInfo -Uri $webhookUri 61 | 62 | # check the status of the workflow 63 | Invoke-RestMethod -Uri $statusUris.StatusQueryGetUri 64 | ``` 65 | 66 | To simulate an error in PDF generation, use: 67 | ```powershell 68 | $orderInfoErr = "{ items: [{productId: 'error', amount:24}], purchaserEmail:'your@email.com' }" 69 | ``` 70 | 71 | To simulate an order needing approval, use: 72 | 73 | ```powershell 74 | $orderInfo = "{ items: [{productId: 'durable-functions', amount:3000}], purchaserEmail:'your@email.com' }" 75 | ``` 76 | 77 | To send an approval to a specific orchestration: 78 | 79 | ```powershell 80 | function Approve-Order { 81 | Param ([String]$orchestrationId) 82 | $approvalResult = "{ orchestrationId: '" + $orchestrationId + "', approved:true }" 83 | $approveOrderUri = "http://localhost:7071/api/ApproveOrder" 84 | Invoke-RestMethod -Method Post -Body $approvalResult -Uri $approveOrderUri 85 | } 86 | Approve-Order -orchestrationId <> 87 | ``` 88 | 89 | -------------------------------------------------------------------------------- /DurableECommerceWeb/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Durable Functions Shop 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 20 |

Durable Functions Demo Shop

21 |

Buy a training course!

22 |
23 | 24 | 25 | 53 | 54 |
55 |
56 |
57 |
58 | 59 |
60 |

{{product.name}}

61 |

{{product.description}}

62 | Add to cart ${{product.price}} 64 |
65 |
66 |
67 |
68 |
69 |
70 | Great choice! 71 | Thank you for your purchase. 72 | Your order number is {{orderId}} 73 |
74 |
75 |
76 |
77 | 78 | 79 | -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/Functions/ExampleOrchestratorFunctions.cs: -------------------------------------------------------------------------------- 1 | using DurableECommerceWorkflowIsolated.Models; 2 | using Microsoft.Azure.Functions.Worker; 3 | using Microsoft.DurableTask; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace DurableECommerceWorkflowIsolated.Functions; 7 | 8 | static class ExampleOrchestratorFunctions 9 | { 10 | 11 | [Function("O_ProcessOrder_V3")] 12 | public static async Task ProcessOrderV3( 13 | [OrchestrationTrigger] TaskOrchestrationContext ctx, 14 | FunctionContext functionContext) 15 | { 16 | var log = functionContext.GetLogger(nameof(ProcessOrderV3)); 17 | var order = ctx.GetInput(); 18 | 19 | if (!ctx.IsReplaying) 20 | log.LogInformation($"Processing order {order.Id}"); 21 | 22 | await ctx.CallActivityAsync("A_SaveOrderToDatabase", order); 23 | 24 | string? pdfLocation = null; 25 | string? videoLocation = null; 26 | try 27 | { 28 | // create files in parallel 29 | var pdfTask = ctx.CallActivityAsync("A_CreatePersonalizedPdf", order); 30 | var videoTask = ctx.CallActivityAsync("A_CreateWatermarkedVideo", order); 31 | await Task.WhenAll(pdfTask, videoTask); 32 | pdfLocation = pdfTask.Result; 33 | videoLocation = videoTask.Result; 34 | } 35 | catch (Exception ex) 36 | { 37 | if (!ctx.IsReplaying) 38 | log.LogError($"Failed to create files", ex); 39 | } 40 | 41 | if (pdfLocation != null && videoLocation != null) 42 | { 43 | await ctx.CallActivityAsync("A_SendOrderConfirmationEmail", 44 | (order, pdfLocation, videoLocation), 45 | new TaskOptions(new TaskRetryOptions(new RetryPolicy(3, TimeSpan.FromSeconds(30))))); 46 | return "Order processed successfully"; 47 | } 48 | await ctx.CallActivityAsync("A_SendProblemEmail", 49 | order, 50 | TaskOptions.FromRetryPolicy(new RetryPolicy(3, TimeSpan.FromSeconds(30)))); 51 | return "There was a problem processing this order"; 52 | } 53 | 54 | 55 | [Function("O_ProcessOrder_V2")] 56 | public static async Task ProcessOrderV2( 57 | [OrchestrationTrigger] TaskOrchestrationContext ctx, 58 | FunctionContext functionContext) 59 | { 60 | var log = functionContext.GetLogger(nameof(ProcessOrderV2)); 61 | var order = ctx.GetInput(); 62 | if (order == null) throw new InvalidOperationException("failed to deserialize orchestration input"); 63 | 64 | if (!ctx.IsReplaying) 65 | log.LogInformation($"Processing order {order.Id}"); 66 | 67 | await ctx.CallActivityAsync("A_SaveOrderToDatabase", order); 68 | 69 | // create files in parallel 70 | var pdfTask = ctx.CallActivityAsync("A_CreatePersonalizedPdf", order); 71 | var videoTask = ctx.CallActivityAsync("A_CreateWatermarkedVideo", order); 72 | await Task.WhenAll(pdfTask, videoTask); 73 | 74 | var pdfLocation = pdfTask.Result; 75 | var videoLocation = videoTask.Result; 76 | 77 | await ctx.CallActivityAsync("A_SendOrderConfirmationEmail", (order, pdfLocation, videoLocation)); 78 | 79 | return "Order processed successfully"; 80 | } 81 | 82 | // basic orchestrator, calls all functions in sequence 83 | [Function("O_ProcessOrder_V1")] 84 | public static async Task ProcessOrderV1( 85 | [OrchestrationTrigger] TaskOrchestrationContext ctx, 86 | FunctionContext functionContext) 87 | { 88 | var log = functionContext.GetLogger(nameof(ProcessOrderV1)); 89 | var order = ctx.GetInput(); 90 | if (order == null) throw new InvalidOperationException("failed to deserialize orchestration input"); 91 | 92 | if (!ctx.IsReplaying) 93 | log.LogInformation($"Processing order {order.Id}"); 94 | 95 | await ctx.CallActivityAsync("A_SaveOrderToDatabase", order); 96 | var pdfLocation = await ctx.CallActivityAsync("A_CreatePersonalizedPdf", order); 97 | var videoLocation = await ctx.CallActivityAsync("A_CreateWatermarkedVideo", order); 98 | await ctx.CallActivityAsync("A_SendOrderConfirmationEmail", (order, pdfLocation, videoLocation)); 99 | 100 | return "Order processed successfully"; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | *.pubxml 13 | 14 | # Build results 15 | [Dd]ebug/ 16 | [Dd]ebugPublic/ 17 | [Rr]elease/ 18 | [Rr]eleases/ 19 | x64/ 20 | x86/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | [Ll]og/ 25 | 26 | # Visual Studio 2015 cache/options directory 27 | .vs/ 28 | # Uncomment if you have tasks that create the project's static files in wwwroot 29 | #wwwroot/ 30 | 31 | # MSTest test Results 32 | [Tt]est[Rr]esult*/ 33 | [Bb]uild[Ll]og.* 34 | 35 | # NUNIT 36 | *.VisualState.xml 37 | TestResult.xml 38 | 39 | # Build Results of an ATL Project 40 | [Dd]ebugPS/ 41 | [Rr]eleasePS/ 42 | dlldata.c 43 | 44 | # DNX 45 | project.lock.json 46 | project.fragment.lock.json 47 | artifacts/ 48 | 49 | *_i.c 50 | *_p.c 51 | *_i.h 52 | *.ilk 53 | *.meta 54 | *.obj 55 | *.pch 56 | *.pdb 57 | *.pgc 58 | *.pgd 59 | *.rsp 60 | *.sbr 61 | *.tlb 62 | *.tli 63 | *.tlh 64 | *.tmp 65 | *.tmp_proj 66 | *.log 67 | *.vspscc 68 | *.vssscc 69 | .builds 70 | *.pidb 71 | *.svclog 72 | *.scc 73 | 74 | # Chutzpah Test files 75 | _Chutzpah* 76 | 77 | # Visual C++ cache files 78 | ipch/ 79 | *.aps 80 | *.ncb 81 | *.opendb 82 | *.opensdf 83 | *.sdf 84 | *.cachefile 85 | *.VC.db 86 | *.VC.VC.opendb 87 | 88 | # Visual Studio profiler 89 | *.psess 90 | *.vsp 91 | *.vspx 92 | *.sap 93 | 94 | # TFS 2012 Local Workspace 95 | $tf/ 96 | 97 | # Guidance Automation Toolkit 98 | *.gpState 99 | 100 | # ReSharper is a .NET coding add-in 101 | _ReSharper*/ 102 | *.[Rr]e[Ss]harper 103 | *.DotSettings.user 104 | 105 | # JustCode is a .NET coding add-in 106 | .JustCode 107 | 108 | # TeamCity is a build add-in 109 | _TeamCity* 110 | 111 | # DotCover is a Code Coverage Tool 112 | *.dotCover 113 | 114 | # NCrunch 115 | _NCrunch_* 116 | .*crunch*.local.xml 117 | nCrunchTemp_* 118 | 119 | # MightyMoose 120 | *.mm.* 121 | AutoTest.Net/ 122 | 123 | # Web workbench (sass) 124 | .sass-cache/ 125 | 126 | # Installshield output folder 127 | [Ee]xpress/ 128 | 129 | # DocProject is a documentation generator add-in 130 | DocProject/buildhelp/ 131 | DocProject/Help/*.HxT 132 | DocProject/Help/*.HxC 133 | DocProject/Help/*.hhc 134 | DocProject/Help/*.hhk 135 | DocProject/Help/*.hhp 136 | DocProject/Help/Html2 137 | DocProject/Help/html 138 | 139 | # Click-Once directory 140 | publish/ 141 | 142 | # Publish Web Output 143 | *.[Pp]ublish.xml 144 | *.azurePubxml 145 | # TODO: Comment the next line if you want to checkin your web deploy settings 146 | # but database connection strings (with potential passwords) will be unencrypted 147 | #*.pubxml 148 | *.publishproj 149 | 150 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 151 | # checkin your Azure Web App publish settings, but sensitive information contained 152 | # in these scripts will be unencrypted 153 | PublishScripts/ 154 | 155 | # NuGet Packages 156 | *.nupkg 157 | # The packages folder can be ignored because of Package Restore 158 | **/packages/* 159 | # except build/, which is used as an MSBuild target. 160 | !**/packages/build/ 161 | # Uncomment if necessary however generally it will be regenerated when needed 162 | #!**/packages/repositories.config 163 | # NuGet v3's project.json files produces more ignoreable files 164 | *.nuget.props 165 | *.nuget.targets 166 | 167 | # Microsoft Azure Build Output 168 | csx/ 169 | *.build.csdef 170 | 171 | # Microsoft Azure Emulator 172 | ecf/ 173 | rcf/ 174 | 175 | # Windows Store app package directories and files 176 | AppPackages/ 177 | BundleArtifacts/ 178 | Package.StoreAssociation.xml 179 | _pkginfo.txt 180 | 181 | # Visual Studio cache files 182 | # files ending in .cache can be ignored 183 | *.[Cc]ache 184 | # but keep track of directories ending in .cache 185 | !*.[Cc]ache/ 186 | 187 | # Others 188 | ClientBin/ 189 | ~$* 190 | *~ 191 | *.dbmdl 192 | *.dbproj.schemaview 193 | *.jfm 194 | *.pfx 195 | *.publishsettings 196 | node_modules/ 197 | orleans.codegen.cs 198 | 199 | # Since there are multiple workflows, uncomment next line to ignore bower_components 200 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 201 | #bower_components/ 202 | 203 | # RIA/Silverlight projects 204 | Generated_Code/ 205 | 206 | # Backup & report files from converting an old project file 207 | # to a newer Visual Studio version. Backup files are not needed, 208 | # because we have git ;-) 209 | _UpgradeReport_Files/ 210 | Backup*/ 211 | UpgradeLog*.XML 212 | UpgradeLog*.htm 213 | 214 | # SQL Server files 215 | *.mdf 216 | *.ldf 217 | 218 | # Business Intelligence projects 219 | *.rdl.data 220 | *.bim.layout 221 | *.bim_*.settings 222 | 223 | # Microsoft Fakes 224 | FakesAssemblies/ 225 | 226 | # GhostDoc plugin setting file 227 | *.GhostDoc.xml 228 | 229 | # Node.js Tools for Visual Studio 230 | .ntvs_analysis.dat 231 | 232 | # Visual Studio 6 build log 233 | *.plg 234 | 235 | # Visual Studio 6 workspace options file 236 | *.opt 237 | 238 | # Visual Studio LightSwitch build output 239 | **/*.HTMLClient/GeneratedArtifacts 240 | **/*.DesktopClient/GeneratedArtifacts 241 | **/*.DesktopClient/ModelManifest.xml 242 | **/*.Server/GeneratedArtifacts 243 | **/*.Server/ModelManifest.xml 244 | _Pvt_Extensions 245 | 246 | # Paket dependency manager 247 | .paket/paket.exe 248 | paket-files/ 249 | 250 | # FAKE - F# Make 251 | .fake/ 252 | 253 | # JetBrains Rider 254 | .idea/ 255 | *.sln.iml 256 | 257 | # CodeRush 258 | .cr/ 259 | 260 | # Python Tools for Visual Studio (PTVS) 261 | __pycache__/ 262 | *.pyc -------------------------------------------------------------------------------- /DurableECommerceWorkflow/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /DurableECommerceTests/OrchestratorFunctionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using DurableECommerceWorkflow.Functions; 5 | using DurableECommerceWorkflow.Models; 6 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 7 | using Microsoft.Extensions.Logging; 8 | using Moq; 9 | using NUnit.Framework; 10 | 11 | namespace DurableECommerceTests; 12 | 13 | public class OrchestratorFunctionTests 14 | { 15 | private ILogger mockLogger; 16 | 17 | [SetUp] 18 | public void Setup() 19 | { 20 | mockLogger = Mock.Of(); 21 | } 22 | 23 | [Test] 24 | public async Task CanSuccessfullyProcessAnOrder() 25 | { 26 | var order = CreateTestOrder(false); 27 | var context = CreateMockDurableOrchestrationContext(order, null); 28 | 29 | var orderResult = await OrchestratorFunctions.ProcessOrder(context.Object, mockLogger); 30 | 31 | context.Verify(c => c.CallActivityAsync("A_SaveOrderToDatabase", order), Times.Once); 32 | context.Verify(c => c.CallActivityAsync("A_RequestOrderApproval", order), Times.Never); 33 | context.Verify(c => c.CallActivityAsync("A_CreatePersonalizedPdf", It.IsAny()), Times.Once); 34 | 35 | context.Verify(c => c.CallActivityWithRetryAsync("A_SendOrderConfirmationEmail", 36 | It.IsAny(), 37 | It.IsAny()), Times.Once); 38 | 39 | Assert.That(orderResult, Is.EqualTo(new OrderResult { Status = "Success", Downloads = new [] { "example.pdf" } }).Using(CompareOrderResult)); 40 | } 41 | 42 | [Test] 43 | public async Task CanSuccessfullyProcessAnOrderWithApproval() 44 | { 45 | var order = CreateTestOrder(true); 46 | var context = CreateMockDurableOrchestrationContext(order, "Approved"); 47 | 48 | var orderResult = await OrchestratorFunctions.ProcessOrder(context.Object, mockLogger); 49 | 50 | context.Verify(c => c.CallActivityAsync("A_SaveOrderToDatabase", order), Times.Once); 51 | context.Verify(c => c.CallActivityAsync("A_RequestOrderApproval", order), Times.Once); 52 | context.Verify(c => c.CallActivityAsync("A_CreatePersonalizedPdf", It.IsAny()), Times.Once); 53 | 54 | context.Verify(c => c.CallActivityWithRetryAsync("A_SendOrderConfirmationEmail", 55 | It.IsAny(), 56 | It.IsAny()), Times.Once); 57 | 58 | Assert.That(orderResult, Is.EqualTo(new OrderResult { Status = "Success", Downloads = new[] { "example.pdf" } }).Using(CompareOrderResult)); 59 | } 60 | 61 | 62 | [Test] 63 | public async Task CanNotifyAnOrderNotApproved() 64 | { 65 | var order = CreateTestOrder(true); 66 | var context = CreateMockDurableOrchestrationContext(order, "Rejected"); 67 | 68 | var orderResult = await OrchestratorFunctions.ProcessOrder(context.Object, mockLogger); 69 | 70 | context.Verify(c => c.CallActivityAsync("A_SaveOrderToDatabase", order), Times.Once); 71 | context.Verify(c => c.CallActivityAsync("A_RequestOrderApproval", order), Times.Once); 72 | context.Verify(c => c.CallActivityAsync("A_SendNotApprovedEmail", order), Times.Once); 73 | 74 | Assert.That(orderResult, Is.EqualTo(new OrderResult { Status = "NotApproved"}).Using(CompareOrderResult)); 75 | } 76 | 77 | [Test] 78 | public async Task SendsProblemEmailOnFailureToTranscode() 79 | { 80 | var order = CreateTestOrder(false); 81 | var context = CreateMockDurableOrchestrationContext(order, null, true); 82 | 83 | var orderResult = await OrchestratorFunctions.ProcessOrder(context.Object, mockLogger); 84 | 85 | context.Verify(c => c.CallActivityAsync("A_SaveOrderToDatabase", order), Times.Once); 86 | context.Verify(c => c.CallActivityAsync("A_RequestOrderApproval", order), Times.Never); 87 | context.Verify(c => c.CallActivityWithRetryAsync("A_SendProblemEmail", It.IsAny(), order), Times.Once); 88 | 89 | Assert.That(orderResult, Is.EqualTo(new OrderResult { Status = "Problem" }).Using(CompareOrderResult)); 90 | 91 | } 92 | 93 | private static Order CreateTestOrder(bool requiresApproval) 94 | { 95 | var order = new Order 96 | { 97 | Id = "102030", 98 | OrchestrationId = "100200", 99 | Items = new[] 100 | { 101 | new OrderItem 102 | { 103 | Amount = requiresApproval ? 12345 : 50, 104 | ProductId = "Prod1", 105 | } 106 | }, 107 | Date = DateTime.Now, 108 | PurchaserEmail = "test@example.com" 109 | }; 110 | return order; 111 | } 112 | 113 | private static Mock CreateMockDurableOrchestrationContext(Order order, string approvalResult, bool throwErrorInPdf = false) 114 | { 115 | var context = new Mock(); 116 | context.Setup(c => c.GetInput()).Returns(order); 117 | context.SetupGet(c => c.InstanceId).Returns("12345"); 118 | if (throwErrorInPdf) 119 | context.Setup(c => c.CallActivityAsync("A_CreatePersonalizedPdf", It.IsAny())).ThrowsAsync(new InvalidOperationException("Failed to create PDF")); 120 | else 121 | context.Setup(c => c.CallActivityAsync("A_CreatePersonalizedPdf", It.IsAny())).ReturnsAsync("example.pdf"); 122 | context.Setup(c => c.WaitForExternalEvent("OrderApprovalResult", It.IsAny(), default, default)).ReturnsAsync(approvalResult); 123 | return context; 124 | } 125 | private static bool CompareOrderResult(OrderResult expected, OrderResult actual) => 126 | expected.Status == actual.Status && 127 | (expected.Downloads == null && actual.Downloads == null) || 128 | (expected.Downloads.Length == actual.Downloads.Length && 129 | expected.Downloads.Zip(actual.Downloads,(a,b) =>(a,b)).All(x => x.Item1 == x.Item2)); 130 | } -------------------------------------------------------------------------------- /DurableECommerceWorkflow/Functions/ActivityFunctions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Azure.Storage.Blobs; 5 | using Azure.Storage.Sas; 6 | using DurableECommerceWorkflow.Models; 7 | using Microsoft.Azure.WebJobs; 8 | using Microsoft.Azure.WebJobs.Extensions.DurableTask; 9 | using Microsoft.Extensions.Logging; 10 | using SendGrid.Helpers.Mail; 11 | 12 | namespace DurableECommerceWorkflow.Functions; 13 | 14 | public static class ActivityFunctions 15 | { 16 | 17 | [FunctionName("A_SaveOrderToDatabase")] 18 | public static async Task SaveOrderToDatabase( 19 | [ActivityTrigger] Order order, 20 | [Table(OrderEntity.TableName)] IAsyncCollector table, 21 | ILogger log) 22 | { 23 | log.LogInformation("Saving order to database"); 24 | await table.AddAsync(new OrderEntity 25 | { 26 | PartitionKey = OrderEntity.OrderPartitionKey, 27 | RowKey = order.Id, 28 | OrchestrationId = order.OrchestrationId, 29 | Items = string.Join(",", order.Items.Select(i => i.ProductId)), 30 | Email = order.PurchaserEmail, 31 | OrderDate = order.Date, 32 | Amount = order.Items.Sum(i => i.Amount) 33 | }); 34 | } 35 | 36 | [FunctionName("A_CreatePersonalizedPdf")] 37 | public static async Task CreatePersonalizedPdf( 38 | [ActivityTrigger] (Order, OrderItem) orderInfo, 39 | [Blob("assets")] BlobContainerClient assetsContainer, 40 | ILogger log) 41 | { 42 | var (order, item) = orderInfo; 43 | log.LogInformation("Creating PDF"); 44 | if (item.ProductId == "error") 45 | throw new InvalidOperationException("Can't create the PDF for this product"); 46 | var fileName = $"{order.Id}/{item.ProductId}-pdf.txt"; 47 | await assetsContainer.CreateIfNotExistsAsync(); 48 | var blob = assetsContainer.GetBlobClient(fileName); 49 | await blob.UploadTextAsync($"Example {item.ProductId} PDF for {order.PurchaserEmail}"); 50 | return blob.GenerateSasUri(BlobSasPermissions.Read, 51 | DateTimeOffset.UtcNow.AddDays(1)).ToString(); 52 | } 53 | 54 | [FunctionName("A_SendOrderConfirmationEmail")] 55 | public static async Task SendOrderConfirmationEmail( 56 | [ActivityTrigger] (Order, string[]) input, 57 | [SendGrid(ApiKey = "SendGridKey")] IAsyncCollector sender, 58 | ILogger log) 59 | { 60 | var (order, files) = input; 61 | log.LogInformation($"Sending Order Confirmation Email to {order.PurchaserEmail}"); 62 | var body = $"Thanks for your order, you can download your files here: " + 63 | string.Join(" ", order.Items.Zip(files, (i, f) => $"{i.ProductId}
")); 64 | var message = GenerateMail(order.PurchaserEmail, $"Your order {order.Id}", body); 65 | await sender.PostAsync(message, log); 66 | } 67 | 68 | [FunctionName("A_SendProblemEmail")] 69 | public static async Task SendProblemEmail( 70 | [ActivityTrigger] Order order, 71 | [SendGrid(ApiKey = "SendGridKey")] IAsyncCollector sender, 72 | ILogger log) 73 | { 74 | log.LogInformation($"Sending Problem Email {order.PurchaserEmail}"); 75 | var body = "We're very sorry there was a problem processing your order.
" + 76 | " Please contact customer support."; 77 | var message = GenerateMail(order.PurchaserEmail, $"Problem with order {order.Id}", body); 78 | await sender.PostAsync(message, log); 79 | } 80 | 81 | private static SendGridMessage GenerateMail(string recipient, string subject, string body) 82 | { 83 | var recipientEmail = new EmailAddress(recipient); 84 | var senderEmail = new EmailAddress(Environment.GetEnvironmentVariable("SenderEmail")); 85 | 86 | var message = new SendGridMessage(); 87 | message.Subject = subject; 88 | message.From = senderEmail; 89 | message.AddTo(recipientEmail); 90 | message.HtmlContent = body; 91 | return message; 92 | } 93 | 94 | [FunctionName("A_SendNotApprovedEmail")] 95 | public static async Task SendNotApprovedEmail( 96 | [ActivityTrigger] Order order, 97 | [SendGrid(ApiKey = "SendGridKey")] IAsyncCollector sender, 98 | ILogger log) 99 | { 100 | log.LogInformation($"Sending Not Approved Email {order.PurchaserEmail}"); 101 | var body = $"We're very sorry we were not able to approve your order #{order.Id}.
" + 102 | " Please contact customer support."; 103 | var message = GenerateMail(order.PurchaserEmail, $"Order {order.Id} rejected", body); 104 | await sender.PostAsync(message, log); 105 | } 106 | 107 | [FunctionName("A_RequestOrderApproval")] 108 | public static async Task RequestOrderApproval( 109 | [ActivityTrigger] Order order, 110 | [SendGrid(ApiKey = "SendGridKey")] IAsyncCollector sender, 111 | ILogger log) 112 | { 113 | log.LogInformation($"Requesting Approval for Order {order.PurchaserEmail}"); 114 | var subject = $"Order {order.Id} requires approval"; 115 | var approverEmail = Environment.GetEnvironmentVariable("ApproverEmail"); 116 | var host = Environment.GetEnvironmentVariable("Host"); 117 | 118 | var approveUrl = $"{host}/manage"; 119 | var body = $"Please review Order {order.Id}
" 120 | + $"for product {string.Join(",", order.Items.Select(o => o.ProductId))}" 121 | + $" and amount ${order.Total()}"; 122 | 123 | var message = GenerateMail(approverEmail, subject, body); 124 | await sender.PostAsync(message, log); 125 | } 126 | 127 | private static async Task PostAsync(this IAsyncCollector sender, SendGridMessage message, ILogger log) 128 | { 129 | var sendGridKey = Environment.GetEnvironmentVariable("SendGridKey"); 130 | // don't actually try to send SendGrid emails if we are just using example or missing email addresses 131 | var testMode = String.IsNullOrEmpty(sendGridKey) || message.Personalizations.SelectMany(p => p.Tos.Select(t => t.Email)) 132 | .Any(e => string.IsNullOrEmpty(e) || e.Contains("@example") || e.Contains("@email")); 133 | if (testMode) 134 | { 135 | log.LogWarning($"Sending email with body {message.HtmlContent}"); 136 | } 137 | else 138 | { 139 | await sender.AddAsync(message); 140 | } 141 | 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/Functions/ActivityFunctions.cs: -------------------------------------------------------------------------------- 1 | using Azure.Data.Tables; 2 | using Azure.Storage.Blobs; 3 | using Azure.Storage.Sas; 4 | using DurableECommerceWorkflowIsolated.Extensions; 5 | using DurableECommerceWorkflowIsolated.Models; 6 | using Microsoft.Azure.Functions.Worker; 7 | using Microsoft.Extensions.Logging; 8 | using SendGrid; 9 | using SendGrid.Helpers.Mail; 10 | 11 | namespace DurableECommerceWorkflowIsolated.Functions; 12 | 13 | public class ActivityFunctions 14 | { 15 | private readonly TableServiceClient tableServiceClient; 16 | private readonly BlobServiceClient blobServiceClient; 17 | private readonly ISendGridClient sendGridClient; 18 | 19 | // doesn't seem like we can mix activity fnctions with table output at the moment? or some other issue 20 | // https://github.com/Azure/azure-functions-dotnet-worker/blob/main/samples/Extensions/Table/TableFunction.cs 21 | // [TableOutput(OrderEntity.TableName, Connection = "AzureWebJobsStorage")] 22 | 23 | public ActivityFunctions(TableServiceClient tableServiceClient, 24 | BlobServiceClient blobServiceClient, 25 | ISendGridClient sendGridClient) 26 | { 27 | this.tableServiceClient = tableServiceClient; 28 | this.blobServiceClient = blobServiceClient; 29 | this.sendGridClient = sendGridClient; 30 | } 31 | 32 | [Function("A_SaveOrderToDatabase")] 33 | public async Task SaveOrderToDatabase( 34 | [ActivityTrigger] Order order, 35 | FunctionContext context) 36 | { 37 | var log = context.GetLogger(nameof(SaveOrderToDatabase)); 38 | // can't inject as function parameter 39 | log.LogInformation($"Saving order {order.Id} to database"); 40 | var tableClient = tableServiceClient.GetTableClient(OrderEntity.TableName); 41 | await tableClient.CreateIfNotExistsAsync(); 42 | 43 | var orderEntity = new OrderEntity 44 | { 45 | PartitionKey = OrderEntity.OrderPartitionKey, 46 | RowKey = order.Id, 47 | OrchestrationId = order.OrchestrationId, 48 | Items = string.Join(",", order.Items.Select(i => i.ProductId)), 49 | Email = order.PurchaserEmail, 50 | OrderDate = DateTime.UtcNow.Date, 51 | Amount = order.Items.Sum(i => i.Amount) 52 | }; 53 | var resp = await tableClient.AddEntityAsync(orderEntity); 54 | if (resp.IsError) 55 | { 56 | log.LogError($"Failed to write order {order.Id} for orchestration {order.OrchestrationId} to table storage"); 57 | } 58 | } 59 | 60 | [Function("A_CreatePersonalizedPdf")] 61 | public async Task CreatePersonalizedPdf( 62 | [ActivityTrigger] PdfInfo pdfInfo, 63 | FunctionContext context) 64 | { 65 | var log = context.GetLogger(nameof(CreatePersonalizedPdf)); 66 | var assetsContainer = blobServiceClient.GetBlobContainerClient("assets"); 67 | 68 | log.LogInformation("Creating PDF"); 69 | if (pdfInfo.ProductId == "error") 70 | throw new InvalidOperationException("Can't create the PDF for this product"); 71 | var fileName = $"{pdfInfo.OrderId}/{pdfInfo.ProductId}-pdf.txt"; 72 | await assetsContainer.CreateIfNotExistsAsync(); 73 | var blob = assetsContainer.GetBlobClient(fileName); 74 | await blob.UploadTextAsync($"Example {pdfInfo.ProductId} PDF for {pdfInfo.PurchaserEmail}"); 75 | return blob.GenerateSasUri(BlobSasPermissions.Read, 76 | DateTimeOffset.UtcNow.AddDays(1)).ToString(); 77 | } 78 | 79 | [Function("A_SendOrderConfirmationEmail")] 80 | public async Task SendOrderConfirmationEmail( 81 | [ActivityTrigger] ConfirmationInfo input, 82 | FunctionContext context) 83 | { 84 | var log = context.GetLogger(nameof(SendOrderConfirmationEmail)); 85 | 86 | log.LogInformation($"Sending Order Confirmation Email to {input.Order.PurchaserEmail}"); 87 | var body = $"Thanks for your order, you can download your files here: " + 88 | string.Join(" ", input.Order.Items?.Zip(input.Files, (i, f) => $"{i.ProductId}
") ?? Array.Empty()); 89 | var message = GenerateMail(input.Order.PurchaserEmail, $"Your order {input.Order.Id}", body); 90 | await sendGridClient.PostAsync(message, log); 91 | } 92 | 93 | [Function("A_SendProblemEmail")] 94 | public async Task SendProblemEmail( 95 | [ActivityTrigger] Order order, 96 | FunctionContext context) 97 | { 98 | var log = context.GetLogger(nameof(SendProblemEmail)); 99 | 100 | log.LogInformation($"Sending Problem Email {order.PurchaserEmail}"); 101 | var body = "We're very sorry there was a problem processing your order.
" + 102 | " Please contact customer support."; 103 | var message = GenerateMail(order.PurchaserEmail, $"Problem with order {order.Id}", body); 104 | await sendGridClient.PostAsync(message, log); 105 | } 106 | 107 | private static SendGridMessage GenerateMail(string recipient, string subject, string body) 108 | { 109 | var recipientEmail = new EmailAddress(recipient); 110 | var senderEmail = new EmailAddress(Environment.GetEnvironmentVariable("SenderEmail")); 111 | 112 | var message = new SendGridMessage(); 113 | message.Subject = subject; 114 | message.From = senderEmail; 115 | message.AddTo(recipientEmail); 116 | message.HtmlContent = body; 117 | return message; 118 | } 119 | 120 | [Function("A_SendNotApprovedEmail")] 121 | public async Task SendNotApprovedEmail( 122 | [ActivityTrigger] Order order, 123 | FunctionContext context) 124 | { 125 | var log = context.GetLogger(nameof(SendNotApprovedEmail)); 126 | 127 | log.LogInformation($"Sending Not Approved Email {order.PurchaserEmail}"); 128 | var body = $"We're very sorry we were not able to approve your order #{order.Id}.
" + 129 | " Please contact customer support."; 130 | var message = GenerateMail(order.PurchaserEmail, $"Order {order.Id} rejected", body); 131 | await sendGridClient.PostAsync(message, log); 132 | } 133 | 134 | [Function("A_RequestOrderApproval")] 135 | public async Task RequestOrderApproval( 136 | [ActivityTrigger] Order order, 137 | FunctionContext context) 138 | { 139 | var log = context.GetLogger(nameof(RequestOrderApproval)); 140 | 141 | log.LogInformation($"Requesting Approval for Order {order.PurchaserEmail}"); 142 | var subject = $"Order {order.Id} requires approval"; 143 | var approverEmail = Environment.GetEnvironmentVariable("ApproverEmail"); 144 | var host = Environment.GetEnvironmentVariable("Host"); 145 | 146 | var approveUrl = $"{host}/manage"; 147 | var body = $"Please review Order {order.Id}
" 148 | + $"for product {string.Join(",", order.Items.Select(o => o.ProductId))}" 149 | + $" and amount ${order.Total()}"; 150 | 151 | var message = GenerateMail(approverEmail, subject, body); 152 | await sendGridClient.PostAsync(message, log); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /DurableECommerceWorkflowIsolated/ApiFunctions/OrderStatusFunctions.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Text.Json; 3 | using Azure.Data.Tables; 4 | using DurableECommerceWorkflowIsolated.Models; 5 | using Microsoft.Azure.Functions.Worker; 6 | using Microsoft.Azure.Functions.Worker.Http; 7 | using Microsoft.DurableTask.Client; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace DurableECommerceWorkflowIsolated.ApiFunctions 12 | { 13 | public static class OrderStatusFunctions 14 | { 15 | [Function(nameof(GetOrderStatus))] 16 | public static async Task GetOrderStatus( 17 | [HttpTrigger(AuthorizationLevel.Anonymous, 18 | "get", Route = "orderstatus/{id}")]HttpRequestData req, 19 | [DurableClient] DurableTaskClient durableTaskClient, 20 | //[TableInput(OrderEntity.TableName, OrderEntity.OrderPartitionKey, "{id}", Connection = "AzureWebJobsStorage")] OrderEntity order, - fails with converting string to OrderEntity 21 | FunctionContext functionContext, string id) 22 | { 23 | var log = functionContext.GetLogger(nameof(GetOrderStatus)); 24 | log.LogInformation($"Checking status of order {id}"); 25 | 26 | var tableServiceClient = functionContext.InstanceServices.GetRequiredService(); 27 | var tableClient = tableServiceClient.GetTableClient(OrderEntity.TableName); 28 | var orderResp = await tableClient.GetEntityIfExistsAsync(OrderEntity.OrderPartitionKey, id); 29 | 30 | if (!orderResp.HasValue) 31 | { 32 | return req.CreateResponse(HttpStatusCode.NotFound); 33 | } 34 | var order = orderResp.Value!; 35 | 36 | var status = await durableTaskClient.GetInstanceAsync(order.OrchestrationId, true); 37 | if (status == null) 38 | { 39 | log.LogError($"Could not fetch instance metadata for order {order.OrchestrationId}"); 40 | return req.CreateResponse(HttpStatusCode.InternalServerError); 41 | } 42 | var statusObj = new 43 | { 44 | status.InstanceId, 45 | CreatedTime = status.CreatedAt, 46 | CustomStatus = DeserializeCustomStatus(status.SerializedCustomStatus), 47 | Output = DeserializeOutput(status.SerializedOutput), 48 | LastUpdatedTime = status.LastUpdatedAt, 49 | status.RuntimeStatus, 50 | order.Items, 51 | order.Amount, 52 | PurchaserEmail = order.Email 53 | }; 54 | 55 | var resp = req.CreateResponse(HttpStatusCode.OK); 56 | await resp.WriteAsJsonAsync(statusObj, CreateOrderFunctions.serializer); 57 | return resp; 58 | } 59 | 60 | [Function("DeleteOrder")] 61 | public static async Task DeleteOrder( 62 | [HttpTrigger(AuthorizationLevel.Anonymous, 63 | "delete", Route = "order/{id}")]HttpRequestData req, 64 | [DurableClient] DurableTaskClient durableTaskClient, 65 | //[TableInput(OrderEntity.TableName, OrderEntity.OrderPartitionKey, "{id}", Connection = "AzureWebJobsStorage")] OrderEntity order, 66 | FunctionContext functionContext, string id) 67 | { 68 | var log = functionContext.GetLogger(nameof(DeleteOrder)); 69 | 70 | var tableServiceClient = functionContext.InstanceServices.GetRequiredService(); 71 | var tableClient = tableServiceClient.GetTableClient(OrderEntity.TableName); 72 | var orderResp = await tableClient.GetEntityIfExistsAsync(OrderEntity.OrderPartitionKey, id); 73 | 74 | if (!orderResp.HasValue) 75 | { 76 | // could be that there is an orchestration for this order, but its not in the order table anymore 77 | log.LogWarning($"Cannot find order {id}"); 78 | return req.CreateResponse(HttpStatusCode.NotFound); 79 | } 80 | var order = orderResp.Value; 81 | log.LogInformation($"Deleting order {id}"); 82 | 83 | var status = await durableTaskClient.GetInstanceAsync(order.OrchestrationId, false); 84 | if (status?.RuntimeStatus == OrchestrationRuntimeStatus.Running) 85 | { 86 | log.LogWarning($"Order is in progress {id}"); 87 | return req.CreateResponse(HttpStatusCode.BadRequest); 88 | } 89 | await durableTaskClient.PurgeInstanceAsync(order.OrchestrationId); 90 | 91 | return req.CreateResponse(HttpStatusCode.OK); 92 | } 93 | 94 | [Function("GetAllOrders")] 95 | public static async Task GetAllOrders( 96 | [HttpTrigger(AuthorizationLevel.Anonymous, 97 | "get", Route = null)]HttpRequestData req, 98 | [DurableClient] DurableTaskClient durableTaskClient, 99 | FunctionContext functionContext) 100 | { 101 | var log = functionContext.GetLogger(nameof(GetAllOrders)); 102 | log.LogInformation("getting all orders."); 103 | // just get orders in the last couple of hours to keep manage screen simple 104 | // interested in orders of all statuses 105 | var metadata = await durableTaskClient.GetAllInstancesAsync( 106 | new OrchestrationQuery(DateTime.Today.AddHours(-8.0), 107 | FetchInputsAndOutputs: true, 108 | Statuses: Enum.GetValues(typeof(OrchestrationRuntimeStatus)).Cast())) 109 | .ToListAsync(); 110 | var statuses = metadata.Select(status => new 111 | { 112 | status.InstanceId, 113 | CreatedTime = status.CreatedAt, 114 | CustomStatus = DeserializeCustomStatus(status.SerializedCustomStatus), 115 | Output = DeserializeOutput(status.SerializedOutput), 116 | Input = DeserializeInput(status.SerializedInput), 117 | LastUpdatedTime = status.LastUpdatedAt, 118 | status.RuntimeStatus 119 | }).ToList(); 120 | var response = req.CreateResponse(HttpStatusCode.OK); 121 | await response.WriteAsJsonAsync(statuses, CreateOrderFunctions.serializer); 122 | return response; 123 | } 124 | 125 | private static object? DeserializeOutput(string? serializedOutput) 126 | { 127 | if (string.IsNullOrEmpty(serializedOutput)) return null; 128 | if (serializedOutput.StartsWith("{")) 129 | return JsonSerializer.Deserialize(serializedOutput); 130 | return serializedOutput; // serialized output might not actually be serialized JSON at all! 131 | } 132 | 133 | private static Order? DeserializeInput(string? serializedInput) 134 | { 135 | return JsonSerializer.Deserialize(serializedInput ?? "{}"); 136 | } 137 | 138 | private static string DeserializeCustomStatus(string? serializedCustomStatus) 139 | { 140 | if (string.IsNullOrEmpty(serializedCustomStatus)) return ""; 141 | var s = JsonSerializer.Deserialize(serializedCustomStatus); 142 | return s ?? ""; 143 | } 144 | } 145 | } --------------------------------------------------------------------------------