'Swagger c# ProduceResponseType for Wrapped AutoWrapper Response

As all of our responses are wrapped in AutoWrapper,

what is the best way to tell swagger that they are wrapped by AutoWrapper ?

Instead of adding this on every controller methods ?

[ProducesResponseType( 200, Type = typeof( AutoWrapper<IEnumerable<WeatherForecast>> ) )]


Solution 1:[1]

You can minimize your effort and create your own API Convention and apply it to your assembly in Startup.cs.

Test model:

public class MyTestModel
{
    public string SomeString { get; set; }
    public int SomeInteger { get; set; }
    public bool SomeBool { get; set; }
}

Response Wrapper (inspired from here):

public class AutoWrapper<T>
{
    public int StatusCode { get; set; }
    public string Message { get; set; }
    public T Result { get; set; }
}

Convention Class:

public static class MyAppConventions
{
    [ProducesResponseType(200, Type = typeof(AutoWrapper<List<MyTestModel>>))]
    [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Suffix)]
    public static void Weather()
    {
    }
}

Controller:

[HttpGet]
public List<MyTestModel> GetWeather()
{
    List<MyTestModel> models = new List<MyTestModel>
    {
        new MyTestModel
        {
            SomeString = "Hello world!",
            SomeInteger = 101,
            SomeBool = true
        }
    };

    return models;
}

Lastly, in the Startup, you need to add a decorator:

[assembly: ApiConventionType(typeof(MyAppConventions))]
namespace NameSpace
{
    public class Startup
    {
      //stuff...
    }
}

In Swagger, this will get illustrated as:

Swagger

Schema view

This approach however some-what relies on a naming standard being established across your controller method names in order to minimize the amount of code you'll need to write in the Convention class. You can use the ApiConventionNameMatch attribute to match on a suffix, prefix, any method name (ignores parameters), and exact name (parameter or method must match). For example, you can cover all of your controller methods in your project that are named Get(int id) by declaring the following in the convention class:

[ProducesResponseType(200, Type = typeof(AutoWrapper<YourObject>))]
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
public static void Get(int id)
{
}

Solution 2:[2]

This Swagger IOperationFilter does the trick for me, it creates an AutoWrapper type for each Operation.

public class ResponseWrapperOperationFilter : IOperationFilter
{
    public void Apply ( OpenApiOperation operation, OperationFilterContext context )
    {
        //eg. IEnumerable<WeatherForecast>
        var responseType = context.ApiDescription.SupportedResponseTypes[0].Type;

        //eg. AutoWrapper<IEnumerable<WeatherForecast>>
        var wrappedResponseType = typeof( AutoWrapperDefinition<> ).MakeGenericType( responseType );

        //new schema, TODO is to check if schema exists
        var schema = context.SchemaGenerator.GenerateSchema(
            wrappedResponseType,
            context.SchemaRepository
            );

        var openApiResponse = new OpenApiResponse
        {
            Content = new Dictionary<string, OpenApiMediaType>()
            {
                ["application/json"] = new OpenApiMediaType()
                {
                    Schema = schema
                }
            },
        };
        operation.Responses.Clear();
        operation.Responses.Add( "200", openApiResponse );
        //TODO: add Response for other status code
    }
}

Solution 3:[3]

Added the following improvements to Lydon Ch answer:

  • Check for existing schema before creating
  • Only remove the 200 response instead of all responses:
public class ResponseWrapperOperationFilter : IOperationFilter
{
    public void Apply ( OpenApiOperation operation, OperationFilterContext context )
    {
        //eg. IEnumerable<WeatherForecast>
        var responseType = context.ApiDescription.SupportedResponseTypes[0].Type;

        //eg. AutoWrapper<IEnumerable<WeatherForecast>>
        var wrappedResponseType = typeof( AutoWrapperDefinition<> ).MakeGenericType( responseType );

        //Added - To Check for existing Schema first
        context.SchemaRepository.TryLookupByType(wrappedResponseType, out var schema );

        if(schema == null)
        {
            schema = context.SchemaGenerator.GenerateSchema(
                wrappedResponseType,
                context.SchemaRepository
            );
        }

        var openApiResponse = new OpenApiResponse
        {
            Content = new Dictionary<string, OpenApiMediaType>()
            {
                ["application/json"] = new OpenApiMediaType()
                {
                    Schema = schema
                }
            },
        };

        operation.Responses.Remove(StatusCodes.Status200OK.ToString());
        operation.Responses.Add(StatusCodes.Status200OK.ToString(), openApiResponse);
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Timothy G.
Solution 2 Lydon Ch
Solution 3 Ryan Gaudion