diff --git a/src/Http/Routing/src/Constraints/NotRouteConstraint.cs b/src/Http/Routing/src/Constraints/NotRouteConstraint.cs
new file mode 100644
index 000000000000..aff2accde757
--- /dev/null
+++ b/src/Http/Routing/src/Constraints/NotRouteConstraint.cs
@@ -0,0 +1,314 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if !COMPONENTS
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Matching;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+#else
+using Microsoft.AspNetCore.Components.Routing;
+#endif
+
+namespace Microsoft.AspNetCore.Routing.Constraints;
+
+///
+/// A route constraint that negates one or more inner constraints. The constraint matches
+/// when none of the inner constraints match the route value.
+///
+///
+///
+/// The implements logical negation for route constraints.
+/// It takes a semicolon-separated list of constraint names and returns true only
+/// when none of the specified constraints match the route value.
+///
+///
+/// Supported Features:
+///
+///
+/// -
+/// Basic type constraints: int, bool, guid, datetime, decimal, double, float, long
+///
+/// -
+/// String constraints: alpha, length(n), minlength(n), maxlength(n)
+///
+/// -
+/// Numeric constraints: min(n), max(n), range(min,max)
+///
+/// -
+/// File constraints: file, nonfile
+///
+/// -
+/// Special constraints: required
+///
+/// -
+/// Multiple constraints with semicolon separation (logical AND of negations)
+///
+/// -
+/// Nested negation patterns (e.g., not(not(int))) - fully supported as recursive constraint evaluation
+///
+///
+///
+/// Examples:
+///
+///
+/// -
+/// not(int)
+/// Matches any value that is NOT an integer (e.g., "abc", "12.5", "true")
+///
+/// -
+/// not(int;bool)
+/// Matches values that are neither integers nor booleans (e.g., "abc", "12.5")
+///
+/// -
+/// not(not(int))
+/// Double negation - matches integers (equivalent to just using int constraint)
+///
+/// -
+/// not(min(18))
+/// Matches integer values less than 18 or non-integer values
+///
+/// -
+/// not(alpha)
+/// Matches non-alphabetic values (e.g., "123", "test123")
+///
+/// -
+/// not(file)
+/// Matches values that don't contain file extensions
+///
+///
+///
+/// Important Notes:
+///
+///
+/// -
+/// Unknown constraint names are ignored and always treated as non-matching, resulting in negation returning true
+///
+/// -
+/// Nested negation patterns are fully supported and work recursively (e.g., not(not(int)) = double negation)
+///
+/// -
+/// Multiple constraints are combined with logical AND - ALL inner constraints must fail for the negation to succeed
+///
+/// -
+/// Works with both route matching and literal parameter matching scenarios
+///
+///
+///
+#if !COMPONENTS
+public class NotRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy
+#else
+internal class NotRouteConstraint : IRouteConstraint
+#endif
+{
+ ///
+ /// Gets the array of inner constraint names to be negated.
+ ///
+ private string[] _inner { get; }
+
+ ///
+ /// Cached constraint map to avoid repeated reflection-based lookups.
+ ///
+ private static IDictionary? _cachedConstraintMap;
+
+ ///
+ /// Initializes a new instance of the class
+ /// with the specified inner constraints.
+ ///
+ ///
+ /// A semicolon-separated string containing the names of constraints to negate.
+ /// Can be a single constraint name (e.g., "int") or multiple constraints (e.g., "int;bool;guid").
+ /// Parameterized constraints are supported (e.g., "min(18);length(5)").
+ /// Unknown constraint names are treated as non-matching constraints.
+ ///
+ ///
+ /// The constraints string is split by semicolons to create individual constraint checks.
+ /// Examples of valid constraint strings:
+ ///
+ /// - "int" - Single type constraint
+ /// - "int;bool" - Multiple type constraints
+ /// - "min(18)" - Parameterized constraint
+ /// - "length(5);alpha" - Mixed constraint types
+ /// - "" - Empty string (always returns true)
+ ///
+ ///
+ public NotRouteConstraint(string constraints)
+ {
+ _inner = constraints.Split(";");
+ }
+
+ private static IDictionary GetConstraintMap()
+ {
+ // Use cached map or fall back to default constraint map
+ return _cachedConstraintMap ??= GetDefaultConstraintMap();
+ }
+
+ private static Dictionary GetDefaultConstraintMap()
+ {
+ // FIXME: I'm not sure if this is a good thing to do because
+ // it requires weak spreading between the ConstraintMap and
+ // RouteOptions. It doesn't seem appropriate to create two
+ // identical variables for this...
+
+ var defaults = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ // Type-specific constraints
+ ["int"] = typeof(IntRouteConstraint),
+ ["bool"] = typeof(BoolRouteConstraint),
+ ["datetime"] = typeof(DateTimeRouteConstraint),
+ ["decimal"] = typeof(DecimalRouteConstraint),
+ ["double"] = typeof(DoubleRouteConstraint),
+ ["float"] = typeof(FloatRouteConstraint),
+ ["guid"] = typeof(GuidRouteConstraint),
+ ["long"] = typeof(LongRouteConstraint),
+
+ // Length constraints
+ ["minlength"] = typeof(MinLengthRouteConstraint),
+ ["maxlength"] = typeof(MaxLengthRouteConstraint),
+ ["length"] = typeof(LengthRouteConstraint),
+
+ // Min/Max value constraints
+ ["min"] = typeof(MinRouteConstraint),
+ ["max"] = typeof(MaxRouteConstraint),
+ ["range"] = typeof(RangeRouteConstraint),
+
+ // Alpha constraint
+ ["alpha"] = typeof(AlphaRouteConstraint),
+
+#if !COMPONENTS
+ ["required"] = typeof(RequiredRouteConstraint),
+#endif
+
+ // File constraints
+ ["file"] = typeof(FileNameRouteConstraint),
+ ["nonfile"] = typeof(NonFileNameRouteConstraint),
+
+ // Not constraint
+ ["not"] = typeof(NotRouteConstraint)
+ };
+
+ return defaults;
+ }
+
+ ///
+ ///
+ ///
+ /// This method implements the core negation logic by:
+ ///
+ ///
+ /// - Resolving each inner constraint name to its corresponding implementation
+ /// - Testing each resolved constraint against the route value
+ /// - Returning false immediately if any constraint matches (short-circuit evaluation)
+ /// - Returning true only if no constraints match
+ ///
+ ///
+ /// The method attempts to use the constraint map from if available via
+ /// the HTTP context's service provider, falling back to the default constraint map if needed.
+ ///
+ ///
+ /// Unknown constraint names are ignored (treated as non-matching), which means they don't affect
+ /// the negation result.
+ ///
+ ///
+ public bool Match(
+#if !COMPONENTS
+ HttpContext? httpContext,
+ IRouter? route,
+ string routeKey,
+ RouteValueDictionary values,
+ RouteDirection routeDirection)
+#else
+ string routeKey,
+ RouteValueDictionary values)
+#endif
+ {
+ ArgumentNullException.ThrowIfNull(routeKey);
+ ArgumentNullException.ThrowIfNull(values);
+
+ // Try to get constraint map from HttpContext first, fallback to default map
+ IDictionary constraintMap;
+ IServiceProvider? serviceProvider = null;
+
+#if !COMPONENTS
+ if (httpContext?.RequestServices != null)
+ {
+ try
+ {
+ var routeOptions = httpContext.RequestServices.GetService>();
+ if (routeOptions != null)
+ {
+ constraintMap = routeOptions.Value.TrimmerSafeConstraintMap;
+ serviceProvider = httpContext.RequestServices;
+ }
+ else
+ {
+ constraintMap = GetConstraintMap();
+ }
+ }
+ catch
+ {
+ constraintMap = GetConstraintMap();
+ }
+ }
+ else
+ {
+ constraintMap = GetConstraintMap();
+ }
+#else
+ constraintMap = GetConstraintMap();
+#endif
+
+ foreach (var constraintText in _inner)
+ {
+ var resolvedConstraint = ParameterPolicyActivator.ResolveParameterPolicy(
+ constraintMap,
+ serviceProvider,
+ constraintText,
+ out _);
+
+ if (resolvedConstraint != null)
+ {
+ // If any inner constraint matches, return false (negation logic)
+#if !COMPONENTS
+ if (resolvedConstraint.Match(httpContext, route, routeKey, values, routeDirection))
+#else
+ if (resolvedConstraint.Match(routeKey, values))
+#endif
+ {
+ return false;
+ }
+ }
+ }
+
+ // If no inner constraints matched, return true (all constraints were negated)
+ return true;
+ }
+
+#if !COMPONENTS
+ bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal)
+ {
+ var constraintMap = GetConstraintMap();
+
+ foreach (var constraintText in _inner)
+ {
+ var resolvedConstraint = ParameterPolicyActivator.ResolveParameterPolicy(
+ constraintMap,
+ null,
+ constraintText,
+ out _);
+
+ if (resolvedConstraint is IParameterLiteralNodeMatchingPolicy literalPolicy)
+ {
+ // If any inner constraint matches the literal, return false (negation logic)
+ if (literalPolicy.MatchesLiteral(parameterName, literal))
+ {
+ return false;
+ }
+ }
+ }
+
+ // If no inner constraints matched the literal, return true
+ return true;
+ }
+#endif
+}
diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt
index 0612dc9ff2b0..c3782771b7c8 100644
--- a/src/Http/Routing/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt
@@ -1,3 +1,6 @@
#nullable enable
Microsoft.AspNetCore.Builder.ValidationEndpointConventionBuilderExtensions
+Microsoft.AspNetCore.Routing.Constraints.NotRouteConstraint
+Microsoft.AspNetCore.Routing.Constraints.NotRouteConstraint.Match(Microsoft.AspNetCore.Http.HttpContext? httpContext, Microsoft.AspNetCore.Routing.IRouter? route, string! routeKey, Microsoft.AspNetCore.Routing.RouteValueDictionary! values, Microsoft.AspNetCore.Routing.RouteDirection routeDirection) -> bool
+Microsoft.AspNetCore.Routing.Constraints.NotRouteConstraint.NotRouteConstraint(string! constraints) -> void
static Microsoft.AspNetCore.Builder.ValidationEndpointConventionBuilderExtensions.DisableValidation(this TBuilder builder) -> TBuilder
diff --git a/src/Http/Routing/src/RouteOptions.cs b/src/Http/Routing/src/RouteOptions.cs
index 098583357514..1be30c41d7db 100644
--- a/src/Http/Routing/src/RouteOptions.cs
+++ b/src/Http/Routing/src/RouteOptions.cs
@@ -141,6 +141,8 @@ private static IDictionary GetDefaultConstraintMap()
AddConstraint(defaults, "file");
AddConstraint(defaults, "nonfile");
+ // Not constraint
+ AddConstraint(defaults, "not");
return defaults;
}
diff --git a/src/Http/Routing/test/UnitTests/Constraints/NotRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/NotRouteConstraintTests.cs
new file mode 100644
index 000000000000..000d2cf608ef
--- /dev/null
+++ b/src/Http/Routing/test/UnitTests/Constraints/NotRouteConstraintTests.cs
@@ -0,0 +1,507 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing.Matching;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Moq;
+
+namespace Microsoft.AspNetCore.Routing.Constraints;
+
+public class NotRouteConstraintTests
+{
+ [Fact]
+ public void Constructor_WithSingleConstraint_ParsesCorrectly()
+ {
+ // Arrange & Act
+ var constraint = new NotRouteConstraint("int");
+
+ // Assert
+ Assert.NotNull(constraint);
+ }
+
+ [Fact]
+ public void Constructor_WithMultipleConstraints_ParsesCorrectly()
+ {
+ // Arrange & Act
+ var constraint = new NotRouteConstraint("int;bool;guid");
+
+ // Assert
+ Assert.NotNull(constraint);
+ }
+
+ [Fact]
+ public void Constructor_WithEmptyString_CreatesConstraint()
+ {
+ // Arrange & Act
+ var constraint = new NotRouteConstraint("");
+
+ // Assert
+ Assert.NotNull(constraint);
+ }
+
+ [Theory]
+ [InlineData("int", "123", false)] // int constraint matches, so NOT should return false
+ [InlineData("int", "abc", true)] // int constraint doesn't match, so NOT should return true
+ [InlineData("bool", "true", false)] // bool constraint matches, so NOT should return false
+ [InlineData("bool", "abc", true)] // bool constraint doesn't match, so NOT should return true
+ [InlineData("guid", "550e8400-e29b-41d4-a716-446655440000", false)] // guid matches, NOT returns false
+ [InlineData("guid", "not-a-guid", true)] // guid doesn't match, NOT returns true
+ public void Match_WithSingleConstraint_ReturnsExpectedResult(string constraintName, string value, bool expected)
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint(constraintName);
+ var values = new RouteValueDictionary { { "test", value } };
+ var httpContext = CreateHttpContext();
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData("int;bool", "123", false)] // int matches, so overall result is false
+ [InlineData("int;bool", "true", false)] // bool matches, so overall result is false
+ [InlineData("int;bool", "abc", true)] // neither matches, so overall result is true
+ [InlineData("min(5);max(10)", "7", false)] // value is between 5 and 10, both constraints match, so false
+ [InlineData("min(15);max(3)", "7", true)] // value is less than 15 and greater than 3, neither matches completely, so true
+ [InlineData("min(5);max(3)", "7", false)] // value is greater than 5, min matches, so false
+ public void Match_WithMultipleConstraints_ReturnsExpectedResult(string constraints, string value, bool expected)
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint(constraints);
+ var values = new RouteValueDictionary { { "test", value } };
+ var httpContext = CreateHttpContext();
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void Match_WithNullHttpContext_UsesDefaultConstraintMap()
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint("int");
+ var values = new RouteValueDictionary { { "test", "123" } };
+
+ // Act
+ var result = constraint.Match(null, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.False(result); // int constraint should match "123", so NOT returns false
+ }
+
+ [Fact]
+ public void Match_WithHttpContextButNoRouteOptions_UsesDefaultConstraintMap()
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint("int");
+ var values = new RouteValueDictionary { { "test", "123" } };
+ var services = new ServiceCollection();
+ var serviceProvider = services.BuildServiceProvider();
+ var httpContext = new DefaultHttpContext { RequestServices = serviceProvider };
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.False(result); // int constraint should match "123", so NOT returns false
+ }
+
+ [Fact]
+ public void Match_WithCustomRouteOptions_UsesCustomConstraintMap()
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint("custom");
+ var values = new RouteValueDictionary { { "test", "value" } };
+
+ var customConstraintMap = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["custom"] = typeof(AlwaysTrueConstraint)
+ };
+
+ var routeOptions = new RouteOptions();
+ typeof(RouteOptions).GetField("_constraintTypeMap", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+ ?.SetValue(routeOptions, customConstraintMap);
+
+ var services = new ServiceCollection();
+ services.AddSingleton(Options.Create(routeOptions));
+ var serviceProvider = services.BuildServiceProvider();
+ var httpContext = new DefaultHttpContext { RequestServices = serviceProvider };
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void Match_WithServiceResolutionException_FallsBackToDefaultMap()
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint("int");
+ var values = new RouteValueDictionary { { "test", "123" } };
+
+ var mockServiceProvider = new Mock();
+ mockServiceProvider.Setup(sp => sp.GetService(It.IsAny()))
+ .Throws(new InvalidOperationException("Service resolution failed"));
+
+ var httpContext = new DefaultHttpContext { RequestServices = mockServiceProvider.Object };
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.False(result); // Should fall back to default map and int constraint should match
+ }
+
+ [Fact]
+ public void Match_ThrowsArgumentNullException_WhenRouteKeyIsNull()
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint("int");
+ var values = new RouteValueDictionary { { "test", "123" } };
+ var httpContext = CreateHttpContext();
+
+ // Act & Assert
+ Assert.Throws(() =>
+ constraint.Match(httpContext, null, null!, values, RouteDirection.IncomingRequest));
+ }
+
+ [Fact]
+ public void Match_ThrowsArgumentNullException_WhenValuesIsNull()
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint("int");
+ var httpContext = CreateHttpContext();
+
+ // Act & Assert
+ Assert.Throws(() =>
+ constraint.Match(httpContext, null, "test", null!, RouteDirection.IncomingRequest));
+ }
+
+ [Theory]
+ [InlineData("int", "123", false)] // int constraint matches literal, NOT returns false
+ [InlineData("int", "abc", true)] // int constraint doesn't match literal, NOT returns true
+ [InlineData("bool", "true", false)] // bool constraint matches literal, NOT returns false
+ [InlineData("bool", "abc", true)] // bool constraint doesn't match literal, NOT returns true
+ public void MatchesLiteral_WithSingleConstraint_ReturnsExpectedResult(string constraintName, string literal, bool expected)
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint(constraintName);
+
+ // Act
+ var result = ((IParameterLiteralNodeMatchingPolicy)constraint).MatchesLiteral("test", literal);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData("int;bool", "123", false)] // int matches literal, so overall result is false
+ [InlineData("int;bool", "true", false)] // bool matches literal, so overall result is false
+ [InlineData("int;bool", "abc", true)] // neither matches literal, so overall result is true
+ public void MatchesLiteral_WithMultipleConstraints_ReturnsExpectedResult(string constraints, string literal, bool expected)
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint(constraints);
+
+ // Act
+ var result = ((IParameterLiteralNodeMatchingPolicy)constraint).MatchesLiteral("test", literal);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void MatchesLiteral_WithUnknownConstraint_ReturnsTrue()
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint("unknownconstraint");
+
+ // Act
+ var result = ((IParameterLiteralNodeMatchingPolicy)constraint).MatchesLiteral("test", "value");
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void MatchesLiteral_WithConstraintThatDoesNotImplementLiteralPolicy_ReturnsTrue()
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint("required"); // RequiredRouteConstraint doesn't implement IParameterLiteralNodeMatchingPolicy
+
+ // Act
+ var result = ((IParameterLiteralNodeMatchingPolicy)constraint).MatchesLiteral("test", "value");
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("single")]
+ [InlineData("multiple;constraints;here")]
+ [InlineData("int;bool;guid;datetime")]
+ public void Constructor_WithVariousConstraintStrings_DoesNotThrow(string constraints)
+ {
+ // Arrange & Act
+ var exception = Record.Exception(() => new NotRouteConstraint(constraints));
+
+ // Assert
+ Assert.Null(exception);
+ }
+
+ [Fact]
+ public void Match_WithComplexConstraints_HandlesCorrectly()
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint("min(10);max(5)");
+ var values = new RouteValueDictionary { { "test", "7" } };
+ var httpContext = CreateHttpContext();
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.True(result); // Neither constraint should match, so NOT returns true
+ }
+
+ [Fact]
+ public void Match_WithParameterizedConstraints_HandlesCorrectly()
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint("length(5);minlength(3)");
+ var values = new RouteValueDictionary { { "test", "hello" } };
+ var httpContext = CreateHttpContext();
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void Match_WithEmptyConstraintString_ReturnsTrue()
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint("");
+ var values = new RouteValueDictionary { { "test", "anyvalue" } };
+ var httpContext = CreateHttpContext();
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Match_WithNonExistentRouteKey_ReturnsTrue()
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint("int");
+ var values = new RouteValueDictionary { { "other", "123" } };
+ var httpContext = CreateHttpContext();
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Match_WithNullRouteValue_ReturnsTrue()
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint("int");
+ var values = new RouteValueDictionary { { "test", null } };
+ var httpContext = CreateHttpContext();
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Theory]
+ [InlineData("alpha", "abc", false)] // alpha matches letters, NOT returns false
+ [InlineData("alpha", "123", true)] // alpha doesn't match numbers, NOT returns true
+ public void Match_WithAlphaConstraints_ReturnsExpectedResult(string constraintName, string value, bool expected)
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint(constraintName);
+ var values = new RouteValueDictionary { { "test", value } };
+ var httpContext = CreateHttpContext();
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void Match_WithRequiredConstraint_HandlesCorrectly()
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint("required");
+ var values = new RouteValueDictionary { { "test", "" } };
+ var httpContext = CreateHttpContext();
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Theory]
+ [InlineData("file", "test.txt", false)] // file constraint matches, NOT returns false
+ [InlineData("file", "test", true)] // file constraint doesn't match, NOT returns true
+ [InlineData("nonfile", "test", false)] // nonfile constraint matches, NOT returns false
+ [InlineData("nonfile", "test.txt", true)] // nonfile constraint doesn't match, NOT returns true
+ public void Match_WithFileConstraints_ReturnsExpectedResult(string constraintName, string value, bool expected)
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint(constraintName);
+ var values = new RouteValueDictionary { { "test", value } };
+ var httpContext = CreateHttpContext();
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData("unknownconstraint", "any", true)]
+ [InlineData("faketype", "value", true)]
+ public void Match_WithUnknownConstraints_ReturnsTrue(string constraintName, string value, bool expected)
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint(constraintName);
+ var values = new RouteValueDictionary { { "test", value } };
+ var httpContext = CreateHttpContext();
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData(RouteDirection.IncomingRequest)]
+ [InlineData(RouteDirection.UrlGeneration)]
+ public void Match_WithDifferentRouteDirections_WorksCorrectly(RouteDirection direction)
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint("int");
+ var values = new RouteValueDictionary { { "test", "123" } };
+ var httpContext = CreateHttpContext();
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, direction);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void MatchesLiteral_WithComplexConstraints_HandlesCorrectly()
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint("min(10);length(3)");
+
+ // Act & Assert
+ Assert.True(((IParameterLiteralNodeMatchingPolicy)constraint).MatchesLiteral("test", "5"));
+ Assert.False(((IParameterLiteralNodeMatchingPolicy)constraint).MatchesLiteral("test", "15"));
+ Assert.False(((IParameterLiteralNodeMatchingPolicy)constraint).MatchesLiteral("test", "abc"));
+ }
+
+ [Theory]
+ [InlineData("not(int)", "123", true)] // Double negation: not(not(int)) with int value should return true
+ [InlineData("not(int)", "abc", false)] // Double negation: not(not(int)) with non-int value should return false
+ [InlineData("not(bool)", "true", true)] // Double negation: not(not(bool)) with bool value should return true
+ [InlineData("not(bool)", "abc", false)] // Triple negation: not(not(bool)) with non-bool value should return false
+ public void Match_WithDoubleNegationPattern_ReturnsExpectedResult(string constraintPattern, string value, bool expected)
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint(constraintPattern);
+ var values = new RouteValueDictionary { { "test", value } };
+ var httpContext = CreateHttpContext();
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData("not(not(int))", "123", false)] // Triple negation: not(not(not(int))) with int value should return false
+ [InlineData("not(not(int))", "abc", true)] // Triple negation: not(not(not(int))) with non-int value should return true
+ [InlineData("not(not(bool))", "true", false)] // Triple negation: not(not(not(bool))) with bool value should return false
+ [InlineData("not(not(bool))", "abc", true)] // Triple negation: not(not(not(bool))) with non-bool value should return true
+ public void Match_WithTripleNegationPattern_ReturnsExpectedResult(string constraintPattern, string value, bool expected)
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint(constraintPattern);
+ var values = new RouteValueDictionary { { "test", value } };
+ var httpContext = CreateHttpContext();
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData("not(not(not(int)))", "123", true)] // Fourth negation: not(not(not(not(int)))) with int value should return true
+ [InlineData("not(not(not(int)))", "abc", false)] // Fourth negation: not(not(not(int))) with non-int value should return false
+ [InlineData("not(not(not(bool)))", "true", true)] // Fourth negation: not(not(not(bool))) with bool value should return true
+ [InlineData("not(not(not(bool)))", "abc", false)] // Fourth negation: not(not(not(bool))) with non-bool value should return false
+ public void Match_WithFourthNegationPattern_ReturnsExpectedResult(string constraintPattern, string value, bool expected)
+ {
+ // Arrange
+ var constraint = new NotRouteConstraint(constraintPattern);
+ var values = new RouteValueDictionary { { "test", value } };
+ var httpContext = CreateHttpContext();
+
+ // Act
+ var result = constraint.Match(httpContext, null, "test", values, RouteDirection.IncomingRequest);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ private static DefaultHttpContext CreateHttpContext()
+ {
+ var services = new ServiceCollection();
+ services.Configure(options => { });
+ var serviceProvider = services.BuildServiceProvider();
+ return new DefaultHttpContext { RequestServices = serviceProvider };
+ }
+
+ private class AlwaysTrueConstraint : IRouteConstraint
+ {
+ public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
+ {
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Http/startvscode.sh b/src/Http/startvscode.sh
old mode 100644
new mode 100755