Skip to content

Commit 59eeda5

Browse files
feat: Implement MintPlayer.AspNetCore.Endpoints library (#27)
* Add MintPlayer.AspNetCore.Endpoints library (Phase 1 + 2) Three-package implementation for class-per-endpoint mapping: - Abstractions: interfaces (IEndpointBase, IEndpoint, IEndpoint<T>, IEndpoint<T,R>), convenience interfaces (IGet/Post/Put/Delete/Patch), IEndpointGroup + IMemberOf<T> for route groups, attributes, descriptor - Runtime: abstract base class hierarchy (EndpointBase<T> -> BodyEndpoint<T> with MVC input formatter content negotiation + JSON fallback, NonBodyEndpoint<T> with forced explicit binding), concrete HTTP method classes, MapEndpoint<T> manual registration extension - Generator: IIncrementalGenerator that discovers endpoints, generates partial class base declarations (Task A) and MapXxxEndpoints() extension method with factory caching, disposal, attribute transfer, group support, and .Produces<TResponse>() for OpenAPI (Task B) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add test app and rewrite generator using MintPlayer.SourceGenerators.Tools - Rewrote generator to use IncrementalGenerator base class and Producer pattern with IndentedTextWriter/OpenBlock from MintPlayer.SourceGenerators.Tools - Fixed ObjectFactory<T> type (was incorrectly using Func<...>) - Removed Emitter.cs, replaced with EndpointGenerator.Producer.cs - Added TestApp exercising all endpoint types: - Raw GET (HealthCheck) - POST with typed request+response in group (CreateUser) - GET with explicit binding in group (GetUser) - PUT with body parsing in group (UpdateUser) - DELETE with explicit binding in group (DeleteUser) - Raw GET in group (ListUsers) - Multi-method endpoint (PreflightEndpoint) - Route group with tags (UsersApi) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Bundle source generator in runtime package and add README - Fix BodyEndpoint content negotiation to guard on IModelMetadataProvider - Apply [assembly: EndpointsMethodName] in test app - Pack generator + dependencies into analyzers/dotnet/cs of runtime package - Add comprehensive README.md included in NuGet package Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * launchsettings * Support nested endpoint groups at any depth - Add GroupInfo model and second syntax provider for group-to-parent discovery - Groups can implement IMemberOf<TParentGroup> for nesting - Generator builds group tree and emits nested MapGroup calls - Add ApiGroup, ProductsApi, and ListProducts to test app - Update README with nested groups documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e98798b commit 59eeda5

40 files changed

Lines changed: 2042 additions & 0 deletions
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace MintPlayer.AspNetCore.Endpoints;
2+
3+
public record EndpointDescriptor(string Name, string Path, IEnumerable<string> Methods, Type HandlerType);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace MintPlayer.AspNetCore.Endpoints;
2+
3+
[AttributeUsage(AttributeTargets.Class)]
4+
public class EndpointNameAttribute(string name) : Attribute
5+
{
6+
public string Name { get; } = name;
7+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace MintPlayer.AspNetCore.Endpoints;
2+
3+
/// <summary>
4+
/// Overrides the name of the generated MapEndpoints extension method for this assembly.
5+
/// Without this attribute, the method name is derived from the assembly name
6+
/// (e.g., assembly "MyApp.Api" -> "MapMyAppApiEndpoints").
7+
/// </summary>
8+
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
9+
public class EndpointsMethodNameAttribute(string methodName) : Attribute
10+
{
11+
public string MethodName { get; } = methodName;
12+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace MintPlayer.AspNetCore.Endpoints;
2+
3+
public interface IDeleteEndpoint : IEndpoint
4+
{
5+
static IEnumerable<string> IEndpointBase.Methods => ["DELETE"];
6+
}
7+
8+
public interface IDeleteEndpoint<TRequest> : IEndpoint<TRequest>
9+
{
10+
static IEnumerable<string> IEndpointBase.Methods => ["DELETE"];
11+
}
12+
13+
public interface IDeleteEndpoint<TRequest, TResponse> : IEndpoint<TRequest, TResponse>
14+
{
15+
static IEnumerable<string> IEndpointBase.Methods => ["DELETE"];
16+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
namespace MintPlayer.AspNetCore.Endpoints;
2+
3+
/// <summary>
4+
/// Level 1: Raw endpoint — full control over HttpContext.
5+
/// </summary>
6+
public interface IEndpoint : IEndpointBase
7+
{
8+
Task<IResult> HandleAsync(HttpContext httpContext);
9+
}
10+
11+
/// <summary>
12+
/// Level 2: Typed request — automatic request binding via the abstract base class.
13+
/// </summary>
14+
public interface IEndpoint<TRequest> : IEndpoint
15+
{
16+
Task<IResult> HandleAsync(TRequest request, CancellationToken cancellationToken);
17+
}
18+
19+
/// <summary>
20+
/// Level 3: Typed request and response — full type information for OpenAPI/Swagger.
21+
/// TResponse is used by the source generator to emit .Produces&lt;TResponse&gt;(statusCode).
22+
/// </summary>
23+
public interface IEndpoint<TRequest, TResponse> : IEndpoint<TRequest>
24+
{
25+
/// <summary>
26+
/// The HTTP status code for successful responses.
27+
/// Used by the source generator for .Produces&lt;TResponse&gt;(statusCode).
28+
/// Default: 200 (OK).
29+
/// </summary>
30+
static virtual int SuccessStatusCode => 200;
31+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace MintPlayer.AspNetCore.Endpoints;
2+
3+
/// <summary>
4+
/// Base contract for all endpoints. Provides route metadata as static abstract members.
5+
/// </summary>
6+
public interface IEndpointBase
7+
{
8+
/// <summary>The route pattern (e.g., "/api/users/{id}").</summary>
9+
static abstract string Path { get; }
10+
11+
/// <summary>
12+
/// The HTTP methods this endpoint handles (e.g., ["GET"], ["POST"], ["GET", "HEAD"]).
13+
/// Convenience interfaces (IGetEndpoint, IPostEndpoint, etc.) provide this automatically.
14+
/// </summary>
15+
static abstract IEnumerable<string> Methods { get; }
16+
17+
/// <summary>
18+
/// Optional hook to configure the route handler (auth, caching, OpenAPI metadata, etc.).
19+
/// Default implementation is a no-op.
20+
/// </summary>
21+
static virtual void Configure(RouteHandlerBuilder builder) { }
22+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace MintPlayer.AspNetCore.Endpoints;
2+
3+
/// <summary>
4+
/// Defines a route group with a shared prefix and optional group-wide configuration.
5+
/// Endpoints join a group by implementing IMemberOf&lt;TGroup&gt;.
6+
/// </summary>
7+
public interface IEndpointGroup
8+
{
9+
/// <summary>The route prefix for all endpoints in this group (e.g., "/api/users").</summary>
10+
static abstract string Prefix { get; }
11+
12+
/// <summary>
13+
/// Optional hook to configure the route group (auth, rate limiting, CORS, tags, etc.).
14+
/// Default implementation is a no-op.
15+
/// </summary>
16+
static virtual void Configure(RouteGroupBuilder group) { }
17+
}
18+
19+
/// <summary>
20+
/// Marker interface that associates an endpoint with an endpoint group.
21+
/// The endpoint's Path becomes relative to the group's Prefix.
22+
/// </summary>
23+
public interface IMemberOf<TGroup> where TGroup : IEndpointGroup;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace MintPlayer.AspNetCore.Endpoints;
2+
3+
public interface IGetEndpoint : IEndpoint
4+
{
5+
static IEnumerable<string> IEndpointBase.Methods => ["GET"];
6+
}
7+
8+
public interface IGetEndpoint<TRequest> : IEndpoint<TRequest>
9+
{
10+
static IEnumerable<string> IEndpointBase.Methods => ["GET"];
11+
}
12+
13+
public interface IGetEndpoint<TRequest, TResponse> : IEndpoint<TRequest, TResponse>
14+
{
15+
static IEnumerable<string> IEndpointBase.Methods => ["GET"];
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace MintPlayer.AspNetCore.Endpoints;
2+
3+
public interface IPatchEndpoint : IEndpoint
4+
{
5+
static IEnumerable<string> IEndpointBase.Methods => ["PATCH"];
6+
}
7+
8+
public interface IPatchEndpoint<TRequest> : IEndpoint<TRequest>
9+
{
10+
static IEnumerable<string> IEndpointBase.Methods => ["PATCH"];
11+
}
12+
13+
public interface IPatchEndpoint<TRequest, TResponse> : IEndpoint<TRequest, TResponse>
14+
{
15+
static IEnumerable<string> IEndpointBase.Methods => ["PATCH"];
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace MintPlayer.AspNetCore.Endpoints;
2+
3+
public interface IPostEndpoint : IEndpoint
4+
{
5+
static IEnumerable<string> IEndpointBase.Methods => ["POST"];
6+
}
7+
8+
public interface IPostEndpoint<TRequest> : IEndpoint<TRequest>
9+
{
10+
static IEnumerable<string> IEndpointBase.Methods => ["POST"];
11+
}
12+
13+
public interface IPostEndpoint<TRequest, TResponse> : IEndpoint<TRequest, TResponse>
14+
{
15+
static IEnumerable<string> IEndpointBase.Methods => ["POST"];
16+
}

0 commit comments

Comments
 (0)