From 1e7f9543116a0caabf3cf26d30d72b512ec1740d Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 23 Aug 2023 10:48:12 +0200 Subject: [PATCH] Implement HTTP correlation ID selectors (#4349) --- src/common/Elsa.Features/Services/IFeature.cs | 3 ++ .../HttpCorrelationIdSelectorBase.cs | 29 +++++++++++ .../HttpWorkflowInstanceIdSelectorBase.cs | 29 +++++++++++ .../Contracts/IHttpCorrelationIdSelector.cs | 19 +++++++ .../IHttpWorkflowInstanceIdSelector.cs | 19 +++++++ src/modules/Elsa.Http/Elsa.Http.csproj | 2 + .../Extensions/ServiceCollectionExtensions.cs | 31 +++++++++++ src/modules/Elsa.Http/Features/HttpFeature.cs | 28 +++++++++- .../Middleware/WorkflowsMiddleware.cs | 51 ++++++++++++++++--- .../HeaderHttpCorrelationIdSelector.cs | 16 ++++++ .../HeaderHttpWorkflowInstanceIdSelector.cs | 16 ++++++ .../QueryStringHttpCorrelationIdSelector.cs | 16 ++++++ ...eryStringHttpWorkflowInstanceIdSelector.cs | 16 ++++++ 13 files changed, 266 insertions(+), 9 deletions(-) create mode 100644 src/modules/Elsa.Http/Abstractions/HttpCorrelationIdSelectorBase.cs create mode 100644 src/modules/Elsa.Http/Abstractions/HttpWorkflowInstanceIdSelectorBase.cs create mode 100644 src/modules/Elsa.Http/Contracts/IHttpCorrelationIdSelector.cs create mode 100644 src/modules/Elsa.Http/Contracts/IHttpWorkflowInstanceIdSelector.cs create mode 100644 src/modules/Elsa.Http/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/modules/Elsa.Http/Selectors/HeaderHttpCorrelationIdSelector.cs create mode 100644 src/modules/Elsa.Http/Selectors/HeaderHttpWorkflowInstanceIdSelector.cs create mode 100644 src/modules/Elsa.Http/Selectors/QueryStringHttpCorrelationIdSelector.cs create mode 100644 src/modules/Elsa.Http/Selectors/QueryStringHttpWorkflowInstanceIdSelector.cs diff --git a/src/common/Elsa.Features/Services/IFeature.cs b/src/common/Elsa.Features/Services/IFeature.cs index 78123e7e5..71c6ed504 100644 --- a/src/common/Elsa.Features/Services/IFeature.cs +++ b/src/common/Elsa.Features/Services/IFeature.cs @@ -1,8 +1,11 @@ +using JetBrains.Annotations; + namespace Elsa.Features.Services; /// /// Represents a feature. /// +[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors | ImplicitUseTargetFlags.Members)] public interface IFeature { /// diff --git a/src/modules/Elsa.Http/Abstractions/HttpCorrelationIdSelectorBase.cs b/src/modules/Elsa.Http/Abstractions/HttpCorrelationIdSelectorBase.cs new file mode 100644 index 000000000..7403aef5c --- /dev/null +++ b/src/modules/Elsa.Http/Abstractions/HttpCorrelationIdSelectorBase.cs @@ -0,0 +1,29 @@ +using Elsa.Http.Contracts; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Http.Abstractions; + +/// +/// Provides a base class for implementing . +/// +public abstract class HttpCorrelationIdSelectorBase : IHttpCorrelationIdSelector +{ + /// + public virtual double Priority => 0; + + /// + /// Override this method to return the correlation ID for the specified HTTP context, or null if no correlation ID could be found. + /// + protected virtual ValueTask GetCorrelationIdAsync(HttpContext httpContext, CancellationToken cancellationToken = default) + { + var correlationId = GetCorrelationId(httpContext); + return new(correlationId); + } + + /// + /// Override this method to return the correlation ID for the specified HTTP context, or null if no correlation ID could be found. + /// + protected virtual string? GetCorrelationId(HttpContext httpContext) => null; + + ValueTask IHttpCorrelationIdSelector.GetCorrelationIdAsync(HttpContext httpContext, CancellationToken cancellationToken) => GetCorrelationIdAsync(httpContext, cancellationToken); +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/Abstractions/HttpWorkflowInstanceIdSelectorBase.cs b/src/modules/Elsa.Http/Abstractions/HttpWorkflowInstanceIdSelectorBase.cs new file mode 100644 index 000000000..6d70522fa --- /dev/null +++ b/src/modules/Elsa.Http/Abstractions/HttpWorkflowInstanceIdSelectorBase.cs @@ -0,0 +1,29 @@ +using Elsa.Http.Contracts; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Http.Abstractions; + +/// +/// Provides a base class for implementing . +/// +public abstract class HttpWorkflowInstanceIdSelectorBase : IHttpWorkflowInstanceIdSelector +{ + /// + public virtual double Priority => 0; + + /// + /// Override this method to return the workflow instance ID for the specified HTTP context, or null if no workflow instance ID could be found. + /// + protected virtual ValueTask GetWorkflowInstanceIdAsync(HttpContext httpContext, CancellationToken cancellationToken = default) + { + var workflowInstanceId = GetWorkflowInstanceId(httpContext); + return new(workflowInstanceId); + } + + /// + /// Override this method to return the workflow instance ID for the specified HTTP context, or null if no workflow instance ID could be found. + /// + protected virtual string? GetWorkflowInstanceId(HttpContext httpContext) => null; + + ValueTask IHttpWorkflowInstanceIdSelector.GetWorkflowInstanceIdAsync(HttpContext httpContext, CancellationToken cancellationToken) => GetWorkflowInstanceIdAsync(httpContext, cancellationToken); +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/Contracts/IHttpCorrelationIdSelector.cs b/src/modules/Elsa.Http/Contracts/IHttpCorrelationIdSelector.cs new file mode 100644 index 000000000..d663acd68 --- /dev/null +++ b/src/modules/Elsa.Http/Contracts/IHttpCorrelationIdSelector.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Http; + +namespace Elsa.Http.Contracts; + +/// +/// Provides a way to select the correlation ID for a request. +/// +public interface IHttpCorrelationIdSelector +{ + /// + /// The priority of this selector. The selector with the highest priority will be used. + /// + double Priority { get; } + + /// + /// Returns the correlation ID for the specified HTTP context, or null if no correlation ID could be found. + /// + ValueTask GetCorrelationIdAsync(HttpContext httpContext, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/Contracts/IHttpWorkflowInstanceIdSelector.cs b/src/modules/Elsa.Http/Contracts/IHttpWorkflowInstanceIdSelector.cs new file mode 100644 index 000000000..28be3880f --- /dev/null +++ b/src/modules/Elsa.Http/Contracts/IHttpWorkflowInstanceIdSelector.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Http; + +namespace Elsa.Http.Contracts; + +/// +/// Provides a way to select the workflow instance ID for a request. +/// +public interface IHttpWorkflowInstanceIdSelector +{ + /// + /// The priority of this selector. The selector with the highest priority will be used. + /// + double Priority { get; } + + /// + /// Returns the workflow instance ID for the specified HTTP context, or null if no workflow instance ID could be found. + /// + ValueTask GetWorkflowInstanceIdAsync(HttpContext httpContext, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/Elsa.Http.csproj b/src/modules/Elsa.Http/Elsa.Http.csproj index 1ceceea66..d0cbfa09e 100644 --- a/src/modules/Elsa.Http/Elsa.Http.csproj +++ b/src/modules/Elsa.Http/Elsa.Http.csproj @@ -29,4 +29,6 @@ + + diff --git a/src/modules/Elsa.Http/Extensions/ServiceCollectionExtensions.cs b/src/modules/Elsa.Http/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..3218b25f0 --- /dev/null +++ b/src/modules/Elsa.Http/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +using Elsa.Http.Contracts; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; + +/// +/// Contains extension methods for the interface. +/// +[PublicAPI] +public static class ServiceCollectionExtensions +{ + /// + /// Adds a implementation to the service collection. + /// + public static IServiceCollection AddHttpCorrelationIdSelector(this IServiceCollection services) where T : class, IHttpCorrelationIdSelector + { + services.AddSingleton(); + return services; + } + + /// + /// Adds a implementation to the service collection. + /// + public static IServiceCollection AddHttpCorrelationIdSelector(this IServiceCollection services, Func factory) + { + services.AddSingleton(factory); + return services; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs index e374ccc65..758be5462 100644 --- a/src/modules/Elsa.Http/Features/HttpFeature.cs +++ b/src/modules/Elsa.Http/Features/HttpFeature.cs @@ -12,6 +12,7 @@ using Elsa.Http.Options; using Elsa.Http.Parsers; using Elsa.Http.PortResolvers; using Elsa.Http.Providers; +using Elsa.Http.Selectors; using Elsa.Http.Services; using Elsa.JavaScript.Features; using Elsa.Liquid.Features; @@ -62,6 +63,24 @@ public class HttpFeature : FeatureBase /// public Action HttpClientBuilder { get; set; } = _ => { }; + /// + /// A list of types to register with the service collection. + /// + public ICollection HttpCorrelationIdSelectorTypes { get; } = new List + { + typeof(HeaderHttpCorrelationIdSelector), + typeof(QueryStringHttpCorrelationIdSelector) + }; + + /// + /// A list of types to register with the service collection. + /// + public ICollection HttpWorkflowInstanceIdSelectorTypes { get; } = new List + { + typeof(HeaderHttpWorkflowInstanceIdSelector), + typeof(QueryStringHttpWorkflowInstanceIdSelector) + }; + /// public override void Configure() { @@ -78,7 +97,7 @@ public class HttpFeature : FeatureBase management.AddActivitiesFrom(); }); - + Services.AddRequestHandler(); } @@ -141,5 +160,12 @@ public class HttpFeature : FeatureBase // AuthenticationBasedHttpEndpointAuthorizationHandler requires Authorization services. // We could consider creating a separate module for installing authorization services. .AddAuthorization(); + + // Add selectors. + foreach (var httpCorrelationIdSelectorType in HttpCorrelationIdSelectorTypes) + Services.AddSingleton(typeof(IHttpCorrelationIdSelector), httpCorrelationIdSelectorType); + + foreach (var httpWorkflowInstanceIdSelectorType in HttpWorkflowInstanceIdSelectorTypes) + Services.AddSingleton(typeof(IHttpWorkflowInstanceIdSelector), httpWorkflowInstanceIdSelectorType); } } \ No newline at end of file diff --git a/src/modules/Elsa.Http/Middleware/WorkflowsMiddleware.cs b/src/modules/Elsa.Http/Middleware/WorkflowsMiddleware.cs index 0b1302341..b0d04a92f 100644 --- a/src/modules/Elsa.Http/Middleware/WorkflowsMiddleware.cs +++ b/src/modules/Elsa.Http/Middleware/WorkflowsMiddleware.cs @@ -27,6 +27,8 @@ public class WorkflowsMiddleware private readonly IWorkflowRuntime _workflowRuntime; private readonly IRouteMatcher _routeMatcher; private readonly IRouteTable _routeTable; + private readonly IEnumerable _correlationIdSelectors; + private readonly IEnumerable _workflowInstanceIdSelectors; private readonly IHttpBookmarkProcessor _httpBookmarkProcessor; private readonly IHttpEndpointWorkflowFaultHandler _httpEndpointWorkflowFaultHandler; private readonly IHttpEndpointAuthorizationHandler _httpEndpointAuthorizationHandler; @@ -52,7 +54,9 @@ public class WorkflowsMiddleware IBookmarkHasher hasher, IBookmarkPayloadSerializer serializer, IRouteMatcher routeMatcher, - IRouteTable routeTable) + IRouteTable routeTable, + IEnumerable correlationIdSelectors, + IEnumerable workflowInstanceIdSelectors) { _next = next; _workflowRuntime = workflowRuntime; @@ -66,6 +70,8 @@ public class WorkflowsMiddleware _serializer = serializer; _routeMatcher = routeMatcher; _routeTable = routeTable; + _correlationIdSelectors = correlationIdSelectors; + _workflowInstanceIdSelectors = workflowInstanceIdSelectors; } /// @@ -97,14 +103,13 @@ public class WorkflowsMiddleware [HttpEndpoint.RequestPathInputKey] = path }; - // TODO: Get correlation ID from query string or header. - var correlationId = default(string); - - var request = httpContext.Request; - var method = request.Method!.ToLowerInvariant(); - var bookmarkPayload = new HttpEndpointBookmarkPayload(matchingPath, method); - var triggerOptions = new TriggerWorkflowsRuntimeOptions(correlationId, default, default, input); var cancellationToken = httpContext.RequestAborted; + var request = httpContext.Request; + var method = request.Method.ToLowerInvariant(); + var correlationId = await GetCorrelationIdAsync(httpContext, httpContext.RequestAborted); + var workflowInstanceId = await GetWorkflowInstanceIdAsync(httpContext, httpContext.RequestAborted); + var bookmarkPayload = new HttpEndpointBookmarkPayload(matchingPath, method); + var triggerOptions = new TriggerWorkflowsRuntimeOptions(correlationId, workflowInstanceId, default, input); var workflowsFilter = new WorkflowsFilter(_activityTypeName, bookmarkPayload, triggerOptions); var workflowMatches = (await _workflowRuntime.FindWorkflowsAsync(workflowsFilter, cancellationToken)).ToList(); @@ -146,6 +151,36 @@ public class WorkflowsMiddleware return routeTemplate; } + private async Task GetCorrelationIdAsync(HttpContext httpContext, CancellationToken cancellationToken) + { + var correlationId = default(string); + + foreach (var selector in _correlationIdSelectors.OrderByDescending(x => x.Priority)) + { + correlationId = await selector.GetCorrelationIdAsync(httpContext, cancellationToken); + + if (correlationId != null) + break; + } + + return correlationId; + } + + private async Task GetWorkflowInstanceIdAsync(HttpContext httpContext, CancellationToken cancellationToken) + { + var workflowInstanceId = default(string); + + foreach (var selector in _workflowInstanceIdSelectors.OrderByDescending(x => x.Priority)) + { + workflowInstanceId = await selector.GetWorkflowInstanceIdAsync(httpContext, cancellationToken); + + if (workflowInstanceId != null) + break; + } + + return workflowInstanceId; + } + private static async Task WriteResponseAsync(HttpContext httpContext, CancellationToken cancellationToken) { var response = httpContext.Response; diff --git a/src/modules/Elsa.Http/Selectors/HeaderHttpCorrelationIdSelector.cs b/src/modules/Elsa.Http/Selectors/HeaderHttpCorrelationIdSelector.cs new file mode 100644 index 000000000..0757f9ab3 --- /dev/null +++ b/src/modules/Elsa.Http/Selectors/HeaderHttpCorrelationIdSelector.cs @@ -0,0 +1,16 @@ +using Elsa.Http.Abstractions; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Http.Selectors; + +/// +/// Returns the correlation ID from the X-Correlation-ID header, if any. +/// +public class HeaderHttpCorrelationIdSelector : HttpCorrelationIdSelectorBase +{ + /// + protected override string? GetCorrelationId(HttpContext httpContext) + { + return httpContext.Request.Headers["X-Correlation-ID"].FirstOrDefault(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/Selectors/HeaderHttpWorkflowInstanceIdSelector.cs b/src/modules/Elsa.Http/Selectors/HeaderHttpWorkflowInstanceIdSelector.cs new file mode 100644 index 000000000..7294d7081 --- /dev/null +++ b/src/modules/Elsa.Http/Selectors/HeaderHttpWorkflowInstanceIdSelector.cs @@ -0,0 +1,16 @@ +using Elsa.Http.Abstractions; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Http.Selectors; + +/// +/// Returns the workflow instance ID from the X-Workflow-Instance-ID header, if any. +/// +public class HeaderHttpWorkflowInstanceIdSelector : HttpWorkflowInstanceIdSelectorBase +{ + /// + protected override string? GetWorkflowInstanceId(HttpContext httpContext) + { + return httpContext.Request.Headers["X-Workflow-Instance-ID"].FirstOrDefault(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/Selectors/QueryStringHttpCorrelationIdSelector.cs b/src/modules/Elsa.Http/Selectors/QueryStringHttpCorrelationIdSelector.cs new file mode 100644 index 000000000..8c632bbb4 --- /dev/null +++ b/src/modules/Elsa.Http/Selectors/QueryStringHttpCorrelationIdSelector.cs @@ -0,0 +1,16 @@ +using Elsa.Http.Abstractions; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Http.Selectors; + +/// +/// Returns the correlation ID from the correlationId query string parameter. +/// +public class QueryStringHttpCorrelationIdSelector : HttpCorrelationIdSelectorBase +{ + /// + protected override string? GetCorrelationId(HttpContext httpContext) + { + return httpContext.Request.Query["correlationId"].FirstOrDefault(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/Selectors/QueryStringHttpWorkflowInstanceIdSelector.cs b/src/modules/Elsa.Http/Selectors/QueryStringHttpWorkflowInstanceIdSelector.cs new file mode 100644 index 000000000..32ade85cc --- /dev/null +++ b/src/modules/Elsa.Http/Selectors/QueryStringHttpWorkflowInstanceIdSelector.cs @@ -0,0 +1,16 @@ +using Elsa.Http.Abstractions; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Http.Selectors; + +/// +/// Returns the workflow instance ID from the X-Workflow-Instance-ID header, if any. +/// +public class QueryStringHttpWorkflowInstanceIdSelector : HttpWorkflowInstanceIdSelectorBase +{ + /// + protected override string? GetWorkflowInstanceId(HttpContext httpContext) + { + return httpContext.Request.Query["workflowInstanceId"].FirstOrDefault(); + } +} \ No newline at end of file