Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configure endpoints #13

Merged
merged 6 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,55 @@ public static partial class GetUsersQuery
In your `Program.cs`, add a call to `app.MapXxxEndpoints()`, where `Xxx` is the shortened form of the project name.
* For a project named `Web`, it will be `app.MapWebEndpoints()`
* For a project named `Application.Web`, it will be `app.MapApplicationWebEndpoints()`

### Customizing the endpoints
#### Authorization

The `[AllowAnonymous]` and `[Authorized("Policy")]` attributes are supported and will be applied to the endpoint.

```cs
[Handler]
[MapGet("/users")]
[AllowAnonymous]
public static partial class GetUsersQuery
{
public record Query;

private static ValueTask<IEnumerable<User>> HandleAsync(
Query _,
UsersService usersService,
CancellationToken token)
{
return usersService.GetUsers();
}
}
```

#### Additional Customization

Additional customization of the endpoint registration can be done by adding a `CustomizeEndpoint` method.

```cs
[Handler]
[MapGet("/users")]
[Authorize(Policies.UserManagement)]
public static partial class GetUsersQuery
{
internal static void CustomizeGetFeaturesEndpoint(IEndpointConventionBuilder endpoint)
=> endpoint
.Produces<IEnumerable<User>>(StatusCodes.Status200OK)
.ProducesValidationProblem()
.ProducesProblem(StatusCodes.Status500InternalServerError)
.WithTags(nameof(User));

public record Query;

private static ValueTask<IEnumerable<User>> HandleAsync(
Query _,
UsersService usersService,
CancellationToken token)
{
return usersService.GetUsers();
}
}
```
1 change: 1 addition & 0 deletions src/Immediate.Apis.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ Rule ID | Category | Severity | Notes
IAPI0001 | ImmediateApis | Error | InvalidHandlerAttributeAnalyzer
IAPI0002 | ImmediateApis | Error | InvalidAuthorizeAttributeAnalyzer
IAPI0003 | ImmediateApis | Warning | InvalidAuthorizeAttributeAnalyzer
IAPI0004 | ImmediateApis | Warning | CustomizeEndpointUsageAnalyzer
93 changes: 93 additions & 0 deletions src/Immediate.Apis.Analyzers/CustomizeEndpointUsageAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Immediate.Apis.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class CustomizeEndpointUsageAnalyzer : DiagnosticAnalyzer
{
public static readonly DiagnosticDescriptor MustBeValidDefinition =
new(
id: DiagnosticIds.IAPI0004CustomizeEndpointInvalid,
title: "`CustomizeEndpoint` requires a specific definition",
messageFormat: "`CustomizeEndpoint` must be `internal static void CustomizeEndpoint(IEndpointConventionBuilder endpoint)`",
category: "ImmediateApis",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "An invalid definition of `CustomizeEndpoint` will not be used in the registration."
);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(
[
MustBeValidDefinition,
]);

public override void Initialize(AnalysisContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));

context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
}

private static void AnalyzeSymbol(SymbolAnalysisContext context)
{
var token = context.CancellationToken;
token.ThrowIfCancellationRequested();

if (context.Symbol is not INamedTypeSymbol namedTypeSymbol)
return;

if (!namedTypeSymbol
.GetAttributes()
.Any(x => Utility.ValidAttributes.Contains(x.AttributeClass?.ToString()))
)
{
return;
}

token.ThrowIfCancellationRequested();

if (namedTypeSymbol
.GetMembers()
.OfType<IMethodSymbol>()
.Where(ims => ims.Name == "CustomizeEndpoint")
.ToList() is not [{ } customizeEndpointMethod])
{
return;
}

if (
customizeEndpointMethod is
{
DeclaredAccessibility: Accessibility.Internal,
IsStatic: true,
ReturnsVoid: true,
Parameters: [{ } param]
}
&& param.Type.ToString() == "Microsoft.AspNetCore.Builder.IEndpointConventionBuilder"
)
{
return;
}

var syntax = (MethodDeclarationSyntax)customizeEndpointMethod
.DeclaringSyntaxReferences[0]
.GetSyntax();

context.ReportDiagnostic(
Diagnostic.Create(
MustBeValidDefinition,
syntax
.Identifier
.GetLocation()
)
);
}
}
1 change: 1 addition & 0 deletions src/Immediate.Apis.Analyzers/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ internal static class DiagnosticIds
public const string IAPI0001MissingHandlerAttribute = "IAPI0001";
public const string IAPI0002InvalidAuthorizeParameter = "IAPI0002";
public const string IAPI0003UsedBothAuthorizeAndAnonymous = "IAPI0003";
public const string IAPI0004CustomizeEndpointInvalid = "IAPI0004";
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ private sealed record Method
public required bool AllowAnonymous { get; init; }
public required bool Authorize { get; init; }
public required string? AuthorizePolicy { get; init; }

