diff --git a/src/modules/Elsa.Http/Activities/HttpEndpoint.cs b/src/modules/Elsa.Http/Activities/HttpEndpoint.cs index 389cf68a3..6684b55c5 100644 --- a/src/modules/Elsa.Http/Activities/HttpEndpoint.cs +++ b/src/modules/Elsa.Http/Activities/HttpEndpoint.cs @@ -1,4 +1,8 @@ -using System.Runtime.CompilerServices; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Elsa.Expressions.Models; using Elsa.Extensions; using Elsa.Http.Bookmarks; @@ -436,7 +440,7 @@ public class HttpEndpoint : Trigger private IEnumerable GetBookmarkPayloads(ExpressionExecutionContext context) { // Generate bookmark data for path and selected methods. - var path = context.Get(Path); + var normalizedRoute = context.Get(Path)!.NormalizeRoute(); var methods = SupportedMethods.GetOrDefault(context) ?? new List { HttpMethods.Get }; var authorize = Authorize.GetOrDefault(context); var policy = Policy.GetOrDefault(context); @@ -444,7 +448,7 @@ public class HttpEndpoint : Trigger var requestSizeLimit = RequestSizeLimit.GetOrDefault(context); return methods - .Select(x => new HttpEndpointBookmarkPayload(path!, x.ToLowerInvariant(), authorize, policy, requestTimeout, requestSizeLimit)) + .Select(x => new HttpEndpointBookmarkPayload(normalizedRoute, x.ToLowerInvariant(), authorize, policy, requestTimeout, requestSizeLimit)) .Cast() .ToArray(); } diff --git a/src/modules/Elsa.Http/Contracts/IRouteMatcher.cs b/src/modules/Elsa.Http/Contracts/IRouteMatcher.cs index 9adaf441b..deea4ae30 100644 --- a/src/modules/Elsa.Http/Contracts/IRouteMatcher.cs +++ b/src/modules/Elsa.Http/Contracts/IRouteMatcher.cs @@ -10,5 +10,5 @@ public interface IRouteMatcher /// /// Matches a given request path against the specified route template. /// - RouteValueDictionary? Match(string routeTemplate, string requestPath); + RouteValueDictionary? Match(string routeTemplate, string route); } \ No newline at end of file diff --git a/src/modules/Elsa.Http/Contracts/IRouteTable.cs b/src/modules/Elsa.Http/Contracts/IRouteTable.cs index 4ebfd3806..fe25b922e 100644 --- a/src/modules/Elsa.Http/Contracts/IRouteTable.cs +++ b/src/modules/Elsa.Http/Contracts/IRouteTable.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace Elsa.Http.Contracts; /// @@ -5,8 +7,27 @@ namespace Elsa.Http.Contracts; /// public interface IRouteTable : IEnumerable { - void Add(string path); - void Remove(string path); - void AddRange(IEnumerable paths); - void RemoveRange(IEnumerable paths); + /// + /// Adds a route to the table. + /// + /// The route to add. + void Add(string route); + + /// + /// Removes a route from the table. + /// + /// The route to remove. + void Remove(string route); + + /// + /// Adds a range of routes to the table. + /// + /// The routes to add. + void AddRange(IEnumerable routes); + + /// + /// Removes a range of routes from the table. + /// + /// The routes to remove. + void RemoveRange(IEnumerable routes); } \ No newline at end of file diff --git a/src/modules/Elsa.Http/Extensions/RouteExtensions.cs b/src/modules/Elsa.Http/Extensions/RouteExtensions.cs new file mode 100644 index 000000000..a59e69013 --- /dev/null +++ b/src/modules/Elsa.Http/Extensions/RouteExtensions.cs @@ -0,0 +1,14 @@ +// ReSharper disable once CheckNamespace + +namespace Elsa.Extensions; + +/// +/// Provides extension methods for strings that represent a route. +/// +public static class RouteExtensions +{ + /// + /// Normalizes a route by ensuring a leading slash, removing any trailing slash and converting the path to lowercase. + /// + public static string NormalizeRoute(this string path) => $"/{path.Trim('/').ToLowerInvariant()}"; +} \ 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 9dddc0025..c0f8cfd9e 100644 --- a/src/modules/Elsa.Http/Middleware/WorkflowsMiddleware.cs +++ b/src/modules/Elsa.Http/Middleware/WorkflowsMiddleware.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Linq; using Elsa.Http.Contracts; using Elsa.Http.Models; using Elsa.Http.Options; @@ -10,6 +13,8 @@ using Microsoft.Extensions.Options; using System.Net; using System.Net.Mime; using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; using Elsa.Extensions; using Elsa.Http.Bookmarks; using Elsa.Workflows.Core; @@ -85,7 +90,7 @@ public class WorkflowsMiddleware public async Task InvokeAsync(HttpContext httpContext) { var path = GetPath(httpContext); - var basePath = _options.BasePath; + var basePath = _options.BasePath?.ToString().NormalizeRoute(); // If the request path does not match the configured base path to handle workflows, then skip. if (!string.IsNullOrWhiteSpace(basePath)) @@ -97,7 +102,7 @@ public class WorkflowsMiddleware } // Strip the base path. - path = path[basePath.Value.Value!.Length..]; + path = path[basePath.Length..]; } var matchingPath = GetMatchingRoute(path); @@ -249,7 +254,7 @@ public class WorkflowsMiddleware } } - private string GetPath(HttpContext httpContext) => httpContext.Request.Path.Value!.ToLowerInvariant(); + private string GetPath(HttpContext httpContext) => httpContext.Request.Path.Value!.NormalizeRoute(); private async Task HandleNoWorkflowsFoundAsync(HttpContext httpContext, ICollection workflowMatches, PathString? basePath) { diff --git a/src/modules/Elsa.Http/Services/RouteMatcher.cs b/src/modules/Elsa.Http/Services/RouteMatcher.cs index 463662cf3..c01fb6aa8 100644 --- a/src/modules/Elsa.Http/Services/RouteMatcher.cs +++ b/src/modules/Elsa.Http/Services/RouteMatcher.cs @@ -1,5 +1,6 @@ // Taken & adapted from https://github.com/markvincze/Stubbery/blob/main/src/Stubbery/RequestMatching/RouteMatcher.cs +using Elsa.Extensions; using Elsa.Http.Contracts; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Template; @@ -10,13 +11,14 @@ namespace Elsa.Http.Services; public class RouteMatcher : IRouteMatcher { /// - public RouteValueDictionary? Match(string routeTemplate, string requestPath) + public RouteValueDictionary? Match(string routeTemplate, string route) { + var normalizedRoute = route.NormalizeRoute(); var template = TemplateParser.Parse(routeTemplate); var matcher = new TemplateMatcher(template, GetDefaults(template)); var values = new RouteValueDictionary(); - return matcher.TryMatch(requestPath, values) ? values : null; + return matcher.TryMatch(normalizedRoute, values) ? values : null; } private static RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate) diff --git a/src/modules/Elsa.Http/Services/RouteTable.cs b/src/modules/Elsa.Http/Services/RouteTable.cs index c1fa9c202..6f98ae7b8 100644 --- a/src/modules/Elsa.Http/Services/RouteTable.cs +++ b/src/modules/Elsa.Http/Services/RouteTable.cs @@ -1,5 +1,7 @@ using System.Collections; using System.Collections.Concurrent; +using System.Collections.Generic; +using Elsa.Extensions; using Elsa.Http.Contracts; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; @@ -25,30 +27,35 @@ public class RouteTable : IRouteTable private ConcurrentDictionary Routes => _cache.GetOrCreate(Key, _ => new ConcurrentDictionary())!; /// - public void Add(string path) + public void Add(string route) { - if (path.Contains("//")) + if (route.Contains("//")) { - _logger.LogWarning("Path cannot contain double slashes. Ignoring path: {Path}", path); + _logger.LogWarning("Path cannot contain double slashes. Ignoring path: {Path}", route); return; } - Routes.TryAdd(path, path); + var normalizedRoute = route.NormalizeRoute(); + Routes.TryAdd(normalizedRoute, normalizedRoute); } /// - public void Remove(string path) => Routes.TryRemove(path, out _); - - /// - public void AddRange(IEnumerable paths) + public void Remove(string route) { - foreach (var path in paths) Add(path); + var normalizedRoute = route.NormalizeRoute(); + Routes.TryRemove(normalizedRoute, out _); } /// - public void RemoveRange(IEnumerable paths) + public void AddRange(IEnumerable routes) { - foreach (var path in paths) Remove(path); + foreach (var route in routes) Add(route); + } + + /// + public void RemoveRange(IEnumerable routes) + { + foreach (var route in routes) Remove(route); } ///