domaindrivendev / Swashbuckle.WebApi

Seamlessly adds a swagger to WebApi projects!
BSD 3-Clause "New" or "Revised" License
3.07k stars 678 forks source link

Documentation is not generated for routes with generic parameters or return types. #749

Open brittonbeckham opened 8 years ago

brittonbeckham commented 8 years ago

In our organization, we have generic abstract controllers that we use as base controllers for all our data access APIs. The current algorithm that swagger uses to generate the documentation doesn't know how to handle this scenario because the method documentation signature has generic heuristics.

Here is an example of what the controller looks like with one of the generic methods included.

public abstract class ReadController<TEntity, TKey, TContext, TFilter, TInclude> :
        ApiController
        where TEntity : class, new()
        where TContext : IDbContext
        where TFilter : class, new()
        where TInclude : struct, IComparable, IConvertible, IFormattable
    {
        /// <summary>
        /// Gets the entity by the given key.
        /// </summary>
        [Route("{key}")]
        [HttpGet]
        public virtual async Task<HttpResponseMessage> Get(TKey key, [FromUri] ICollection<TInclude?> includes = null, [FromUri] ICollection<string> fields = null)
        {
        }
}

The XML that is generated for this method output by MSbuild is as follows:

<member name="M:Jane.Data.WebHost.ReadController`5.Get(`1,System.Collections.Generic.ICollection{System.Nullable{`4}},System.Collections.Generic.ICollection{System.String})">
  <summary>
    Gets the entity by the given key.
  </summary>
</member>

Even though we are including this XML file in the bin folder and ensure it gets added through the swagger call IncludeXmlComments(), no documentation is added to the route in the SwaggerUI.

The issue lies in the ApplyXmlActionComments.Apply() method in Swashbuckle.Swagger; essentially, it just fails to comprehend this method signature in the XML file and therefore cannot match the generic method to its documentation.

Would be nice to have this scenario accounted for so as to make generic routes display documentation as desired.

mikeandike commented 8 years ago

I have the same issue!

kalski commented 7 years ago

Same issue here. When is 6.0.0 planned for?

rythmicMonk commented 7 years ago

Same issue - only occuring when a generic type is used as a parameter. Outputs being generic is just fine.

zibrohimov commented 7 years ago

Same here. Can't believe this is still open. Any workaround?

heldersepu commented 7 years ago

Can anyone post a link to a sample project that reproduces this issue?

MikeDPeterson commented 7 years ago

As far as a workaround, one way that might help is to implement an IOperationFilter and fill it in

public class MyOperationFilter : IOperationFilter
    {
        public void Apply( Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription )
        {
            if ( string.IsNullOrEmpty( operation.summary ) )
            {
                // Manually set the documentation summary if swashbuckle couldn't figure it out. Swashbuckle 5 isn't able to figure these out since they have Generic parameters
                if ( apiDescription.HttpMethod.Method == "POST" )
                {
                    operation.summary = "POST endpoint. Use this to add a record";
                }
                else if ( apiDescription.HttpMethod.Method == "PUT" )
                {
                    operation.summary = "PUT endpoint. Use this to update a record";
                }
            }
        }
    }
heldersepu commented 7 years ago

@MikeDPeterson a workaround that I would not recommend ... but yes it is a valid workaround! I would love to troubleshoot this. do you have a simple project that can reproduce this bug?

zibrohimov commented 7 years ago

@heldersepu I can't post a link to the project unfortunately because it is under VPN. But I can describe my situation.

There is a web API project, with an abstract base controller which has CRUD operations.

public class TestController : BaseController<TestType>

Methods of BaseController is like this:

public virtual TEntity Post(TEntity entity) public virtual TEntity Get(string id) public virtual TEntity Put(TEntity entity) public virtual Task Delete(string id)

XML comments for GET and DELETE is visible. But it is not visible for PUT and POST as they have a generic type as a method parameter.

heldersepu commented 7 years ago

@zibrohimov Can you add the project to GitHub?

zibrohimov commented 7 years ago

@heldersepu I'm afraid I can't because I'm not allowed to do so. But I can give you relevant information about my projects nature if need any details. Here is a screenshot about how does that look. https://www.screencast.com/t/koF4v4HNt

heldersepu commented 7 years ago

@zibrohimov Just help me out troubleshooting, create a simple project that reproduces the issue and add it to your GitHub

