Implement HTTP correlation ID selectors (#4349)

This commit is contained in:
Sipke Schoorstra 2023-08-23 10:48:12 +02:00 committed by GitHub
parent 46c9a76259
commit 1e7f954311
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 266 additions and 9 deletions

View file

@ -1,8 +1,11 @@
using JetBrains.Annotations;
namespace Elsa.Features.Services;
/// <summary>
/// Represents a feature.
/// </summary>
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors | ImplicitUseTargetFlags.Members)]
public interface IFeature
{
/// <summary>

View file

@ -0,0 +1,29 @@
using Elsa.Http.Contracts;
using Microsoft.AspNetCore.Http;
namespace Elsa.Http.Abstractions;
/// <summary>
/// Provides a base class for implementing <see cref="IHttpCorrelationIdSelector"/>.
/// </summary>
public abstract class HttpCorrelationIdSelectorBase : IHttpCorrelationIdSelector
{
/// <inheritdoc />
public virtual double Priority => 0;
/// <summary>
/// Override this method to return the correlation ID for the specified HTTP context, or <c>null</c> if no correlation ID could be found.
/// </summary>
protected virtual ValueTask<string?> GetCorrelationIdAsync(HttpContext httpContext, CancellationToken cancellationToken = default)
{
var correlationId = GetCorrelationId(httpContext);
return new(correlationId);
}
/// <summary>
/// Override this method to return the correlation ID for the specified HTTP context, or <c>null</c> if no correlation ID could be found.
/// </summary>
protected virtual string? GetCorrelationId(HttpContext httpContext) => null;
ValueTask<string?> IHttpCorrelationIdSelector.GetCorrelationIdAsync(HttpContext httpContext, CancellationToken cancellationToken) => GetCorrelationIdAsync(httpContext, cancellationToken);
}

View file

@ -0,0 +1,29 @@
using Elsa.Http.Contracts;
using Microsoft.AspNetCore.Http;
namespace Elsa.Http.Abstractions;
/// <summary>
/// Provides a base class for implementing <see cref="IHttpCorrelationIdSelector"/>.
/// </summary>
public abstract class HttpWorkflowInstanceIdSelectorBase : IHttpWorkflowInstanceIdSelector
{
/// <inheritdoc />
public virtual double Priority => 0;
/// <summary>
/// Override this method to return the workflow instance ID for the specified HTTP context, or <c>null</c> if no workflow instance ID could be found.
/// </summary>
protected virtual ValueTask<string?> GetWorkflowInstanceIdAsync(HttpContext httpContext, CancellationToken cancellationToken = default)
{
var workflowInstanceId = GetWorkflowInstanceId(httpContext);
return new(workflowInstanceId);
}
/// <summary>
/// Override this method to return the workflow instance ID for the specified HTTP context, or <c>null</c> if no workflow instance ID could be found.
/// </summary>
protected virtual string? GetWorkflowInstanceId(HttpContext httpContext) => null;
ValueTask<string?> IHttpWorkflowInstanceIdSelector.GetWorkflowInstanceIdAsync(HttpContext httpContext, CancellationToken cancellationToken) => GetWorkflowInstanceIdAsync(httpContext, cancellationToken);
}

View file

@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Http;
namespace Elsa.Http.Contracts;
/// <summary>
/// Provides a way to select the correlation ID for a request.
/// </summary>
public interface IHttpCorrelationIdSelector
{
/// <summary>
/// The priority of this selector. The selector with the highest priority will be used.
/// </summary>
double Priority { get; }
/// <summary>
/// Returns the correlation ID for the specified HTTP context, or <c>null</c> if no correlation ID could be found.
/// </summary>
ValueTask<string?> GetCorrelationIdAsync(HttpContext httpContext, CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Http;
namespace Elsa.Http.Contracts;
/// <summary>
/// Provides a way to select the workflow instance ID for a request.
/// </summary>
public interface IHttpWorkflowInstanceIdSelector
{
/// <summary>
/// The priority of this selector. The selector with the highest priority will be used.
/// </summary>
double Priority { get; }
/// <summary>
/// Returns the workflow instance ID for the specified HTTP context, or <c>null</c> if no workflow instance ID could be found.
/// </summary>
ValueTask<string?> GetWorkflowInstanceIdAsync(HttpContext httpContext, CancellationToken cancellationToken = default);
}

View file

@ -29,4 +29,6 @@
<ProjectReference Include="..\Elsa.JavaScript\Elsa.JavaScript.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,31 @@
using Elsa.Http.Contracts;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Contains extension methods for the <see cref="IServiceCollection"/> interface.
/// </summary>
[PublicAPI]
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds a <see cref="IHttpCorrelationIdSelector"/> implementation to the service collection.
/// </summary>
public static IServiceCollection AddHttpCorrelationIdSelector<T>(this IServiceCollection services) where T : class, IHttpCorrelationIdSelector
{
services.AddSingleton<IHttpCorrelationIdSelector, T>();
return services;
}
/// <summary>
/// Adds a <see cref="IHttpCorrelationIdSelector"/> implementation to the service collection.
/// </summary>
public static IServiceCollection AddHttpCorrelationIdSelector(this IServiceCollection services, Func<IServiceProvider, IHttpCorrelationIdSelector> factory)
{
services.AddSingleton(factory);
return services;
}
}

View file

@ -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
/// </summary>
public Action<IHttpClientBuilder> HttpClientBuilder { get; set; } = _ => { };
/// <summary>
/// A list of <see cref="IHttpCorrelationIdSelector"/> types to register with the service collection.
/// </summary>
public ICollection<Type> HttpCorrelationIdSelectorTypes { get; } = new List<Type>
{
typeof(HeaderHttpCorrelationIdSelector),
typeof(QueryStringHttpCorrelationIdSelector)
};
/// <summary>
/// A list of <see cref="IHttpWorkflowInstanceIdSelector"/> types to register with the service collection.
/// </summary>
public ICollection<Type> HttpWorkflowInstanceIdSelectorTypes { get; } = new List<Type>
{
typeof(HeaderHttpWorkflowInstanceIdSelector),
typeof(QueryStringHttpWorkflowInstanceIdSelector)
};
/// <inheritdoc />
public override void Configure()
{
@ -78,7 +97,7 @@ public class HttpFeature : FeatureBase
management.AddActivitiesFrom<HttpFeature>();
});
Services.AddRequestHandler<ValidateWorkflowRequestHandler, ValidateWorkflowRequest, ValidateWorkflowResponse>();
}
@ -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);
}
}

