private static async Task RaiseEventIfFileExists(DurableOrchestrationClient starter, TraceWriter log, string instanceId, string folderName, string batchId, string filename)
        {
            var concatenatedFilename = $"{batchId}_{filename}";

            if (File.Exists(Path.Combine(folderName, concatenatedFilename)))
            {
                log.Info($"*** TRIGGER: file {concatenatedFilename} for batch {batchId} - found");
                await starter.RaiseEventAsync(instanceId, EventNames.NewFile(filename), null);
            }
            else
            {
                log.Info($"*** TRIGGER: file {concatenatedFilename} for batch {batchId} - missing");
            }
        }
        public static async Task <BatchResponse> RunOrchestrator(
            [OrchestrationTrigger] DurableOrchestrationContext context,
            TraceWriter log)
        {
            var batchContext = context.GetInput <BatchContext>();

            log.Info($"*** ORCHESTRATOR Starting: {batchContext.BatchId} (folder: {batchContext.FolderName})");

            using (var cts = new CancellationTokenSource())
            {
                // Wait for events for all required files
                var requiredFileTasks = batchContext.RequiredFiles
                                        .Select(f => context.WaitForExternalEvent <object>(EventNames.NewFile(f)))
                                        .ToArray();
                var gotFilesTask = Task.WhenAll(requiredFileTasks);


                var timeoutTask = context.CreateTimer(context.CurrentUtcDateTime.Add(WaitForFilesTimeout), cts.Token);

                var firedTask = await Task.WhenAny(gotFilesTask, timeoutTask);

                if (firedTask == timeoutTask)
                {
                    log.Info($"*** ORCHESTRATOR Timeout waiting for batch files for batch {batchContext.BatchId}");
                    // TODO take whatever action is required here (e.g. escalate for human intervention)
                    return(new BatchResponse
                    {
                        BatchId = batchContext.BatchId,
                        Success = false
                    });
                }
                cts.Cancel(); // cancel the timeout and continue processing...
            }

            // Currently process all files in a single activity function
            // If each file can be processed independently then could split into multiple activity invocations
            log.Info($"** ORCHESTRATOR calling Process files");
            await context.CallActivityAsync("ProcessFiles", batchContext);

            log.Info($"*** ORCHESTRATOR Done: {batchContext.BatchId} (folder: {batchContext.FolderName})");
            return(new BatchResponse
            {
                BatchId = batchContext.BatchId,
                Success = true
            });
        }
        public static async Task <HttpResponseMessage> HttpStart(
            // TODO - replace HttpTrigger with eventgrid trigger for blobs
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestMessage req,
            [OrchestrationClient] DurableOrchestrationClient starter,
            TraceWriter log)
        {
            // Determine path
            var query = req.RequestUri.ParseQueryString();
            var path  = query["path"];

            if (string.IsNullOrEmpty(path))
            {
                log.Error("*** path querystring value missing");
                return(req.CreateResponse(HttpStatusCode.BadRequest, "path querystring value missing", new JsonMediaTypeFormatter()));
            }

            // Get context (required files, instance id, ...)
            var batchContext = GetBatchContextFromPath(path);
            var instanceId   = $"instance-{batchContext.BatchId}";

            // Determin if the path is for a file that we care about
            var filename = Path.GetFileName(path);

            if (batchContext.RequiredFiles.Any(f => $"{batchContext.BatchId}_{f}" == filename))
            {
                log.Info($"*** Batch {batchContext.BatchId} - notification for {filename}");
            }
            else
            {
                log.Info($"*** Ignoring path: {path}");
                return(req.CreateResponse(HttpStatusCode.NoContent, "Path ignored", new JsonMediaTypeFormatter()));
            }


            // Find or start an orchestration instance
            log.Info($"*** TRIGGER: Looking up instance: {instanceId}");
            var status = await starter.GetStatusAsync(instanceId);

            if (status == null)
            {
                log.Info($"*** TRIGGER: no instance found - {instanceId} - starting...");
                await starter.StartNewAsync("StorageBatches", instanceId, batchContext);

                log.Info($"*** TRIGGER: Started orchestration with ID = '{instanceId}'.");

                // workaround for https://github.com/Azure/azure-functions-durable-extension/issues/101
                log.Info($"*** TRIGGER: Checking for orchestration with ID {instanceId}...");
                while (null == await starter.GetStatusAsync(instanceId))
                {
                    System.Threading.Thread.Sleep(500);
                    log.Info($"*** TRIGGER: Checking for orchestration with ID {instanceId}...");
                }
                log.Info($"*** TRIGGER: Checking for orchestration with ID {instanceId}... found it!");
            }
            else
            {
                log.Info($"*** TRIGGER: Got existing instance for {instanceId} (name {status.Name}). status {status.RuntimeStatus})");
            }


            // Raise event for the file that triggered us
            log.Info($"*** TRIGGER: {instanceId}: Raising event for file {filename}");
            await starter.RaiseEventAsync(instanceId, EventNames.NewFile(filename), null);

            return(starter.CreateCheckStatusResponse(req, instanceId));
        }