dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
34.87k stars 9.85k forks source link

Microsoft.AspNetCore.OpenApi should allow overriding the schema for specific types #56448

Open michael-wolfenden opened 2 weeks ago

michael-wolfenden commented 2 weeks ago

Is there an existing issue for this?

Is your feature request related to a problem? Please describe the problem.

Using Microsoft.AspNetCore.OpenApi -Version 9.0.0-preview.5.24306.11

The following code:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();

var app = builder.Build();
app.MapOpenApi();

app.MapGet("/{orderId:long}", (long orderId) => orderId);

app.Run();

correctly generates an OpenAPI specification with an int64 parameter and an int64 response

{
  "openapi": "3.0.1",
  "info": {
    "title": "...",
    "version": "1.0.0"
  },
  "paths": {
    "/{orderId}": {
      "get": {
        "tags": [],
        "parameters": [
          {
            "name": "orderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "integer",
                  "format": "int64"
                }
              }
            }
          }
        }
      }
    }
  },
  "tags": []
}

If I want to use a strongly typed Id for OrderId instead of a primitive,

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();

var app = builder.Build();
app.MapOpenApi();

app.MapGet("/{orderId:long}", (OrderId orderId) => orderId);

app.Run();

public readonly record struct OrderId(long Value) : IParsable<OrderId>;

The generated OpenAPI specification has a with an int64 / object hybrid parameter (I assume due the long constraint) and an object response.

{
  "openapi": "3.0.1",
  "info": {
    "title": "...",
    "version": "1.0.0"
  },
  "paths": {
    "/{orderId}": {
      "get": {
        "tags": [],
        "parameters": [
          {
            "name": "orderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "properties": {
                "value": {
                  "type": "integer",
                  "format": "int64"
                }
              },
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "value": {
                      "type": "integer",
                      "format": "int64"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "tags": []
}

I would prefer to map OrderId as a primitive int64 as per the first example.

Describe the solution you'd like

Squashbuckle has the ability to override the schema for specific types, for example

services.AddSwaggerGen(c =>
{
    ...
    c.MapType<OrderId>(() => new OpenApiSchema { Type = "integer" });
};

I would like to see the same functionality in Microsoft.AspNetCore.OpenApi

Additional context

No response

captainsafia commented 1 week ago

@michael-wolfenden Thanks for sharing this idea! I definitely see the value of a MapType API, however, I don't think that's something we'll include out-of-the-box in the Microsoft.AspNetCore.OpenApi package.

It's possible to model the behavior of a MapType method using schema transformers, like in the code sample below. This also gives you some extra abilities, like being able to modify what schemaFunc looks like.

I'll mark this is a docs-related issue with the goal of implementing a MapType implementation in a sample. In the future, you're welcome to use its source in your own code or ship a helper package for it.

Let me know if you have any questions about this!

public static OpenApiOptions MapType<T>(this OpenApiOptions options, Action<OpenApiSchema> schemaFunc)
{
    return options.UseSchemaTransformer((schema, context, ct) =>
    {
        if (context.Type == typeof(T))
        {
            var targetSchema = schemaFunc();
            schema.Type = targetSchema.Type;
        }
        return Task.CompletedTask;
    });
}