View file

@ -27,6 +27,8 @@ public class WorkflowsMiddleware
private readonly IWorkflowRuntime _workflowRuntime;
private readonly IRouteMatcher _routeMatcher;
private readonly IRouteTable _routeTable;
private readonly IEnumerable<IHttpCorrelationIdSelector> _correlationIdSelectors;
private readonly IEnumerable<IHttpWorkflowInstanceIdSelector> _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<IHttpCorrelationIdSelector> correlationIdSelectors,
IEnumerable<IHttpWorkflowInstanceIdSelector> workflowInstanceIdSelectors)
{
_next = next;
_workflowRuntime = workflowRuntime;
@ -66,6 +70,8 @@ public class WorkflowsMiddleware
_serializer = serializer;
_routeMatcher = routeMatcher;
_routeTable = routeTable;
_correlationIdSelectors = correlationIdSelectors;
_workflowInstanceIdSelectors = workflowInstanceIdSelectors;
}
/// <summary>
@ -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<string?> 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<string?> 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;

View file

@ -0,0 +1,16 @@
using Elsa.Http.Abstractions;
using Microsoft.AspNetCore.Http;
namespace Elsa.Http.Selectors;
/// <summary>
/// Returns the correlation ID from the <c>X-Correlation-ID</c> header, if any.
/// </summary>
public class HeaderHttpCorrelationIdSelector : HttpCorrelationIdSelectorBase
{
/// <inheritdoc />
protected override string? GetCorrelationId(HttpContext httpContext)
{
return httpContext.Request.Headers["X-Correlation-ID"].FirstOrDefault();
}
}

View file

@ -0,0 +1,16 @@
using Elsa.Http.Abstractions;
using Microsoft.AspNetCore.Http;
namespace Elsa.Http.Selectors;
/// <summary>
/// Returns the workflow instance ID from the <c>X-Workflow-Instance-ID</c> header, if any.
/// </summary>
public class HeaderHttpWorkflowInstanceIdSelector : HttpWorkflowInstanceIdSelectorBase
{
/// <inheritdoc />
protected override string? GetWorkflowInstanceId(HttpContext httpContext)
{
return httpContext.Request.Headers["X-Workflow-Instance-ID"].FirstOrDefault();
}
}

View file

@ -0,0 +1,16 @@
using Elsa.Http.Abstractions;
using Microsoft.AspNetCore.Http;
namespace Elsa.Http.Selectors;
/// <summary>
/// Returns the correlation ID from the <c>correlationId</c> query string parameter.
/// </summary>
public class QueryStringHttpCorrelationIdSelector : HttpCorrelationIdSelectorBase
{
/// <inheritdoc />
protected override string? GetCorrelationId(HttpContext httpContext)
{
return httpContext.Request.Query["correlationId"].FirstOrDefault();
}
}

View file

@ -0,0 +1,16 @@
using Elsa.Http.Abstractions;
using Microsoft.AspNetCore.Http;
namespace Elsa.Http.Selectors;
/// <summary>
/// Returns the workflow instance ID from the <c>X-Workflow-Instance-ID</c> header, if any.
/// </summary>
public class QueryStringHttpWorkflowInstanceIdSelector : HttpWorkflowInstanceIdSelectorBase
{
/// <inheritdoc />
protected override string? GetWorkflowInstanceId(HttpContext httpContext)
{
return httpContext.Request.Query["workflowInstanceId"].FirstOrDefault();
}
}