public required bool UseCustomization { get; init; }
}

private static readonly string[] s_methodAttributes =
Expand Down
37 changes: 29 additions & 8 deletions src/Immediate.Apis.Generators/ImmediateApisGenerator.Transform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ CancellationToken token

var attribute = attributes[methodIndex];
var httpMethod = attribute.AttributeClass!.Name[..^9];
var route = (string?)attribute.ConstructorArguments.FirstOrDefault().Value;

if (route == null)
if (attribute.ConstructorArguments.FirstOrDefault().Value is not string route)
return null;

token.ThrowIfCancellationRequested();
Expand All @@ -56,7 +55,10 @@ CancellationToken token
if (argument.Key != "Policy")
return null;

authorizePolicy = (string)argument.Value.Value!;
if (argument.Value.Value is not string ap)
return null;

authorizePolicy = ap;
}
}
}
Expand All @@ -70,6 +72,10 @@ CancellationToken token

token.ThrowIfCancellationRequested();

var useCustomization = HasCustomizationMethod(symbol);

token.ThrowIfCancellationRequested();

return new()
{
HttpMethod = httpMethod,
Expand All @@ -81,7 +87,9 @@ CancellationToken token

AllowAnonymous = allowAnonymous,
Authorize = authorize,
AuthorizePolicy = authorizePolicy
AuthorizePolicy = authorizePolicy,

UseCustomization = useCustomization,
};
}

Expand All @@ -103,10 +111,7 @@ CancellationToken token
.GetMembers()
.OfType<IMethodSymbol>()
.Where(m => m.IsStatic)
.Where(m =>
m.Name.Equals("Handle", StringComparison.Ordinal)
|| m.Name.Equals("HandleAsync", StringComparison.Ordinal)
)
.Where(m => m.Name is "Handle" or "HandleAsync")
.ToList() is not [var handleMethod])
{
return null;
Expand All @@ -118,4 +123,20 @@ CancellationToken token

return handleMethod;
}

private static bool HasCustomizationMethod(INamedTypeSymbol symbol)
=> symbol
.GetMembers()
.OfType<IMethodSymbol>()
.Any(m =>
m is
{
Name: "CustomizeEndpoint",
IsStatic: true,
DeclaredAccessibility: Accessibility.Internal,
ReturnsVoid: true,
Parameters: [{ } param]
}
&& param.Type.ToString() == "Microsoft.AspNetCore.Builder.IEndpointConventionBuilder"
);
}
8 changes: 6 additions & 2 deletions src/Immediate.Apis.Generators/Templates/Route.sbntxt
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ public static partial class {{ assembly }}RoutesBuilder
CancellationToken token
) => await handler.HandleAsync(parameters, token)
);


{{~ if method.allow_anonymous ~}}

_ = endpoint.AllowAnonymous();
{{~ else if method.authorize ~}}

_ = endpoint.RequireAuthorization({{ if !string.empty method.authorize_policy }}"{{ method.authorize_policy }}"{{ end }})
{{~ end ~}}
{{~ if method.use_customization ~}}

{{ method.class_name }}.CustomizeEndpoint(endpoint);
{{~ end ~}}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ namespace Immediate.Apis.FunctionalTests.Features.WeatherForecast;
[AllowAnonymous]
public static partial class Get
{
public static void CustomizeEndpoint(IEndpointConventionBuilder endpoint)
=> endpoint
.WithDescription("Gets the current weather forecast");

public sealed record Query;

public sealed record Result
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using Immediate.Apis.Shared;
using Immediate.Handlers.Shared;
using Microsoft.AspNetCore.Authorization;

namespace Immediate.Apis.FunctionalTests.Features.WeatherForecast;

[Handler]
[MapPut("/forecast")]
[AllowAnonymous]
public static partial class Put
{
public sealed record Command
Expand Down
Loading
Loading