From 2863c980f32c561d06fcfbce32d5f91ad17059bb Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 19 Jun 2026 21:29:00 +0200 Subject: [PATCH] Surface publish validation warnings on API responses In lenient mode (FailOnValidationErrors = false), PublishAsync returns Succeeded = true while ValidationErrors may still be non-empty. Previously the publish API endpoints discarded those warnings, so lenient-mode callers got a 200 OK with no indication of the validation issues. - Publish and Post (save-and-publish) responses now include a ValidationErrors collection populated with the publish result's validation messages. - BulkPublish response now includes a Warnings dictionary mapping each successfully-published definition id to its validation warning messages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs | 6 +++++- .../Endpoints/WorkflowDefinitions/BulkPublish/Models.cs | 3 ++- .../Endpoints/WorkflowDefinitions/Post/Endpoint.cs | 3 ++- .../Endpoints/WorkflowDefinitions/Post/Models.cs | 2 +- .../Endpoints/WorkflowDefinitions/Publish/Endpoint.cs | 3 ++- .../Endpoints/WorkflowDefinitions/Publish/Models.cs | 2 +- 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs index dbff37959..053491b2e 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs @@ -42,6 +42,7 @@ internal class BulkPublish( var skipped = new List(); var updatedConsumers = new List(); var failed = new List(); + var warnings = new Dictionary>(); var publishableDefinitions = new List<(string DefinitionId, WorkflowDefinition Definition)>(); var definitions = (await store.FindManyAsync(new WorkflowDefinitionFilter @@ -97,12 +98,15 @@ internal class BulkPublish( } published.Add(definitionId); + + if (result.ValidationErrors.Count > 0) + warnings[definitionId] = result.ValidationErrors.Select(x => x.Message).ToList(); if (result.AffectedWorkflows.WorkflowDefinitions.Count > 0) updatedConsumers.AddRange(result.AffectedWorkflows.WorkflowDefinitions.Select(x => x.DefinitionId)); } - return new(published, alreadyPublished, notFound, skipped, updatedConsumers, failed); + return new(published, alreadyPublished, notFound, skipped, updatedConsumers, failed, warnings); } } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Models.cs index af74688c0..879a86321 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Models.cs @@ -5,7 +5,7 @@ internal class Request public ICollection DefinitionIds { get; set; } = default!; } -internal class Response(ICollection published, ICollection alreadyPublished, ICollection notFound, ICollection skipped, ICollection updatedConsumers, ICollection failed) +internal class Response(ICollection published, ICollection alreadyPublished, ICollection notFound, ICollection skipped, ICollection updatedConsumers, ICollection failed, IDictionary> warnings) { public ICollection Published { get; } = published; public ICollection AlreadyPublished { get; } = alreadyPublished; @@ -13,4 +13,5 @@ internal class Response(ICollection published, ICollection alrea public ICollection Skipped { get; } = skipped; public ICollection UpdatedConsumers { get; } = updatedConsumers; public ICollection Failed { get; } = failed; + public IDictionary> Warnings { get; } = warnings; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs index a0895762e..1dfd307c7 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs @@ -116,7 +116,8 @@ internal class Post( var mappedDefinition = await linker.MapAsync(draft, cancellationToken); var affectedWorkflows = result?.AffectedWorkflows?.WorkflowDefinitions ?? []; - var response = new Response(mappedDefinition, false, affectedWorkflows.Count); + var validationErrors = result?.ValidationErrors.Select(e => e.Message).ToList() ?? []; + var response = new Response(mappedDefinition, false, affectedWorkflows.Count, validationErrors); await HttpContext.Response.WriteAsJsonAsync(response, serializerOptions, cancellationToken); } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Models.cs index baceba1df..847f83179 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Models.cs @@ -2,4 +2,4 @@ using Elsa.Workflows.Api.Models; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Post; -internal record Response(LinkedWorkflowDefinitionModel WorkflowDefinition, bool AlreadyPublished, int ConsumingWorkflowCount); \ No newline at end of file +internal record Response(LinkedWorkflowDefinitionModel WorkflowDefinition, bool AlreadyPublished, int ConsumingWorkflowCount, ICollection ValidationErrors); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs index 54c4f6949..c5f0116b1 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs @@ -70,8 +70,9 @@ internal class Publish( return; } + var validationErrors = result?.ValidationErrors.Select(e => e.Message).ToList() ?? []; var mappedDefinition = await linker.MapAsync(definition, cancellationToken); - var response = new Response(mappedDefinition, isPublished, result?.AffectedWorkflows.WorkflowDefinitions.Count ?? 0); + var response = new Response(mappedDefinition, isPublished, result?.AffectedWorkflows.WorkflowDefinitions.Count ?? 0, validationErrors); await Send.OkAsync(response, cancellationToken); } } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Models.cs index a4ed95916..2554d668a 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Models.cs @@ -7,4 +7,4 @@ internal class Request public string DefinitionId { get; set; } = default!; } -internal record Response(LinkedWorkflowDefinitionModel WorkflowDefinition, bool AlreadyPublished, int ConsumingWorkflowCount); \ No newline at end of file +internal record Response(LinkedWorkflowDefinitionModel WorkflowDefinition, bool AlreadyPublished, int ConsumingWorkflowCount, ICollection ValidationErrors); \ No newline at end of file