zibrohimov commented 7 years ago

@heldersepu https://github.com/zibrohimov/SwashbuckleDemo

heldersepu commented 7 years ago

The error is here: https://github.com/domaindrivendev/Swashbuckle/blob/master/Swashbuckle.Core/Swagger/XmlComments/XmlCommentsIdHelper.cs#L63

that if (type.IsGenericType) always return false

heldersepu commented 7 years ago

If anyone wants to take on the challenge of figure out why IsGenericType does not work as expected here is a small console application reproducing the issue:

using System;

abstract class Parent<T> {
    public virtual T Put(T id, char value) {
        return id;}}

class Child : Parent<int> {
    public virtual string Get(Guid guid) {
        return "aei";}}

class Program {
    static void Main(string[] args) {
        foreach (var methodInfo in typeof(Child).GetMethods()) {
            if (methodInfo.IsVirtual && methodInfo.IsSecurityCritical) {
                Console.WriteLine(methodInfo.Name);
                foreach (var param in methodInfo.GetParameters()) {
                    Console.Write("  " + param.Name + " IsGenericType=");
                    Console.WriteLine(param.ParameterType.IsGenericType);}}}
        Console.ReadKey();}}

http://rextester.com/DUPC80090

dweggemans commented 7 years ago

I ran into the same issue and found the cause. Your controller has a generic type argument TKey that is used as argument in your Get method. In the xml documentation this argument is not expanded. You'll see something like member name="M:Your.Namespace.ReadController`5.Get(`1, [... more ...])"

In XmlCommentsIdHelper.AppendMethodName I've added a workaround for this (not thoroughly tested, but it works for me)

private static void AppendMethodName(MethodInfo methodInfo, StringBuilder builder)
{
    builder.Append(methodInfo.Name);

    var parameters = methodInfo.GetParameters();
    if (parameters.Length == 0) return;

    builder.Append("(");
    foreach (var param in parameters)
    {
        //Change: if the parameter type is a generic type argument replace it with the placeholder instead of the actual type
        if (methodInfo.DeclaringType.GenericTypeArguments.Contains(param.ParameterType))
        {
            builder.Append($"`{Array.IndexOf(methodInfo.DeclaringType.GenericTypeArguments, param.ParameterType)}");
        }
        else //do it the regular way
        {
            AppendFullTypeName(param.ParameterType, builder, true);
        }

        builder.Append(",");
    }
    builder.Replace(",", ")", builder.Length - 1, 1);
}
domaindrivendev commented 7 years ago

Ultimately, I don't think this is related to the use of a generic parameter. SB supports generic parameters and return types and has passing tests to prove it:

https://github.com/domaindrivendev/Swashbuckle/blob/v5.6.0/Swashbuckle.Tests/Swagger/XmlCommentsTests.cs#L192

I think it's actually due to the use of inherited/abstract actions. By design, SB will not pull XML comments from inherited actions. Doing so would add a ton of complexity and have a significant impact on performance. Furthermore, I don't think it makes a ton of sense. Abstract/generic actions can, by their nature, only be accompanied by fairly abstract descriptions and I don't think these are particularly suitable for describing concrete API actions. "Gets the entity by the given key" isn't a particularly useful description.

With that said, I would recommend using a simple OperationFilter instead of XML comments to describe specific implementations of generic actions. Check out the following for an example. It's from the ASP.NET Core-specific SB project, has some minor syntactic differences, but should give you the idea:

https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/v1.0.0/test/WebSites/GenericControllers/Swagger/ApplySummariesOperationFilter.cs

vdbg commented 7 years ago

Hi,

I'm hitting a somewhat related issue. I've been able to narrow down the repro to:

  1. member function being part of a templated abstract class and
  2. presence of a nullable type in the function's parameters: reference and value type parameters work fine - only nullable value types don't.

