Update HTTP module with route data support

This commit is contained in:
Sipke Schoorstra 2022-04-27 20:29:09 +02:00
parent 6f9c6fd87d
commit 5d5cb8ed68
7 changed files with 149 additions and 4 deletions

View file

@ -15,7 +15,13 @@
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Routing" Version="2.2.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Scripting" />
</ItemGroup>
</Project>

View file

@ -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;
}
}
}

View file

@ -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<string, string> Routes => _cache.GetOrCreate(Key, _ => new ConcurrentDictionary<string, string>());
public void Add(string path) => Routes.TryAdd(path, path);
public void Remove(string path) => Routes.TryRemove(path, out _);
public void AddRange(IEnumerable<string> paths)
{
foreach (var path in paths) Add(path);
}
public void RemoveRange(IEnumerable<string> paths)
{
foreach (var path in paths) Remove(path);
}
public IEnumerator<string> GetEnumerator() => Routes.Values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

View file

@ -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<HttpEndpoint>();
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<string, object>() { [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<ExecuteWorkflowInstructionResult> 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<IRouteTable>();
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();
}

View file

@ -1,5 +1,13 @@
using System;
using System.Collections.Generic;
namespace Elsa.Modules.Http.Models;
public record HttpRequestModel(Uri RequestUri);
public record HttpRequestModel(
Uri RequestUri,
string Path,
string Method,
IDictionary<string, string> QueryString,
IDictionary<string, object> RouteValues,
IDictionary<string, string> Headers
);

View file

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Routing;
namespace Elsa.Modules.Http.Services;
/// <summary>
/// Matches a given request path against the specified route template.
/// </summary>
public interface IRouteMatcher
{
/// <summary>
/// Matches a given request path against the specified route template.
/// </summary>
RouteValueDictionary? Match(string routeTemplate, string requestPath);
}

View file

@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Elsa.Modules.Http.Services;
/// <summary>
/// Stores a list of all routes provided by <see cref="HttpEndpoint"/> activities.
/// </summary>
public interface IRouteTable : IEnumerable<string>
{
void Add(string path);
void Remove(string path);
void AddRange(IEnumerable<string> paths);
void RemoveRange(IEnumerable<string> paths);
}