domaindrivendev / Swashbuckle.AspNetCore

Swagger tools for documenting API's built on ASP.NET Core
MIT License
5.21k stars 1.3k forks source link

Using [AsParameters] annotation in minimal api does not lift summary comments of the model #2658

Open andreasuvoss opened 1 year ago

andreasuvoss commented 1 year ago

Swashbuckle.AspNetCore: 6.5.0 .NET 7

I am not sure if this is a bug or a feature request.

When defining APIs using the minimal API approach, the summaries from models are not lifted from the XML documentation and put into the resulting swagger.json like I would expect. Before migrating to minimal API we used controllers, and the [FromQuery] annotation in a similar way. This gave the expected result. However [FromQuery] is not supported when using minimal API, instead the [AsParameters] annotation was added in .NET 7 .

The summaries are present in the XML file generated when building the project, but not in the swagger.json file.

I have created a minimal example showing that the summaries are not present (in both swagger.json or SwaggerUI) when using the [AsParameter] annotation, but they are included in the schema on the post request using the [FromBody] annotation.

I have a very ugly workaround using the WithOpenApi extension method from the NuGet package Microsoft.AspNetCore.OpenApi to add descriptions to the individual indices of each parameter by modifying the mapping from my minimal example like so:

app.MapGet("/AsParameters", ([AsParameters] AsParametersArgument request) => "Hello World!")
    .WithOpenApi(operation =>
    {
        operation.Parameters[0].Description = "Parameter one description";
        operation.Parameters[1].Description = "Parameter two description";
        return operation;
    });

Minimal example:

Project file:

<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <GenerateDocumentationFile>true</GenerateDocumentationFile>
        <NoWarn>1701;1702;1591</NoWarn>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
    </ItemGroup>

</Project>

Program.cs:

using System.Reflection;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    var assemblyName = Assembly.GetExecutingAssembly().FullName?.Split(',')[0];
    options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, $"{assemblyName}.xml"));
});

var app = builder.Build();

app.MapGet("/AsParameters", ([AsParameters] AsParametersArgument request) => "Hello World!");

app.MapPost("/FromBody", ([FromBody] FromBodyArgument request) => "Hello World!");

app.UseSwagger();
app.UseSwaggerUI();

app.Run();

internal struct AsParametersArgument
{
    /// <summary>
    /// This is a property with the number one - This is nowhere in SwaggerUI
    /// </summary>
    public string PropertyOne { get; set; }

    /// <summary>
    /// This is a property with the number two - This is nowhere in SwaggerUI
    /// </summary>
    public string PropertyTwo { get; set; }
}

internal struct FromBodyArgument
{
    /// <summary>
    /// This is a property with the number eight (this description is present in Swagger)
    /// </summary>
    public string PropertyEight { get; set; }

    /// <summary>
    /// This is a property with the number nine (this description is present in Swagger)
    /// </summary>
    public string PropertyNine { get; set; }
}
mr-davidc commented 1 year ago

I think I'm running in to a similar issue. I'm trying to set a boolean parameter to show as NOT required in Swagger UI. It seems the model property attributes dont flow up

I have the following:

app.MapGet("/", ([AsParameters] GetRequestModel request) => "Hello World!");

public class GetRequestModel
{
    [FromQuery(Name = "unassigned_only")]
    [JsonPropertyName("unassigned_only")]
    [SwaggerParameter(Description = "Property description", Required = false)]
    [DefaultValue(false)]
    public bool UnassignedOnly { get; set; }
}

The DefaultValue is displayed on Swagger UI fine, but the SwaggerParameter seems to have no effect. The Description isnt displayed on the Swagger UI and the parameter also still shows as "Required". I also tried using SwaggerSchema attribute, but same result.

codelovercc commented 1 year ago

I think I'm running in to a similar issue. I'm trying to set a boolean parameter to show as NOT required in Swagger UI. It seems the model property attributes dont flow up

I have the following:

app.MapGet("/", ([AsParameters] GetRequestModel request) => "Hello World!");

public class GetRequestModel
{
    [FromQuery(Name = "unassigned_only")]
    [JsonPropertyName("unassigned_only")]
    [SwaggerParameter(Description = "Property description", Required = false)]
    [DefaultValue(false)]
    public bool UnassignedOnly { get; set; }
}

The DefaultValue is displayed on Swagger UI fine, but the SwaggerParameter seems to have no effect. The Description isnt displayed on the Swagger UI and the parameter also still shows as "Required". I also tried using SwaggerSchema attribute, but same result.

Try change type bool to bool?, then enable nullable reference type for project and Swashbuckle.AspNetCore

mr-davidc commented 1 year ago

@codelovercc Thanks for the reply.

While changing it the bool? does work, its not really desired since I don't want to handle nullable value on the API side. I simply want to display it as optional on the Swagger UI and default to false.

codelovercc commented 1 year ago

@mr-davidc Welcome, I'm happy to help. In my case, with project nullable enabled <Nullable>enable</Nullable>, and

       services.AddSwaggerGen(options =>
        {
            options.SupportNonNullableReferenceTypes();
        });
public class MyDto
{
    public bool Encrypted {get;set;}
}

MyDto.Encrypted will be optional on the Swagger UI.

image

image

mr-davidc commented 1 year ago

Hi again @codelovercc,