code snippets:

        /// <summary>Gets a blob</summary>
        /// <param name="id">Blob Id</param>
        /// <param name="includes">Comma separated list of what to include, for example "ContentInfo,Description". Defaults to None</param>
        /// <returns>The blob's metadata</returns>
        [Route("blobsfail/{id}")]
        [HttpGet]
        [ResponseType(typeof(BlobMetadataRetrieve))]
        public IHttpActionResult RetrieveBlobMetadataByIdFail(Guid id, BlobMetadataIncludes? includes = null)
        {
            return RunGet(() => blobRepo.RetrieveById(id, includes.GetValueOrDefault(BlobMetadataIncludes.None)));
        }

        /// <summary>Gets a blob</summary>
        /// <param name="id">Blob Id</param>
        /// <param name="includes">Comma separated list of what to include, for example "ContentInfo,Description". Defaults to None</param>
        /// <returns>The blob's metadata</returns>
        [Route("blobsok/{id}")]
        [HttpGet]
        [ResponseType(typeof(BlobMetadataRetrieve))]
        public IHttpActionResult RetrieveBlobMetadataByIdOk(Guid id, BlobMetadataIncludes includes = BlobMetadataIncludes.None)
        {
            return RunGet(() => blobRepo.RetrieveById(id, includes));
        }

This is the resulting xmldoc:

        <member name="M:AssemblyName.Controllers.BlobsController`1.RetrieveBlobMetadataByIdFail(System.Guid,System.Nullable{AssemblyName.DataModel.BlobMetadataIncludes})">
            <summary>Gets a blob</summary>
            <param name="id">Blob Id</param>
            <param name="includes">Comma separated list of what to include, for example "ContentInfo,Description". Defaults to None</param>
            <returns>The blob's metadata</returns>
        </member>
        <member name="M:AssemblyName.Controllers.BlobsController`1.RetrieveBlobMetadataByIdOk(System.Guid,AssemblyName.DataModel.BlobMetadataIncludes)">
            <summary>Gets a blob</summary>
            <param name="id">Blob Id</param>
            <param name="includes">Comma separated list of what to include, for example "ContentInfo,Description". Defaults to None</param>
            <returns>The blob's metadata</returns>
        </member>

The blobsfail route does not get its documentation populated (route summary and doc for parameters), while the blobsok one gets it. Screenshot:

image

Upgrading to the latest version on NuGet.org (5.6.0) did not help.

Any ideas or workarounds? Routing does not like value types with defaults, which is why we have to use nullable. Nullable works fine as long as they are not parameters of functions in templated classes.

Thanks,

Greg

heldersepu commented 7 years ago

@vdbg I tried to reproduce with your comments... since you didn't share the code for the "templated abstract class" I replace it with an int ...and it works fine for me: http://swashbuckletest.azurewebsites.net/swagger/ui/index?filter=Blob Code behind that is here: https://github.com/heldersepu/SwashbuckleTest/commit/7817595a15c3e88999cc0e525ee69a9ac2b2ea10

vdbg commented 7 years ago

@heldersepu : updated the repro to add the "member function being part of a templated abstract class" part (both this and nullable param are required)

https://github.com/vdbg/SwashbuckleTest/commit/7d4f48f41f01904115022699f72c3ab7b1dafdf9

image

heldersepu commented 7 years ago

@vdbg Thanks that gets me a lot closer, I'm trying to get a minimal Controller reproducing this issue... So far it looks like that inheritance is not the problem https://github.com/heldersepu/SwashbuckleTest/commit/a356655fee6c3575e50f367b4a674754d54334d6

heldersepu commented 7 years ago

@vdbg Here is my minimal case to reproduce your scenario:

using System.Web.Http;

namespace Swagger_Test.Controllers
{
    public abstract class Blob<T> : ApiController
    {
        /// <summary> Get a Bad Blob </summary>
        public string GetBad(int? x) { return "Bad"; }

        /// <summary> Get an Ok Blob </summary>
        public string PostOk(int x) { return "Ok"; }
    }

    public class Foo { }

    public class BlobController : Blob<Foo> { }
}

The problem is the combination of generics and the nullable param, If I remove any of those it work for me... Now the real troubleshooting can start!

heldersepu commented 7 years ago

For any one interested on scoring an easy PR the bug is here: https://github.com/domaindrivendev/Swashbuckle/blob/master/Swashbuckle.Core/Swagger/XmlComments/XmlCommentsIdHelper.cs#L75

That Replace must be done only to the args not the whole builder

vdbg commented 7 years ago

Thanks for looking into it ! What are the odds / timelines of having a fix pushed on NuGet.org ?

heldersepu commented 7 years ago

@vdbg I made the fix on my repo, now it loads correctly: http://swashbuckletest.azurewebsites.net/swagger/ui/index?filter=Blob

My package has a different name in nuget: https://www.nuget.org/packages/Swagger-Net/

