Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

5.0.0 - How to Write ISchemaFilter for ProblemDetails? #985

Closed
RehanSaeed opened this issue Dec 23, 2018 · 8 comments
Closed

5.0.0 - How to Write ISchemaFilter for ProblemDetails? #985

RehanSaeed opened this issue Dec 23, 2018 · 8 comments

Comments

@RehanSaeed
Copy link
Contributor

RehanSaeed commented Dec 23, 2018

I'm trying to write a ISchemaFilter for ProblemDetails in ASP.NET Core 3.0. ProblemDetails is returned for a lot of different status codes e.g. 400, 500 etc. How can I provide a different example of ProblemDetails depending on the status code?

public class ProblemDetailsSchemaFilter : ISchemaFilter
{
    private static readonly OpenApiObject Status400ProblemDetails = new OpenApiObject()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc7231#section-6.5.1"),
        ["title"] = new OpenApiString("Bad Request"),
        ["status"] = new OpenApiInteger(StatusCodes.Status400BadRequest),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
        ["errors"] = new OpenApiObject()
        {
            ["property1"] = new OpenApiArray()
            {
                new OpenApiString("The property field is required"),
            },
        },
    };

    private static readonly OpenApiObject Status401ProblemDetails = new OpenApiObject()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc7235#section-3.1"),
        ["title"] = new OpenApiString("Unauthorized"),
        ["status"] = new OpenApiInteger(StatusCodes.Status401Unauthorized),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
    };

    private static readonly OpenApiObject Status403ProblemDetails = new OpenApiObject()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc7231#section-6.5.3"),
        ["title"] = new OpenApiString("Forbidden"),
        ["status"] = new OpenApiInteger(StatusCodes.Status403Forbidden),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
    };

    private static readonly OpenApiObject Status404ProblemDetails = new OpenApiObject()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc7231#section-6.5.4"),
        ["title"] = new OpenApiString("Not Found"),
        ["status"] = new OpenApiInteger(StatusCodes.Status404NotFound),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
    };

    private static readonly OpenApiObject Status406ProblemDetails = new OpenApiObject()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc7231#section-6.5.6"),
        ["title"] = new OpenApiString("Not Acceptable"),
        ["status"] = new OpenApiInteger(StatusCodes.Status406NotAcceptable),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
    };

    private static readonly OpenApiObject Status409ProblemDetails = new OpenApiObject()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc7231#section-6.5.8"),
        ["title"] = new OpenApiString("Conflict"),
        ["status"] = new OpenApiInteger(StatusCodes.Status409Conflict),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
    };

    private static readonly OpenApiObject Status415ProblemDetails = new OpenApiObject()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc7231#section-6.5.13"),
        ["title"] = new OpenApiString("Unsupported Media Type"),
        ["status"] = new OpenApiInteger(StatusCodes.Status415UnsupportedMediaType),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
    };

    private static readonly OpenApiObject Status422ProblemDetails = new OpenApiObject()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc4918#section-11.2"),
        ["title"] = new OpenApiString("Unprocessable Entity"),
        ["status"] = new OpenApiInteger(StatusCodes.Status422UnprocessableEntity),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
    };

    private static readonly OpenApiObject Status500ProblemDetails = new OpenApiObject()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc7231#section-6.6.1"),
        ["title"] = new OpenApiString("Internal Server Error"),
        ["status"] = new OpenApiInteger(StatusCodes.Status500InternalServerError),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
    };

    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        if (context.ApiModel.Type == typeof(ProblemDetails))
        {
            // TODO: Set the default and example based on the status code.
            // schema.Default = ProblemDetails;
            // schema.Example = ProblemDetails;
        }
    }
}
@RehanSaeed RehanSaeed changed the title How to get ISchemaFilter Status Code? How to Write ISchemaFilter for ProblemDetails in Swashbuckle 5? Nov 3, 2019
@RehanSaeed RehanSaeed changed the title How to Write ISchemaFilter for ProblemDetails in Swashbuckle 5? 5.0.0 - How to Write ISchemaFilter for ProblemDetails? Nov 3, 2019
@RehanSaeed
Copy link
Contributor Author

@domaindrivendev Is this possible?

RehanSaeed added a commit to Dotnet-Boxed/Framework that referenced this issue Nov 11, 2019
# Boxed.AspNetCore

