using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Workflows;
using FastEndpoints;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
///
/// Provides extension methods to add FastEndpoints configured for use with Elsa API endpoints.
///
[PublicAPI]
public static class WebApplicationExtensions
{
private static readonly RequestDelegate NotFoundRequestDelegate = context =>
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
return Task.CompletedTask;
};
///
/// Registers the FastEndpoints middleware configured for use with Elsa API endpoints.
///
///
/// The route prefix to apply to Elsa API endpoints.
/// E.g. "elsa/api" will expose endpoints like this: "/elsa/api/workflow-definitions"
public static IApplicationBuilder UseWorkflowsApi(this IApplicationBuilder app, string routePrefix = "elsa/api")
{
return app.UseFastEndpoints(config => ConfigureWorkflowsApi(config, routePrefix));
}
///
/// Applies an ASP.NET Core rate limiting policy to requests targeting the Elsa API route prefix.
///
/// The application builder.
/// The route prefix used by Elsa API endpoints.
/// The registered ASP.NET Core rate limiting policy name. Leave empty to skip rate limiting.
public static IApplicationBuilder UseWorkflowsApiRateLimiting(this IApplicationBuilder app, string routePrefix = "elsa/api", string? policyName = null)
{
if (string.IsNullOrWhiteSpace(policyName))
return app;
var pathPrefix = NormalizeRoutePrefixPath(routePrefix);
return app.UseRateLimitingPolicyForPath(pathPrefix, policyName, "Elsa API rate limiting endpoint", requireMatchedEndpoint: true);
}
///
/// Maps FastEndpoints endpoint routes configured for use with Elsa API endpoints.
///
/// The to register the endpoints with.
/// The route prefix to apply to Elsa API endpoints.
/// E.g. "elsa/api" will expose endpoints like this: "/elsa/api/workflow-definitions"
public static IEndpointRouteBuilder MapWorkflowsApi(this IEndpointRouteBuilder routes, string routePrefix = "elsa/api") =>
routes.MapFastEndpoints(config => ConfigureWorkflowsApi(config, routePrefix));
///
/// Applies an ASP.NET Core rate limiting policy to requests targeting the specified path prefix.
///
/// The application builder.
/// The path prefix to protect.
/// The registered ASP.NET Core rate limiting policy name.
/// The endpoint display name used for rate limiting metadata.
///
/// This method only attaches rate limiting metadata. In endpoint-routed pipelines, call this after routing has selected an endpoint
/// and before the host's single app.UseRateLimiter() middleware. ASP.NET Core validates the configured policy when the
/// rate limiter middleware handles matching requests.
///
public static IApplicationBuilder UseRateLimitingPolicyForPath(this IApplicationBuilder app, PathString pathPrefix, string policyName, string displayName) =>
app.UseRateLimitingPolicyForPath(pathPrefix, policyName, displayName, true);
///
/// Applies an ASP.NET Core rate limiting policy to requests targeting the specified path prefix.
///
/// The application builder.
/// The path prefix to protect.
/// The registered ASP.NET Core rate limiting policy name.
/// The endpoint display name used for rate limiting metadata.
/// Whether to skip rate limiting when endpoint routing selected no endpoint.
public static IApplicationBuilder UseRateLimitingPolicyForPath(this IApplicationBuilder app, PathString pathPrefix, string policyName, string displayName, bool requireMatchedEndpoint)
{
if (!pathPrefix.HasValue || string.IsNullOrWhiteSpace(policyName))
return app;
var rateLimitingMetadata = new EnableRateLimitingAttribute(policyName);
var fallbackEndpoint = CreateRateLimitingEndpoint(null, rateLimitingMetadata, displayName);
var endpointCache = new ConditionalWeakTable();
return app.UseWhen(
context => context.Request.Path.StartsWithSegments(pathPrefix, StringComparison.OrdinalIgnoreCase),
branch =>
{
branch.Use(async (context, next) =>
{
var originalEndpoint = context.GetEndpoint();
if (requireMatchedEndpoint && originalEndpoint == null)
{
await next(context);
return;
}
var rateLimitingEndpoint = originalEndpoint == null
? fallbackEndpoint
: endpointCache.GetValue(originalEndpoint, endpoint => CreateRateLimitingEndpoint(endpoint, rateLimitingMetadata, displayName));
context.SetEndpoint(rateLimitingEndpoint);
try
{
await next(context);
}
finally
{
if (ReferenceEquals(context.GetEndpoint(), rateLimitingEndpoint))
context.SetEndpoint(originalEndpoint);
}
});
});
}
private static PathString NormalizeRoutePrefixPath(string routePrefix)
{
var value = routePrefix.Trim().Trim('/');
return string.IsNullOrEmpty(value) ? PathString.Empty : new PathString("/" + value);
}
private static void ConfigureWorkflowsApi(Config config, string routePrefix)
{
config.Endpoints.RoutePrefix = routePrefix;
config.Serializer.RequestDeserializer = DeserializeRequestAsync;
config.Serializer.ResponseSerializer = SerializeRequestAsync;
config.Binding.ValueParserFor(s =>
new(DateTimeOffset.TryParse(s.ToString(), CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var result), result));
}
private static Endpoint CreateRateLimitingEndpoint(Endpoint? originalEndpoint, EnableRateLimitingAttribute rateLimitingMetadata, string displayName)
{
var metadata = originalEndpoint == null
? new EndpointMetadataCollection(rateLimitingMetadata)
: new EndpointMetadataCollection(originalEndpoint.Metadata.Where(x => x is not EnableRateLimitingAttribute and not DisableRateLimitingAttribute).Concat([rateLimitingMetadata]));
if (originalEndpoint is RouteEndpoint routeEndpoint)
return new RouteEndpoint(routeEndpoint.RequestDelegate ?? NotFoundRequestDelegate, routeEndpoint.RoutePattern, routeEndpoint.Order, metadata, routeEndpoint.DisplayName ?? displayName);
return new Endpoint(originalEndpoint?.RequestDelegate ?? NotFoundRequestDelegate, metadata, originalEndpoint?.DisplayName ?? displayName);
}
private static ValueTask