vdbg commented 7 years ago

@heldersepu : what's the difference between Swagger-Net and Swashbuckle ?

heldersepu commented 7 years ago

Swagger-Net was a fork of Swashbuckle. ...but I decided to detach it and rename it, that way I can fix bugs, add features and do releases at a much faster pace.

If you have time try both post back the differences you see?

yojagad commented 7 years ago

Hello, so I tried the latest nuget package and I'm still hitting the same issue when it comes to enum type parameters (both nullable and non-nullable).

image

I created a minimal repro of the same here...

yojagad/SwashbuckleTest@7d4f48f

heldersepu commented 7 years ago

@yojagad you are bringing something new to the equation. It is not related to generics or the nullable param, I thing is because the nested enum...

heldersepu commented 7 years ago

@yojagad the minimal repro is much appreciated! I should have a solution shortly...

heldersepu commented 7 years ago

@yojagad your issue has been fixed.

yojagad commented 7 years ago

@heldersepu Thank you so much! When approximately will I see the updated version on nuget.org..?

heldersepu commented 7 years ago

@yojagad Done the update version is on nuget.org: https://www.nuget.org/packages/Swagger-Net/8.3.3.103

yojagad commented 7 years ago

Thanks @heldersepu . The fix works for the issue I mentioned.. I did uncover one another issue with respect to generic type parameters though. Added minimal repro for the same.

yojagad/SwashbuckleTest@a5099d75

heldersepu commented 7 years ago

@yojagad I made the correction on the MyGet package, when you have time please verify and I will update Nuget

yojagad commented 7 years ago

@heldersepu I tested it out from MyGet. It works. Thank you!

heldersepu commented 7 years ago

@yojagad I just uploaded to Nuget give it a few minutes for the indexing to complete... If you find any other issue please report it directly on Swagger-Net

yojagad commented 7 years ago

@heldersepu Will do, thanks again!

PaulDotNet commented 6 years ago

XmlComments functionality need some serious refactoring. First it should be user-replaceable so that quick bugs could be fixed by users without need to go through slow and unpredictable swashbckle update process.

heldersepu commented 6 years ago

@PaulDotNet remember this project is OpenSource! everything is user-replaceable.

PaulDotNet commented 6 years ago

Well, if fork goes too far from the original then updating becomes problematic. I am using my own fork currently and merging changes from GitHub is not fun. Significant change may break my fork as well. Forking is a short term solution. This is the reason why I stay away from forked nugget packages. Their updates stop after a while.

heldersepu commented 6 years ago

Sorry @PaulDotNet but I do not see you have a fork of this project... You do have one of Swashbuckle.AspNetCore that is a different project

nowakptr commented 6 years ago

Hello, I'm having a similar problem where if a controller method has a generic model as [FromBody] parameter, if type of that model is set to nullable, the swagger documentation is not produced:

public ValidationMessage ValidateDateOfBirth([FromBody] SingleInput<DateTime> input) works fine, but

public ValidationMessage ValidateDateOfBirth([FromBody] SingleInput<DateTime?> input) doesn't work ;<

I'm on version 8.3.7.2

The xml: <member name="M:Api.Controllers.ValidationController.ValidateDateOfBirth(Api.Models.SingleInput{System.DateTime})">

<member name="M:Api.Controllers.ValidationController.ValidateDateOfBirth(Api.Models.SingleInput{System.Nullable{System.DateTime}})">

domaindrivendev commented 6 years ago

@PaulDotNet - XML functionality is user replaceable. It’s implemented as an IOperationFilter and ISchemaFilter which are registered when you call “IncludeXmlComments”. If you want to override, don’t call this and register derived versions or wholesale replacements manually

rheynen commented 2 years ago

Following - I am also having the same issue...

duaneking commented 2 years ago

Just ran into this.

MamboJoel commented 1 year ago

Same here.

Whenever I add a generic route in a generic abstact controller that is concretly derived for a specific type and hit F5 I get a error:

image

Kampfmoehre commented 4 months ago

I am not using any abstract classes, I am just trying to make it possible to use different subtypes when posting to a controller action. I have set up a minimal repo here. When starting with dotnet run and navigating to http://localhost:5085/swagger/index.html one can see that there is only one endpoint with only the base class in the docs but the other with the generic type param is missing. How can I make it work with a generic request body parameter?