< Summary

Information
Class: Elsa.Extensions.WebApplicationExtensions
Assembly: Elsa.Api.Common
File(s): /home/runner/work/elsa-core/elsa-core/src/common/Elsa.Api.Common/Extensions/WebApplicationExtensions.cs
Line coverage
27%
Covered lines: 20
Uncovered lines: 52
Coverable lines: 72
Total lines: 178
Line coverage: 27.7%
Branch coverage
12%
Covered branches: 5
Total branches: 40
Branch coverage: 12.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/elsa-core/elsa-core/src/common/Elsa.Api.Common/Extensions/WebApplicationExtensions.cs

#LineLine coverage
 1using System.Globalization;
 2using System.Runtime.CompilerServices;
 3using System.Text.Json;
 4using System.Text.Json.Serialization;
 5using Elsa.Workflows;
 6using FastEndpoints;
 7using JetBrains.Annotations;
 8using Microsoft.AspNetCore.Builder;
 9using Microsoft.AspNetCore.Http;
 10using Microsoft.AspNetCore.RateLimiting;
 11using Microsoft.AspNetCore.Routing;
 12using Microsoft.Extensions.DependencyInjection;
 13
 14// ReSharper disable once CheckNamespace
 15namespace Elsa.Extensions;
 16
 17/// <summary>
 18/// Provides extension methods to add FastEndpoints configured for use with Elsa API endpoints.
 19/// </summary>
 20[PublicAPI]
 21public static class WebApplicationExtensions
 22{
 023    private static readonly RequestDelegate NotFoundRequestDelegate = context =>
 024    {
 025        context.Response.StatusCode = StatusCodes.Status404NotFound;
 026        return Task.CompletedTask;
 027    };
 28
 29    /// <summary>
 30    /// Registers the FastEndpoints middleware configured for use with Elsa API endpoints.
 31    /// </summary>
 32    /// <param name="app"></param>
 33    /// <param name="routePrefix">The route prefix to apply to Elsa API endpoints.</param>
 34    /// <example>E.g. "elsa/api" will expose endpoints like this: "/elsa/api/workflow-definitions"</example>
 35    public static IApplicationBuilder UseWorkflowsApi(this IApplicationBuilder app, string routePrefix = "elsa/api")
 36    {
 037        return app.UseFastEndpoints(config => ConfigureWorkflowsApi(config, routePrefix));
 38    }
 39
 40    /// <summary>
 41    /// Applies an ASP.NET Core rate limiting policy to requests targeting the Elsa API route prefix.
 42    /// </summary>
 43    /// <param name="app">The application builder.</param>
 44    /// <param name="routePrefix">The route prefix used by Elsa API endpoints.</param>
 45    /// <param name="policyName">The registered ASP.NET Core rate limiting policy name. Leave empty to skip rate limitin
 46    public static IApplicationBuilder UseWorkflowsApiRateLimiting(this IApplicationBuilder app, string routePrefix = "el
 47    {
 348        if (string.IsNullOrWhiteSpace(policyName))
 349            return app;
 50
 051        var pathPrefix = NormalizeRoutePrefixPath(routePrefix);
 052        return app.UseRateLimitingPolicyForPath(pathPrefix, policyName, "Elsa API rate limiting endpoint", requireMatche
 53    }
 54
 55    /// <summary>
 56    /// Maps FastEndpoints endpoint routes configured for use with Elsa API endpoints.
 57    /// </summary>
 58    /// <param name="routes">The <see cref="IEndpointRouteBuilder"/> to register the endpoints with.</param>
 59    /// <param name="routePrefix">The route prefix to apply to Elsa API endpoints.</param>
 60    /// <example>E.g. "elsa/api" will expose endpoints like this: "/elsa/api/workflow-definitions"</example>
 61    public static IEndpointRouteBuilder MapWorkflowsApi(this IEndpointRouteBuilder routes, string routePrefix = "elsa/ap
 662        routes.MapFastEndpoints(config => ConfigureWorkflowsApi(config, routePrefix));
 63
 64    /// <summary>
 65    /// Applies an ASP.NET Core rate limiting policy to requests targeting the specified path prefix.
 66    /// </summary>
 67    /// <param name="app">The application builder.</param>
 68    /// <param name="pathPrefix">The path prefix to protect.</param>
 69    /// <param name="policyName">The registered ASP.NET Core rate limiting policy name.</param>
 70    /// <param name="displayName">The endpoint display name used for rate limiting metadata.</param>
 71    /// <remarks>
 72    /// This method only attaches rate limiting metadata. In endpoint-routed pipelines, call this after routing has sele
 73    /// and before the host's single <c>app.UseRateLimiter()</c> middleware. ASP.NET Core validates the configured polic
 74    /// rate limiter middleware handles matching requests.
 75    /// </remarks>
 76    public static IApplicationBuilder UseRateLimitingPolicyForPath(this IApplicationBuilder app, PathString pathPrefix, 
 077        app.UseRateLimitingPolicyForPath(pathPrefix, policyName, displayName, true);
 78
 79    /// <summary>
 80    /// Applies an ASP.NET Core rate limiting policy to requests targeting the specified path prefix.
 81    /// </summary>
 82    /// <param name="app">The application builder.</param>
 83    /// <param name="pathPrefix">The path prefix to protect.</param>
 84    /// <param name="policyName">The registered ASP.NET Core rate limiting policy name.</param>
 85    /// <param name="displayName">The endpoint display name used for rate limiting metadata.</param>
 86    /// <param name="requireMatchedEndpoint">Whether to skip rate limiting when endpoint routing selected no endpoint.</
 87    public static IApplicationBuilder UseRateLimitingPolicyForPath(this IApplicationBuilder app, PathString pathPrefix, 
 88    {
 089        if (!pathPrefix.HasValue || string.IsNullOrWhiteSpace(policyName))
 090            return app;
 91
 092        var rateLimitingMetadata = new EnableRateLimitingAttribute(policyName);
 093        var fallbackEndpoint = CreateRateLimitingEndpoint(null, rateLimitingMetadata, displayName);
 094        var endpointCache = new ConditionalWeakTable<Endpoint, Endpoint>();
 95
 096        return app.UseWhen(
 097            context => context.Request.Path.StartsWithSegments(pathPrefix, StringComparison.OrdinalIgnoreCase),
 098            branch =>
 099            {
 0100                branch.Use(async (context, next) =>
 0101                {
 0102                    var originalEndpoint = context.GetEndpoint();
 0103                    if (requireMatchedEndpoint && originalEndpoint == null)
 0104                    {
 0105                        await next(context);
 0106                        return;
 0107                    }
 0108
 0109                    var rateLimitingEndpoint = originalEndpoint == null
 0110                        ? fallbackEndpoint
 0111                        : endpointCache.GetValue(originalEndpoint, endpoint => CreateRateLimitingEndpoint(endpoint, rate
 0112
 0113                    context.SetEndpoint(rateLimitingEndpoint);
 0114
 0115                    try
 0116                    {
 0117                        await next(context);
 0118                    }
 0119                    finally
 0120                    {
 0121                        if (ReferenceEquals(context.GetEndpoint(), rateLimitingEndpoint))
 0122                            context.SetEndpoint(originalEndpoint);
 0123                    }
 0124                });
 0125            });
 126    }
 127
 128    private static PathString NormalizeRoutePrefixPath(string routePrefix)
 129    {
 0130        var value = routePrefix.Trim().Trim('/');
 131
 0132        return string.IsNullOrEmpty(value) ? PathString.Empty : new PathString("/" + value);
 133    }
 134
 135    private static void ConfigureWorkflowsApi(Config config, string routePrefix)
 136    {
 3137        config.Endpoints.RoutePrefix = routePrefix;
 3138        config.Serializer.RequestDeserializer = DeserializeRequestAsync;
 3139        config.Serializer.ResponseSerializer = SerializeRequestAsync;
 140
 3141        config.Binding.ValueParserFor<DateTimeOffset>(s =>
 3142            new(DateTimeOffset.TryParse(s.ToString(), CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out va
 3143    }
 144
 145    private static Endpoint CreateRateLimitingEndpoint(Endpoint? originalEndpoint, EnableRateLimitingAttribute rateLimit
 146    {
 0147        var metadata = originalEndpoint == null
 0148            ? new EndpointMetadataCollection(rateLimitingMetadata)
 0149            : new EndpointMetadataCollection(originalEndpoint.Metadata.Where(x => x is not EnableRateLimitingAttribute a
 150
 0151        if (originalEndpoint is RouteEndpoint routeEndpoint)
 0152            return new RouteEndpoint(routeEndpoint.RequestDelegate ?? NotFoundRequestDelegate, routeEndpoint.RoutePatter
 153
 0154        return new Endpoint(originalEndpoint?.RequestDelegate ?? NotFoundRequestDelegate, metadata, originalEndpoint?.Di
 155    }
 156
 157    private static ValueTask<object?> DeserializeRequestAsync(HttpRequest httpRequest, Type modelType, JsonSerializerCon
 158    {
 5159        var serializer = httpRequest.HttpContext.RequestServices.GetRequiredService<IApiSerializer>();
 5160        var options = serializer.GetOptions();
 161
 5162        return serializerContext == null
 5163            ? JsonSerializer.DeserializeAsync(httpRequest.Body, modelType, options, cancellationToken)
 5164            : JsonSerializer.DeserializeAsync(httpRequest.Body, modelType, serializerContext, cancellationToken);
 165    }
 166
 167    private static Task SerializeRequestAsync(HttpResponse httpResponse, object? dto, string contentType, JsonSerializer
 168    {
 5169        var serializer = httpResponse.HttpContext.RequestServices.GetRequiredService<IApiSerializer>();
 5170        var options = serializer.GetOptions();
 171
 5172        httpResponse.ContentType = contentType;
 5173        return serializerContext == null
 5174            ? JsonSerializer.SerializeAsync(httpResponse.Body, dto, dto?.GetType() ?? typeof(object), options, cancellat
 5175            : JsonSerializer.SerializeAsync(httpResponse.Body, dto, dto?.GetType() ?? typeof(object), serializerContext,
 176    }
 177
 178}