diff --git a/Net.Sdk.Web.Extensions.SourceGenerators/Constants.cs b/Net.Sdk.Web.Extensions.SourceGenerators/Constants.cs
index 2ae0288..e132e90 100644
--- a/Net.Sdk.Web.Extensions.SourceGenerators/Constants.cs
+++ b/Net.Sdk.Web.Extensions.SourceGenerators/Constants.cs
@@ -12,6 +12,9 @@ public static class Constants
public const string IntType = "int";
public const string ObjectType = "object";
public const string Default = "default";
+ public const string Typeof = "typeof";
+ public const string Readonly = "readonly";
+ public const string Async = "async";
public const string Namespace = "Net.Sdk.Web";
public const string UsingSystemThreading = "System.Threading";
public const string UsingSystemRuntimeCompilerServices = "System.Runtime.CompilerServices";
@@ -81,9 +84,38 @@ public static class Constants
public const string RouteFilterAttributeShortName = "RouteFilter";
public const string RouteFilterTypePropertyName = "RouteFilterType";
+ public const string DeferredResultClass = "DeferredResult";
+ public const string IResultInterface = "IResult";
+ public const string Task = "Task";
+ public const string ExecuteAsyncMethodName = "ExecuteAsync";
+
public const string T = "T";
public const string IEndpointFilterName = "IEndpointFilter";
public const string MethodImplAttribute = "MethodImpl";
public const string MethodImplArgument = "MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization | MethodImplOptions.PreserveSig";
+
+ public const string ProducesResponseTypeType = "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute";
+ public const string OpenApiConvetionBuilderExtensionsType = "Microsoft.AspNetCore.Builder.OpenApiEndpointConventionBuilderExtensions";
+
+ public const string EndpointNameAttributeName = "EndpointNameAttribute";
+ public const string EndpointNameAttributeShortName = "EndpointName";
+ public const string WithNameExtension = "WithName";
+ public const string EndpointSummaryAttributeName = "EndpointSummaryAttribute";
+ public const string EndpointSummaryAttributeShortName = "EndpointSummary";
+ public const string WithSummaryExtension = "WithSummary";
+ public const string EndpointDescriptionAttributeName = "EndpointDescriptionAttribute";
+ public const string EndpointDescriptionAttributeShortName = "EndpointDescription";
+ public const string WithDescriptionExtension = "WithDescription";
+ public const string ProducesAttributeName = "ProducesAttribute";
+ public const string ProducesAttributeShortName = "Produces";
+ public const string ProducesResponseTypeAttributeName = "ProducesResponseTypeAttribute";
+ public const string ProducesResponseTypeAttributeShortName = "ProducesResponseType";
+ public const string AuthorizeAttributeName = "AuthorizeAttribute";
+ public const string AuthorizeAttributeShortName = "Authorize";
+
+ public const string RequiresAuthorization = "RequireAuthorization";
+ public const string WithOpenApiExtension = "WithOpenApi";
+
+ public const string Default200Status = "StatusCodes.Status200OK";
}
diff --git a/Net.Sdk.Web.Extensions.SourceGenerators/DeferredResultGenerator.cs b/Net.Sdk.Web.Extensions.SourceGenerators/DeferredResultGenerator.cs
new file mode 100644
index 0000000..b2a41fa
--- /dev/null
+++ b/Net.Sdk.Web.Extensions.SourceGenerators/DeferredResultGenerator.cs
@@ -0,0 +1,48 @@
+using Microsoft.CodeAnalysis;
+using Sybil;
+using System;
+
+namespace Net.Sdk.Web.Extensions.SourceGenerators;
+
+[Generator(LanguageNames.CSharp)]
+#nullable enable
+public class DeferredResultGenerator : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ context.RegisterPostInitializationOutput(GenerateRouteFilterAttribute);
+ }
+
+ private static void GenerateRouteFilterAttribute(IncrementalGeneratorPostInitializationContext context)
+ {
+ var builder = SyntaxBuilder.CreateCompilationUnit()
+ .WithNamespace(
+ SyntaxBuilder.CreateNamespace(Constants.Namespace)
+ .WithUsing(Constants.UsingSystemThreading)
+ .WithUsing(Constants.UsingMicrosoftAspNetCoreHttp)
+ .WithClass(SyntaxBuilder.CreateClass(Constants.DeferredResultClass)
+ .WithBaseClass(Constants.IResultTypeName)
+ .WithModifier(Constants.Public)
+ .WithModifier(Constants.Sealed)
+ .WithField(SyntaxBuilder.CreateField($"Task<{Constants.IResultInterface}>", "inner")
+ .WithModifier(Constants.Public)
+ .WithModifier(Constants.Readonly))
+ .WithConstructor(SyntaxBuilder.CreateConstructor(Constants.DeferredResultClass)
+ .WithModifier(Constants.Public)
+ .WithParameter($"Task<{Constants.IResultInterface}>", "inner")
+ .WithBody("this.inner = inner;"))
+ .WithMethod(SyntaxBuilder.CreateMethod(Constants.Task, Constants.ExecuteAsyncMethodName)
+ .WithParameter("HttpContext", "httpContext")
+ .WithModifier(Constants.Public)
+ .WithModifier(Constants.Async)
+ .WithBody(@$"
+ var result = await this.inner;
+ await result.ExecuteAsync(httpContext);"))));
+
+ var syntax = builder.Build();
+ var source = syntax.ToFullString();
+ context.AddSource($"{Constants.DeferredResultClass}.g", $"{source}");
+ }
+}
+
+#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 1e04d7d..4cd9dc8 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.9.0
+ 0.9.1
latest
true
Alexandru Macocian
@@ -13,6 +13,7 @@
true
true
true
+ Enable
diff --git a/Net.Sdk.Web.Extensions.SourceGenerators/UseRoutesGenerator.cs b/Net.Sdk.Web.Extensions.SourceGenerators/UseRoutesGenerator.cs
index 782645e..f7a6213 100644
--- a/Net.Sdk.Web.Extensions.SourceGenerators/UseRoutesGenerator.cs
+++ b/Net.Sdk.Web.Extensions.SourceGenerators/UseRoutesGenerator.cs
@@ -2,8 +2,10 @@
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;
@@ -64,6 +66,13 @@ public class UseRoutesGenerator : IIncrementalGenerator
return;
}
+ var openApiSupport = false;
+ if (compilation.GetTypeByMetadataName(Constants.ProducesResponseTypeType) is not null &&
+ compilation.GetTypeByMetadataName(Constants.OpenApiConvetionBuilderExtensionsType) is not null)
+ {
+ openApiSupport = true;
+ }
+
var languageVersion = maybeLanguageVersion.Value;
var routeUsings = new HashSet
{
@@ -83,9 +92,7 @@ public class UseRoutesGenerator : IIncrementalGenerator
namespaceBuilder.WithClass(webAppBuilder);
var useRoutesWebAppMethodBuilder = SyntaxBuilder.CreateMethod(Constants.WebApplicationTypeName, Constants.UseRoutesMethodName)
.WithThisParameter(Constants.WebApplicationTypeName, Constants.BuilderParameterName)
- .WithModifiers($"{Constants.Public} {Constants.Static}")
- .WithAttribute(SyntaxBuilder.CreateAttribute(Constants.MethodImplAttribute)
- .WithRawArgument(Constants.MethodImplArgument));
+ .WithModifiers($"{Constants.Public} {Constants.Static}");
webAppBuilder.WithMethod(useRoutesWebAppMethodBuilder);
var useRoutesWebAppBody = new StringBuilder();
@@ -157,22 +164,22 @@ public class UseRoutesGenerator : IIncrementalGenerator
var finalPattern = $"{outerPattern}{innerPattern}".Replace("//", "/");
if (methodAttributes.FirstOrDefault(a => a.Name.ToString() is Constants.GetAttributeName or Constants.GetAttributeShortName) is not null &&
- GetMethodBodyByType("Get", finalPattern, classDeclarationSyntax, methodDeclarationSyntax, compilation, routeUsings) is string getMethodBody)
+ GetMethodBodyByType("Get", finalPattern, classDeclarationSyntax, methodDeclarationSyntax, compilation, routeUsings, openApiSupport) is string getMethodBody)
{
useRoutesWebAppBody.AppendLine(getMethodBody);
}
else if (methodAttributes.FirstOrDefault(a => a.Name.ToString() is Constants.PostAttributeName or Constants.PostAttributeShortName) is not null &&
- GetMethodBodyByType("Post", finalPattern, classDeclarationSyntax, methodDeclarationSyntax, compilation, routeUsings) is string postMethodBody)
+ GetMethodBodyByType("Post", finalPattern, classDeclarationSyntax, methodDeclarationSyntax, compilation, routeUsings, openApiSupport) is string postMethodBody)
{
useRoutesWebAppBody.AppendLine(postMethodBody);
}
else if (methodAttributes.FirstOrDefault(a => a.Name.ToString() is Constants.PutAttributeName or Constants.PutAttributeShortName) is not null &&
- GetMethodBodyByType("Put", finalPattern, classDeclarationSyntax, methodDeclarationSyntax, compilation, routeUsings) is string putMethodBoty)
+ GetMethodBodyByType("Put", finalPattern, classDeclarationSyntax, methodDeclarationSyntax, compilation, routeUsings, openApiSupport) is string putMethodBoty)
{
useRoutesWebAppBody.AppendLine(putMethodBoty);
}
else if (methodAttributes.FirstOrDefault(a => a.Name.ToString() is Constants.DeleteAttributeName or Constants.DeleteAttributeShortName) is not null &&
- GetMethodBodyByType("Delete", finalPattern, classDeclarationSyntax, methodDeclarationSyntax, compilation, routeUsings) is string deleteMethodBody)
+ GetMethodBodyByType("Delete", finalPattern, classDeclarationSyntax, methodDeclarationSyntax, compilation, routeUsings, openApiSupport) is string deleteMethodBody)
{
useRoutesWebAppBody.AppendLine(deleteMethodBody);
}
@@ -196,54 +203,55 @@ public class UseRoutesGenerator : IIncrementalGenerator
ClassDeclarationSyntax classDeclarationSyntax,
MethodDeclarationSyntax methodDeclarationSyntax,
Compilation compilation,
- HashSet usings)
+ HashSet usings,
+ bool openApiSupport)
{
// Go over all attributes on the method and parent class and find all RouteFilters
var semanticModel = compilation.GetSemanticModel(methodDeclarationSyntax.SyntaxTree);
- var routeFilterSb = new StringBuilder();
- foreach(var attribute in methodDeclarationSyntax.AttributeLists
+ var extensionsBuilder = new StringBuilder();
+
+ foreach (var attribute in methodDeclarationSyntax.AttributeLists
.Concat(classDeclarationSyntax.AttributeLists)
.SelectMany(a => a.Attributes)
- .OfType()
- .Where(attribute =>
- {
- var attributeSymbol = semanticModel.GetSymbolInfo(attribute).Symbol as IMethodSymbol;
- if (attributeSymbol?.ContainingType == null)
- {
- return false;
- }
-
- var attributeType = attributeSymbol.ContainingType;
- if (attributeType.Name is not Constants.RouteFilterAttributeName or Constants.RouteFilterAttributeShortName)
- {
- return false;
- }
-
- return true;
- }))
+ .OfType())
{
- if (attribute.Name is not GenericNameSyntax genericNameSyntax || genericNameSyntax.TypeArgumentList.Arguments.Count != 1)
+ var attributeSymbol = semanticModel.GetSymbolInfo(attribute).Symbol as IMethodSymbol;
+ AddRouteFilters(semanticModel, attribute, attributeSymbol, extensionsBuilder, usings);
+ if (openApiSupport)
{
- continue;
- }
+ AddAttributeExtension(
+ attribute,
+ attributeSymbol,
+ extensionsBuilder,
+ Constants.EndpointNameAttributeName,
+ Constants.EndpointNameAttributeShortName,
+ Constants.WithNameExtension);
- var routeFilterTypeSyntax = genericNameSyntax.TypeArgumentList.Arguments[0];
- if (semanticModel.GetSymbolInfo(routeFilterTypeSyntax).Symbol is not ITypeSymbol routeFilterTypeSymbol)
- {
- continue;
- }
+ AddAttributeExtension(
+ attribute,
+ attributeSymbol,
+ extensionsBuilder,
+ Constants.EndpointSummaryAttributeName,
+ Constants.EndpointSummaryAttributeShortName,
+ Constants.WithSummaryExtension);
- var routeFilterType = routeFilterTypeSymbol.ToDisplayString();
- var namespaceIndex = routeFilterType.LastIndexOf(".");
- if (namespaceIndex > 0)
- {
- var routeFilterNamespace = routeFilterType.Substring(0, namespaceIndex);
- routeFilterType = routeFilterType.Substring(namespaceIndex + 1);
- usings.Add(routeFilterNamespace);
- }
+ AddAttributeExtension(
+ attribute,
+ attributeSymbol,
+ extensionsBuilder,
+ Constants.EndpointDescriptionAttributeName,
+ Constants.EndpointDescriptionAttributeShortName,
+ Constants.WithDescriptionExtension);
- routeFilterSb.Append('\n')
- .Append(@$".AddEndpointFilter<{routeFilterType}>()");
+ AddAuthorizeExtension(attribute, attributeSymbol, extensionsBuilder);
+ }
+ }
+
+ if (openApiSupport)
+ {
+ var methodSymbol = semanticModel.GetDeclaredSymbol(methodDeclarationSyntax);
+ AddProducesResponseTypeExtension(methodSymbol, extensionsBuilder);
+ extensionsBuilder.AppendLine($"\n.{Constants.WithOpenApiExtension}()");
}
/*
@@ -255,32 +263,143 @@ public class UseRoutesGenerator : IIncrementalGenerator
var variables = string.Join(", ", methodDeclarationSyntax.ParameterList.Parameters
.Select(param => param.Identifier.Text));
+ var newMethodName = $"{classDeclarationSyntax.Identifier}{methodDeclarationSyntax.Identifier}";
/*
* Generate an async lambda for Task or a synchronous lambda for IResult returns. If neither are matched, do not generate any method
*/
var returnTypeSymbol = semanticModel.GetSymbolInfo(methodDeclarationSyntax.ReturnType).Symbol as ITypeSymbol;
- return returnTypeSymbol?.ToDisplayString() switch
+ var returnType = returnTypeSymbol?.ToDisplayString();
+ var isAsync = returnType switch
{
- "System.Threading.Tasks.Task" => @$"
- builder.Map{type}(""{pattern}"", async (HttpContext httpContext, {classDeclarationSyntax.Identifier} route{(parameters.Length > 0 ? $", {parameters}" : "")}) =>
- {{
- var cancellationToken = httpContext.RequestAborted;
- return await route.{methodDeclarationSyntax.Identifier}({variables});
- }}){routeFilterSb};",
- "Microsoft.AspNetCore.Http.IResult" => @$"
- builder.Map{type}(""{pattern}"", (HttpContext httpContext, {classDeclarationSyntax.Identifier} route{(parameters.Length > 0 ? $", {parameters}" : "")}) =>
- {{
- var cancellationToken = httpContext.RequestAborted;
- return route.{methodDeclarationSyntax.Identifier}({variables});
- }}){routeFilterSb};",
- "System.Threading.Tasks.ValueTask" => @$"
- builder.Map{type}(""{pattern}"", (HttpContext httpContext, {classDeclarationSyntax.Identifier} route{(parameters.Length > 0 ? $", {parameters}" : "")}) =>
- {{
- var cancellationToken = httpContext.RequestAborted;
- return route.{methodDeclarationSyntax.Identifier}({variables});
- }}){routeFilterSb};",
+ "System.Threading.Tasks.Task" => (bool?)true,
+ "System.Threading.Tasks.ValueTask" => (bool?)true,
+ "Microsoft.AspNetCore.Http.IResult" => (bool?)false,
_ => default
};
+
+ if (isAsync is null)
+ {
+ return default;
+ }
+
+ var lambdaBody = @$"
+ builder.Map{type}(""{pattern}"", static (HttpContext httpContext, {classDeclarationSyntax.Identifier} route{(parameters.Length > 0 ? $", {parameters}" : "")}) =>
+ {{
+ var cancellationToken = httpContext.RequestAborted;
+ return {(isAsync is true ? $"new DeferredResult(route.{methodDeclarationSyntax.Identifier}({variables}))" : $"route.{methodDeclarationSyntax.Identifier}({variables})")};
+ }})
+ {extensionsBuilder};";
+ return lambdaBody;
+ }
+
+ private static void AddProducesResponseTypeExtension(
+ IMethodSymbol? methodSymbol,
+ StringBuilder extensionsBuilder)
+ {
+ foreach (var attributeData in methodSymbol?.GetAttributes() ?? [])
+ {
+ var attrClassName = attributeData.AttributeClass?.Name;
+ if (attrClassName is not Constants.ProducesResponseTypeAttributeName or Constants.ProducesResponseTypeAttributeShortName)
+ {
+ continue;
+ }
+
+ var typeArg = (attributeData.NamedArguments.FirstOrDefault(na => na.Key == "Type").Value.Value as ITypeSymbol)?.ToString();
+ if (attributeData.ApplicationSyntaxReference?.GetSyntax() is AttributeSyntax attributeSyntax &&
+ attributeSyntax.Name is GenericNameSyntax genericNameSyntax &&
+ genericNameSyntax.TypeArgumentList.Arguments.Count == 1)
+ {
+ typeArg = genericNameSyntax.TypeArgumentList.Arguments[0].ToString();
+ }
+
+ var statusCodeArg = attributeData.NamedArguments.FirstOrDefault(na => na.Key == "StatusCode").Value.Value as int? ?? 200;
+ if (typeArg is null)
+ {
+ extensionsBuilder.Append($"\n.{Constants.ProducesAttributeShortName}(statusCode: {statusCodeArg})");
+ }
+ else
+ {
+ extensionsBuilder.Append($"\n.{Constants.ProducesAttributeShortName}(statusCode: {statusCodeArg}, responseType: typeof({typeArg}))");
+ }
+ }
+ }
+
+ private static void AddAuthorizeExtension(
+ AttributeSyntax attributeSyntax,
+ IMethodSymbol? attributeSymbol,
+ StringBuilder extensionsBuilder)
+ {
+ if (attributeSymbol?.ContainingType is not INamedTypeSymbol attributeType ||
+ attributeType.Name is not Constants.AuthorizeAttributeName or Constants.AuthorizeAttributeShortName)
+ {
+ return;
+ }
+
+ if (attributeSyntax.ArgumentList?.Arguments.Count > 0)
+ {
+ var policy = attributeSyntax.ArgumentList.Arguments[0].ToString().Trim('"');
+ extensionsBuilder.Append($"\n.{Constants.RequiresAuthorization}(\"{policy}\")");
+ }
+ else
+ {
+ extensionsBuilder.Append($"\n.{Constants.RequiresAuthorization}()");
+ }
+ }
+
+ private static void AddAttributeExtension(
+ AttributeSyntax attributeSyntax,
+ IMethodSymbol? attributeSymbol,
+ StringBuilder extensionsBuilder,
+ string attributeName,
+ string attributeShortName,
+ string extensionMethod)
+ {
+ if (attributeSymbol?.ContainingType is not INamedTypeSymbol attributeType ||
+ (attributeType.Name != attributeName && attributeType.Name != attributeShortName))
+ {
+ return;
+ }
+
+ if (attributeSyntax.ArgumentList is null ||
+ attributeSyntax.ArgumentList.Arguments.Count <= 0)
+ {
+ return;
+ }
+
+ var name = attributeSyntax.ArgumentList.Arguments[0].ToString().Trim('"');
+ extensionsBuilder.Append($"\n.{extensionMethod}(\"{name}\")");
+ }
+
+ private static void AddRouteFilters(
+ SemanticModel semanticModel,
+ AttributeSyntax attributeSyntax,
+ IMethodSymbol? attributeSymbol,
+ StringBuilder extensionsBuilder,
+ HashSet usings)
+ {
+ if (attributeSymbol?.ContainingType is not INamedTypeSymbol attributeType ||
+ attributeType.Name is not Constants.RouteFilterAttributeName or Constants.RouteFilterAttributeShortName ||
+ attributeSyntax.Name is not GenericNameSyntax genericNameSyntax || genericNameSyntax.TypeArgumentList.Arguments.Count != 1)
+ {
+ return;
+ }
+
+ var routeFilterTypeSyntax = genericNameSyntax.TypeArgumentList.Arguments[0];
+ if (semanticModel.GetSymbolInfo(routeFilterTypeSyntax).Symbol is not ITypeSymbol routeFilterTypeSymbol)
+ {
+ return;
+ }
+
+ var routeFilterType = routeFilterTypeSymbol.ToDisplayString();
+ var namespaceIndex = routeFilterType.LastIndexOf(".");
+ if (namespaceIndex > 0)
+ {
+ var routeFilterNamespace = routeFilterType.Substring(0, namespaceIndex);
+ routeFilterType = routeFilterType.Substring(namespaceIndex + 1);
+ usings.Add(routeFilterNamespace);
+ }
+
+ extensionsBuilder.Append(@$"\n.AddEndpointFilter<{routeFilterType}>()");
}
private static string GetMethodParameters(MethodDeclarationSyntax methodDeclaration)
@@ -303,31 +422,6 @@ public class UseRoutesGenerator : IIncrementalGenerator
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(SyntaxNode syntaxNode)
{
if (syntaxNode.Parent is null)