- Upgrade to .NET Core 3 SDK and target `netcoreapp3.0`.
- Switch from NuGet `PackageIconUrl` to `PackageIcon`, so the icon is now embedded in the package.
- Upgrade `Micorosoft.Extensions.*` NuGet packages to `3.0.0`.
- Switch from `Newtonsoft.Json` to `System.Text.Json`.
- `DistributedCacheExtensions` now uses `System.Text.Json` and serializes directly to UTF8 for performance reasons.
- Use Ordinal string comparisons.
- Switch from `IHostingEnvironment` to `IWebHostEnvironment`.
- `RedirectToCanonicalUrlRule` implements `Microsoft.AspNetCore.Rewrite.IRule` and can be used to redirect to a single canonical URL. This used to be an MVC filter.
- `HttpExceptionMiddleware` now implements `IMiddleware`.
- Remove site map code. Look at the source code for the The ASP.NET Core standup site for how to do this in a better way.
- Remove `UrlHelperExtensions`. Use `LinkGenerator` instead.
- Remove `ConfigurationExtensions.GetSection<T>`. Use `IConfiguration.Get` instead.
- Remove `InternalServerErrorOnExceptionMiddleware` since it was unused.
- Remove `NoCacheAttribute`.

# Boxed.AspNetCore.Swagger

- Upgrade to .NET Core 3 SDK and target `netcoreapp3.0`.
- Upgrade to `Swashbuckle.AspNetCore` version `5.0.0-rc4` for Open API 3 support.
- Switch from NuGet `PackageIconUrl` to `PackageIcon`, so the icon is now embedded in the package.
- Added `CorrelationIdOperationFilter` operation filter.
- Added `ProblemDetails` schema filter but this is not yet working so it's internal for now. See domaindrivendev/Swashbuckle.AspNetCore#985.
- Removed `ModelStateDictionary`'s schema filter.

# Boxed.AspNetCore.TagHelpers

- Upgrade to .NET Core 3 SDK and target `netcoreapp3.0`.
- Switch from NuGet `PackageIconUrl` to `PackageIcon`, so the icon is now embedded in the package.

# Boxed.DotnetNewTest

- Upgrade to .NET Core 3 SDK and target `netstandard2.1` and `netcoreapp3.0`.
- Switch from NuGet `PackageIconUrl` to `PackageIcon`, so the icon is now embedded in the package.
- Use Ordinal file path string comparisons.
- Move experimental `DotnetRunInMemoryAsync` to be `netcoreapp3.0` only due to API limitations.

# Boxed.Mapping

- Upgrade to .NET Core 3 SDK and target `netstandard1.3`, `netstandard2.0` and `netstandard2.1`.
- Switch from NuGet `PackageIconUrl` to `PackageIcon`, so the icon is now embedded in the package.
- Add extension methods adding support for `IAsyncEnumerable<T>`.
@kaylumah
Copy link

@RehanSaeed did you find a solution for this?

@RehanSaeed
Copy link
Contributor Author

Not yet unfortunately.

@kaylumah
Copy link

@RehanSaeed
Something like this does work,

using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace Platform.Client.Gateway.Utilities
{
    public class AuthorizeCheckOperationFilter : IOperationFilter
    {
        private static readonly OpenApiObject _status401Object = new OpenApiObject()
        {
            ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc7235#section-3.1"),
            ["title"] = new OpenApiString("Unauthorized"),
            ["status"] = new OpenApiInteger(StatusCodes.Status401Unauthorized),
            ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
        };

        public void Apply(OpenApiOperation operation, OperationFilterContext context)

        {
            var hasAuthorize =
          context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any()
          || context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();

            if (hasAuthorize)
            {
                var openApi401Response = new OpenApiResponse
                {
                    Description = "Unauthorized",
                    Content = new Dictionary<string, OpenApiMediaType>()
                    {
                        {
                            $"application/problem+json", new OpenApiMediaType()
                            {
                                Schema = new OpenApiSchema {
                                    Default = _status401Object,
                                    Example = _status401Object
                                }
                            }
                        }
                    }
                };

                operation.Responses.Add($"{StatusCodes.Status401Unauthorized}", openApi401Response);
                // operation.Responses.Add($"{StatusCodes.Status403Forbidden}", _openApi403Response);

                var oAuthScheme = new OpenApiSecurityScheme
                {
                    Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
                };
                operation.Security = new List<OpenApiSecurityRequirement>
                {
                new OpenApiSecurityRequirement
                    {
                        [ oAuthScheme ] = new List<string>()
                    }
                };
            }
        }
    }
}

But I am not sure if this is the way to go in Swagger, and it only handles 401s and 403
Note this is an extension on the sample in the docs about operation filter, and uses the schemas you provided

@domaindrivendev
Copy link
Owner

In Swagger / OpenAPI, Schemas are a lower level concept that don't have knowledge of status codes etc. So, this isn't something that can be done at that level. As noted above, you need to use an IOperationFilter for this.

@RehanSaeed
Copy link
Contributor Author

Is it possible to get hold of the status code in an IOperationFilter?

@kaylumah
Copy link

@RehanSaeed as you can see in my example code you can use StatusCodes.Status401Unauthorized. You do these operation filters when generating the documentation spec. This is not done at runtime that the response changes. What it does is document it as an allowed response with a specific scheme, and it is not responsible for sending the response :-)

