Swashbuckle.WebApi icon indicating copy to clipboard operation
Swashbuckle.WebApi copied to clipboard

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

Open brittonbeckham opened this issue 9 years ago • 47 comments
trafficstars

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.

brittonbeckham avatar May 06 '16 22:05 brittonbeckham

I have the same issue!

mikeandike avatar May 06 '16 23:05 mikeandike

Same issue here. When is 6.0.0 planned for?

kalski avatar Jan 26 '17 08:01 kalski

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

rythmicMonk avatar Feb 10 '17 16:02 rythmicMonk

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

zibrohimov avatar Aug 09 '17 07:08 zibrohimov

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

heldersepu avatar Aug 09 '17 13:08 heldersepu

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";
                }
            }
        }
    }

MikeDPeterson avatar Aug 09 '17 16:08 MikeDPeterson

@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?

heldersepu avatar Aug 09 '17 16:08 heldersepu

@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.

zibrohimov avatar Aug 09 '17 17:08 zibrohimov

@zibrohimov Can you add the project to GitHub?

heldersepu avatar Aug 09 '17 17:08 heldersepu

@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

zibrohimov avatar Aug 09 '17 17:08 zibrohimov

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

heldersepu avatar Aug 09 '17 17:08 heldersepu

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

zibrohimov avatar Aug 09 '17 18:08 zibrohimov

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 avatar Aug 10 '17 00:08 heldersepu

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

heldersepu avatar Aug 10 '17 00:08 heldersepu

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);
}

dweggemans avatar Aug 30 '17 08:08 dweggemans

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

domaindrivendev avatar Sep 13 '17 21:09 domaindrivendev

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

vdbg avatar Sep 20 '17 22:09 vdbg

@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

heldersepu avatar Sep 21 '17 01:09 heldersepu

@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

vdbg avatar Sep 21 '17 04:09 vdbg

@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 avatar Sep 21 '17 12:09 heldersepu

@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 avatar Sep 21 '17 13:09 heldersepu

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

heldersepu avatar Sep 21 '17 17:09 heldersepu

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

vdbg avatar Sep 25 '17 21:09 vdbg

@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/

heldersepu avatar Sep 25 '17 21:09 heldersepu

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

vdbg avatar Sep 27 '17 15:09 vdbg

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?

heldersepu avatar Sep 27 '17 16:09 heldersepu

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

yojagad avatar Oct 10 '17 23:10 yojagad

@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 avatar Oct 11 '17 00:10 heldersepu

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

heldersepu avatar Oct 11 '17 00:10 heldersepu

@yojagad your issue has been fixed.

heldersepu avatar Oct 13 '17 14:10 heldersepu