From 5d5cb8ed68a00dabf07ee2f34b97567bc51c382d Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 27 Apr 2022 20:29:09 +0200 Subject: [PATCH] Update HTTP module with route data support --- .../Elsa.Modules.Http.csproj | 6 +++ .../Implementations/RouteMatcher.cs | 31 ++++++++++++ .../Implementations/RouteTable.cs | 31 ++++++++++++ .../Middleware/HttpTriggerMiddleware.cs | 47 +++++++++++++++++-- .../Models/HttpRequestModel.cs | 10 +++- .../Services/IRouteMatcher.cs | 14 ++++++ .../Elsa.Modules.Http/Services/IRouteTable.cs | 14 ++++++ 7 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 src/modules/Elsa.Modules.Http/Implementations/RouteMatcher.cs create mode 100644 src/modules/Elsa.Modules.Http/Implementations/RouteTable.cs create mode 100644 src/modules/Elsa.Modules.Http/Services/IRouteMatcher.cs create mode 100644 src/modules/Elsa.Modules.Http/Services/IRouteTable.cs diff --git a/src/modules/Elsa.Modules.Http/Elsa.Modules.Http.csproj b/src/modules/Elsa.Modules.Http/Elsa.Modules.Http.csproj index d80a392e1..a53b476b7 100644 --- a/src/modules/Elsa.Modules.Http/Elsa.Modules.Http.csproj +++ b/src/modules/Elsa.Modules.Http/Elsa.Modules.Http.csproj @@ -15,7 +15,13 @@ + + + + + + diff --git a/src/modules/Elsa.Modules.Http/Implementations/RouteMatcher.cs b/src/modules/Elsa.Modules.Http/Implementations/RouteMatcher.cs new file mode 100644 index 000000000..67b26ba2d --- /dev/null +++ b/src/modules/Elsa.Modules.Http/Implementations/RouteMatcher.cs @@ -0,0 +1,31 @@ +// Taken & adapted from https://github.com/markvincze/Stubbery/blob/main/src/Stubbery/RequestMatching/RouteMatcher.cs + +using Elsa.Modules.Http.Services; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Template; + +namespace Elsa.Modules.Http.Implementations +{ + public class RouteMatcher : IRouteMatcher + { + public RouteValueDictionary? Match(string routeTemplate, string requestPath) + { + var template = TemplateParser.Parse(routeTemplate); + var matcher = new TemplateMatcher(template, GetDefaults(template)); + var values = new RouteValueDictionary(); + + return matcher.TryMatch(requestPath, values) ? values : null; + } + + private static RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate) + { + var result = new RouteValueDictionary(); + + foreach (var parameter in parsedTemplate.Parameters) + if (parameter.DefaultValue != null) + result.Add(parameter.Name, parameter.DefaultValue); + + return result; + } + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Modules.Http/Implementations/RouteTable.cs b/src/modules/Elsa.Modules.Http/Implementations/RouteTable.cs new file mode 100644 index 000000000..9018ea6cd --- /dev/null +++ b/src/modules/Elsa.Modules.Http/Implementations/RouteTable.cs @@ -0,0 +1,31 @@ +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Elsa.Modules.Http.Services; +using Microsoft.Extensions.Caching.Memory; + +namespace Elsa.Modules.Http.Implementations; + +public class RouteTable : IRouteTable +{ + private static readonly object Key = new(); + private readonly IMemoryCache _cache; + + public RouteTable(IMemoryCache cache) => _cache = cache; + private ConcurrentDictionary Routes => _cache.GetOrCreate(Key, _ => new ConcurrentDictionary()); + public void Add(string path) => Routes.TryAdd(path, path); + public void Remove(string path) => Routes.TryRemove(path, out _); + + public void AddRange(IEnumerable paths) + { + foreach (var path in paths) Add(path); + } + + public void RemoveRange(IEnumerable paths) + { + foreach (var path in paths) Remove(path); + } + + public IEnumerator GetEnumerator() => Routes.Values.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} \ No newline at end of file diff --git a/src/modules/Elsa.Modules.Http/Middleware/HttpTriggerMiddleware.cs b/src/modules/Elsa.Modules.Http/Middleware/HttpTriggerMiddleware.cs index 05f5cce3f..d7f7f76d0 100644 --- a/src/modules/Elsa.Modules.Http/Middleware/HttpTriggerMiddleware.cs +++ b/src/modules/Elsa.Modules.Http/Middleware/HttpTriggerMiddleware.cs @@ -3,14 +3,18 @@ using System.Collections.Generic; using System.Linq; using System.Net.Mime; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Elsa.Helpers; using Elsa.Modules.Http.Models; +using Elsa.Modules.Http.Services; using Elsa.Runtime.Models; using Elsa.Runtime.Services; using Elsa.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; namespace Elsa.Modules.Http.Middleware; @@ -25,7 +29,7 @@ public class HttpTriggerMiddleware _hasher = hasher; } - public async Task InvokeAsync(HttpContext httpContext, IWorkflowService workflowService) + public async Task InvokeAsync(HttpContext httpContext, IWorkflowService workflowService, IRouteMatcher routeMatcher) { var path = GetPath(httpContext); var request = httpContext.Request; @@ -33,7 +37,17 @@ public class HttpTriggerMiddleware var abortToken = httpContext.RequestAborted; var hash = _hasher.Hash(new HttpBookmarkData(path, method)); var activityTypeName = TypeNameHelper.GenerateTypeName(); - var requestModel = new HttpRequestModel(new Uri(request.GetEncodedUrl())); + var routeData = GetRouteData(httpContext, routeMatcher, path); + + var requestModel = new HttpRequestModel( + new Uri(request.GetEncodedUrl()), + request.Path, + request.Method, + request.Query.ToDictionary(x => x.Key, x => x.Value.ToString()), + routeData.Values, + request.Headers.ToDictionary(x => x.Key, x => x.Value.ToString()) + ); + var input = new Dictionary() { [HttpEndpoint.InputKey] = requestModel }; var stimulus = Stimulus.Standard(activityTypeName, hash, input); var executionResults = (await workflowService.ExecuteStimulusAsync(stimulus, abortToken)).ToList(); @@ -44,6 +58,11 @@ public class HttpTriggerMiddleware return; } + await WriteResponseAsync(httpContext, executionResults, abortToken); + } + + private static async Task WriteResponseAsync(HttpContext httpContext, IEnumerable executionResults, CancellationToken cancellationToken) + { var response = httpContext.Response; if (!response.HasStarted) @@ -57,9 +76,31 @@ public class HttpTriggerMiddleware }; var json = JsonSerializer.Serialize(model); - await response.WriteAsync(json, abortToken); + await response.WriteAsync(json, cancellationToken); } } + private static RouteData GetRouteData(HttpContext httpContext, IRouteMatcher routeMatcher, string path) + { + var routeData = httpContext.GetRouteData(); + var routeTable = httpContext.RequestServices.GetRequiredService(); + + var matchingRouteQuery = + from route in routeTable + let routeValues = routeMatcher.Match(route, path) + where routeValues != null + select new { route, routeValues }; + + var matchingRoute = matchingRouteQuery.FirstOrDefault(); + + if (matchingRoute == null) + return routeData; + + foreach (var (key, value) in matchingRoute.routeValues!) + routeData.Values[key] = value; + + return routeData; + } + private string GetPath(HttpContext httpContext) => httpContext.Request.Path.Value.ToLowerInvariant(); } \ No newline at end of file diff --git a/src/modules/Elsa.Modules.Http/Models/HttpRequestModel.cs b/src/modules/Elsa.Modules.Http/Models/HttpRequestModel.cs index 1f616c640..ca02a2bc0 100644 --- a/src/modules/Elsa.Modules.Http/Models/HttpRequestModel.cs +++ b/src/modules/Elsa.Modules.Http/Models/HttpRequestModel.cs @@ -1,5 +1,13 @@ using System; +using System.Collections.Generic; namespace Elsa.Modules.Http.Models; -public record HttpRequestModel(Uri RequestUri); \ No newline at end of file +public record HttpRequestModel( + Uri RequestUri, + string Path, + string Method, + IDictionary QueryString, + IDictionary RouteValues, + IDictionary Headers +); \ No newline at end of file diff --git a/src/modules/Elsa.Modules.Http/Services/IRouteMatcher.cs b/src/modules/Elsa.Modules.Http/Services/IRouteMatcher.cs new file mode 100644 index 000000000..e87e1045b --- /dev/null +++ b/src/modules/Elsa.Modules.Http/Services/IRouteMatcher.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Routing; + +namespace Elsa.Modules.Http.Services; + +/// +/// Matches a given request path against the specified route template. +/// +public interface IRouteMatcher +{ + /// + /// Matches a given request path against the specified route template. + /// + RouteValueDictionary? Match(string routeTemplate, string requestPath); +} \ No newline at end of file diff --git a/src/modules/Elsa.Modules.Http/Services/IRouteTable.cs b/src/modules/Elsa.Modules.Http/Services/IRouteTable.cs new file mode 100644 index 000000000..7073d1077 --- /dev/null +++ b/src/modules/Elsa.Modules.Http/Services/IRouteTable.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Elsa.Modules.Http.Services; + +/// +/// Stores a list of all routes provided by activities. +/// +public interface IRouteTable : IEnumerable +{ + void Add(string path); + void Remove(string path); + void AddRange(IEnumerable paths); + void RemoveRange(IEnumerable paths); +} \ No newline at end of file