Skip to content

Instantly share code, notes, and snippets.

@PROGrand
Last active April 16, 2024 21:45
Show Gist options
  • Save PROGrand/018aff221ed7a87992e0925815147381 to your computer and use it in GitHub Desktop.
Save PROGrand/018aff221ed7a87992e0925815147381 to your computer and use it in GitHub Desktop.
SwaggerResponse and ProducesResponseType attributes checking

SwaggerResponse and ProducesResponseType attributes checking

You can use this extension in dotnet (.NET 6 in my case) for enforcing developers to sync OpenAPI (Swagger) descriptions with implementations of methods.

3 different usage choices

Across all app controllers:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
        app.UseSwaggerResponseCheck();
  //...
}

Per controller action using ValidateStatusCodes attribute:

[ApiController]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
    [HttpGet]
    [ValidateStatusCodes] // <-- Use this
    [SwaggerOperation("LoginUser")]
    [SwaggerResponse(statusCode: StatusCodes.Status200OK, type: null, description: "signed user email account")]
    [SwaggerResponse(statusCode: StatusCodes.Status400BadRequest, type: null, description: "wrong email or password")]
    [Route("/users/login")]
    public virtual IActionResult LoginUser([FromQuery][Required()] string email, [FromQuery] string password)
    {
            if (email == "[email protected]")
              return Ok("success");
            else if (email == "")
              return BadRequest("email required");
            else
              return NotFound("user not found"); // 500 - InternalServerError because not attributed with SwaggerResponse.
    }
    // ...
    [HttpGet]
    [ValidateStatusCodes] // <-- Use this
    [ProducesResponseType(type: typeof(Account), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [Route("/users/login2")]
    public virtual IActionResult LoginUser2([FromQuery][Required()] string email, [FromQuery] string password)
    {
            if (email == "[email protected]")
              return Ok("success").Validate();
            else if (email == "")
              return BadRequest("email required").Validate();
            else
              return NotFound("user not found").Validate(); // Throws error in DEBUG or Development.
    }
}

Per result using IStatusCodeActionResult.Validate():

[ApiController]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
    [HttpGet]
    [SwaggerOperation("LoginUser")]
    [SwaggerResponse(statusCode: StatusCodes.Status200OK, type: null, description: "signed user email account")]
    [SwaggerResponse(statusCode: StatusCodes.Status400BadRequest, type: null, description: "wrong email or password")]
    [Route("/users/login")]
    public virtual IActionResult LoginUser([FromQuery][Required()] string email, [FromQuery] string password)
    {
            if (email == "[email protected]")
              return Ok("success").Validate();
            else if (email == "")
              return BadRequest("email required").Validate();
            else if (email == "secret")
              return Unauthorized("hello");
                 // Passed, independent of SwaggerResponse attribute.
            else
              return NotFound("user not found").Validate();
                 // 500 - InternalServerError because not attributed with SwaggerResponse.
    }
    // ...
    [HttpGet]
    [ProducesResponseType(type: typeof(Account), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [Route("/users/login2")]
    public virtual IActionResult LoginUser2([FromQuery][Required()] string email, [FromQuery] string password)
    {
            if (email == "[email protected]")
              return Ok("success").Validate();
            else if (email == "")
              return BadRequest("email required").Validate();
            else
              return NotFound("user not found").Validate(); // Throws error in DEBUG or Development.
    }
}
[ApiController]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
// SwaggerResponse supported, i prefer old one.
[HttpGet]
[SwaggerOperation("LoginUser")]
[SwaggerResponse(statusCode: StatusCodes.Status200OK, type: null, description: "signed user email account")]
[SwaggerResponse(statusCode: StatusCodes.Status400BadRequest, type: null, description: "wrong email or password")]
[Route("/users/login")]
public virtual IActionResult LoginUser([FromQuery][Required()] string email, [FromQuery] string password)
{
if (email == "[email protected]")
return Ok("success").Validate();
else if (email == "")
return BadRequest("email required").Validate();
else
return NotFound("user not found").Validate(); // Throws error in DEBUG or Development.
}
// Modern ProducesResponseType supported.
[HttpGet]
[ProducesResponseType(type: typeof(Account), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Route("/users/login2")]
public virtual IActionResult LoginUser2([FromQuery][Required()] string email, [FromQuery] string password)
{
if (email == "[email protected]")
return Ok("success").Validate();
else if (email == "")
return BadRequest("email required").Validate();
else
return NotFound("user not found").Validate(); // Throws error in DEBUG or Development.
}
}
using Microsoft.AspNetCore.Http.Features;
using Swashbuckle.AspNetCore.Annotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Controllers;
namespace SwaggerExtensions;
public class ValidateStatusCodesAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext context)
{
bool check = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development";
#if DEBUG
check = true;
#endif
if (check)
{
if (context.Result is Microsoft.AspNetCore.Mvc.Infrastructure.IStatusCodeActionResult result)
{
if (context.ActionDescriptor is ControllerActionDescriptor descriptor)
{
var customAttributes = descriptor.MethodInfo.CustomAttributes.Where(a =>
a.AttributeType.FullName == "Swashbuckle.AspNetCore.Annotations.SwaggerResponseAttribute" ||
a.AttributeType.FullName == "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute");
var args = customAttributes.Select(s => s.ConstructorArguments.Where(arg => arg.ArgumentType == typeof(int)));
var res = args.Any(a => a.Any(v => v.ArgumentType.Name == "Int32" && v.Value!.Equals((int)result!.StatusCode!)));
if (!res)
{
context.Result = new ObjectResult($"OpenAPI specification exception, unsupported status code: {result!.StatusCode}\n" +
$"action: {context.ActionDescriptor.DisplayName}")
{ StatusCode = 500 };
}
}
}
}
}
}
public class SwaggerResponseCheck
{
private readonly RequestDelegate _next;
public SwaggerResponseCheck(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
bool check = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development";
#if DEBUG
check = true;
#endif
if (check)
{
using (var buffer = new MemoryStream())
{
var stream = context.Response.Body;
context.Response.Body = buffer;
await _next(context);
var endpoint = context.Features.Get<IEndpointFeature>()?.Endpoint;
var attributes = endpoint?.Metadata.OfType<SwaggerResponseAttribute>();
if (attributes != null)
{
if (!attributes.Any(a => a.StatusCode == context.Response.StatusCode))
{
context.Response.StatusCode = 500;
buffer.Seek(0, SeekOrigin.Begin);
await context.Response.WriteAsync($"OpenAPI specification exception, unsupported status code: {context.Response.StatusCode}\npath: {context.Request.Path}");
}
}
buffer.Seek(0, SeekOrigin.Begin);
await context.Response.Body.CopyToAsync(stream);
context.Response.Body = stream;
}
}
else
{
await _next(context);
}
}
}
public static class StatusCodeExtensions
{
public static IApplicationBuilder UseSwaggerResponseCheck(this IApplicationBuilder app)
{
return app.UseMiddleware<SwaggerResponseCheck>();
}
public static IActionResult Validate(this Microsoft.AspNetCore.Mvc.Infrastructure.IStatusCodeActionResult result)
{
bool check = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development";
#if DEBUG
check = true;
#endif
if (check)
{
var m = System.Reflection.MethodBase.GetCurrentMethod();
var frames = new System.Diagnostics.StackTrace(false).GetFrames();
var st = Array.FindIndex(frames, frame =>
frame.GetMethod() == m) + 1;
var customAttributes = frames[st].GetMethod()!.CustomAttributes.Where(a =>
a.AttributeType.FullName == "Swashbuckle.AspNetCore.Annotations.SwaggerResponseAttribute" ||
a.AttributeType.FullName == "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute");
var args = customAttributes.Select(s => s.ConstructorArguments.Where(arg => arg.ArgumentType == typeof(int)));
var res = args.Any(a => a.Any(v => v.ArgumentType.Name == "Int32" && v.Value!.Equals((int)result.StatusCode!)));
if (!res)
{
throw new InvalidOperationException($"OpenAPI specification exception, unsupported status code: {result.StatusCode}");
}
}
return result;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment