From fa2256b5adf133209c408235f4be8453a976f9a9 Mon Sep 17 00:00:00 2001 From: Alex Macocian Date: Mon, 23 Sep 2024 16:51:12 +0200 Subject: [PATCH] Support for filters --- .../Net.Sdk.Web.Extensions.Azure.csproj | 2 +- .../Builder.cs | 2 +- .../Filters/SimpleFilter.cs | 10 + .../Routes/SimpleRoute.cs | 6 +- .../Constants.cs | 5 + .../MapEndpointGenerator.cs | 185 +++++++++++------- ...Sdk.Web.Extensions.SourceGenerators.csproj | 2 +- .../Net.Sdk.Web.Extensions.csproj | 2 +- 8 files changed, 136 insertions(+), 78 deletions(-) create mode 100644 Net.Sdk.Web.Extensions.SourceGenerators.Tests/Filters/SimpleFilter.cs diff --git a/Net.Sdk.Web.Extensions.Azure/Net.Sdk.Web.Extensions.Azure.csproj b/Net.Sdk.Web.Extensions.Azure/Net.Sdk.Web.Extensions.Azure.csproj index 0c5b07a..c0f3590 100644 --- a/Net.Sdk.Web.Extensions.Azure/Net.Sdk.Web.Extensions.Azure.csproj +++ b/Net.Sdk.Web.Extensions.Azure/Net.Sdk.Web.Extensions.Azure.csproj @@ -6,7 +6,7 @@ enable Library true - 0.8.1 + 0.8.2 Alexandru Macocian LICENSE true diff --git a/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Builder.cs b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Builder.cs index 104ba4e..6202cdd 100644 --- a/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Builder.cs +++ b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Builder.cs @@ -5,6 +5,6 @@ public static class Builder public static void Build() { EndpointRouteBuilder builder = new EndpointRouteBuilder(); - builder.MapDeleteSimpleRoute(); + builder.MapGetSimpleRoute(); } } diff --git a/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Filters/SimpleFilter.cs b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Filters/SimpleFilter.cs new file mode 100644 index 0000000..e5a323e --- /dev/null +++ b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Filters/SimpleFilter.cs @@ -0,0 +1,10 @@ + +namespace Net.Sdk.Web.Extensions.SourceGenerators.Tests.Filters; + +public class SimpleFilter : IEndpointFilter +{ + public ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + throw new NotImplementedException(); + } +} diff --git a/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Routes/SimpleRoute.cs b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Routes/SimpleRoute.cs index b0ec87d..05d35ba 100644 --- a/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Routes/SimpleRoute.cs +++ b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Routes/SimpleRoute.cs @@ -1,8 +1,10 @@ -using Net.Sdk.Web.Extensions.SourceGenerators.Tests.Models; +using Net.Sdk.Web.Extensions.SourceGenerators.Tests.Filters; +using Net.Sdk.Web.Extensions.SourceGenerators.Tests.Models; namespace Net.Sdk.Web.Extensions.SourceGenerators.Tests.Routes; -[GenerateMapDelete(Pattern = "simple")] +[GenerateMapGet(Pattern = "simple")] +[RouteFilter(RouteFilterType = typeof(SimpleFilter))] public sealed class SimpleRoute : IRoute { public Task HandleRequest(SimpleRequest? request, CancellationToken cancellationToken) diff --git a/Net.Sdk.Web.Extensions.SourceGenerators/Constants.cs b/Net.Sdk.Web.Extensions.SourceGenerators/Constants.cs index 1678dda..c18d963 100644 --- a/Net.Sdk.Web.Extensions.SourceGenerators/Constants.cs +++ b/Net.Sdk.Web.Extensions.SourceGenerators/Constants.cs @@ -8,6 +8,7 @@ public static class Constants public const string Sealed = "sealed"; public const string Static = "static"; public const string StringType = "string"; + public const string TypeType = "Type"; public const string IntType = "int"; public const string ObjectType = "object"; public const string Default = "default"; @@ -75,4 +76,8 @@ public static class Constants public const string WebApplicationBuilderTypeName = "WebApplicationBuilder"; public const string WithRoutesName = "WithRoutes"; public const string BuilderParameterName = "builder"; + + public const string RouteFilterAttributeName = "RouteFilterAttribute"; + public const string RouteFilterAttributeShortName = "RouteFilter"; + public const string RouteFilterTypePropertyName = "RouteFilterType"; } diff --git a/Net.Sdk.Web.Extensions.SourceGenerators/MapEndpointGenerator.cs b/Net.Sdk.Web.Extensions.SourceGenerators/MapEndpointGenerator.cs index d03aed4..4907aac 100644 --- a/Net.Sdk.Web.Extensions.SourceGenerators/MapEndpointGenerator.cs +++ b/Net.Sdk.Web.Extensions.SourceGenerators/MapEndpointGenerator.cs @@ -2,9 +2,12 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Sybil; +using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Linq; +using System.Text; namespace Net.Sdk.Web.Extensions.SourceGenerators; @@ -14,6 +17,8 @@ public class MapEndpointGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { + context.RegisterPostInitializationOutput(GenerateRouteFilterAttribute); + var classDeclarations = context.SyntaxProvider.CreateSyntaxProvider( predicate: static (s, _) => s is ClassDeclarationSyntax, transform: static (ctx, _) => GetFilteredClassDeclarationSyntax(ctx)).Where(static c => c is not null); @@ -85,95 +90,35 @@ public class MapEndpointGenerator : IIncrementalGenerator if (attributes.FirstOrDefault(a => a.Name.ToString() == Constants.GetAttributeName || a.Name.ToString() == Constants.GetAttributeShortName) is AttributeSyntax getAttributeSyntax) { - var methodBuilder = SyntaxBuilder.CreateMethod(Constants.IEndpointRouteBuilderTypeName, $"MapGet{classDeclarationSyntax.Identifier}") - .WithThisParameter(Constants.IEndpointRouteBuilderTypeName, Constants.BuilderParameterName) - .WithModifiers($"{Constants.Public} {Constants.Static}"); - - var pattern = getAttributeSyntax.ArgumentList?.Arguments.OfType().FirstOrDefault(a => a.NameEquals?.Name.Identifier.Text == Constants.PatternPropertyName)?.Expression.ToString().Trim('"'); - if (pattern is null) + var methodBuilder = GetMethodBuilderByType("Get", classDeclarationSyntax, getAttributeSyntax, compilation, attributes, routeUsings); + if (methodBuilder is not null) { - continue; + extensionClassBuilder.WithMethod(methodBuilder); } - - methodBuilder.WithBody(@$" - builder.MapGet(""{pattern}"", async (HttpContext context, {classDeclarationSyntax.Identifier} route) => - {{ - var request = await route.PreProcess(context, context.RequestAborted); - var response = await route.HandleRequest(request, context.RequestAborted); - return response.GetResult(); - }}); - - return builder;"); - extensionClassBuilder.WithMethod(methodBuilder); } else if (attributes.FirstOrDefault(a => a.Name.ToString() == Constants.PostAttributeName || a.Name.ToString() == Constants.PostAttributeShortName) is AttributeSyntax postAttributeSyntax) { - var methodBuilder = SyntaxBuilder.CreateMethod(Constants.IEndpointRouteBuilderTypeName, $"MapPost{classDeclarationSyntax.Identifier}") - .WithThisParameter(Constants.IEndpointRouteBuilderTypeName, Constants.BuilderParameterName) - .WithModifiers($"{Constants.Public} {Constants.Static}"); - - var pattern = postAttributeSyntax.ArgumentList?.Arguments.OfType().FirstOrDefault(a => a.NameEquals?.Name.Identifier.Text == Constants.PatternPropertyName)?.Expression.ToString().Trim('"'); - if (pattern is null) + var methodBuilder = GetMethodBuilderByType("Post", classDeclarationSyntax, postAttributeSyntax, compilation, attributes, routeUsings); + if (methodBuilder is not null) { - continue; + extensionClassBuilder.WithMethod(methodBuilder); } - - methodBuilder.WithBody(@$" - builder.MapPost(""{pattern}"", async (HttpContext context, {classDeclarationSyntax.Identifier} route) => - {{ - var request = await route.PreProcess(context, context.RequestAborted); - var response = await route.HandleRequest(request, context.RequestAborted); - return response.GetResult(); - }}); - - return builder;"); - extensionClassBuilder.WithMethod(methodBuilder); } else if (attributes.FirstOrDefault(a => a.Name.ToString() == Constants.PutAttributeName || a.Name.ToString() == Constants.PutAttributeShortName) is AttributeSyntax putAttributeSyntax) { - var methodBuilder = SyntaxBuilder.CreateMethod(Constants.IEndpointRouteBuilderTypeName, $"MapPut{classDeclarationSyntax.Identifier}") - .WithThisParameter(Constants.IEndpointRouteBuilderTypeName, Constants.BuilderParameterName) - .WithModifiers($"{Constants.Public} {Constants.Static}"); - - var pattern = putAttributeSyntax.ArgumentList?.Arguments.OfType().FirstOrDefault(a => a.NameEquals?.Name.Identifier.Text == Constants.PatternPropertyName)?.Expression.ToString().Trim('"'); - if (pattern is null) + var methodBuilder = GetMethodBuilderByType("Put", classDeclarationSyntax, putAttributeSyntax, compilation, attributes, routeUsings); + if (methodBuilder is not null) { - continue; + extensionClassBuilder.WithMethod(methodBuilder); } - - methodBuilder.WithBody(@$" - builder.MapPut(""{pattern}"", async (HttpContext context, {classDeclarationSyntax.Identifier} route) => - {{ - var request = await route.PreProcess(context, context.RequestAborted); - var response = await route.HandleRequest(request, context.RequestAborted); - return response.GetResult(); - }}); - - return builder;"); - extensionClassBuilder.WithMethod(methodBuilder); } else if (attributes.FirstOrDefault(a => a.Name.ToString() == Constants.DeleteAttributeName || a.Name.ToString() == Constants.DeleteAttributeShortName) is AttributeSyntax deleteAttributeSyntax) { - var methodBuilder = SyntaxBuilder.CreateMethod(Constants.IEndpointRouteBuilderTypeName, $"MapDelete{classDeclarationSyntax.Identifier}") - .WithThisParameter(Constants.IEndpointRouteBuilderTypeName, Constants.BuilderParameterName) - .WithModifiers($"{Constants.Public} {Constants.Static}"); - - var pattern = deleteAttributeSyntax.ArgumentList?.Arguments.OfType().FirstOrDefault(a => a.NameEquals?.Name.Identifier.Text == Constants.PatternPropertyName)?.Expression.ToString().Trim('"'); - if (pattern is null) + var methodBuilder = GetMethodBuilderByType("Delete", classDeclarationSyntax, deleteAttributeSyntax, compilation, attributes, routeUsings); + if (methodBuilder is not null) { - continue; + extensionClassBuilder.WithMethod(methodBuilder); } - - methodBuilder.WithBody(@$" - builder.MapDelete(""{pattern}"", async (HttpContext context, {classDeclarationSyntax.Identifier} route) => - {{ - var request = await route.PreProcess(context, context.RequestAborted); - var response = await route.HandleRequest(request, context.RequestAborted); - return response.GetResult(); - }}); - - return builder;"); - extensionClassBuilder.WithMethod(methodBuilder); } } @@ -186,6 +131,77 @@ public class MapEndpointGenerator : IIncrementalGenerator sourceProductionContext.AddSource($"{Constants.EndpointRouteBuilderExtensionsName}.g", fileSource); } + private static MethodBuilder? GetMethodBuilderByType(string type, ClassDeclarationSyntax classDeclarationSyntax, AttributeSyntax mapAttributeSyntax, Compilation compilation, List attributes, HashSet usings) + { + var methodBuilder = SyntaxBuilder.CreateMethod(Constants.IEndpointRouteBuilderTypeName, $"Map{type}{classDeclarationSyntax.Identifier}") + .WithThisParameter(Constants.IEndpointRouteBuilderTypeName, Constants.BuilderParameterName) + .WithModifiers($"{Constants.Public} {Constants.Static}"); + + var pattern = mapAttributeSyntax.ArgumentList?.Arguments.OfType().FirstOrDefault(a => a.NameEquals?.Name.Identifier.Text == Constants.PatternPropertyName)?.Expression.ToString().Trim('"'); + if (pattern is null) + { + return default; + } + + var routeFilterSb = new StringBuilder(); + foreach(var attribute in attributes.Where(a => a.Name.ToString() == Constants.RouteFilterAttributeName || a.Name.ToString() == Constants.RouteFilterAttributeShortName)) + { + var semanticModel = compilation.GetSemanticModel(classDeclarationSyntax.SyntaxTree); + if (GetRouteFilterTypeName(semanticModel, attribute) is not string routeFilterType) + { + continue; + } + + var namespaceIndex = routeFilterType.LastIndexOf("."); + if (namespaceIndex > 0) + { + var routeFilterNamespace = routeFilterType.Substring(0, namespaceIndex); + routeFilterType = routeFilterType.Substring(namespaceIndex + 1); + usings.Add(routeFilterNamespace); + } + + routeFilterSb.Append('\n') + .Append(@$".AddEndpointFilter<{routeFilterType}>()"); + } + + methodBuilder.WithBody(@$" + builder.MapGet(""{pattern}"", async (HttpContext context, {classDeclarationSyntax.Identifier} route) => + {{ + var request = await route.PreProcess(context, context.RequestAborted); + var response = await route.HandleRequest(request, context.RequestAborted); + return response.GetResult(); + }}){routeFilterSb}; + + return builder;"); + + return methodBuilder; + } + + public static string? GetRouteFilterTypeName(SemanticModel semanticModel, AttributeSyntax attributeSyntax) + { + var routeFilterTypeArgument = attributeSyntax.ArgumentList?.Arguments + .FirstOrDefault(arg => arg.NameEquals?.Name.Identifier.Text == Constants.RouteFilterTypePropertyName); + + if (routeFilterTypeArgument is null) + { + return default; + } + + if (routeFilterTypeArgument.Expression is not TypeOfExpressionSyntax typeExpression) + { + return default; + } + + var typeSymbol = semanticModel.GetTypeInfo(typeExpression.Type); + if (typeSymbol.Type is INamedTypeSymbol namedTypeSymbol) + { + // Get the fully qualified name (namespace + type name) + return namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + + return default; + } + private static T? GetParentOfType(SyntaxNode syntaxNode) { if (syntaxNode.Parent is null) @@ -200,6 +216,31 @@ public class MapEndpointGenerator : IIncrementalGenerator return GetParentOfType(syntaxNode.Parent); } + + private static void GenerateRouteFilterAttribute(IncrementalGeneratorPostInitializationContext context) + { + var builder = SyntaxBuilder.CreateCompilationUnit() + .WithNamespace( + SyntaxBuilder.CreateNamespace(Constants.Namespace) + .WithClass(SyntaxBuilder.CreateClass(Constants.RouteFilterAttributeName) + .WithModifier(Constants.Public) + .WithConstructor(SyntaxBuilder.CreateConstructor(Constants.RouteFilterAttributeName) + .WithModifier(Constants.Public)) + .WithAttribute(SyntaxBuilder.CreateAttribute("AttributeUsage") + .WithArgument(AttributeTargets.Class) + .WithArgument("Inherited", false) + .WithArgument("AllowMultiple", true)) + .WithBaseClass(nameof(Attribute)) + .WithProperty(SyntaxBuilder.CreateProperty($"{Constants.TypeType}?", Constants.RouteFilterTypePropertyName) + .WithModifier(Constants.Public) + .WithAccessor(SyntaxBuilder.CreateGetter()) + .WithAccessor(SyntaxBuilder.CreateSetter())))); + + + var syntax = builder.Build(); + var source = syntax.ToFullString(); + context.AddSource($"{Constants.RouteFilterAttributeName}.g", $"#nullable enable\n{source}\n#nullable disable\n"); + } } #nullable disable \ No newline at end of file diff --git a/Net.Sdk.Web.Extensions.SourceGenerators/Net.Sdk.Web.Extensions.SourceGenerators.csproj b/Net.Sdk.Web.Extensions.SourceGenerators/Net.Sdk.Web.Extensions.SourceGenerators.csproj index d870250..1d3c109 100644 --- a/Net.Sdk.Web.Extensions.SourceGenerators/Net.Sdk.Web.Extensions.SourceGenerators.csproj +++ b/Net.Sdk.Web.Extensions.SourceGenerators/Net.Sdk.Web.Extensions.SourceGenerators.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 0.8.1 + 0.8.2 latest true Alexandru Macocian diff --git a/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj b/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj index fd1032f..45c83b2 100644 --- a/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj +++ b/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj @@ -6,7 +6,7 @@ enable Library true - 0.8.1 + 0.8.2 Alexandru Macocian LICENSE true