diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml
index 69d4811..90b8fed 100644
--- a/.github/workflows/cd.yaml
+++ b/.github/workflows/cd.yaml
@@ -24,6 +24,7 @@ jobs:
Solution_Path: Net.Sdk.Web.Extensions.sln
Source_Project_Path: Net.Sdk.Web.Extensions\Net.Sdk.Web.Extensions.csproj
Azure_Project_path: Net.Sdk.Web.Extensions.Azure\Net.Sdk.Web.Extensions.Azure.csproj
+ SourceGenerators_Project_path: Net.Sdk.Web.Extensions.SourceGenerators\Net.Sdk.Web.Extensions.SourceGenerators.csproj
Actions_Allow_Unsecure_Commands: true
steps:
@@ -57,5 +58,11 @@ jobs:
- name: Package Net.Sdk.Extensions.Azure
run: dotnet pack -c Release -o . $env:Azure_Project_Path
+ - name: Build Net.Sdk.Extensions.SourceGenerators project
+ run: dotnet build $env:SourceGenerators_Project_path -c $env:Configuration
+
+ - name: Package Net.Sdk.Extensions.SourceGenerators
+ run: dotnet pack -c Release -o . $env:SourceGenerators_Project_path
+
- name: Publish
run: dotnet nuget push *.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
\ No newline at end of file
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 d4ca07c..3145016 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.7
+ 0.8
Alexandru Macocian
LICENSE
true
@@ -23,11 +23,11 @@
-
-
+
+
-
-
+
+
diff --git a/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Builder.cs b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Builder.cs
new file mode 100644
index 0000000..104ba4e
--- /dev/null
+++ b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Builder.cs
@@ -0,0 +1,10 @@
+namespace Net.Sdk.Web.Extensions.SourceGenerators.Tests;
+
+public static class Builder
+{
+ public static void Build()
+ {
+ EndpointRouteBuilder builder = new EndpointRouteBuilder();
+ builder.MapDeleteSimpleRoute();
+ }
+}
diff --git a/Net.Sdk.Web.Extensions.SourceGenerators.Tests/EndpointRouteBuilder.cs b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/EndpointRouteBuilder.cs
new file mode 100644
index 0000000..2da8714
--- /dev/null
+++ b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/EndpointRouteBuilder.cs
@@ -0,0 +1,13 @@
+
+namespace Net.Sdk.Web.Extensions.SourceGenerators.Tests;
+
+public class EndpointRouteBuilder : IEndpointRouteBuilder
+{
+ public IServiceProvider ServiceProvider { get; }
+ public ICollection DataSources { get; }
+
+ public IApplicationBuilder CreateApplicationBuilder()
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Models/SimpleRequest.cs b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Models/SimpleRequest.cs
new file mode 100644
index 0000000..59d905b
--- /dev/null
+++ b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Models/SimpleRequest.cs
@@ -0,0 +1,5 @@
+namespace Net.Sdk.Web.Extensions.SourceGenerators.Tests.Models;
+
+public class SimpleRequest
+{
+}
diff --git a/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Net.Sdk.Web.Extensions.SourceGenerators.Tests.csproj b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Net.Sdk.Web.Extensions.SourceGenerators.Tests.csproj
new file mode 100644
index 0000000..45aad04
--- /dev/null
+++ b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Net.Sdk.Web.Extensions.SourceGenerators.Tests.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net8.0
+ enable
+ enable
+ Library
+ latest
+ true
+ true
+ $(BaseIntermediateOutputPath)\Generated
+
+
+
+
+
+
+
+
diff --git a/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Routes/SimpleRoute.cs b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Routes/SimpleRoute.cs
new file mode 100644
index 0000000..b0ec87d
--- /dev/null
+++ b/Net.Sdk.Web.Extensions.SourceGenerators.Tests/Routes/SimpleRoute.cs
@@ -0,0 +1,17 @@
+using Net.Sdk.Web.Extensions.SourceGenerators.Tests.Models;
+
+namespace Net.Sdk.Web.Extensions.SourceGenerators.Tests.Routes;
+
+[GenerateMapDelete(Pattern = "simple")]
+public sealed class SimpleRoute : IRoute
+{
+ public Task HandleRequest(SimpleRequest? request, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task PreProcess(HttpContext? context, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/Net.Sdk.Web.Extensions.SourceGenerators/Constants.cs b/Net.Sdk.Web.Extensions.SourceGenerators/Constants.cs
new file mode 100644
index 0000000..da6d409
--- /dev/null
+++ b/Net.Sdk.Web.Extensions.SourceGenerators/Constants.cs
@@ -0,0 +1,75 @@
+namespace Net.Sdk.Web.Extensions.SourceGenerators;
+
+public static class Constants
+{
+ public const string Partial = "partial";
+ public const string Public = "public";
+ public const string Abstract = "abstract";
+ public const string Sealed = "sealed";
+ public const string Static = "static";
+ public const string StringType = "string";
+ public const string IntType = "int";
+ public const string ObjectType = "object";
+ public const string Default = "default";
+ public const string Namespace = "Net.Sdk.Web";
+ public const string UsingSystemThreading = "System.Threading";
+ public const string UsingMicrosoftAspNetCoreHttp = "Microsoft.AspNetCore.Http";
+ public const string UsingMicrosoftAspNetCoreRouting = "Microsoft.AspNetCore.Routing";
+ public const string UsingMicrosoftAspNetCoreBuilder = "Microsoft.AspNetCore.Builder";
+
+ public const string RouteFileName = "Route";
+ public const string RouteInterface = "IRoute";
+ public const string RouteTypeParameter = "TRequest";
+ public const string PreProcessRequestReturnType = "Task";
+ public const string PreProcessRequestMethodName = "PreProcess";
+ public const string HandleRequestReturnType = "Task";
+ public const string HandleRequestMethodName = "HandleRequest";
+ public const string HttpContextTypeName = "HttpContext?";
+ public const string HttpContextParameterName = "context";
+ public const string RequestParameterName = "request";
+
+ public const string ResponseTypeName = "Response";
+ public const string IResultTypeName = "IResult";
+ public const string SuccessResponseTypeName = "SuccessResponse";
+ public const string FailureResponseTypeName = "FailureResponse";
+ public const string ContentResponseTypeName = "ContentResponse";
+ public const string JsonResponseTypeName = "JsonResponse";
+ public const string StatusCodePropertyName = "StatusCode";
+ public const string ErrorMessagePropertyName = "ErrorMessage";
+ public const string ContentStringPropertyName = "ContentString";
+ public const string ValuePropertyName = "Value";
+ public const string ContentTypePropertyName = "ContentType";
+ public const string OkSuccessResponse = "Ok";
+ public const string CreatedSuccessResponse = "Created";
+ public const string ContentContentResponse = "Content";
+ public const string JsonJsonResponse = "Json";
+ public const string PageContentResponse = "Page";
+ public const string BadRequestFailureResponse = "BadRequest";
+ public const string UnauthorizedFailureResponse = "Unauthorized";
+ public const string ForbiddenFailureResponse = "Forbidden";
+ public const string NotFoundFailureResponse = "NotFound";
+ public const string ServerErrorFailureResponse = "ServerError";
+ public const string GetResult = "GetResult";
+ public const string ContentParameterName = "content";
+ public const string ContentTypeParameterName = "contentType";
+ public const string ValueParameterName = "value";
+ public const string ErrorMessageParameterName = "errorMessage";
+
+ public const string CancellationTokenTypeName = "CancellationToken";
+ public const string CancellationTokenParameterName = "cancellationToken";
+
+ public const string GetAttributeName = "GenerateMapGetAttribute";
+ public const string GetAttributeShortName = "GenerateMapGet";
+ public const string PostAttributeName = "GenerateMapPostAttribute";
+ public const string PostAttributeShortName = "GenerateMapPost";
+ public const string PutAttributeName = "GenerateMapPutAttribute";
+ public const string PutAttributeShortName = "GenerateMapPut";
+ public const string DeleteAttributeName = "GenerateMapDeleteAttribute";
+ public const string DeleteAttributeShortName = "GenerateMapDelete";
+
+ public const string PatternPropertyName = "Pattern";
+
+ public const string EndpointRouteBuilderExtensionsName = "EndpointRouteBuilderExtensions";
+ public const string IEndpointRouteBuilderTypeName = "IEndpointRouteBuilder";
+ public const string BuilderParameterName = "builder";
+}
diff --git a/Net.Sdk.Web.Extensions.SourceGenerators/MapEndpointGenerator.cs b/Net.Sdk.Web.Extensions.SourceGenerators/MapEndpointGenerator.cs
new file mode 100644
index 0000000..d03aed4
--- /dev/null
+++ b/Net.Sdk.Web.Extensions.SourceGenerators/MapEndpointGenerator.cs
@@ -0,0 +1,205 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Sybil;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+
+namespace Net.Sdk.Web.Extensions.SourceGenerators;
+
+[Generator(LanguageNames.CSharp)]
+#nullable enable
+public class MapEndpointGenerator : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ 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()
+ .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 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();
+
+ var namespaceBuilder = languageVersion >= LanguageVersion.CSharp10 ? SyntaxBuilder.CreateFileScopedNamespace(Constants.Namespace) : SyntaxBuilder.CreateNamespace(Constants.Namespace);
+ builder.WithNamespace(namespaceBuilder);
+
+ var extensionClassBuilder = SyntaxBuilder.CreateClass(Constants.EndpointRouteBuilderExtensionsName)
+ .WithModifiers($"{Constants.Public} {Constants.Static}");
+ namespaceBuilder.WithClass(extensionClassBuilder);
+
+ foreach(var classDeclarationSyntax in classes)
+ {
+ if (classDeclarationSyntax is null)
+ {
+ continue;
+ }
+
+ if (GetParentOfType(classDeclarationSyntax) is BaseNamespaceDeclarationSyntax baseNamespaceDeclarationSyntax)
+ {
+ routeUsings.Add(baseNamespaceDeclarationSyntax.Name.ToString());
+ }
+
+ var attributes = classDeclarationSyntax.AttributeLists
+ .SelectMany(l => l.Attributes)
+ .OfType().ToList();
+
+ 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)
+ {
+ continue;
+ }
+
+ 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)
+ {
+ continue;
+ }
+
+ 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)
+ {
+ continue;
+ }
+
+ 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)
+ {
+ continue;
+ }
+
+ 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);
+ }
+ }
+
+ foreach(var classUsing in routeUsings)
+ {
+ builder.WithUsing(classUsing);
+ }
+
+ var fileSource = builder.Build().ToFullString();
+ sourceProductionContext.AddSource($"{Constants.EndpointRouteBuilderExtensionsName}.g", fileSource);
+ }
+
+ private static T? GetParentOfType(SyntaxNode syntaxNode)
+ {
+ if (syntaxNode.Parent is null)
+ {
+ return default;
+ }
+
+ if (syntaxNode.Parent is T parentNode)
+ {
+ return parentNode;
+ }
+
+ return GetParentOfType(syntaxNode.Parent);
+ }
+}
+
+#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
new file mode 100644
index 0000000..b2dd7d7
--- /dev/null
+++ b/Net.Sdk.Web.Extensions.SourceGenerators/Net.Sdk.Web.Extensions.SourceGenerators.csproj
@@ -0,0 +1,32 @@
+
+
+
+ netstandard2.0
+ 0.8
+ latest
+ true
+ Alexandru Macocian
+ Source generators extensions for Net.Sdk.Web.
+ true
+ true
+ true
+ true
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Net.Sdk.Web.Extensions.SourceGenerators/ResponseGenerator.cs b/Net.Sdk.Web.Extensions.SourceGenerators/ResponseGenerator.cs
new file mode 100644
index 0000000..5f7c9a8
--- /dev/null
+++ b/Net.Sdk.Web.Extensions.SourceGenerators/ResponseGenerator.cs
@@ -0,0 +1,121 @@
+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
\ No newline at end of file
diff --git a/Net.Sdk.Web.Extensions.SourceGenerators/RouteAttributeGenerator.cs b/Net.Sdk.Web.Extensions.SourceGenerators/RouteAttributeGenerator.cs
new file mode 100644
index 0000000..0c9a4c1
--- /dev/null
+++ b/Net.Sdk.Web.Extensions.SourceGenerators/RouteAttributeGenerator.cs
@@ -0,0 +1,47 @@
+using System;
+using Microsoft.CodeAnalysis;
+using Sybil;
+
+namespace Net.Sdk.Web.Extensions.SourceGenerators;
+
+[Generator(LanguageNames.CSharp)]
+#nullable enable
+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);
+ });
+ }
+
+ 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.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()))));
+
+ var syntax = builder.Build();
+ var source = syntax.ToFullString();
+ context.AddSource($"{attributeName}.g", source);
+ }
+}
+
+#nullable disable
\ No newline at end of file
diff --git a/Net.Sdk.Web.Extensions.SourceGenerators/RouteInterfaceGenerator.cs b/Net.Sdk.Web.Extensions.SourceGenerators/RouteInterfaceGenerator.cs
new file mode 100644
index 0000000..6ba08f6
--- /dev/null
+++ b/Net.Sdk.Web.Extensions.SourceGenerators/RouteInterfaceGenerator.cs
@@ -0,0 +1,43 @@
+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
\ No newline at end of file
diff --git a/Net.Sdk.Web.Extensions.sln b/Net.Sdk.Web.Extensions.sln
index f53a5ac..297e396 100644
--- a/Net.Sdk.Web.Extensions.sln
+++ b/Net.Sdk.Web.Extensions.sln
@@ -23,7 +23,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
.github\workflows\ci.yaml = .github\workflows\ci.yaml
EndProjectSection
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Net.Sdk.Web.Extensions.Azure", "Net.Sdk.Web.Extensions.Azure\Net.Sdk.Web.Extensions.Azure.csproj", "{F9D782AF-2E99-497A-B5C2-5DF6FB254EE5}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net.Sdk.Web.Extensions.Azure", "Net.Sdk.Web.Extensions.Azure\Net.Sdk.Web.Extensions.Azure.csproj", "{F9D782AF-2E99-497A-B5C2-5DF6FB254EE5}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net.Sdk.Web.Extensions.SourceGenerators", "Net.Sdk.Web.Extensions.SourceGenerators\Net.Sdk.Web.Extensions.SourceGenerators.csproj", "{A76058F7-5073-472E-B6A7-9467E5FB11D0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net.Sdk.Web.Extensions.SourceGenerators.Tests", "Net.Sdk.Web.Extensions.SourceGenerators.Tests\Net.Sdk.Web.Extensions.SourceGenerators.Tests.csproj", "{77F194BA-FB4C-427D-A0D4-0C790937C5C5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -39,6 +43,14 @@ Global
{F9D782AF-2E99-497A-B5C2-5DF6FB254EE5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F9D782AF-2E99-497A-B5C2-5DF6FB254EE5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F9D782AF-2E99-497A-B5C2-5DF6FB254EE5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A76058F7-5073-472E-B6A7-9467E5FB11D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A76058F7-5073-472E-B6A7-9467E5FB11D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A76058F7-5073-472E-B6A7-9467E5FB11D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A76058F7-5073-472E-B6A7-9467E5FB11D0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {77F194BA-FB4C-427D-A0D4-0C790937C5C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {77F194BA-FB4C-427D-A0D4-0C790937C5C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {77F194BA-FB4C-427D-A0D4-0C790937C5C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {77F194BA-FB4C-427D-A0D4-0C790937C5C5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj b/Net.Sdk.Web.Extensions/Net.Sdk.Web.Extensions.csproj
index 66da70c..1999831 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.7.1
+ 0.8
Alexandru Macocian
LICENSE
true
@@ -25,9 +25,11 @@
-
-
-
+
+
+
+
+