I tried those options you mentioned above, but still my bool properties come through to Swagger UI as required. I really think the [AsParameters] is having an impact here.

I also still have an issue where the endpoint Summary doesn't come through to Swagger UI either, UNLESS I set .WithOpenApi() but then I lose all of my SwaggerParameter descriptions...

public static RouteGroupBuilder GetItemsRoute(this RouteGroupBuilder routeGroupBuilder)
{
    routeGroupBuilder
        .MapGet("/", GetItems)
        .WithSummary("Retrieve a list of items")                    <------- This doesnt display on Swagger UI
        .ProducesProblem(StatusCodes.Status400BadRequest)
        .ProducesProblem(StatusCodes.Status500InternalServerError);

    return routeGroupBuilder;
}

Any other things I can try to fix either of these issues?

AmielCyber commented 1 year ago

I have the same problem. Using the data annotation [AsParameters] to bind to an object. The data annotations seem to work like [DefaultValue()], but the summary for any property in the object with AsParameters attribute is not shown in SwaggerUI.

kaloyantihomirov commented 1 year ago

Hi! I want to share my experience as well. I'm also using the data annotation [AsParameters] to bind to an object. The SwaggerParameter seems to have no effect in the Swagger UI.

record Fruit(string Name, int Stock);

record struct CreateFruitModel
    ([FromServices] LinkGenerator Links,
    [FromRoute]
    [SwaggerParameter(Description = "The id of the fruit that will be created", Required = true)] string Id,
    [FromBody] Fruit Fruit);
app.MapPost("/fruit/{id}", handler.CreateFruit)
    .WithName("CreateFruit");
internal class FruitHandler
{
    private readonly ConcurrentDictionary<string, Fruit> _fruit;

    public FruitHandler(ConcurrentDictionary<string, Fruit> fruit)
    {
        _fruit = fruit;
    }

    /// <summary>
    /// Creates a fruit with the given ID, or returns 400 if a fruit with the given ID exists
    /// </summary>
    /// <param name="model">A model for the parameters we need to create a fruit</param>
    /// <response code="201">The fruit was created successfully</response>
    /// <response code="400">A fruit with the given ID already exists in the dict</response>
    /// 
    [ProducesResponseType(typeof(Fruit), 201)]
    [ProducesResponseType(typeof(HttpValidationProblemDetails), 400, "application/problem+json")]
    [Tags("fruit")]
    public IResult CreateFruit([AsParameters] CreateFruitModel model)
     => _fruit.TryAdd(model.Id, model.Fruit)
            ? TypedResults.Created(
                model.Links.GetPathByName("GetFruitById", new { model.Id })
                    ?? "N/A",
                model.Fruit)
            : Results.ValidationProblem(new Dictionary<string, string[]>
            {
                { "id", new[] { "A fruit with this id already exists" } }
            });
}

This is the only thing that I have found to work:

var _fruit = new ConcurrentDictionary<string, Fruit>();

var handler = new FruitHandler(_fruit);

app.MapPost("/fruit/{id}", handler.CreateFruit)
    .WithName("CreateFruit")
    .WithOpenApi(o =>
    {
        o.Parameters[0].Description = "The id of the fruit that will be created";

        return o;
    });

It's not really clear for me which parameters we access with o.Parameters. Here it seems that the collection contains only one parameter: the route id parameter. image

BoasHoeven commented 8 months ago

Seems like this feature is still missing, anyone know?

github-actions[bot] commented 2 months ago

This issue is stale because it has been open for 60 days with no activity. It will be automatically closed in 14 days if no further updates are made.

jgarciadelanoceda commented 2 months ago

Hi @andreasuvoss, can you test if this is solved with the addition of #2943. You can download the latest bits using myGet. I think that the PR has just done it

jgarciadelanoceda commented 2 months ago

@kaloyantihomirov Reviewing this item a bit further I think that you did not use the EnableAnnotations extension method that comes from the nuget package Swashbuckle.AspNetCore.Annotations. Unfortunuately if you apply it, it throws an error that is only present for this specific scenario. I have got it and soon I will write a PR for this matter

jgarciadelanoceda commented 2 months ago

@kaloyantihomirov can you test using myGet?, my changes are already merged

andreasuvoss commented 2 months ago

Hi @andreasuvoss, can you test if this is solved with the addition of #2943. You can download the latest bits using myGet. I think that the PR has just done it

I’m not at my computer at the moment, I will try to test this sometime next week, maybe already tomorrow.

jgarciadelanoceda commented 2 months ago

Hi @andreasuvoss, reviewing the issues with MinimalApi, the issue is that if you use AsParameters the ContainerType is not present over the Parameters of the endpoint(The AsParametersArgument class is missing). I think that we cannot do anything (Let me review it later), apart from recommending you to use the SwaggerParameter attribute

jgarciadelanoceda commented 2 months ago

Hi @martincostello, I wasn't able to get the XML parameters to work when they are members of a AsParameters request. It's a limitation on the OpenApi provider that Swashbuckle uses under the hood. Should we create an issue in their GitHub repo or as it's supported with the Attributes, it's at least covered

martincostello commented 2 months ago

Sure, that sounds sensible.

jgarciadelanoceda commented 3 weeks ago

It will be supported in dotnet10. dotnet/aspnetcore#56764