Support for filters

This commit is contained in:
2024-09-23 16:51:12 +02:00
parent e902aa12f3
commit fa2256b5ad
8 changed files with 136 additions and 78 deletions
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
<IsPackable>true</IsPackable>
<Version>0.8.1</Version>
<Version>0.8.2</Version>
<Authors>Alexandru Macocian</Authors>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
@@ -5,6 +5,6 @@ public static class Builder
public static void Build()
{
EndpointRouteBuilder builder = new EndpointRouteBuilder();
builder.MapDeleteSimpleRoute();
builder.MapGetSimpleRoute();
}
}
@@ -0,0 +1,10 @@
namespace Net.Sdk.Web.Extensions.SourceGenerators.Tests.Filters;
public class SimpleFilter : IEndpointFilter
{
public ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
throw new NotImplementedException();
}
}
@@ -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<SimpleRequest>
{
public Task<Response> HandleRequest(SimpleRequest? request, CancellationToken cancellationToken)
@@ -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";
}
@@ -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<AttributeArgumentSyntax>().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<AttributeArgumentSyntax>().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<AttributeArgumentSyntax>().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<AttributeArgumentSyntax>().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<AttributeSyntax> attributes, HashSet<string> 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<AttributeArgumentSyntax>().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<T>(SyntaxNode syntaxNode)
{
if (syntaxNode.Parent is null)
@@ -200,6 +216,31 @@ public class MapEndpointGenerator : IIncrementalGenerator
return GetParentOfType<T>(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
@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Version>0.8.1</Version>
<Version>0.8.2</Version>
<LangVersion>latest</LangVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Alexandru Macocian</Authors>
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
<IsPackable>true</IsPackable>
<Version>0.8.1</Version>
<Version>0.8.2</Version>
<Authors>Alexandru Macocian</Authors>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>