@CumpsD
Copy link

CumpsD commented Mar 8, 2023

This works:

using System.Collections.Generic;
using System.Net.Mime;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

public class ProblemDetailsOperationFilter : IOperationFilter
{
    private static readonly OpenApiObject _status400ProblemDetails = new()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc7231#section-6.5.1"),
        ["title"] = new OpenApiString(ReasonPhrases.GetReasonPhrase(StatusCodes.Status400BadRequest)),
        ["status"] = new OpenApiInteger(StatusCodes.Status400BadRequest),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
        ["errors"] = new OpenApiObject
        {
            ["property1"] = new OpenApiArray
            {
                new OpenApiString("The property field is required"),
            },
        },
    };

    private static readonly OpenApiObject _status401ProblemDetails = new()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc7235#section-3.1"),
        ["title"] = new OpenApiString(ReasonPhrases.GetReasonPhrase(StatusCodes.Status401Unauthorized)),
        ["status"] = new OpenApiInteger(StatusCodes.Status401Unauthorized),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
    };

    private static readonly OpenApiObject _status403ProblemDetails = new()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc7231#section-6.5.3"),
        ["title"] = new OpenApiString(ReasonPhrases.GetReasonPhrase(StatusCodes.Status403Forbidden)),
        ["status"] = new OpenApiInteger(StatusCodes.Status403Forbidden),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
    };

    private static readonly OpenApiObject _status404ProblemDetails = new()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc7231#section-6.5.4"),
        ["title"] = new OpenApiString(ReasonPhrases.GetReasonPhrase(StatusCodes.Status404NotFound)),
        ["status"] = new OpenApiInteger(StatusCodes.Status404NotFound),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
    };

    private static readonly OpenApiObject _status406ProblemDetails = new()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc7231#section-6.5.6"),
        ["title"] = new OpenApiString(ReasonPhrases.GetReasonPhrase(StatusCodes.Status406NotAcceptable)),
        ["status"] = new OpenApiInteger(StatusCodes.Status406NotAcceptable),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
    };

    private static readonly OpenApiObject _status409ProblemDetails = new()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc7231#section-6.5.8"),
        ["title"] = new OpenApiString(ReasonPhrases.GetReasonPhrase(StatusCodes.Status409Conflict)),
        ["status"] = new OpenApiInteger(StatusCodes.Status409Conflict),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
    };

    private static readonly OpenApiObject _status415ProblemDetails = new()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc7231#section-6.5.13"),
        ["title"] = new OpenApiString(ReasonPhrases.GetReasonPhrase(StatusCodes.Status415UnsupportedMediaType)),
        ["status"] = new OpenApiInteger(StatusCodes.Status415UnsupportedMediaType),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
    };

    private static readonly OpenApiObject _status422ProblemDetails = new()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc4918#section-11.2"),
        ["title"] = new OpenApiString(ReasonPhrases.GetReasonPhrase(StatusCodes.Status422UnprocessableEntity)),
        ["status"] = new OpenApiInteger(StatusCodes.Status422UnprocessableEntity),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
    };

    private static readonly OpenApiObject _status500ProblemDetails = new()
    {
        ["type"] = new OpenApiString("https://tools.ietf.org/html/rfc7231#section-6.6.1"),
        ["title"] = new OpenApiString(ReasonPhrases.GetReasonPhrase(StatusCodes.Status500InternalServerError)),
        ["status"] = new OpenApiInteger(StatusCodes.Status500InternalServerError),
        ["traceId"] = new OpenApiString("00-982607166a542147b435be3a847ddd71-fc75498eb9f09d48-00"),
    };

    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        var problemDetails = new Dictionary<string, IOpenApiAny>
        {
            { StatusCodes.Status400BadRequest.ToString(), _status400ProblemDetails },
            { StatusCodes.Status401Unauthorized.ToString(), _status401ProblemDetails },
            { StatusCodes.Status403Forbidden.ToString(), _status403ProblemDetails },
            { StatusCodes.Status404NotFound.ToString(), _status404ProblemDetails },
            { StatusCodes.Status406NotAcceptable.ToString(), _status406ProblemDetails },
            { StatusCodes.Status409Conflict.ToString(), _status409ProblemDetails },
            { StatusCodes.Status415UnsupportedMediaType.ToString(), _status415ProblemDetails },
            { StatusCodes.Status422UnprocessableEntity.ToString(), _status422ProblemDetails },
            { StatusCodes.Status500InternalServerError.ToString(), _status500ProblemDetails },
        };

        foreach (var operationResponse in operation.Responses)
        {
            if (problemDetails.TryGetValue(operationResponse.Key, out var problemDetail))
                operationResponse.Value.Content[MediaTypeNames.Application.Json].Example = problemDetail;
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants