Skip to content
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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ by keeping endpoint definitions inside their features while generating the boile
injection.
* **Metadata composition** – mix class-level and method-level attributes for tags, authorization requirements, content
negotiation, and antiforgery/anonymous settings. The generator merges everything into the produced endpoint builder.
* **Rich request/response contracts** – describe the shape of your API surface with `[Accepts]`, `[Produces]`, `[ProducesProblem]`,
* **Rich request/response contracts** – describe the shape of your API surface with `[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`,
and `[ProducesValidationProblem]` so OpenAPI and client tooling stay accurate.
* **Minimal boilerplate** – `AddEndpointHandlers` auto-registers instance handlers with DI, and `MapEndpointHandlers`
registers every attribute-decorated method.
Expand Down Expand Up @@ -216,7 +216,7 @@ public sealed class CreateTodo

The method is only generated once per handler class, so any conventions you add will automatically flow to all endpoints defined within that class.

### 5. Describe contracts with `Accepts` and `Produces`
### 5. Describe contracts with `Accepts` and `ProducesResponse`

GeneratedEndpoints ships with helper attributes for request and response metadata. Apply them to either a handler class or
individual methods to keep your OpenAPI description in sync with the implementation. Attributes on the class are merged into
Expand All @@ -229,7 +229,7 @@ using Microsoft.AspNetCore.Http.HttpResults;
namespace Todos.Features;

[Accepts<CreateTodoRequest>("application/json", "application/xml")]
[Produces<Todo>(StatusCodes.Status201Created)]
[ProducesResponse<Todo>(StatusCodes.Status201Created)]
[ProducesProblem(StatusCodes.Status500InternalServerError)]
public sealed class CreateTodo
{
Expand All @@ -253,7 +253,7 @@ calls on the endpoint builder.
| `[AllowAnonymous]` | Class or method | Explicitly opts a method (or all methods in a class) into anonymous access, overriding `[RequireAuthorization]`. |
| `[DisableAntiforgery]` | Class or method | Calls `.DisableAntiforgery()` on the generated endpoint, matching the ASP.NET Core extension. |
| `[Accepts]` / `[Accepts<TRequest>]` | Class or method | Emits `.Accepts<TRequest>(contentType, additionalContentTypes...)` to document supported request bodies. Multiple attributes are allowed per endpoint. |
| `[Produces]` / `[Produces<TResponse>]` | Class or method | Emits `.Produces<TResponse>(statusCode, contentTypes...)` for each documented response type. Multiple attributes are allowed. |
| `[ProducesResponse]` / `[ProducesResponse<TResponse>]` | Class or method | Emits `.Produces<TResponse>(statusCode, contentTypes...)` for each documented response type. Multiple attributes are allowed. |
| `[ProducesProblem]` | Class or method | Emits `.ProducesProblem(statusCode, contentTypes...)` for endpoints that return RFC 7807 problem details. |
| `[ProducesValidationProblem]` | Class or method | Emits `.ProducesValidationProblem(statusCode, contentTypes...)` when validation failures are returned. |

Expand Down
20 changes: 10 additions & 10 deletions src/GeneratedEndpoints/MinimalApiGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator
private const string AcceptsAttributeFullyQualifiedName = $"{AttributesNamespace}.{AcceptsAttributeName}";
private const string AcceptsAttributeHint = $"{AcceptsAttributeFullyQualifiedName}.gs.cs";

private const string ProducesAttributeName = "ProducesAttribute";
private const string ProducesAttributeFullyQualifiedName = $"{AttributesNamespace}.{ProducesAttributeName}";
private const string ProducesAttributeHint = $"{ProducesAttributeFullyQualifiedName}.gs.cs";
private const string ProducesResponseAttributeName = "ProducesResponseAttribute";
private const string ProducesResponseAttributeFullyQualifiedName = $"{AttributesNamespace}.{ProducesResponseAttributeName}";
private const string ProducesResponseAttributeHint = $"{ProducesResponseAttributeFullyQualifiedName}.gs.cs";

private const string ProducesProblemAttributeName = "ProducesProblemAttribute";
private const string ProducesProblemAttributeFullyQualifiedName = $"{AttributesNamespace}.{ProducesProblemAttributeName}";
Expand Down Expand Up @@ -372,7 +372,7 @@ namespace {{AttributesNamespace}};
/// Specifies a response type, status code, and content types produced by the annotated endpoint or class.
/// </summary>
[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
internal sealed class {{ProducesAttributeName}} : global::System.Attribute
internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute
{
/// <summary>
/// Gets the response type produced by the endpoint.
Expand All @@ -395,13 +395,13 @@ internal sealed class {{ProducesAttributeName}} : global::System.Attribute
public string[] AdditionalContentTypes { get; }

/// <summary>
/// Initializes a new instance of the <see cref="{{ProducesAttributeName}}"/> class.
/// Initializes a new instance of the <see cref="{{ProducesResponseAttributeName}}"/> class.
/// </summary>
/// <param name="responseType">The CLR type of the response body.</param>
/// <param name="statusCode">The HTTP status code returned by the endpoint.</param>
/// <param name="contentType">The primary content type produced by the endpoint.</param>
/// <param name="additionalContentTypes">Additional content types produced by the endpoint.</param>
public {{ProducesAttributeName}}(global::System.Type responseType, int statusCode = 200, string? contentType = null, params string[] additionalContentTypes)
public {{ProducesResponseAttributeName}}(global::System.Type responseType, int statusCode = 200, string? contentType = null, params string[] additionalContentTypes)
{
ResponseType = responseType ?? throw new global::System.ArgumentNullException(nameof(responseType));
StatusCode = statusCode;
Expand All @@ -415,7 +415,7 @@ internal sealed class {{ProducesAttributeName}} : global::System.Attribute
/// </summary>
/// <typeparam name="TResponse">The CLR type of the response body.</typeparam>
[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
internal sealed class {{ProducesAttributeName}}<TResponse> : global::System.Attribute
internal sealed class {{ProducesResponseAttributeName}}<TResponse> : global::System.Attribute
{
/// <summary>
/// Gets the response type produced by the endpoint.
Expand Down Expand Up @@ -443,7 +443,7 @@ internal sealed class {{ProducesAttributeName}}<TResponse> : global::System.Attr
/// <param name="statusCode">The HTTP status code returned by the endpoint.</param>
/// <param name="contentType">The primary content type produced by the endpoint.</param>
/// <param name="additionalContentTypes">Additional content types produced by the endpoint.</param>
public {{ProducesAttributeName}}(int statusCode = 200, string? contentType = null, params string[] additionalContentTypes)
public {{ProducesResponseAttributeName}}(int statusCode = 200, string? contentType = null, params string[] additionalContentTypes)
{
StatusCode = statusCode;
ContentType = contentType;
Expand All @@ -452,7 +452,7 @@ internal sealed class {{ProducesAttributeName}}<TResponse> : global::System.Attr
}

""";
context.AddSource(ProducesAttributeHint, SourceText.From(producesSource, Encoding.UTF8));
context.AddSource(ProducesResponseAttributeHint, SourceText.From(producesSource, Encoding.UTF8));

// ProducesProblem
var producesProblemSource = $$"""
Expand Down Expand Up @@ -795,7 +795,7 @@ ref List<ProducesValidationProblemMetadata>? producesValidationProblem
continue;
}

if (IsGeneratedAttribute(fullyQualifiedName, ProducesAttributeName))
if (IsGeneratedAttribute(fullyQualifiedName, ProducesResponseAttributeName))
{
TryAddProducesMetadata(attribute, attributeClass, ref produces);
continue;
Expand Down
4 changes: 2 additions & 2 deletions tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ internal static class GetUserEndpoint
[AllowAnonymous]
[Accepts(typeof(GetUserRequest), "application/json", "application/xml")]
[Accepts<GetUserMetadata>("application/json", "application/xml")]
[Produces(typeof(UserProfile), StatusCodes.Status200OK, "application/json")]
[Produces<UserProfile>(StatusCodes.Status202Accepted, "application/json")]
[ProducesResponse(typeof(UserProfile), StatusCodes.Status200OK, "application/json")]
[ProducesResponse<UserProfile>(StatusCodes.Status202Accepted, "application/json")]
[ProducesProblem(StatusCodes.Status500InternalServerError, "application/problem+json")]
[ProducesValidationProblem(StatusCodes.Status400BadRequest, "application/problem+json")]
[MapGet("/users/{id:int}", Name = nameof(GetUser), Summary = "Gets a user by ID.", Description = "Gets a user by ID when the ID is greater than zero.")]
Expand Down
4 changes: 2 additions & 2 deletions tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public async Task MapAllAttributesAndHttpMethods(bool withNamespace)
[RequireAuthorization("PolicyA", "PolicyB")]
[DisableAntiforgery]
[Accepts(typeof(ClassLevelRequest), "application/xml", "text/xml")]
[Microsoft.AspNetCore.Generated.Attributes.Produces(typeof(ClassLevelResponse), 201, "application/json", "text/json")]
[Microsoft.AspNetCore.Generated.Attributes.ProducesResponse(typeof(ClassLevelResponse), 201, "application/json", "text/json")]
[ProducesProblem(503, "application/problem+json")]
[ProducesValidationProblem(409, "application/problem+json", "text/plain")]
internal sealed class ComplexEndpoints
Expand All @@ -138,7 +138,7 @@ public static void Configure<TBuilder>(TBuilder builder, IServiceProvider servic
[Tags("MethodLevel")]
[RequireAuthorization("MethodPolicy")]
[Accepts<GetRequest>("application/custom", "text/custom")]
[Microsoft.AspNetCore.Generated.Attributes.Produces<GetResponse>(200, "application/json", "text/json")]
[Microsoft.AspNetCore.Generated.Attributes.ProducesResponse<GetResponse>(200, "application/json", "text/json")]
[ProducesProblem(400, "application/problem+json", "text/plain")]
[ProducesValidationProblem(422, "application/problem+json", "text/plain")]
public async Task<Results<Ok<GetResponse>, NotFound>> GetComplex(
Expand Down
Loading