diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs
index e48dd9445bfc..c05874bf536a 100644
--- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs
+++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis;
namespace Microsoft.AspNetCore.Analyzers;
@@ -248,4 +249,18 @@ internal static class DiagnosticDescriptors
DiagnosticSeverity.Info,
isEnabledByDefault: true,
helpLinkUri: AnalyzersLink);
+
+ internal static readonly DiagnosticDescriptor InvalidRouteConstraintForParameterType = CreateDescriptor(
+ "ASP0029",
+ Usage,
+ DiagnosticSeverity.Error);
+
+ private static DiagnosticDescriptor CreateDescriptor(string id, string category, DiagnosticSeverity defaultSeverity, bool isEnabledByDefault = true, [CallerMemberName] string? name = null) => new(
+ id,
+ CreateLocalizableResourceString($"Analyzer_{name}_Title"),
+ CreateLocalizableResourceString($"Analyzer_{name}_Message"),
+ category,
+ defaultSeverity,
+ isEnabledByDefault,
+ helpLinkUri: AnalyzersLink);
}
diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx
index 8c9397f5be64..601155ef337f 100644
--- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx
+++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx
@@ -333,4 +333,10 @@
If the server does not specifically reject IPv6, IPAddress.IPv6Any is preferred over IPAddress.Any usage for safety and performance reasons. See https://aka.ms/aspnetcore-warnings/ASP0028 for more details.
+
+ Invalid constraint for parameter type
+
+
+ The constraint '{0}' on parameter '{1}' can't be used with type '{2}'
+
diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/RouteHandlerAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/RouteHandlerAnalyzer.cs
index 274bf3d4d4ce..13b793790b73 100644
--- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/RouteHandlerAnalyzer.cs
+++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/RouteHandlerAnalyzer.cs
@@ -4,12 +4,14 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
+using Microsoft.CodeAnalysis.Text;
namespace Microsoft.AspNetCore.Analyzers.RouteHandlers;
@@ -20,7 +22,8 @@ public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer
{
private const int DelegateParameterOrdinal = 2;
- public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(
+ public override ImmutableArray SupportedDiagnostics { get; } =
+ [
DiagnosticDescriptors.DoNotUseModelBindingAttributesOnRouteHandlerParameters,
DiagnosticDescriptors.DoNotReturnActionResultsFromRouteHandlers,
DiagnosticDescriptors.DetectMisplacedLambdaAttribute,
@@ -28,8 +31,9 @@ public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer
DiagnosticDescriptors.RouteParameterComplexTypeIsNotParsable,
DiagnosticDescriptors.BindAsyncSignatureMustReturnValueTaskOfT,
DiagnosticDescriptors.AmbiguousRouteHandlerRoute,
- DiagnosticDescriptors.AtMostOneFromBodyAttribute
- );
+ DiagnosticDescriptors.AtMostOneFromBodyAttribute,
+ DiagnosticDescriptors.InvalidRouteConstraintForParameterType
+ ];
public override void Initialize(AnalysisContext context)
{
@@ -74,15 +78,9 @@ void DoOperationAnalysis(OperationAnalysisContext context, ConcurrentDictionary<
return;
}
- IDelegateCreationOperation? delegateCreation = null;
- foreach (var argument in invocation.Arguments)
- {
- if (argument.Parameter?.Ordinal == DelegateParameterOrdinal)
- {
- delegateCreation = argument.Descendants().OfType().FirstOrDefault();
- break;
- }
- }
+ // Already checked there are 3 arguments
+ var deleateArg = invocation.Arguments[DelegateParameterOrdinal];
+ var delegateCreation = (IDelegateCreationOperation?)deleateArg.Descendants().FirstOrDefault(static d => d is IDelegateCreationOperation);
if (delegateCreation is null)
{
@@ -100,6 +98,8 @@ void DoOperationAnalysis(OperationAnalysisContext context, ConcurrentDictionary<
return;
}
+ AnalyzeRouteConstraints(routeUsage, wellKnownTypes, context);
+
mapOperations.TryAdd(MapOperation.Create(invocation, routeUsage), value: default);
if (delegateCreation.Target.Kind == OperationKind.AnonymousFunction)
@@ -172,23 +172,16 @@ void DoOperationAnalysis(OperationAnalysisContext context, ConcurrentDictionary<
private static bool TryGetStringToken(IInvocationOperation invocation, out SyntaxToken token)
{
- IArgumentOperation? argumentOperation = null;
- foreach (var argument in invocation.Arguments)
- {
- if (argument.Parameter?.Ordinal == 1)
- {
- argumentOperation = argument;
- }
- }
+ var argumentOperation = invocation.Arguments[1];
- if (argumentOperation?.Syntax is not ArgumentSyntax routePatternArgumentSyntax ||
- routePatternArgumentSyntax.Expression is not LiteralExpressionSyntax routePatternArgumentLiteralSyntax)
+ if (argumentOperation.Value is not ILiteralOperation literal)
{
token = default;
return false;
}
- token = routePatternArgumentLiteralSyntax.Token;
+ var syntax = (LiteralExpressionSyntax)literal.Syntax;
+ token = syntax.Token;
return true;
}
@@ -218,6 +211,133 @@ static bool IsCompatibleDelegateType(WellKnownTypes wellKnownTypes, IMethodSymbo
}
}
+ private static void AnalyzeRouteConstraints(RouteUsageModel routeUsage, WellKnownTypes wellKnownTypes, OperationAnalysisContext context)
+ {
+ foreach (var routeParam in routeUsage.RoutePattern.RouteParameters)
+ {
+ var handlerParam = GetHandlerParam(routeParam.Name, routeUsage);
+
+ if (handlerParam is null)
+ {
+ continue;
+ }
+
+ foreach (var policy in routeParam.Policies)
+ {
+ if (IsConstraintInvalidForType(policy, handlerParam.Type, wellKnownTypes))
+ {
+ var descriptor = DiagnosticDescriptors.InvalidRouteConstraintForParameterType;
+ var start = routeParam.Span.Start + routeParam.Name.Length + 2; // including '{' and ':'
+ var textSpan = new TextSpan(start, routeParam.Span.End - start - 1); // excluding '}'
+ var location = Location.Create(context.FilterTree, textSpan);
+ var diagnostic = Diagnostic.Create(descriptor, location, policy.AsMemory(1), routeParam.Name, handlerParam.Type.ToString());
+
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+ }
+
+ private static bool IsConstraintInvalidForType(string policy, ITypeSymbol type, WellKnownTypes wellKnownTypes)
+ {
+ if (policy.EndsWith(")", StringComparison.Ordinal)) // Parameterized constraint
+ {
+ var braceIndex = policy.IndexOf('(');
+
+ if (braceIndex == -1)
+ {
+ return false;
+ }
+
+ var constraint = policy.AsSpan(1, braceIndex - 1);
+
+ return constraint switch
+ {
+ "length" or "minlength" or "maxlength" or "regex" when type.SpecialType is not SpecialType.System_String => true,
+ "min" or "max" or "range" when !IsIntegerType(type) && !IsNullableIntegerType(type) => true,
+ _ => false
+ };
+ }
+ else // Simple constraint
+ {
+ var constraint = policy.AsSpan(1);
+
+ return constraint switch
+ {
+ "int" when !IsIntegerType(type) && !IsNullableIntegerType(type) => true,
+ "bool" when !IsValueTypeOrNullableValueType(type, SpecialType.System_Boolean) => true,
+ "datetime" when !IsValueTypeOrNullableValueType(type, SpecialType.System_DateTime) => true,
+ "double" when !IsValueTypeOrNullableValueType(type, SpecialType.System_Double) => true,
+ "guid" when !IsGuidType(type, wellKnownTypes) && !IsNullableGuidType(type, wellKnownTypes) => true,
+ "long" when !IsLongType(type) && !IsNullableLongType(type) => true,
+ "decimal" when !IsValueTypeOrNullableValueType(type, SpecialType.System_Decimal) => true,
+ "float" when !IsValueTypeOrNullableValueType(type, SpecialType.System_Single) => true,
+ "alpha" when type.SpecialType is not SpecialType.System_String => true,
+ "file" or "nonfile" when type.SpecialType is not SpecialType.System_String => true,
+ _ => false
+ };
+ }
+ }
+
+ private static IParameterSymbol? GetHandlerParam(string name, RouteUsageModel routeUsage)
+ {
+ foreach (var param in routeUsage.UsageContext.Parameters)
+ {
+ if (param.Name.Equals(name, StringComparison.Ordinal))
+ {
+ return (IParameterSymbol)param;
+ }
+ }
+
+ return null;
+ }
+
+ private static bool IsGuidType(ITypeSymbol type, WellKnownTypes wellKnownTypes)
+ {
+ return type.Equals(wellKnownTypes.Get(WellKnownType.System_Guid), SymbolEqualityComparer.Default);
+ }
+
+ private static bool IsIntegerType(ITypeSymbol type)
+ {
+ return type.SpecialType >= SpecialType.System_SByte && type.SpecialType <= SpecialType.System_UInt64;
+ }
+
+ private static bool IsLongType(ITypeSymbol type)
+ {
+ return type.SpecialType is SpecialType.System_Int64 or SpecialType.System_UInt64;
+ }
+
+ private static bool IsNullableGuidType(ITypeSymbol type, WellKnownTypes wellKnownTypes)
+ {
+ return IsNullableType(type, out var namedType) && IsGuidType(namedType.TypeArguments[0], wellKnownTypes);
+ }
+
+ private static bool IsNullableIntegerType(ITypeSymbol type)
+ {
+ return IsNullableType(type, out var namedType) && IsIntegerType(namedType.TypeArguments[0]);
+ }
+
+ private static bool IsNullableLongType(ITypeSymbol type)
+ {
+ return IsNullableType(type, out var namedType) && IsLongType(namedType.TypeArguments[0]);
+ }
+
+ public static bool IsNullableType(ITypeSymbol type, [NotNullWhen(true)] out INamedTypeSymbol? namedType)
+ {
+ namedType = type as INamedTypeSymbol;
+ return namedType != null && namedType.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T;
+ }
+
+ private static bool IsNullableValueType(ITypeSymbol type, SpecialType specialType)
+ {
+ return IsNullableType(type, out var namedType) && namedType.TypeArguments[0].SpecialType == specialType;
+ }
+
+ private static bool IsValueTypeOrNullableValueType(ITypeSymbol type, SpecialType specialType)
+ {
+ return type.SpecialType == specialType || IsNullableValueType(type, specialType);
+ }
+
private record struct MapOperation(IOperation? Builder, IInvocationOperation Operation, RouteUsageModel RouteUsageModel)
{
public static MapOperation Create(IInvocationOperation operation, RouteUsageModel routeUsageModel)
diff --git a/src/Framework/AspNetCoreAnalyzers/test/Extensions/CSharpAnalyzerTestExtensions.cs b/src/Framework/AspNetCoreAnalyzers/test/Extensions/CSharpAnalyzerTestExtensions.cs
new file mode 100644
index 000000000000..a341d4758b9d
--- /dev/null
+++ b/src/Framework/AspNetCoreAnalyzers/test/Extensions/CSharpAnalyzerTestExtensions.cs
@@ -0,0 +1,43 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Analyzers.Verifiers;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Testing;
+
+namespace Microsoft.AspNetCore.Analyzers;
+
+public static class CSharpAnalyzerTestExtensions
+{
+ extension(CSharpAnalyzerTest)
+ where TAnalyzer : DiagnosticAnalyzer, new()
+ where TVerifier : IVerifier, new()
+ {
+ public static CSharpAnalyzerTest Create([StringSyntax("C#-test")] string source, params ReadOnlySpan expectedDiagnostics)
+ {
+ var test = new CSharpAnalyzerTest
+ {
+ TestCode = source.ReplaceLineEndings(),
+ // We need to set the output type to an exe to properly
+ // support top-level programs in the tests. Otherwise,
+ // the test infra will assume we are trying to build a library.
+ TestState = { OutputKind = OutputKind.ConsoleApplication },
+ ReferenceAssemblies = CSharpAnalyzerVerifier.GetReferenceAssemblies(),
+ };
+
+ test.ExpectedDiagnostics.AddRange(expectedDiagnostics);
+ return test;
+ }
+ }
+
+ public static CSharpAnalyzerTest WithSource(this CSharpAnalyzerTest test, [StringSyntax("C#-test")] string source)
+ where TAnalyzer : DiagnosticAnalyzer, new()
+ where TVerifier : IVerifier, new()
+ {
+ test.TestState.Sources.Add(source);
+ return test;
+ }
+}
diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteHandlers/DetectMismatchedParameterOptionalityTest.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteHandlers/DetectMismatchedParameterOptionalityTest.cs
index ab0c98cae9e0..cec926d5fe9c 100644
--- a/src/Framework/AspNetCoreAnalyzers/test/RouteHandlers/DetectMismatchedParameterOptionalityTest.cs
+++ b/src/Framework/AspNetCoreAnalyzers/test/RouteHandlers/DetectMismatchedParameterOptionalityTest.cs
@@ -300,7 +300,7 @@ public async Task OptionalRouteParamRequiredArgument_WithRegexConstraint_Produce
using Microsoft.AspNetCore.Builder;
var app = WebApplication.Create();
-app.MapGet(""/hello/{age:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)?}"", ({|#0:int age|}) => $""Age: {age}"");
+app.MapGet(""/hello/{age:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)?}"", ({|#0:string age|}) => $""Age: {age}"");
";
var fixedSource = @"
@@ -308,7 +308,7 @@ public async Task OptionalRouteParamRequiredArgument_WithRegexConstraint_Produce
using Microsoft.AspNetCore.Builder;
var app = WebApplication.Create();
-app.MapGet(""/hello/{age:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)?}"", (int? age) => $""Age: {age}"");
+app.MapGet(""/hello/{age:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)?}"", (string? age) => $""Age: {age}"");
";
var expectedDiagnostic = new DiagnosticResult(DiagnosticDescriptors.DetectMismatchedParameterOptionality).WithArguments("age").WithLocation(0);
diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteHandlers/InvalidRouteConstraintForParameterType.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteHandlers/InvalidRouteConstraintForParameterType.cs
new file mode 100644
index 000000000000..988dc0544c09
--- /dev/null
+++ b/src/Framework/AspNetCoreAnalyzers/test/RouteHandlers/InvalidRouteConstraintForParameterType.cs
@@ -0,0 +1,355 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Data;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+
+using CSTest = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest<
+ Microsoft.AspNetCore.Analyzers.RouteHandlers.RouteHandlerAnalyzer,
+ Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
+
+namespace Microsoft.AspNetCore.Analyzers.RouteHandlers;
+
+public class InvalidRouteConstraintForParameterType
+{
+ private const string Program = $$$"""
+ using Microsoft.AspNetCore.Builder;
+
+ var webApp = WebApplication.Create();
+ """;
+
+ private static string[] IntConstraints = ["int", "min(10)", "max(10)", "range(1,10)"];
+ private static string[] IntTypes = ["byte", "sbyte", "short", "ushort", "int", "uint", "long", "ulong"];
+
+ public static TheoryData MapMethods { get; } = ["Map", "MapDelete", "MapFallback", "MapGet", "MapPatch", "MapPost", "MapPut"];
+
+ [Theory]
+ [MemberData(nameof(MapMethods))]
+ public async Task LambdaWithValidConstraint_NoDiagnostics(string methodName)
+ {
+ // Arrange
+ var test = CSTest.Create(Program);
+ var i = 0;
+
+ foreach (var (constraint, type) in GetValidCombinations())
+ {
+ test.WithSource($$$"""
+ using System;
+ using Microsoft.AspNetCore.Builder;
+
+ public static class Endpoints{{{i++}}}
+ {
+ public static void Map(WebApplication app)
+ {
+ app.{{{methodName}}}(@"/api/{param:{{{constraint}}}}", ({{{type}}} param) => { });
+ }
+ }
+ """);
+ }
+
+ // Act & Assert
+ await test.RunAsync();
+ }
+
+ [Theory]
+ [MemberData(nameof(MapMethods))]
+ public async Task LambdaWithInvalidConstraint_HasDiagnostics(string methodName)
+ {
+ // Arrange
+ var test = CSTest.Create(Program);
+ var i = 0;
+
+ foreach (var (constraint, type) in GetInvalidCombinations())
+ {
+ test.WithSource($$$"""
+ using System;
+ using Microsoft.AspNetCore.Builder;
+
+ public static class Endpoints{{{i}}}
+ {
+ public static void Map(WebApplication app)
+ {
+ app.{{{methodName}}}(@"/api/{param:{|#{{{i}}}:{{{constraint}}}|}}", ({{{type}}} param) => { });
+ }
+ }
+ """);
+
+ test.ExpectedDiagnostics.Add(CreateDiagnostic(constraint, "param", type, location: i++));
+ }
+
+ // Act & Assert
+ await test.RunAsync();
+ }
+
+ [Theory]
+ [MemberData(nameof(MapMethods))]
+ public async Task LocalFunctionWithValidConstraint_NoDiagnostics(string methodName)
+ {
+ // Arrange
+ var test = CSTest.Create(Program);
+ var i = 0;
+
+ foreach (var (constraint, type) in GetValidCombinations())
+ {
+ test.WithSource($$$"""
+ using System;
+ using Microsoft.AspNetCore.Builder;
+
+ public static class Endpoints{{{i++}}}
+ {
+ public static void Map(WebApplication app)
+ {
+ app.{{{methodName}}}(@"/api/{param:{{{constraint}}}}", LocalFunction);
+
+ string LocalFunction({{{type}}} param) => param.ToString();
+ }
+ }
+ """);
+ }
+
+ // Act & Assert
+ await test.RunAsync();
+ }
+
+ [Theory]
+ [MemberData(nameof(MapMethods))]
+ public async Task LocalFunctionWithInvalidConstraint_HasDiagnostics(string methodName)
+ {
+ // Arrange
+ var test = CSTest.Create(Program);
+ var i = 0;
+
+ foreach (var (constraint, type) in GetInvalidCombinations())
+ {
+ test.WithSource($$$"""
+ using System;
+ using Microsoft.AspNetCore.Builder;
+
+ public static class Endpoints{{{i}}}
+ {
+ public static void Map(WebApplication app)
+ {
+ app.{{{methodName}}}(@"/api/{param:{|#{{{i}}}:{{{constraint}}}|}}", LocalFunction);
+
+ string LocalFunction({{{type}}} param) => param.ToString();
+ }
+ }
+ """);
+
+ test.ExpectedDiagnostics.Add(CreateDiagnostic(constraint, "param", type, location: i++));
+ }
+
+ // Act & Assert
+ await test.RunAsync();
+ }
+
+ [Theory]
+ [MemberData(nameof(MapMethods))]
+ public async Task InstanceMethodWithValidConstraint_NoDiagnostics(string methodName)
+ {
+ // Arrange
+ var test = CSTest.Create(Program);
+ var i = 0;
+
+ foreach (var (constraint, type) in GetValidCombinations())
+ {
+ test.WithSource($$$"""
+ using System;
+ using Microsoft.AspNetCore.Builder;
+
+ public static class Endpoints{{{i++}}}
+ {
+ public static void Map(WebApplication app)
+ {
+ var handler = new Handler();
+ app.{{{methodName}}}(@"/api/{param:{{{constraint}}}}", handler.Handle);
+ }
+
+ private class Handler
+ {
+ public string Handle({{{type}}} param) => param.ToString();
+ }
+ }
+ """);
+ }
+
+ // Act & Assert
+ await test.RunAsync();
+ }
+
+ [Theory]
+ [MemberData(nameof(MapMethods))]
+ public async Task InstanceMethodWithInvalidConstraint_HasDiagnostics(string methodName)
+ {
+ // Arrange
+ var test = CSTest.Create(Program);
+ var i = 0;
+
+ foreach (var (constraint, type) in GetInvalidCombinations())
+ {
+ test.WithSource($$$"""
+ using System;
+ using Microsoft.AspNetCore.Builder;
+
+ public static class Endpoints{{{i}}}
+ {
+ public static void Map(WebApplication app)
+ {
+ var handler = new Handler();
+ app.{{{methodName}}}(@"/api/{param:{|#{{{i}}}:{{{constraint}}}|}}", handler.Handle);
+ }
+
+ private class Handler
+ {
+ public string Handle({{{type}}} param) => param.ToString();
+ }
+ }
+ """);
+
+ test.ExpectedDiagnostics.Add(CreateDiagnostic(constraint, "param", type, location: i++));
+ }
+
+ // Act & Assert
+ await test.RunAsync();
+ }
+
+ [Theory]
+ [MemberData(nameof(MapMethods))]
+ public async Task StaticMethodWithValidConstraint_NoDiagnostics(string methodName)
+ {
+ // Arrange
+ var test = CSTest.Create(Program);
+ var i = 0;
+
+ foreach (var (constraint, type) in GetValidCombinations())
+ {
+ test.WithSource($$$"""
+ using System;
+ using Microsoft.AspNetCore.Builder;
+
+ public static class Endpoints{{{i++}}}
+ {
+ public static void Map(WebApplication app)
+ {
+ app.{{{methodName}}}(@"/api/{param:{{{constraint}}}}", Handler.Handle);
+ }
+
+ private static class Handler
+ {
+ public static string Handle({{{type}}} param) => param.ToString();
+ }
+ }
+ """);
+ }
+
+ // Act & Assert
+ await test.RunAsync();
+ }
+
+ [Theory]
+ [MemberData(nameof(MapMethods))]
+ public async Task StaticMethodWithInvalidConstraint_HasDiagnostics(string methodName)
+ {
+ // Arrange
+ var test = CSTest.Create(Program);
+ var i = 0;
+
+ foreach (var (constraint, type) in GetInvalidCombinations())
+ {
+ test.WithSource($$$"""
+ using System;
+ using Microsoft.AspNetCore.Builder;
+
+ public static class Endpoints{{{i}}}
+ {
+ public static void Map(WebApplication app)
+ {
+ app.{{{methodName}}}(@"/api/{param:{|#{{{i}}}:{{{constraint}}}|}}", Handler.Handle);
+ }
+
+ public static class Handler
+ {
+ public static string Handle({{{type}}} param) => param.ToString();
+ }
+ }
+ """);
+
+ test.ExpectedDiagnostics.Add(CreateDiagnostic(constraint, "param", type, location: i++));
+ }
+
+ // Act & Assert
+ await test.RunAsync();
+ }
+
+ public static IEnumerable<(string constraint, string type)> GetValidCombinations()
+ {
+ yield return ("bool", "bool");
+ yield return ("datetime", "DateTime");
+ yield return ("decimal", "decimal");
+ yield return ("double", "double");
+ yield return ("float", "float");
+ yield return ("guid", "Guid");
+
+ yield return ("alpha", "string");
+ yield return ("file", "string");
+ yield return ("nonfile", "string");
+
+ yield return ("length(10)", "string");
+ yield return ("minlength(10)", "string");
+ yield return ("maxlength(10)", "string");
+ yield return (@"regex(\w+)", "string");
+
+ yield return ("long", "long");
+ yield return ("long", "ulong");
+
+ foreach (var constraint in IntConstraints)
+ {
+ foreach (var type in IntTypes)
+ {
+ yield return (constraint, type);
+ }
+ }
+ }
+
+ public static IEnumerable<(string constraint, string type)> GetInvalidCombinations()
+ {
+ yield return ("bool", "int");
+ yield return ("datetime", "int");
+ yield return ("decimal", "int");
+ yield return ("double", "int");
+ yield return ("float", "int");
+ yield return ("guid", "int");
+
+ yield return ("alpha", "int");
+ yield return ("file", "int");
+ yield return ("nonfile", "int");
+
+ yield return ("length(10)", "int");
+ yield return ("minlength(10)", "int");
+ yield return ("maxlength(10)", "int");
+ yield return (@"regex(\w+)", "int");
+
+ yield return ("long", "byte");
+ yield return ("long", "sbyte");
+ yield return ("long", "short");
+ yield return ("long", "ushort");
+ yield return ("long", "int");
+ yield return ("long", "uint");
+
+ foreach (var constraint in IntConstraints)
+ {
+ yield return (constraint, "string");
+ }
+ }
+
+ private static DiagnosticResult CreateDiagnostic(string constraint, string parameter, string typeName, int location = 0)
+ {
+ return new DiagnosticResult(DiagnosticDescriptors.InvalidRouteConstraintForParameterType)
+ .WithArguments(constraint, parameter, typeName)
+ .WithLocation(location);
+ }
+}
diff --git a/src/Shared/RoslynUtils/WellKnownTypeData.cs b/src/Shared/RoslynUtils/WellKnownTypeData.cs
index a64dd1a426e8..981fd3d50150 100644
--- a/src/Shared/RoslynUtils/WellKnownTypeData.cs
+++ b/src/Shared/RoslynUtils/WellKnownTypeData.cs
@@ -35,6 +35,7 @@ public enum WellKnownType
System_Collections_IEnumerable,
System_DateOnly,
System_DateTimeOffset,
+ System_Guid,
System_IO_Stream,
System_IO_Pipelines_PipeReader,
System_IFormatProvider,
@@ -159,6 +160,7 @@ public enum WellKnownType
"System.Collections.IEnumerable",
"System.DateOnly",
"System.DateTimeOffset",
+ "System.Guid",
"System.IO.Stream",
"System.IO.Pipelines.PipeReader",
"System.IFormatProvider",