From 7a995cbfc10bd6a25c8020b4d4cb6dff8f4efbae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Ku=C5=BEela?= Date: Thu, 18 Aug 2022 10:41:10 +0200 Subject: [PATCH 1/2] Add task to store files from multipart/form-data POST request, extend send email task to handle attachments --- .../OrchardCore.Email.csproj | 2 + .../Views/Items/EmailTask.Fields.Edit.cshtml | 16 +++ .../Workflows/Activities/EmailTask.cs | 74 +++++++++++++- .../Drivers/EmailTaskDisplayDriver.cs | 4 + .../ViewModels/EmailTaskViewModel.cs | 2 + .../Activities/SaveFormAttachmentsTask.cs | 99 +++++++++++++++++++ .../SaveFormAttachmentsTaskDisplayDriver.cs | 22 +++++ .../OrchardCore.Workflows/Manifest.cs | 8 ++ .../OrchardCore.Workflows.csproj | 2 + .../OrchardCore.Workflows/Startup.cs | 9 ++ .../SaveFormAttachmentsTaskViewModel.cs | 13 +++ ...veFormAttachmentsTask.Fields.Design.cshtml | 9 ++ ...SaveFormAttachmentsTask.Fields.Edit.cshtml | 14 +++ ...ormAttachmentsTask.Fields.Thumbnail.cshtml | 2 + .../Workflows/EmailTaskTests.cs | 5 + 15 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 src/OrchardCore.Modules/OrchardCore.Workflows/Activities/SaveFormAttachmentsTask.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Workflows/Drivers/SaveFormAttachmentsTaskDisplayDriver.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Workflows/ViewModels/SaveFormAttachmentsTaskViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/SaveFormAttachmentsTask.Fields.Design.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/SaveFormAttachmentsTask.Fields.Edit.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/SaveFormAttachmentsTask.Fields.Thumbnail.cshtml diff --git a/src/OrchardCore.Modules/OrchardCore.Email/OrchardCore.Email.csproj b/src/OrchardCore.Modules/OrchardCore.Email/OrchardCore.Email.csproj index eda177bd7df..fe961a192b3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Email/OrchardCore.Email.csproj +++ b/src/OrchardCore.Modules/OrchardCore.Email/OrchardCore.Email.csproj @@ -18,6 +18,8 @@ + + diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Views/Items/EmailTask.Fields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Email/Views/Items/EmailTask.Fields.Edit.cshtml index 147dec3add6..b10fde7b6c1 100644 --- a/src/OrchardCore.Modules/OrchardCore.Email/Views/Items/EmailTask.Fields.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Email/Views/Items/EmailTask.Fields.Edit.cshtml @@ -50,6 +50,22 @@ @T["The subject of the email message. With Liquid support."] +
+
+ + + @T["If checked, attachments are included into email. They are also stored in predefined folder."] +
+
+ +
+
+ + + @T["If checked, attachments are removed from file storage after email is sent."] +
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Workflows/Activities/EmailTask.cs b/src/OrchardCore.Modules/OrchardCore.Email/Workflows/Activities/EmailTask.cs index f9100bcca87..fa2ea445605 100644 --- a/src/OrchardCore.Modules/OrchardCore.Email/Workflows/Activities/EmailTask.cs +++ b/src/OrchardCore.Modules/OrchardCore.Email/Workflows/Activities/EmailTask.cs @@ -1,8 +1,14 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using OrchardCore.Environment.Shell; +using OrchardCore.FileStorage; +using OrchardCore.FileStorage.FileSystem; +using OrchardCore.Media; using OrchardCore.Workflows.Abstractions.Models; using OrchardCore.Workflows.Activities; using OrchardCore.Workflows.Models; @@ -16,17 +22,26 @@ public class EmailTask : TaskActivity private readonly IWorkflowExpressionEvaluator _expressionEvaluator; private readonly IStringLocalizer S; private readonly HtmlEncoder _htmlEncoder; + private readonly IFileStore _fileStore; + private readonly IOptions _shellOptions; + readonly ShellSettings _shellSettings; public EmailTask( ISmtpService smtpService, IWorkflowExpressionEvaluator expressionEvaluator, IStringLocalizer localizer, + IMediaFileStore mediaFileStore, + IOptions shellOptions, + ShellSettings shellSettings, HtmlEncoder htmlEncoder ) { _smtpService = smtpService; _expressionEvaluator = expressionEvaluator; S = localizer; + _fileStore = mediaFileStore; + _shellOptions = shellOptions; + _shellSettings = shellSettings; _htmlEncoder = htmlEncoder; } @@ -101,6 +116,18 @@ public bool IsBodyText set => SetProperty(value); } + public bool IncludeStoredAttachments + { + get => GetProperty(() => true); + set => SetProperty(value); + } + + public bool RemoveStoredAttachments + { + get => GetProperty(() => true); + set => SetProperty(value); + } + public override IEnumerable GetPossibleOutcomes(WorkflowExecutionContext workflowContext, ActivityContext activityContext) { return Outcomes(S["Done"], S["Failed"]); @@ -117,6 +144,7 @@ public override async Task ExecuteAsync(WorkflowExecuti var subject = await _expressionEvaluator.EvaluateAsync(Subject, workflowContext, null); var body = await _expressionEvaluator.EvaluateAsync(Body, workflowContext, _htmlEncoder); var bodyText = await _expressionEvaluator.EvaluateAsync(BodyText, workflowContext, null); + IFileStore fileStore = null; var message = new MailMessage { @@ -131,7 +159,7 @@ public override async Task ExecuteAsync(WorkflowExecuti Body = body?.Trim(), BodyText = bodyText?.Trim(), IsBodyHtml = IsBodyHtml, - IsBodyText = IsBodyText + IsBodyText = IsBodyText, }; if (!String.IsNullOrWhiteSpace(sender)) @@ -139,7 +167,10 @@ public override async Task ExecuteAsync(WorkflowExecuti message.Sender = sender.Trim(); } + fileStore = await AddMailAttachments(workflowContext, fileStore, message); var result = await _smtpService.SendAsync(message); + await DeleteStoredAttachments(workflowContext, fileStore, result, message); + workflowContext.LastResult = result; if (!result.Succeeded) @@ -149,5 +180,46 @@ public override async Task ExecuteAsync(WorkflowExecuti return Outcomes("Done"); } + + private async Task DeleteStoredAttachments(WorkflowExecutionContext workflowContext, IFileStore fileStore, SmtpResult result, MailMessage message) + { + if (RemoveStoredAttachments && result.Succeeded && fileStore != null && workflowContext.Properties.ContainsKey("FormAttachments")) + { + // close open file streams + message.Attachments.ForEach(p => p.Stream.Dispose()); + + foreach (var filePath in (List)workflowContext.Properties["FormAttachments"]) + { + var success = await fileStore?.TryDeleteFileAsync(filePath); + } + } + } + + private async Task AddMailAttachments(WorkflowExecutionContext workflowContext, IFileStore fileStore, MailMessage message) + { + if (IncludeStoredAttachments + && workflowContext.Properties.ContainsKey("FormAttachmentsUseMediaFileStore") + && workflowContext.Properties.ContainsKey("FormAttachments")) + { + bool useMediaFileStore = (bool)workflowContext.Properties["FormAttachmentsUseMediaFileStore"]; + fileStore = useMediaFileStore ? _fileStore : CreateDefaultFileStore(); + foreach (var filePath in (List)workflowContext.Properties["FormAttachments"]) + { + var fileInfo = await fileStore.GetFileInfoAsync(filePath); + var fileStream = await fileStore.GetFileStreamAsync(fileInfo); + message.Attachments.Add(new MailMessageAttachment() { Filename = fileInfo.Name, Stream = fileStream }); + } + } + + return fileStore; + } + + private IFileStore CreateDefaultFileStore() + { + var shell = _shellOptions.Value; + var innerPath = PathExtensions.Combine(shell.ShellsContainerName, _shellSettings.Name); + var directory = PathExtensions.Combine(shell.ShellsApplicationDataPath, innerPath); + return new FileSystemStore(directory); + } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Workflows/Drivers/EmailTaskDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Email/Workflows/Drivers/EmailTaskDisplayDriver.cs index 987845834c3..f2fc4dee0a2 100644 --- a/src/OrchardCore.Modules/OrchardCore.Email/Workflows/Drivers/EmailTaskDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Email/Workflows/Drivers/EmailTaskDisplayDriver.cs @@ -20,6 +20,8 @@ protected override void EditActivity(EmailTask activity, EmailTaskViewModel mode model.IsBodyText = activity.IsBodyText; model.BccExpression = activity.Bcc.Expression; model.CcExpression = activity.Cc.Expression; + model.IncludeStoredAttachments = activity.IncludeStoredAttachments; + model.RemoveStoredAttachments = activity.RemoveStoredAttachments; } protected override void UpdateActivity(EmailTaskViewModel model, EmailTask activity) @@ -35,6 +37,8 @@ protected override void UpdateActivity(EmailTaskViewModel model, EmailTask activ activity.IsBodyText = model.IsBodyText; activity.Bcc = new WorkflowExpression(model.BccExpression); activity.Cc = new WorkflowExpression(model.CcExpression); + activity.IncludeStoredAttachments = model.IncludeStoredAttachments; + activity.RemoveStoredAttachments = model.RemoveStoredAttachments; } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Workflows/ViewModels/EmailTaskViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Email/Workflows/ViewModels/EmailTaskViewModel.cs index 49dcef21211..0ddee01a350 100644 --- a/src/OrchardCore.Modules/OrchardCore.Email/Workflows/ViewModels/EmailTaskViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Email/Workflows/ViewModels/EmailTaskViewModel.cs @@ -23,5 +23,7 @@ public class EmailTaskViewModel public bool IsBodyHtml { get; set; } public bool IsBodyText { get; set; } + public bool IncludeStoredAttachments { get; set; } + public bool RemoveStoredAttachments { get; set; } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Activities/SaveFormAttachmentsTask.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Activities/SaveFormAttachmentsTask.cs new file mode 100644 index 00000000000..0d584a0cd1e --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Activities/SaveFormAttachmentsTask.cs @@ -0,0 +1,99 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using OrchardCore.Environment.Shell; +using OrchardCore.FileStorage; +using OrchardCore.FileStorage.FileSystem; +using OrchardCore.Media; +using OrchardCore.Workflows.Abstractions.Models; +using OrchardCore.Workflows.Models; +using OrchardCore.Workflows.Services; + +namespace OrchardCore.Workflows.Activities +{ + public class SaveFormAttachmentsTask : TaskActivity + { + readonly IOptions _shellOptions; + protected readonly IStringLocalizer S; + readonly ShellSettings _shellSettings; + private readonly IFileStore _fileStore; + private readonly IHttpContextAccessor _http; + private readonly IWorkflowExpressionEvaluator _expressionEvaluator; + + public SaveFormAttachmentsTask( + IWorkflowExpressionEvaluator expressionEvaluator, + IStringLocalizer localizer, + IOptions shellOptions, + ShellSettings shellSettings, + IMediaFileStore mediaFileStore, + IHttpContextAccessor httpContextAccessor) + { + _shellOptions = shellOptions; + _shellSettings = shellSettings; + _fileStore = mediaFileStore; + _http = httpContextAccessor; + S = localizer; + _expressionEvaluator = expressionEvaluator; + } + + public override string Name => nameof(SaveFormAttachmentsTask); + + public override LocalizedString DisplayText => S["Save Form Attachments Task"]; + + public override LocalizedString Category => S["UI"]; + + public override IEnumerable GetPossibleOutcomes(WorkflowExecutionContext workflowContext, ActivityContext activityContext) + { + return Outcomes(S["Done"]); + } + + public override bool CanExecute(WorkflowExecutionContext workflowContext, ActivityContext activityContext) + { + return _http.HttpContext?.Request?.Form != null; + } + + public override async Task ExecuteAsync(WorkflowExecutionContext workflowContext, ActivityContext activityContext) + { + // use either configured mediaFileStore or create new FileSystemStore --> TODO: mediaFileStore does not resolve permissions, everything is in public folders and can be accessible. + // to put files in different folder, we would need either to register some private MediaFileStore, or allow to specify folder outside base path (?) + var fileStore = UseMediaFileStore ? _fileStore : CreateDefaultFileStore(); + var filePaths = new List(); + foreach (var file in _http.HttpContext.Request.Form.Files) + { + var filePath = PathExtensions.Combine(Folder, $"{workflowContext.WorkflowId}-{file.FileName}"); + filePaths.Add(await fileStore.CreateFileFromStreamAsync(filePath, file.OpenReadStream())); + } + + workflowContext.Properties["FormAttachments"] = filePaths; + workflowContext.Properties["FormAttachmentsUseMediaFileStore"] = UseMediaFileStore; + return Outcomes("Done"); + } + + public string Folder + { + get => GetProperty(() => string.Empty); + set => SetProperty(value); + } + + public bool UseMediaFileStore + { + get => GetProperty(); + set => SetProperty(value); + } + + private IFileStore CreateDefaultFileStore() + { + var shell = _shellOptions.Value; + var innerPath = PathExtensions.Combine(shell.ShellsContainerName, _shellSettings.Name); + var directory = PathExtensions.Combine(shell.ShellsApplicationDataPath, innerPath); + + // do not include Folder in FileSystemStore path, it is included in file path. + var destPath = PathExtensions.Combine(directory, Folder); + Directory.CreateDirectory(destPath); + return new FileSystemStore(directory); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Drivers/SaveFormAttachmentsTaskDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Drivers/SaveFormAttachmentsTaskDisplayDriver.cs new file mode 100644 index 00000000000..0ed5faa604c --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Drivers/SaveFormAttachmentsTaskDisplayDriver.cs @@ -0,0 +1,22 @@ +using OrchardCore.Workflows.Activities; +using OrchardCore.Workflows.Display; +using OrchardCore.Workflows.Models; +using OrchardCore.Workflows.ViewModels; + +namespace OrchardCore.Workflows.Drivers +{ + public class SaveFormAttachmentsTaskDisplayDriver : ActivityDisplayDriver + { + protected override void EditActivity(SaveFormAttachmentsTask activity, SaveFormAttachmentsTaskViewModel model) + { + model.Folder = activity.Folder; + model.UseMediaFileStore = activity.UseMediaFileStore; + } + + protected override void UpdateActivity(SaveFormAttachmentsTaskViewModel model, SaveFormAttachmentsTask activity) + { + activity.Folder = model.Folder; + activity.UseMediaFileStore = model.UseMediaFileStore; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Manifest.cs index fe61bfb27b6..598159d4b2a 100644 --- a/src/OrchardCore.Modules/OrchardCore.Workflows/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Manifest.cs @@ -40,3 +40,11 @@ Dependencies = new[] { "OrchardCore.Workflows" }, Category = "Workflows" )] + +[assembly: Feature( + Id = "OrchardCore.Workflows.SaveFormAttachments", + Name = "Save Form Attachments Activities", + Description = "Provides Save Form Attachments activity.", + Dependencies = new[] { "OrchardCore.Workflows", "OrchardCore.Media", "OrchardCore.FileStorage" }, + Category = "Workflows" +)] diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/OrchardCore.Workflows.csproj b/src/OrchardCore.Modules/OrchardCore.Workflows/OrchardCore.Workflows.csproj index b45a448ce03..60c2f1d4137 100644 --- a/src/OrchardCore.Modules/OrchardCore.Workflows/OrchardCore.Workflows.csproj +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/OrchardCore.Workflows.csproj @@ -28,6 +28,8 @@ + + diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Startup.cs index 104b9e08178..47e9ba23b87 100644 --- a/src/OrchardCore.Modules/OrchardCore.Workflows/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Startup.cs @@ -143,4 +143,13 @@ public override void ConfigureServices(IServiceCollection services) services.AddActivity(); } } + + [Feature("OrchardCore.Workflows.SaveFormAttachments")] + public class SaveFormAttachmentsStartup : StartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.AddActivity(); + } + } } diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/ViewModels/SaveFormAttachmentsTaskViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/ViewModels/SaveFormAttachmentsTaskViewModel.cs new file mode 100644 index 00000000000..52ce8068c9c --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/ViewModels/SaveFormAttachmentsTaskViewModel.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace OrchardCore.Workflows.ViewModels +{ + public class SaveFormAttachmentsTaskViewModel + { + [Required] + public string Folder { get; set; } + + [Required] + public bool UseMediaFileStore { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/SaveFormAttachmentsTask.Fields.Design.cshtml b/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/SaveFormAttachmentsTask.Fields.Design.cshtml new file mode 100644 index 00000000000..bcba60d5893 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/SaveFormAttachmentsTask.Fields.Design.cshtml @@ -0,0 +1,9 @@ +@model OrchardCore.Workflows.ViewModels.ActivityViewModel + + +
+

@Model.Activity.GetTitleOrDefault(() => T["Save form attachments"])

+
+ + +@Model.Activity.Folder \ No newline at end of file diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/SaveFormAttachmentsTask.Fields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/SaveFormAttachmentsTask.Fields.Edit.cshtml new file mode 100644 index 00000000000..773fabe8660 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/SaveFormAttachmentsTask.Fields.Edit.cshtml @@ -0,0 +1,14 @@ +@model OrchardCore.Workflows.ViewModels.SaveFormAttachmentsTaskViewModel + +
+ + + + @T["The folder name. In case of not using media file storage, folder is created under tenant folder."] +
+ +
+ + + @T["If checked, configured media file storage is used. Beware, currently not secured, so your files would be visible."] +
\ No newline at end of file diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/SaveFormAttachmentsTask.Fields.Thumbnail.cshtml b/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/SaveFormAttachmentsTask.Fields.Thumbnail.cshtml new file mode 100644 index 00000000000..ab672a65093 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/SaveFormAttachmentsTask.Fields.Thumbnail.cshtml @@ -0,0 +1,2 @@ +

@T["Save form attachments"]

+

@T["Stores attachments from POST request into file system or predefined media file storage. Request has to be of multipart/form-data enctype. Attachments can be consumed by Send Email Task."]

\ No newline at end of file diff --git a/test/OrchardCore.Tests/Modules/OrchardCore.Email/Workflows/EmailTaskTests.cs b/test/OrchardCore.Tests/Modules/OrchardCore.Email/Workflows/EmailTaskTests.cs index d86d77ff58f..495bd2c2a11 100644 --- a/test/OrchardCore.Tests/Modules/OrchardCore.Email/Workflows/EmailTaskTests.cs +++ b/test/OrchardCore.Tests/Modules/OrchardCore.Email/Workflows/EmailTaskTests.cs @@ -10,6 +10,8 @@ using OrchardCore.Email; using OrchardCore.Email.Services; using OrchardCore.Email.Workflows.Activities; +using OrchardCore.Environment.Shell; +using OrchardCore.Media; using OrchardCore.Workflows.Models; using OrchardCore.Workflows.Services; using Xunit; @@ -28,6 +30,9 @@ public async Task ExecuteTask_WhenToAndCcAndBccAreNotSet_ShouldFails() smtpService, new SimpleWorkflowExpressionEvaluator(), Mock.Of>(), + Mock.Of(), + Mock.Of>(), + Mock.Of(), HtmlEncoder.Default) { Subject = new WorkflowExpression("Test"), From 6e09e748ac310db8f21b821dbc236c4705f26884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Ku=C5=BEela?= Date: Thu, 18 Aug 2022 12:58:35 +0200 Subject: [PATCH 2/2] update based on review --- .../Activities/SaveFormAttachmentsTask.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Activities/SaveFormAttachmentsTask.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Activities/SaveFormAttachmentsTask.cs index 0d584a0cd1e..6e5223f02b0 100644 --- a/src/OrchardCore.Modules/OrchardCore.Workflows/Activities/SaveFormAttachmentsTask.cs +++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Activities/SaveFormAttachmentsTask.cs @@ -43,7 +43,7 @@ public SaveFormAttachmentsTask( public override LocalizedString DisplayText => S["Save Form Attachments Task"]; - public override LocalizedString Category => S["UI"]; + public override LocalizedString Category => S["Media"]; public override IEnumerable GetPossibleOutcomes(WorkflowExecutionContext workflowContext, ActivityContext activityContext) { @@ -63,7 +63,7 @@ public override async Task ExecuteAsync(WorkflowExecuti var filePaths = new List(); foreach (var file in _http.HttpContext.Request.Form.Files) { - var filePath = PathExtensions.Combine(Folder, $"{workflowContext.WorkflowId}-{file.FileName}"); + var filePath = fileStore.Combine(Folder, $"{workflowContext.WorkflowId}-{file.FileName}"); filePaths.Add(await fileStore.CreateFileFromStreamAsync(filePath, file.OpenReadStream())); }