Skip to content

Commit cce4f48

Browse files
Address PR #7195 review feedback: optimize caching, improve exceptions, add test coverage (#7196)
* Initial plan * Apply PR review feedback: Fix exceptions, optimize caching, improve error handling Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Add unit tests for MaterializerRegistry and LocalWorkflowClient exception handling Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Add unit tests for BackgroundWorkflowCancellationDispatcher tenant headers Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Refactor `WorkflowMaterializerNotFoundException` to improve structure and usability, update related references, and simplify object initialization in test cases. * Update `WorkflowDefinitionServiceTests` to use `WorkflowMaterializerNotFoundException` in place of `InvalidOperationException` for materializer not found scenario --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
1 parent 9bb32ae commit cce4f48

15 files changed

Lines changed: 495 additions & 45 deletions

File tree

src/modules/Elsa.Caching/Services/CacheManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,6 @@ public ValueTask TriggerTokenAsync(string key, CancellationToken cancellationTok
3939
/// <exception cref="InvalidOperationException">Thrown if the factory function returns null.</exception>
4040
public async Task<TItem> GetOrCreateAsync<TItem>(object key, Func<ICacheEntry, Task<TItem>> factory)
4141
{
42-
return await memoryCache.GetOrCreateAsync(key, async entry => await factory(entry)) ?? throw new InvalidOperationException("Factory returned null.");
42+
return await memoryCache.GetOrCreateAsync(key, async entry => await factory(entry)) ?? throw new InvalidOperationException($"Factory returned null for cache key: {key}.");
4343
}
4444
}

src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/GetByDefinitionId/Endpoint.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public override async Task HandleAsync(Request request, CancellationToken cancel
3131
if (!materializerRegistry.IsMaterializerAvailable(definition.MaterializerName))
3232
{
3333
AddError($"The workflow materializer '{definition.MaterializerName}' is not available. The materializer may be disabled or not registered.");
34-
await Send.ErrorsAsync(StatusCodes.Status503ServiceUnavailable, cancellationToken);
34+
await Send.ErrorsAsync(StatusCodes.Status422UnprocessableEntity, cancellationToken);
3535
return;
3636
}
3737

src/modules/Elsa.Workflows.Management/Contracts/IWorkflowDefinitionService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ public interface IWorkflowDefinitionService
115115
/// <param name="filter">The filter specifying criteria to identify the workflow definitions to search for.</param>
116116
/// <param name="cancellationToken">A token to observe while waiting for the task to complete.</param>
117117
/// <returns>
118-
/// A <see cref="WorkflowGraphFindResult"/> containing the workflow graph and its associated definition,
119-
/// or indicating that they do not exist.
118+
/// A collection of <see cref="WorkflowGraphFindResult"/> instances, each containing a workflow graph and its associated definition
119+
/// for every matching workflow definition. If no matching workflow definitions are found, the collection is empty.
120120
/// </returns>
121121
Task<IEnumerable<WorkflowGraphFindResult>> TryFindWorkflowGraphsAsync(WorkflowDefinitionFilter filter, CancellationToken cancellationToken = default);
122122
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Elsa.Workflows.Management.Exceptions;
2+
3+
public class WorkflowMaterializerNotFoundException(string materializerName, string message = "Materializer not found. The materializer may be disabled or not registered") : Exception(message)
4+
{
5+
public string MaterializerName { get; } = materializerName;
6+
}

src/modules/Elsa.Workflows.Management/Services/CachingWorkflowDefinitionService.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,17 @@ public async Task<IEnumerable<WorkflowGraphFindResult>> TryFindWorkflowGraphsAsy
156156
var results = new List<WorkflowGraphFindResult>();
157157
foreach (var workflowDefinition in workflowDefinitions)
158158
{
159+
if (!materializerRegistry.IsMaterializerAvailable(workflowDefinition.MaterializerName))
160+
{
161+
var unavailableResult = new WorkflowGraphFindResult(workflowDefinition, null);
162+
results.Add(unavailableResult);
163+
continue;
164+
}
165+
159166
var cacheKey = cacheManager.CreateWorkflowVersionCacheKey(workflowDefinition.Id);
160167
var workflowGraph = await FindFromCacheAsync(
161168
cacheKey,
162-
async () => !materializerRegistry.IsMaterializerAvailable(workflowDefinition.MaterializerName)
163-
? null
164-
: await MaterializeWorkflowAsync(workflowDefinition, cancellationToken),
169+
async () => await MaterializeWorkflowAsync(workflowDefinition, cancellationToken),
165170
wf => wf.Workflow.Identity.DefinitionId);
166171

167172
var result = new WorkflowGraphFindResult(workflowDefinition, workflowGraph);

src/modules/Elsa.Workflows.Management/Services/MaterializerRegistry.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@ namespace Elsa.Workflows.Management.Services;
33
/// <inheritdoc />
44
public class MaterializerRegistry(Func<IEnumerable<IWorkflowMaterializer>> materializers) : IMaterializerRegistry
55
{
6+
private readonly Lazy<IReadOnlyCollection<IWorkflowMaterializer>> _materializers = new(() => materializers().ToArray());
7+
68
/// <inheritdoc />
7-
public IEnumerable<IWorkflowMaterializer> GetMaterializers() => materializers();
9+
public IEnumerable<IWorkflowMaterializer> GetMaterializers() => _materializers.Value;
810

911
/// <inheritdoc />
1012
public IWorkflowMaterializer? GetMaterializer(string name)
1113
{
12-
return materializers().FirstOrDefault(x => x.Name == name);
14+
return _materializers.Value.FirstOrDefault(x => x.Name == name);
1315
}
1416

1517
/// <inheritdoc />
1618
public bool IsMaterializerAvailable(string name)
1719
{
18-
return materializers().Any(x => x.Name == name);
20+
return _materializers.Value.Any(x => x.Name == name);
1921
}
2022
}

src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionService.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Elsa.Common.Models;
22
using Elsa.Workflows.Management.Entities;
3+
using Elsa.Workflows.Management.Exceptions;
34
using Elsa.Workflows.Management.Filters;
45
using Elsa.Workflows.Management.Models;
56
using Elsa.Workflows.Models;
@@ -21,7 +22,7 @@ public async Task<WorkflowGraph> MaterializeWorkflowAsync(WorkflowDefinition def
2122
var materializer = materializerRegistry.GetMaterializer(definition.MaterializerName);
2223

2324
if (materializer == null)
24-
throw new($"Materializer '{definition.MaterializerName}' not found. The materializer may be disabled or not registered.");
25+
throw new WorkflowMaterializerNotFoundException(definition.MaterializerName);
2526

2627
var workflow = await materializer.MaterializeAsync(definition, cancellationToken);
2728
return await workflowGraphBuilder.BuildAsync(workflow, cancellationToken);

src/modules/Elsa.Workflows.Runtime/Exceptions/WorkflowMaterializerNotFoundException.cs

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Elsa.Workflows.Management;
22
using Elsa.Workflows.Management.Entities;
3+
using Elsa.Workflows.Management.Exceptions;
34
using Elsa.Workflows.Management.Filters;
45
using Elsa.Workflows.Management.Mappers;
56
using Elsa.Workflows.Management.Options;
@@ -208,8 +209,8 @@ private async Task<WorkflowGraph> GetWorkflowGraphAsync(WorkflowInstance workflo
208209
private async Task<WorkflowGraph> GetWorkflowGraphAsync(WorkflowDefinitionHandle definitionHandle, CancellationToken cancellationToken)
209210
{
210211
var result = await workflowDefinitionService.TryFindWorkflowGraphAsync(definitionHandle, cancellationToken);
211-
if (!result.WorkflowDefinitionExists) throw new WorkflowGraphNotFoundException("Workflow graph not found.", definitionHandle);
212-
if (!result.WorkflowGraphExists) throw new WorkflowMaterializerNotFoundException("Workflow materializer not found.", definitionHandle);
212+
if (!result.WorkflowDefinitionExists) throw new WorkflowDefinitionNotFoundException("Workflow definition not found.", definitionHandle);
213+
if (!result.WorkflowGraphExists) throw new WorkflowMaterializerNotFoundException(result.WorkflowDefinition!.MaterializerName);
213214
return result.WorkflowGraph!;
214215
}
215216
}

test/unit/Elsa.Workflows.Management.UnitTests/Services/CachingWorkflowDefinitionServiceTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ private void SetupCacheManager()
481481
private WorkflowGraphFindResult CreateWorkflowGraphFindResult(string definitionId = "def-1", string materializerName = "materializer")
482482
{
483483
var definition = TestHelpers.CreateWorkflowDefinition(definitionId, materializerName);
484-
var (workflow, workflowGraph) = CreateWorkflowAndGraph(definitionId);
484+
var (_, workflowGraph) = CreateWorkflowAndGraph(definitionId);
485485
return new WorkflowGraphFindResult(definition, workflowGraph);
486486
}
487487
}

0 commit comments

Comments
 (0)