diff --git a/nanoFramework.WebServer/WebServer.cs b/nanoFramework.WebServer/WebServer.cs
index b781f4f..f552e22 100644
--- a/nanoFramework.WebServer/WebServer.cs
+++ b/nanoFramework.WebServer/WebServer.cs
@@ -159,6 +159,101 @@ public static UrlParameter[] DecodeParam(string parameter)
return retParams;
}
+ ///
+ /// Extracts route parameters from a URL that matches a parameterized route.
+ ///
+ /// The route template with parameters (e.g., "/api/devices/{id}").
+ /// The actual URL being requested.
+ /// Whether the comparison should be case sensitive.
+ /// An array of UrlParameter objects containing the parameter names and values, or null if the route doesn't match.
+ public static UrlParameter[] ExtractRouteParameters(string route, string rawUrl, bool caseSensitive = false)
+ {
+ if (string.IsNullOrEmpty(route) || string.IsNullOrEmpty(rawUrl))
+ {
+ return null;
+ }
+
+ // Remove query parameters from the URL for matching
+ var urlParam = rawUrl.IndexOf(ParamStart);
+ var urlPath = urlParam > 0 ? rawUrl.Substring(0, urlParam) : rawUrl;
+
+ // Normalize the URL path and route for comparison
+ var urlToCompare = caseSensitive ? urlPath : urlPath.ToLower();
+ var routeToCompare = caseSensitive ? route : route.ToLower();
+
+ // Ensure both paths start with '/' for consistent segment splitting
+ if (!urlToCompare.StartsWith("/"))
+ {
+ urlToCompare = "/" + urlToCompare;
+ }
+ if (!routeToCompare.StartsWith("/"))
+ {
+ routeToCompare = "/" + routeToCompare;
+ }
+
+ // Split into segments
+ var urlSegments = urlToCompare.Split('/');
+ var routeSegments = routeToCompare.Split('/');
+
+ // Number of segments must match
+ if (urlSegments.Length != routeSegments.Length)
+ {
+ return null;
+ }
+
+ ArrayList parameters = new ArrayList();
+
+ // Compare each segment and extract parameters
+ for (int i = 0; i < routeSegments.Length; i++)
+ {
+ var routeSegment = routeSegments[i];
+ var urlSegment = urlSegments[i];
+
+ // Skip empty segments (from leading slash)
+ if (string.IsNullOrEmpty(routeSegment) && string.IsNullOrEmpty(urlSegment))
+ {
+ continue;
+ }
+
+ // Check if this is a parameter segment (starts and ends with curly braces)
+ if (routeSegment.Length > 2 &&
+ routeSegment.StartsWith("{") &&
+ routeSegment.EndsWith("}"))
+ {
+ // Parameter segment matches any non-empty segment that doesn't contain '/'
+ if (string.IsNullOrEmpty(urlSegment) || urlSegment.IndexOf('/') >= 0)
+ {
+ return null;
+ }
+
+ // Extract parameter name (remove curly braces)
+ var paramName = routeSegment.Substring(1, routeSegment.Length - 2);
+ parameters.Add(new UrlParameter { Name = paramName, Value = urlSegments[i] }); // Use original case for value
+ continue;
+ }
+
+ // Exact match required for non-parameter segments
+ if (routeSegment != urlSegment)
+ {
+ return null;
+ }
+ }
+
+ // Convert ArrayList to array
+ if (parameters.Count == 0)
+ {
+ return null;
+ }
+
+ var result = new UrlParameter[parameters.Count];
+ for (int i = 0; i < parameters.Count; i++)
+ {
+ result[i] = (UrlParameter)parameters[i];
+ }
+
+ return result;
+ }
+
#endregion
#region Constructors
@@ -695,29 +790,68 @@ public static bool IsRouteMatch(CallbackRoutes route, string method, string rawU
return false;
}
+ // Remove query parameters from the URL for matching
var urlParam = rawUrl.IndexOf(ParamStart);
- var incForSlash = route.Route.IndexOf('/') == 0 ? 0 : 1;
- var rawUrlToCompare = route.CaseSensitive ? rawUrl : rawUrl.ToLower();
+ var urlPath = urlParam > 0 ? rawUrl.Substring(0, urlParam) : rawUrl;
+
+ // Normalize the URL path and route for comparison
+ var urlToCompare = route.CaseSensitive ? urlPath : urlPath.ToLower();
var routeToCompare = route.CaseSensitive ? route.Route : route.Route.ToLower();
- bool isFound;
-
- if (urlParam > 0)
+
+ // Ensure both paths start with '/' for consistent segment splitting
+ if (!urlToCompare.StartsWith("/"))
{
- isFound = urlParam == routeToCompare.Length + incForSlash;
+ urlToCompare = "/" + urlToCompare;
}
- else
+
+ if (!routeToCompare.StartsWith("/"))
{
- isFound = rawUrlToCompare.Length == routeToCompare.Length + incForSlash;
+ routeToCompare = "/" + routeToCompare;
}
-
- // Matching the route name
- // Matching the method type
- if (!isFound ||
- (!string.IsNullOrEmpty(routeToCompare) && rawUrlToCompare.IndexOf(routeToCompare) != incForSlash))
+
+ // Split into segments
+ var urlSegments = urlToCompare.Split('/');
+ var routeSegments = routeToCompare.Split('/');
+
+ // Number of segments must match
+ if (urlSegments.Length != routeSegments.Length)
{
return false;
}
-
+
+ // Compare each segment
+ for (int i = 0; i < routeSegments.Length; i++)
+ {
+ var routeSegment = routeSegments[i];
+ var urlSegment = urlSegments[i];
+
+ // Skip empty segments (from leading slash)
+ if (string.IsNullOrEmpty(routeSegment) && string.IsNullOrEmpty(urlSegment))
+ {
+ continue;
+ }
+
+ // Check if this is a parameter segment (starts and ends with curly braces)
+ if (routeSegment.Length > 2 &&
+ routeSegment.StartsWith("{") &&
+ routeSegment.EndsWith("}"))
+ {
+ // Parameter segment matches any non-empty segment that doesn't contain '/'
+ if (string.IsNullOrEmpty(urlSegment) || urlSegment.IndexOf('/') >= 0)
+ {
+ return false;
+ }
+ // Parameter matches, continue to next segment
+ continue;
+ }
+
+ // Exact match required for non-parameter segments
+ if (routeSegment != urlSegment)
+ {
+ return false;
+ }
+ }
+
return true;
}
@@ -728,7 +862,15 @@ public static bool IsRouteMatch(CallbackRoutes route, string method, string rawU
/// Context of current request.
protected virtual void InvokeRoute(CallbackRoutes route, HttpListenerContext context)
{
- route.Callback.Invoke(null, new object[] { new WebServerEventArgs(context) });
+ // Extract route parameters if the route contains parameter placeholders
+ var routeParameters = ExtractRouteParameters(route.Route, context.Request.RawUrl, route.CaseSensitive);
+
+ // Create WebServerEventArgs with or without route parameters
+ var eventArgs = routeParameters != null
+ ? new WebServerEventArgs(context, routeParameters)
+ : new WebServerEventArgs(context);
+
+ route.Callback.Invoke(null, new object[] { eventArgs });
}
private static void HandleContextResponse(HttpListenerContext context)
diff --git a/nanoFramework.WebServer/WebServerEventArgs.cs b/nanoFramework.WebServer/WebServerEventArgs.cs
index 6d5c10e..1bae96f 100644
--- a/nanoFramework.WebServer/WebServerEventArgs.cs
+++ b/nanoFramework.WebServer/WebServerEventArgs.cs
@@ -18,11 +18,49 @@ public class WebServerEventArgs
public WebServerEventArgs(HttpListenerContext context)
{
Context = context;
+ RouteParameters = null;
+ }
+
+ ///
+ /// Constructor for the event arguments with route parameters
+ ///
+ public WebServerEventArgs(HttpListenerContext context, UrlParameter[] routeParameters)
+ {
+ Context = context;
+ RouteParameters = routeParameters;
}
///
/// The response class
///
public HttpListenerContext Context { get; protected set; }
+
+ ///
+ /// Route parameters extracted from the URL (if any)
+ ///
+ public UrlParameter[] RouteParameters { get; protected set; }
+
+ ///
+ /// Gets the value of a route parameter by name
+ ///
+ /// The name of the parameter to retrieve
+ /// The parameter value if found, otherwise null
+ public string GetRouteParameter(string parameterName)
+ {
+ if (RouteParameters == null || string.IsNullOrEmpty(parameterName))
+ {
+ return null;
+ }
+
+ foreach (UrlParameter param in RouteParameters)
+ {
+ if (param.Name.ToLower() == parameterName.ToLower())
+ {
+ return param.Value;
+ }
+ }
+
+ return null;
+ }
}
}
diff --git a/tests/nanoFramework.WebServer.Tests/WebServerTests.cs b/tests/nanoFramework.WebServer.Tests/WebServerTests.cs
index 016e1ad..21fea69 100644
--- a/tests/nanoFramework.WebServer.Tests/WebServerTests.cs
+++ b/tests/nanoFramework.WebServer.Tests/WebServerTests.cs
@@ -1,4 +1,4 @@
-//
+//
// Copyright (c) 2020 Laurent Ellerbach and the project contributors
// See LICENSE file in the project root for full license information.
//
@@ -95,5 +95,111 @@ public void IsRouteMatch_Should_ReturnTrueForMatchingMethodAndRouteCaseSensitive
Assert.IsTrue(resultMatch);
Assert.IsFalse(resultNotMatch);
}
+
+ [TestMethod]
+ [DataRow("/api/devices/{id}", "/api/devices/123", true)]
+ [DataRow("/api/devices/{id}", "/api/devices/device123", true)]
+ [DataRow("/api/devices/{id}", "/api/devices/123abc", true)]
+ [DataRow("/api/devices/{id}/actions", "/api/devices/123/actions", true)]
+ [DataRow("/api/devices/{deviceId}/sensors/{sensorId}", "/api/devices/123/sensors/456", true)]
+ [DataRow("/api/devices/{id}", "/api/devices/", false)]
+ [DataRow("/api/devices/{id}", "/api/devices/123/456", false)]
+ [DataRow("/api/devices/{id}", "/api/devices", false)]
+ [DataRow("/api/devices/{id}", "/api/different/123", false)]
+ [DataRow("/api/devices/{id}/actions", "/api/devices/123", false)]
+ [DataRow("/api/devices/{deviceId}/sensors/{sensorId}", "/api/devices/123/sensors", false)]
+ public void IsRouteMatch_Should_HandleParameterizedRoutes(string routeTemplate, string requestUrl, bool shouldMatch)
+ {
+ // Arrange
+ var route = new CallbackRoutes()
+ {
+ Method = "GET",
+ Route = routeTemplate,
+ CaseSensitive = false
+ };
+
+ // Act
+ var result = WebServer.IsRouteMatch(route, "GET", requestUrl);
+
+ // Assert
+ if (shouldMatch)
+ {
+ Assert.IsTrue(result, $"Route '{routeTemplate}' should match URL '{requestUrl}'");
+ }
+ else
+ {
+ Assert.IsFalse(result, $"Route '{routeTemplate}' should not match URL '{requestUrl}'");
+ }
+ }
+
+ [TestMethod]
+ [DataRow("/api/devices/{id}", "/api/devices/123", "id", "123")]
+ [DataRow("/api/devices/{deviceId}/sensors/{sensorId}", "/api/devices/mydevice/sensors/mysensor", "deviceId", "mydevice")]
+ [DataRow("/api/devices/{deviceId}/sensors/{sensorId}", "/api/devices/mydevice/sensors/mysensor", "sensorId", "mysensor")]
+ [DataRow("/users/{userId}/posts/{postId}/comments", "/users/john/posts/100/comments", "userId", "john")]
+ [DataRow("/users/{userId}/posts/{postId}/comments", "/users/john/posts/100/comments", "postId", "100")]
+ public void ExtractRouteParameters_Should_ExtractParameterValues(string routeTemplate, string requestUrl, string paramName, string expectedValue)
+ {
+ // Act
+ var parameters = WebServer.ExtractRouteParameters(routeTemplate, requestUrl, false);
+
+ // Assert
+ Assert.IsNotNull(parameters, "Route parameters should not be null");
+
+ string actualValue = null;
+ foreach (UrlParameter param in parameters)
+ {
+ if (param.Name.ToLower() == paramName.ToLower())
+ {
+ actualValue = param.Value;
+ break;
+ }
+ }
+
+ Assert.AreEqual(expectedValue, actualValue, $"Parameter '{paramName}' should have value '{expectedValue}'");
+ }
+
+ [TestMethod]
+ public void ExtractRouteParameters_Should_ReturnNullForNonMatchingRoute()
+ {
+ // Act
+ var parameters = WebServer.ExtractRouteParameters("/api/devices/{id}", "/api/users/123", false);
+
+ // Assert
+ Assert.IsNull(parameters, "Should return null for non-matching routes");
+ }
+
+ [TestMethod]
+ public void ExtractRouteParameters_Should_ReturnNullForEmptyInputs()
+ {
+ // Act
+ var parameters1 = WebServer.ExtractRouteParameters("", "/api/test", false);
+ var parameters2 = WebServer.ExtractRouteParameters("/api/test", "", false);
+ var parameters3 = WebServer.ExtractRouteParameters(null, "/api/test", false);
+ var parameters4 = WebServer.ExtractRouteParameters("/api/test", null, false);
+
+ // Assert
+ Assert.IsNull(parameters1, "Should return null for empty route template");
+ Assert.IsNull(parameters2, "Should return null for empty URL");
+ Assert.IsNull(parameters3, "Should return null for null route template");
+ Assert.IsNull(parameters4, "Should return null for null URL");
+ }
+
+ [TestMethod]
+ public void ExtractRouteParameters_Should_HandleQueryParameters()
+ {
+ // Arrange
+ var routeTemplate = "/api/devices/{id}";
+ var requestUrl = "/api/devices/123?filter=active&sort=name";
+
+ // Act
+ var parameters = WebServer.ExtractRouteParameters(routeTemplate, requestUrl, false);
+
+ // Assert
+ Assert.IsNotNull(parameters, "Route parameters should not be null");
+ Assert.AreEqual(1, parameters.Length, "Should have exactly one route parameter");
+ Assert.AreEqual("id", parameters[0].Name, "Parameter name should be 'id'");
+ Assert.AreEqual("123", parameters[0].Value, "Parameter value should be '123'");
+ }
}
}