mirror of
https://github.com/AlexMacocian/Net.Sdk.Web.Extensions.git
synced 2026-07-15 13:39:29 +00:00
Setup generators like Mvc
This commit is contained in:
@@ -4,7 +4,11 @@ public static class Builder
|
||||
{
|
||||
public static void Build()
|
||||
{
|
||||
EndpointRouteBuilder builder = new EndpointRouteBuilder();
|
||||
builder.MapGetSimpleRoute();
|
||||
var builder = WebApplication.CreateSlimBuilder();
|
||||
builder.WithRoutes();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseRoutes();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
|
||||
namespace Net.Sdk.Web.Extensions.SourceGenerators.Tests;
|
||||
|
||||
public class EndpointRouteBuilder : IEndpointRouteBuilder
|
||||
{
|
||||
public IServiceProvider ServiceProvider { get; }
|
||||
public ICollection<EndpointDataSource> DataSources { get; }
|
||||
|
||||
public IApplicationBuilder CreateApplicationBuilder()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,17 @@
|
||||
using Net.Sdk.Web.Extensions.SourceGenerators.Tests.Filters;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Net.Sdk.Web.Extensions.SourceGenerators.Tests.Filters;
|
||||
using Net.Sdk.Web.Extensions.SourceGenerators.Tests.Models;
|
||||
|
||||
namespace Net.Sdk.Web.Extensions.SourceGenerators.Tests.Routes;
|
||||
|
||||
[GenerateMapGet(Pattern = "simple")]
|
||||
[GenerateRoute(Pattern = "simple")]
|
||||
[RouteFilter(RouteFilterType = typeof(SimpleFilter))]
|
||||
public sealed class SimpleRoute : IRoute<SimpleRequest>
|
||||
[RouteFilter(RouteFilterType = typeof(SimpleFilter))]
|
||||
public sealed class SimpleRoute
|
||||
{
|
||||
public Task<Response> HandleRequest(SimpleRequest? request, CancellationToken cancellationToken)
|
||||
[GenerateMapGet(Pattern = "get/{api}/{id}")]
|
||||
public async Task<IResult> GetSimple(string api, string id, [FromBody] SimpleRequest reques)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<SimpleRequest?> PreProcess(HttpContext? context, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
return Results.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Net.Sdk.Web.Extensions.SourceGenerators.Tests.Routes;
|
||||
|
||||
[GenerateRoute]
|
||||
public class SimpleRoute2
|
||||
{
|
||||
[GenerateMapPost(Pattern = "somethingSimple")]
|
||||
public async Task<IResult> GetSomething()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -17,17 +17,10 @@ public static class Constants
|
||||
public const string UsingMicrosoftAspNetCoreHttp = "Microsoft.AspNetCore.Http";
|
||||
public const string UsingMicrosoftAspNetCoreRouting = "Microsoft.AspNetCore.Routing";
|
||||
public const string UsingMicrosoftAspNetCoreBuilder = "Microsoft.AspNetCore.Builder";
|
||||
public const string UsingMicrosoftAspNetCoreMvc = "Microsoft.AspNetCore.Mvc";
|
||||
|
||||
public const string RouteFileName = "Route";
|
||||
public const string RouteInterface = "IRoute";
|
||||
public const string RouteTypeParameter = "TRequest";
|
||||
public const string PreProcessRequestReturnType = "Task<TRequest?>";
|
||||
public const string PreProcessRequestMethodName = "PreProcess";
|
||||
public const string HandleRequestReturnType = "Task<Response>";
|
||||
public const string HandleRequestMethodName = "HandleRequest";
|
||||
public const string HttpContextTypeName = "HttpContext?";
|
||||
public const string HttpContextParameterName = "context";
|
||||
public const string RequestParameterName = "request";
|
||||
public const string RouteAttributeName = "GenerateRouteAttribute";
|
||||
public const string RouteAttributeShortName = "GenerateRoute";
|
||||
|
||||
public const string ResponseTypeName = "Response";
|
||||
public const string IResultTypeName = "IResult";
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Sybil;
|
||||
|
||||
namespace Net.Sdk.Web.Extensions.SourceGenerators;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
#nullable enable
|
||||
public class MapAttributeGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterPostInitializationOutput(context =>
|
||||
{
|
||||
GenerateMapAttribute(context, Constants.GetAttributeName);
|
||||
GenerateMapAttribute(context, Constants.PostAttributeName);
|
||||
GenerateMapAttribute(context, Constants.PutAttributeName);
|
||||
GenerateMapAttribute(context, Constants.DeleteAttributeName);
|
||||
});
|
||||
}
|
||||
|
||||
private static void GenerateMapAttribute(IncrementalGeneratorPostInitializationContext context, string attributeName)
|
||||
{
|
||||
var builder = SyntaxBuilder.CreateCompilationUnit()
|
||||
.WithNamespace(
|
||||
SyntaxBuilder.CreateNamespace(Constants.Namespace)
|
||||
.WithClass(SyntaxBuilder.CreateClass(attributeName)
|
||||
.WithModifier(Constants.Public)
|
||||
.WithConstructor(SyntaxBuilder.CreateConstructor(attributeName)
|
||||
.WithModifier(Constants.Public))
|
||||
.WithAttribute(SyntaxBuilder.CreateAttribute("AttributeUsage")
|
||||
.WithArgument(AttributeTargets.Method)
|
||||
.WithArgument("Inherited", false)
|
||||
.WithArgument("AllowMultiple", false))
|
||||
.WithBaseClass(nameof(Attribute))
|
||||
.WithProperty(SyntaxBuilder.CreateProperty(Constants.StringType, Constants.PatternPropertyName)
|
||||
.WithModifier(Constants.Public)
|
||||
.WithAccessor(SyntaxBuilder.CreateGetter())
|
||||
.WithAccessor(SyntaxBuilder.CreateSetter()))));
|
||||
|
||||
var syntax = builder.Build();
|
||||
var source = syntax.ToFullString();
|
||||
context.AddSource($"{attributeName}.g", source);
|
||||
}
|
||||
}
|
||||
|
||||
#nullable disable
|
||||
@@ -1,276 +0,0 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Sybil;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Net.Sdk.Web.Extensions.SourceGenerators;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
#nullable enable
|
||||
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);
|
||||
var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect());
|
||||
context.RegisterSourceOutput(compilationAndClasses, (sourceProductionContext, tuple) => Execute(tuple.Left, tuple.Right, sourceProductionContext));
|
||||
}
|
||||
|
||||
private static ClassDeclarationSyntax? GetFilteredClassDeclarationSyntax(GeneratorSyntaxContext context)
|
||||
{
|
||||
var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node;
|
||||
if (classDeclarationSyntax.AttributeLists
|
||||
.SelectMany(l => l.Attributes)
|
||||
.OfType<AttributeSyntax>()
|
||||
.Any(s => s.Name.ToString() == Constants.GetAttributeName || s.Name.ToString() == Constants.GetAttributeShortName ||
|
||||
s.Name.ToString() == Constants.PostAttributeName || s.Name.ToString() == Constants.PostAttributeShortName ||
|
||||
s.Name.ToString() == Constants.PutAttributeName || s.Name.ToString() == Constants.PutAttributeShortName ||
|
||||
s.Name.ToString() == Constants.DeleteAttributeName || s.Name.ToString() == Constants.DeleteAttributeShortName))
|
||||
{
|
||||
return classDeclarationSyntax;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void Execute(Compilation compilation, ImmutableArray<ClassDeclarationSyntax?> classes, SourceProductionContext sourceProductionContext)
|
||||
{
|
||||
if (classes.IsDefaultOrEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var maybeLanguageVersion = (compilation.SyntaxTrees.FirstOrDefault()?.Options as CSharpParseOptions)?.LanguageVersion;
|
||||
if (!maybeLanguageVersion.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var languageVersion = maybeLanguageVersion.Value;
|
||||
var builder = SyntaxBuilder.CreateCompilationUnit()
|
||||
.WithUsing(Constants.UsingSystemThreading)
|
||||
.WithUsing(Constants.UsingMicrosoftAspNetCoreRouting)
|
||||
.WithUsing(Constants.UsingMicrosoftAspNetCoreHttp)
|
||||
.WithUsing(Constants.UsingMicrosoftAspNetCoreBuilder);
|
||||
|
||||
var routeUsings = new HashSet<string>();
|
||||
|
||||
var namespaceBuilder = languageVersion >= LanguageVersion.CSharp10 ? SyntaxBuilder.CreateFileScopedNamespace(Constants.Namespace) : SyntaxBuilder.CreateNamespace(Constants.Namespace);
|
||||
builder.WithNamespace(namespaceBuilder);
|
||||
|
||||
var webAppBuilder = SyntaxBuilder.CreateClass(Constants.WebApplicationExtensionsName)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Static}");
|
||||
|
||||
var extensionClassBuilder = SyntaxBuilder.CreateClass(Constants.EndpointRouteBuilderExtensionsName)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Static}");
|
||||
namespaceBuilder.WithClass(webAppBuilder);
|
||||
namespaceBuilder.WithClass(extensionClassBuilder);
|
||||
|
||||
var useRoutesWebAppMethodBuilder = SyntaxBuilder.CreateMethod(Constants.WebApplicationTypeName, Constants.UseRoutesMethodName)
|
||||
.WithThisParameter(Constants.WebApplicationTypeName, Constants.BuilderParameterName)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Static}");
|
||||
|
||||
var useRoutesMethodBuilder = SyntaxBuilder.CreateMethod(Constants.IEndpointRouteBuilderTypeName, Constants.MapAllRoutes)
|
||||
.WithThisParameter(Constants.IEndpointRouteBuilderTypeName, Constants.BuilderParameterName)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Static}");
|
||||
var useRoutesBody = new StringBuilder();
|
||||
var useRoutesWebAppBody = new StringBuilder();
|
||||
foreach (var classDeclarationSyntax in classes)
|
||||
{
|
||||
if (classDeclarationSyntax is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GetParentOfType<BaseNamespaceDeclarationSyntax>(classDeclarationSyntax) is BaseNamespaceDeclarationSyntax baseNamespaceDeclarationSyntax)
|
||||
{
|
||||
routeUsings.Add(baseNamespaceDeclarationSyntax.Name.ToString());
|
||||
}
|
||||
|
||||
var attributes = classDeclarationSyntax.AttributeLists
|
||||
.SelectMany(l => l.Attributes)
|
||||
.OfType<AttributeSyntax>().ToList();
|
||||
|
||||
if (attributes.FirstOrDefault(a => a.Name.ToString() == Constants.GetAttributeName || a.Name.ToString() == Constants.GetAttributeShortName) is AttributeSyntax getAttributeSyntax)
|
||||
{
|
||||
var methodName = $"MapGet{classDeclarationSyntax.Identifier}";
|
||||
var methodBuilder = GetMethodBuilderByType("Get", classDeclarationSyntax, getAttributeSyntax, compilation, attributes, routeUsings);
|
||||
if (methodBuilder is not null)
|
||||
{
|
||||
useRoutesBody.AppendLine($"{methodName}(builder);");
|
||||
useRoutesWebAppBody.AppendLine($"{Constants.EndpointRouteBuilderExtensionsName}.{methodName}(builder);");
|
||||
extensionClassBuilder.WithMethod(methodBuilder);
|
||||
}
|
||||
}
|
||||
else if (attributes.FirstOrDefault(a => a.Name.ToString() == Constants.PostAttributeName || a.Name.ToString() == Constants.PostAttributeShortName) is AttributeSyntax postAttributeSyntax)
|
||||
{
|
||||
var methodName = $"MapPost{classDeclarationSyntax.Identifier}";
|
||||
var methodBuilder = GetMethodBuilderByType("Post", classDeclarationSyntax, postAttributeSyntax, compilation, attributes, routeUsings);
|
||||
if (methodBuilder is not null)
|
||||
{
|
||||
useRoutesBody.AppendLine($"{methodName}(builder);");
|
||||
useRoutesWebAppBody.AppendLine($"{Constants.EndpointRouteBuilderExtensionsName}.{methodName}(builder);");
|
||||
extensionClassBuilder.WithMethod(methodBuilder);
|
||||
}
|
||||
}
|
||||
else if (attributes.FirstOrDefault(a => a.Name.ToString() == Constants.PutAttributeName || a.Name.ToString() == Constants.PutAttributeShortName) is AttributeSyntax putAttributeSyntax)
|
||||
{
|
||||
var methodName = $"MapPut{classDeclarationSyntax.Identifier}";
|
||||
var methodBuilder = GetMethodBuilderByType("Put", classDeclarationSyntax, putAttributeSyntax, compilation, attributes, routeUsings);
|
||||
if (methodBuilder is not null)
|
||||
{
|
||||
useRoutesBody.AppendLine($"{methodName}(builder);");
|
||||
useRoutesWebAppBody.AppendLine($"{Constants.EndpointRouteBuilderExtensionsName}.{methodName}(builder);");
|
||||
extensionClassBuilder.WithMethod(methodBuilder);
|
||||
}
|
||||
}
|
||||
else if (attributes.FirstOrDefault(a => a.Name.ToString() == Constants.DeleteAttributeName || a.Name.ToString() == Constants.DeleteAttributeShortName) is AttributeSyntax deleteAttributeSyntax)
|
||||
{
|
||||
var methodName = $"MapDelete{classDeclarationSyntax.Identifier}";
|
||||
var methodBuilder = GetMethodBuilderByType("Delete", classDeclarationSyntax, deleteAttributeSyntax, compilation, attributes, routeUsings);
|
||||
if (methodBuilder is not null)
|
||||
{
|
||||
useRoutesBody.AppendLine($"{methodName}(builder);");
|
||||
useRoutesWebAppBody.AppendLine($"{Constants.EndpointRouteBuilderExtensionsName}.{methodName}(builder);");
|
||||
extensionClassBuilder.WithMethod(methodBuilder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useRoutesBody.AppendLine("return builder;");
|
||||
useRoutesWebAppBody.AppendLine($"return builder;");
|
||||
useRoutesMethodBuilder.WithBody(useRoutesBody.ToString());
|
||||
useRoutesWebAppMethodBuilder.WithBody(useRoutesWebAppBody.ToString());
|
||||
extensionClassBuilder.WithMethod(useRoutesMethodBuilder);
|
||||
webAppBuilder.WithMethod(useRoutesWebAppMethodBuilder);
|
||||
foreach (var classUsing in routeUsings)
|
||||
{
|
||||
builder.WithUsing(classUsing);
|
||||
}
|
||||
|
||||
var fileSource = builder.Build().ToFullString();
|
||||
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.Map{type}(""{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)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (syntaxNode.Parent is T parentNode)
|
||||
{
|
||||
return parentNode;
|
||||
}
|
||||
|
||||
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
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<Version>0.8.7</Version>
|
||||
<Version>0.8.8</Version>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<Authors>Alexandru Macocian</Authors>
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Sybil;
|
||||
|
||||
namespace Net.Sdk.Web.Extensions.SourceGenerators;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
#nullable enable
|
||||
public class ResponseGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterPostInitializationOutput(context =>
|
||||
{
|
||||
GenerateResponse(context);
|
||||
});
|
||||
}
|
||||
|
||||
private static void GenerateResponse(IncrementalGeneratorPostInitializationContext context)
|
||||
{
|
||||
var responseBaseClassBuilder = SyntaxBuilder.CreateCompilationUnit()
|
||||
.WithUsing(Constants.UsingMicrosoftAspNetCoreHttp)
|
||||
.WithNamespace(SyntaxBuilder.CreateNamespace(Constants.Namespace)
|
||||
.WithClass(SyntaxBuilder.CreateClass(Constants.ResponseTypeName)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Abstract}")
|
||||
.WithMethod(SyntaxBuilder.CreateMethod(Constants.IResultTypeName, Constants.GetResult)
|
||||
.WithModifier(Constants.Public)
|
||||
.WithBody(
|
||||
@"return this switch
|
||||
{
|
||||
SuccessResponse success => Results.StatusCode(success.StatusCode),
|
||||
FailureResponse failure => Results.Problem(
|
||||
detail: string.IsNullOrWhiteSpace(failure.ErrorMessage) is false ? failure.ErrorMessage : default,
|
||||
statusCode: failure.StatusCode),
|
||||
ContentResponse content => Results.Content(content.ContentString, content.ContentType),
|
||||
JsonResponse json => Results.Ok(json.Value),
|
||||
_ => throw new InvalidOperationException($""Unexpected response type {this.GetType().Name}"")
|
||||
};"))
|
||||
.WithMethod(SyntaxBuilder.CreateMethod(Constants.SuccessResponseTypeName, Constants.OkSuccessResponse)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Static}")
|
||||
.WithExpression("new() { StatusCode = 200 };"))
|
||||
.WithMethod(SyntaxBuilder.CreateMethod(Constants.SuccessResponseTypeName, Constants.CreatedSuccessResponse)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Static}")
|
||||
.WithExpression("new() { StatusCode = 201 };"))
|
||||
.WithMethod(SyntaxBuilder.CreateMethod(Constants.ContentResponseTypeName, Constants.ContentContentResponse)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Static}")
|
||||
.WithParameter(Constants.StringType, Constants.ContentParameterName)
|
||||
.WithParameter($"{Constants.StringType}?", Constants.ContentTypeParameterName, Constants.Default)
|
||||
.WithExpression("new() { ContentString = content, ContentType = contentType };"))
|
||||
.WithMethod(SyntaxBuilder.CreateMethod(Constants.JsonResponseTypeName, Constants.JsonJsonResponse)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Static}")
|
||||
.WithParameter(Constants.ObjectType, Constants.ValueParameterName)
|
||||
.WithExpression("new() { Value = value };"))
|
||||
.WithMethod(SyntaxBuilder.CreateMethod(Constants.ContentResponseTypeName, Constants.PageContentResponse)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Static}")
|
||||
.WithParameter(Constants.StringType, Constants.ContentParameterName)
|
||||
.WithExpression(@"new() { ContentString = content, ContentType = ""text/html"" };"))
|
||||
.WithMethod(SyntaxBuilder.CreateMethod(Constants.FailureResponseTypeName, Constants.BadRequestFailureResponse)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Static}")
|
||||
.WithParameter(Constants.StringType, Constants.ErrorMessageParameterName)
|
||||
.WithExpression(@"new() { StatusCode = 400, ErrorMessage = errorMessage };"))
|
||||
.WithMethod(SyntaxBuilder.CreateMethod(Constants.FailureResponseTypeName, Constants.UnauthorizedFailureResponse)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Static}")
|
||||
.WithParameter(Constants.StringType, Constants.ErrorMessageParameterName)
|
||||
.WithExpression(@"new() { StatusCode = 401, ErrorMessage = errorMessage };"))
|
||||
.WithMethod(SyntaxBuilder.CreateMethod(Constants.FailureResponseTypeName, Constants.ForbiddenFailureResponse)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Static}")
|
||||
.WithParameter(Constants.StringType, Constants.ErrorMessageParameterName)
|
||||
.WithExpression(@"new() { StatusCode = 403, ErrorMessage = errorMessage };"))
|
||||
.WithMethod(SyntaxBuilder.CreateMethod(Constants.FailureResponseTypeName, Constants.NotFoundFailureResponse)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Static}")
|
||||
.WithParameter(Constants.StringType, Constants.ErrorMessageParameterName)
|
||||
.WithExpression(@"new() { StatusCode = 404, ErrorMessage = errorMessage };"))
|
||||
.WithMethod(SyntaxBuilder.CreateMethod(Constants.FailureResponseTypeName, Constants.ServerErrorFailureResponse)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Static}")
|
||||
.WithParameter(Constants.StringType, Constants.ErrorMessageParameterName)
|
||||
.WithExpression(@"new() { StatusCode = 500, ErrorMessage = errorMessage };")))
|
||||
.WithClass(SyntaxBuilder.CreateClass(Constants.SuccessResponseTypeName)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Sealed}")
|
||||
.WithBaseClass(Constants.ResponseTypeName)
|
||||
.WithProperty(SyntaxBuilder.CreateProperty(Constants.IntType, Constants.StatusCodePropertyName)
|
||||
.WithModifier(Constants.Public)
|
||||
.WithAccessor(SyntaxBuilder.CreateGetter())
|
||||
.WithAccessor(SyntaxBuilder.CreateSetter())))
|
||||
.WithClass(SyntaxBuilder.CreateClass(Constants.FailureResponseTypeName)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Sealed}")
|
||||
.WithBaseClass(Constants.ResponseTypeName)
|
||||
.WithProperty(SyntaxBuilder.CreateProperty(Constants.IntType, Constants.StatusCodePropertyName)
|
||||
.WithModifier(Constants.Public)
|
||||
.WithAccessor(SyntaxBuilder.CreateGetter())
|
||||
.WithAccessor(SyntaxBuilder.CreateSetter()))
|
||||
.WithProperty(SyntaxBuilder.CreateProperty($"{Constants.StringType}?", Constants.ErrorMessagePropertyName)
|
||||
.WithModifier(Constants.Public)
|
||||
.WithAccessor(SyntaxBuilder.CreateGetter())
|
||||
.WithAccessor(SyntaxBuilder.CreateSetter())))
|
||||
.WithClass(SyntaxBuilder.CreateClass(Constants.ContentResponseTypeName)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Sealed}")
|
||||
.WithBaseClass(Constants.ResponseTypeName)
|
||||
.WithProperty(SyntaxBuilder.CreateProperty($"{Constants.StringType}?", Constants.ContentStringPropertyName)
|
||||
.WithModifier(Constants.Public)
|
||||
.WithAccessor(SyntaxBuilder.CreateGetter())
|
||||
.WithAccessor(SyntaxBuilder.CreateSetter()))
|
||||
.WithProperty(SyntaxBuilder.CreateProperty($"{Constants.StringType}?", Constants.ContentTypePropertyName)
|
||||
.WithModifier(Constants.Public)
|
||||
.WithAccessor(SyntaxBuilder.CreateGetter())
|
||||
.WithAccessor(SyntaxBuilder.CreateSetter())))
|
||||
.WithClass(SyntaxBuilder.CreateClass(Constants.JsonResponseTypeName)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Sealed}")
|
||||
.WithBaseClass(Constants.ResponseTypeName)
|
||||
.WithProperty(SyntaxBuilder.CreateProperty($"{Constants.ObjectType}?", Constants.ValuePropertyName)
|
||||
.WithModifier(Constants.Public)
|
||||
.WithAccessor(SyntaxBuilder.CreateGetter())
|
||||
.WithAccessor(SyntaxBuilder.CreateSetter())))
|
||||
);
|
||||
|
||||
var syntax = responseBaseClassBuilder.Build();
|
||||
var source = syntax.ToFullString();
|
||||
context.AddSource($"Response.g", $"#nullable enable\n{source}\n#nullable disable\n");
|
||||
}
|
||||
}
|
||||
|
||||
#nullable disable
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Sybil;
|
||||
using System;
|
||||
|
||||
namespace Net.Sdk.Web.Extensions.SourceGenerators;
|
||||
|
||||
@@ -10,37 +10,28 @@ public class RouteAttributeGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterPostInitializationOutput(context =>
|
||||
{
|
||||
GenerateMapAttribute(context, Constants.GetAttributeName);
|
||||
GenerateMapAttribute(context, Constants.PostAttributeName);
|
||||
GenerateMapAttribute(context, Constants.PutAttributeName);
|
||||
GenerateMapAttribute(context, Constants.DeleteAttributeName);
|
||||
});
|
||||
context.RegisterPostInitializationOutput(GenerateAttribute);
|
||||
}
|
||||
|
||||
private static void GenerateMapAttribute(IncrementalGeneratorPostInitializationContext context, string attributeName)
|
||||
private static void GenerateAttribute(IncrementalGeneratorPostInitializationContext context)
|
||||
{
|
||||
var builder = SyntaxBuilder.CreateCompilationUnit()
|
||||
.WithNamespace(
|
||||
SyntaxBuilder.CreateNamespace(Constants.Namespace)
|
||||
.WithClass(SyntaxBuilder.CreateClass(attributeName)
|
||||
.WithNamespace(SyntaxBuilder.CreateNamespace(Constants.Namespace)
|
||||
.WithClass(SyntaxBuilder.CreateClass(Constants.RouteAttributeName)
|
||||
.WithBaseClass(nameof(Attribute))
|
||||
.WithModifiers($"{Constants.Public} {Constants.Sealed}")
|
||||
.WithAttribute(SyntaxBuilder.CreateAttribute("AttributeUsage")
|
||||
.WithArgument(AttributeTargets.Class)
|
||||
.WithArgument("Inherited", false)
|
||||
.WithArgument("AllowMultiple", false))
|
||||
.WithProperty(SyntaxBuilder.CreateProperty(Constants.StringType, Constants.PatternPropertyName)
|
||||
.WithModifier(Constants.Public)
|
||||
.WithConstructor(SyntaxBuilder.CreateConstructor(attributeName)
|
||||
.WithModifier(Constants.Public))
|
||||
.WithAttribute(SyntaxBuilder.CreateAttribute("AttributeUsage")
|
||||
.WithArgument(AttributeTargets.Class)
|
||||
.WithArgument("Inherited", false)
|
||||
.WithArgument("AllowMultiple", false))
|
||||
.WithBaseClass(nameof(Attribute))
|
||||
.WithProperty(SyntaxBuilder.CreateProperty(Constants.StringType, Constants.PatternPropertyName)
|
||||
.WithModifier(Constants.Public)
|
||||
.WithAccessor(SyntaxBuilder.CreateGetter())
|
||||
.WithAccessor(SyntaxBuilder.CreateSetter()))));
|
||||
.WithAccessor(SyntaxBuilder.CreateGetter())
|
||||
.WithAccessor(SyntaxBuilder.CreateSetter()))));
|
||||
|
||||
var syntax = builder.Build();
|
||||
var source = syntax.ToFullString();
|
||||
context.AddSource($"{attributeName}.g", source);
|
||||
context.AddSource($"{Constants.RouteAttributeName}.g", source);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Sybil;
|
||||
|
||||
namespace Net.Sdk.Web.Extensions.SourceGenerators;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
#nullable enable
|
||||
public class RouteInterfaceGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterPostInitializationOutput(context =>
|
||||
{
|
||||
GenerateInterface(context);
|
||||
});
|
||||
}
|
||||
|
||||
private static void GenerateInterface(IncrementalGeneratorPostInitializationContext context)
|
||||
{
|
||||
var builder = SyntaxBuilder.CreateCompilationUnit()
|
||||
.WithNamespace(SyntaxBuilder.CreateNamespace(Constants.Namespace)
|
||||
.WithInterface(SyntaxBuilder.CreateInterface(Constants.RouteInterface)
|
||||
.WithTypeParameter(SyntaxBuilder.CreateTypeParameter(Constants.RouteTypeParameter))
|
||||
.WithTypeParameterConstraint(SyntaxBuilder.CreateTypeParameterConstraint(Constants.RouteTypeParameter)
|
||||
.WithClass())
|
||||
.WithMethod(SyntaxBuilder.CreateMethod(Constants.PreProcessRequestReturnType, Constants.PreProcessRequestMethodName)
|
||||
.WithModifier(Constants.Public)
|
||||
.WithParameter(Constants.HttpContextTypeName, Constants.HttpContextParameterName)
|
||||
.WithParameter(Constants.CancellationTokenTypeName, Constants.CancellationTokenParameterName)
|
||||
.WithNoBody())
|
||||
.WithMethod(SyntaxBuilder.CreateMethod(Constants.HandleRequestReturnType, Constants.HandleRequestMethodName)
|
||||
.WithModifier(Constants.Public)
|
||||
.WithParameter($"{Constants.RouteTypeParameter}?", Constants.RequestParameterName)
|
||||
.WithParameter(Constants.CancellationTokenTypeName, Constants.CancellationTokenParameterName)
|
||||
.WithNoBody())));
|
||||
|
||||
var syntax = builder.Build();
|
||||
var source = syntax.ToFullString();
|
||||
context.AddSource($"{Constants.RouteFileName}.g", source);
|
||||
}
|
||||
}
|
||||
|
||||
#nullable disable
|
||||
@@ -0,0 +1,323 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Sybil;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Net.Sdk.Web.Extensions.SourceGenerators;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
#nullable enable
|
||||
public class UseRoutesGenerator : 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);
|
||||
var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect());
|
||||
context.RegisterSourceOutput(compilationAndClasses, (sourceProductionContext, tuple) => Execute(tuple.Left, tuple.Right, sourceProductionContext));
|
||||
}
|
||||
|
||||
private static (ClassDeclarationSyntax, List<MethodDeclarationSyntax>)? GetFilteredClassDeclarationSyntax(GeneratorSyntaxContext context)
|
||||
{
|
||||
var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node;
|
||||
if (classDeclarationSyntax.AttributeLists
|
||||
.SelectMany(l => l.Attributes)
|
||||
.OfType<AttributeSyntax>()
|
||||
.Any(s => s.Name.ToString() is Constants.RouteAttributeName or Constants.RouteAttributeShortName))
|
||||
{
|
||||
return (
|
||||
classDeclarationSyntax,
|
||||
classDeclarationSyntax.Members
|
||||
.OfType<MethodDeclarationSyntax>()
|
||||
.Where(m => m.AttributeLists
|
||||
.SelectMany(l => l.Attributes)
|
||||
.OfType<AttributeSyntax>()
|
||||
.Any(s => s.Name.ToString() is Constants.GetAttributeName
|
||||
or Constants.GetAttributeShortName
|
||||
or Constants.PostAttributeName
|
||||
or Constants.PostAttributeShortName
|
||||
or Constants.PutAttributeName
|
||||
or Constants.PutAttributeShortName
|
||||
or Constants.DeleteAttributeName
|
||||
or Constants.DeleteAttributeShortName))
|
||||
.ToList());
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void Execute(Compilation compilation, ImmutableArray<(ClassDeclarationSyntax, List<MethodDeclarationSyntax>)?> classToMethodMapping, SourceProductionContext sourceProductionContext)
|
||||
{
|
||||
if (classToMethodMapping.IsDefaultOrEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var maybeLanguageVersion = (compilation.SyntaxTrees.FirstOrDefault()?.Options as CSharpParseOptions)?.LanguageVersion;
|
||||
if (!maybeLanguageVersion.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var languageVersion = maybeLanguageVersion.Value;
|
||||
var builder = SyntaxBuilder.CreateCompilationUnit()
|
||||
.WithUsing(Constants.UsingSystemThreading)
|
||||
.WithUsing(Constants.UsingMicrosoftAspNetCoreRouting)
|
||||
.WithUsing(Constants.UsingMicrosoftAspNetCoreHttp)
|
||||
.WithUsing(Constants.UsingMicrosoftAspNetCoreBuilder)
|
||||
.WithUsing(Constants.UsingMicrosoftAspNetCoreMvc);
|
||||
|
||||
var routeUsings = new HashSet<string>();
|
||||
|
||||
var namespaceBuilder = languageVersion >= LanguageVersion.CSharp10 ? SyntaxBuilder.CreateFileScopedNamespace(Constants.Namespace) : SyntaxBuilder.CreateNamespace(Constants.Namespace);
|
||||
builder.WithNamespace(namespaceBuilder);
|
||||
var webAppBuilder = SyntaxBuilder.CreateClass(Constants.WebApplicationExtensionsName)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Static}");
|
||||
namespaceBuilder.WithClass(webAppBuilder);
|
||||
var useRoutesWebAppMethodBuilder = SyntaxBuilder.CreateMethod(Constants.WebApplicationTypeName, Constants.UseRoutesMethodName)
|
||||
.WithThisParameter(Constants.WebApplicationTypeName, Constants.BuilderParameterName)
|
||||
.WithModifiers($"{Constants.Public} {Constants.Static}");
|
||||
webAppBuilder.WithMethod(useRoutesWebAppMethodBuilder);
|
||||
|
||||
var useRoutesWebAppBody = new StringBuilder();
|
||||
foreach (var classToMethodMap in classToMethodMapping)
|
||||
{
|
||||
if (classToMethodMap is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var classDeclarationSyntax = classToMethodMap.Value.Item1;
|
||||
var methodDeclarationSyntaxes = classToMethodMap.Value.Item2;
|
||||
if (methodDeclarationSyntaxes is null ||
|
||||
methodDeclarationSyntaxes.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GetParentOfType<BaseNamespaceDeclarationSyntax>(classDeclarationSyntax) is BaseNamespaceDeclarationSyntax baseNamespaceDeclarationSyntax)
|
||||
{
|
||||
routeUsings.Add(baseNamespaceDeclarationSyntax.Name.ToString());
|
||||
foreach(var u in baseNamespaceDeclarationSyntax.Usings)
|
||||
{
|
||||
routeUsings.Add(u.NamespaceOrType.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
if (GetParentOfType<CompilationUnitSyntax>(classDeclarationSyntax) is CompilationUnitSyntax parentUnitSyntax)
|
||||
{
|
||||
foreach (var u in parentUnitSyntax.Usings)
|
||||
{
|
||||
routeUsings.Add(u.NamespaceOrType.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
var classAttributes = classDeclarationSyntax.AttributeLists
|
||||
.SelectMany(l => l.Attributes)
|
||||
.OfType<AttributeSyntax>().ToList();
|
||||
var basePattern = classAttributes.FirstOrDefault(a => a.Name.ToString() == Constants.RouteAttributeName || a.Name.ToString() == Constants.RouteAttributeShortName)?
|
||||
.ArgumentList?.Arguments.OfType<AttributeArgumentSyntax>()
|
||||
.FirstOrDefault(a => a.NameEquals?.Name.Identifier.Text == Constants.PatternPropertyName)?.Expression.ToString().Trim('"');
|
||||
basePattern ??= string.Empty;
|
||||
foreach(var methodDeclarationSyntax in methodDeclarationSyntaxes)
|
||||
{
|
||||
var methodAttributes = methodDeclarationSyntax.AttributeLists
|
||||
.SelectMany(l => l.Attributes)
|
||||
.OfType<AttributeSyntax>().ToList();
|
||||
var pattern = methodAttributes.FirstOrDefault(a => a.Name.ToString() is Constants.GetAttributeName
|
||||
or Constants.GetAttributeShortName
|
||||
or Constants.PostAttributeName
|
||||
or Constants.PostAttributeShortName
|
||||
or Constants.PutAttributeName
|
||||
or Constants.PutAttributeShortName
|
||||
or Constants.DeleteAttributeName
|
||||
or Constants.DeleteAttributeShortName)
|
||||
.ArgumentList?.Arguments
|
||||
.OfType<AttributeArgumentSyntax>()
|
||||
.FirstOrDefault(a => a.NameEquals?.Name.Identifier.Text == Constants.PatternPropertyName)
|
||||
?.Expression.ToString().Trim('"');
|
||||
|
||||
if (string.IsNullOrWhiteSpace(basePattern) &&
|
||||
string.IsNullOrWhiteSpace(pattern))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var outerPattern = $"/{basePattern.Trim('/')}";
|
||||
var innerPattern = $"/{pattern?.Trim('/')}";
|
||||
var finalPattern = $"{outerPattern}{innerPattern}".Replace("//", "/");
|
||||
|
||||
if (methodAttributes.FirstOrDefault(a => a.Name.ToString() is Constants.GetAttributeName or Constants.GetAttributeShortName) is AttributeSyntax &&
|
||||
GetMethodBodyByType("Get", finalPattern, classDeclarationSyntax, methodDeclarationSyntax, compilation, routeUsings) is string getMethodBody)
|
||||
{
|
||||
useRoutesWebAppBody.AppendLine(getMethodBody);
|
||||
}
|
||||
else if (methodAttributes.FirstOrDefault(a => a.Name.ToString() is Constants.PostAttributeName or Constants.PostAttributeShortName) is AttributeSyntax &&
|
||||
GetMethodBodyByType("Post", finalPattern, classDeclarationSyntax, methodDeclarationSyntax, compilation, routeUsings) is string postMethodBody)
|
||||
{
|
||||
useRoutesWebAppBody.AppendLine(postMethodBody);
|
||||
}
|
||||
else if (methodAttributes.FirstOrDefault(a => a.Name.ToString() is Constants.PutAttributeName or Constants.PutAttributeShortName) is AttributeSyntax &&
|
||||
GetMethodBodyByType("Put", finalPattern, classDeclarationSyntax, methodDeclarationSyntax, compilation, routeUsings) is string putMethodBoty)
|
||||
{
|
||||
useRoutesWebAppBody.AppendLine(putMethodBoty);
|
||||
}
|
||||
else if (methodAttributes.FirstOrDefault(a => a.Name.ToString() is Constants.DeleteAttributeName or Constants.DeleteAttributeShortName) is AttributeSyntax &&
|
||||
GetMethodBodyByType("Delete", finalPattern, classDeclarationSyntax, methodDeclarationSyntax, compilation, routeUsings) is string deleteMethodBody)
|
||||
{
|
||||
useRoutesWebAppBody.AppendLine(deleteMethodBody);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useRoutesWebAppBody.AppendLine("return builder;");
|
||||
useRoutesWebAppMethodBuilder.WithBody(useRoutesWebAppBody.ToString());
|
||||
foreach (var classUsing in routeUsings)
|
||||
{
|
||||
builder.WithUsing(classUsing);
|
||||
}
|
||||
|
||||
var fileSource = builder.Build().ToFullString();
|
||||
sourceProductionContext.AddSource($"{Constants.EndpointRouteBuilderExtensionsName}.g", fileSource);
|
||||
}
|
||||
|
||||
private static string? GetMethodBodyByType(
|
||||
string type,
|
||||
string pattern,
|
||||
ClassDeclarationSyntax classDeclarationSyntax,
|
||||
MethodDeclarationSyntax methodDeclarationSyntax,
|
||||
Compilation compilation,
|
||||
HashSet<string> usings)
|
||||
{
|
||||
// Go over all attributes on the method and parent class and find all RouteFilters
|
||||
var routeFilterSb = new StringBuilder();
|
||||
foreach(var attribute in methodDeclarationSyntax.AttributeLists
|
||||
.Concat(classDeclarationSyntax.AttributeLists)
|
||||
.SelectMany(a => a.Attributes)
|
||||
.OfType<AttributeSyntax>()
|
||||
.Where(a => a.Name.ToString() == Constants.RouteFilterAttributeName || a.Name.ToString() == Constants.RouteFilterAttributeShortName))
|
||||
{
|
||||
var semanticModel = compilation.GetSemanticModel(methodDeclarationSyntax.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}>()");
|
||||
}
|
||||
|
||||
// Get the method signature and generate it in Map
|
||||
var parameters = GetMethodParameters(methodDeclarationSyntax);
|
||||
var variables = string.Join(", ", methodDeclarationSyntax.ParameterList.Parameters
|
||||
.Select(param => param.Identifier.Text));
|
||||
|
||||
return @$"
|
||||
builder.Map{type}(""{pattern}"", async ({classDeclarationSyntax.Identifier} route{(parameters.Length > 0 ? $", {parameters}" : "")}) =>
|
||||
{{
|
||||
return await route.{methodDeclarationSyntax.Identifier}({variables});
|
||||
}}){routeFilterSb};";
|
||||
}
|
||||
|
||||
private static string GetMethodParameters(MethodDeclarationSyntax methodDeclaration)
|
||||
{
|
||||
var parameters = methodDeclaration.ParameterList.Parameters
|
||||
.Select(param =>
|
||||
{
|
||||
var parameterType = param.Type?.ToString();
|
||||
var parameterName = param.Identifier.Text;
|
||||
var attributes = param.AttributeLists
|
||||
.SelectMany(attrList => attrList.Attributes)
|
||||
.Select(attr => $"[{attr.Name}]");
|
||||
|
||||
var attributesString = string.Join(" ", attributes);
|
||||
return $"{attributesString} {parameterType} {parameterName}".Trim();
|
||||
});
|
||||
|
||||
var parametersString = string.Join(", ", parameters);
|
||||
return parametersString;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (syntaxNode.Parent is T parentNode)
|
||||
{
|
||||
return parentNode;
|
||||
}
|
||||
|
||||
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
|
||||
@@ -28,10 +28,7 @@ public class WithRoutesGenerator : IIncrementalGenerator
|
||||
if (classDeclarationSyntax.AttributeLists
|
||||
.SelectMany(l => l.Attributes)
|
||||
.OfType<AttributeSyntax>()
|
||||
.Any(s => s.Name.ToString() == Constants.GetAttributeName || s.Name.ToString() == Constants.GetAttributeShortName ||
|
||||
s.Name.ToString() == Constants.PostAttributeName || s.Name.ToString() == Constants.PostAttributeShortName ||
|
||||
s.Name.ToString() == Constants.PutAttributeName || s.Name.ToString() == Constants.PutAttributeShortName ||
|
||||
s.Name.ToString() == Constants.DeleteAttributeName || s.Name.ToString() == Constants.DeleteAttributeShortName))
|
||||
.Any(s => s.Name.ToString() == Constants.RouteAttributeName || s.Name.ToString() == Constants.RouteAttributeShortName))
|
||||
{
|
||||
return classDeclarationSyntax;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user