Azure / azure-functions-openapi-extension

This extension provides an Azure Functions app with Open API capability for better discoverability to consuming parties
https://www.nuget.org/packages/Microsoft.Azure.WebJobs.Extensions.OpenApi/
MIT License
368 stars 193 forks source link

Swashbuckle MapType equivalent #537

Open Econest opened 1 year ago

Econest commented 1 year ago

Describe the issue We are looking at moving our application to Azure functions v4. We were using an App Service beforehand and swagger works correctly there (we added some configurations for a certain 3rd party library). We used Swashbuckle's MapType function to assert that the type should use the custom schema we define and the schema is then referenced from the containing object correctly. I am not sure of the equivalent functionality here if any.

To Reproduce The project file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
    <OutputType>Exe</OutputType>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.10.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.0.13" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.OpenApi" Version="1.5.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.7.0" />
    <PackageReference Include="Microsoft.OpenApi" Version="1.4.5" />
    <PackageReference Include="Microsoft.OpenApi.Readers" Version="1.4.5" />
    <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
    <PackageReference Include="UnitsNet.Serialization.JsonNet" Version="4.6.0" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
  <ItemGroup>
    <Using Include="System.Threading.ExecutionContext" Alias="ExecutionContext" />
  </ItemGroup>
</Project>

The Schema I add:

var quantitySchema = new OpenApiSchema
{
   Properties =
      {
          {
              "unit",
              new OpenApiSchema
              {
                  Title = "Quantity",
                  Type = "string",
                  OneOf = new[]
                  {
                          new OpenApiSchema
                      { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(EnergyUnit) } },
                          new OpenApiSchema
                      { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(MassUnit) } },
                          new OpenApiSchema
                      { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(VolumeUnit) } },
                          new OpenApiSchema
                      { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(LengthUnit) } },
                          new OpenApiSchema
                      { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(AreaUnit) } }
                  }
              }
          },
          { "value", new OpenApiSchema { Type = "number" } }
      }
};

options.DocumentFilters.Add(new CustomModelDocumentFilter<UnitsNet.IQuantity>(quantitySchema));

The filter to add above schema:

public class CustomModelDocumentFilter<T> : IDocumentFilter
{
    private readonly OpenApiSchema openApiSchema;

    public CustomModelDocumentFilter(OpenApiSchema schema)
    {
        this.openApiSchema = schema;
    }

    public void Apply(IHttpRequestDataObject req, OpenApiDocument document)
    {
        document.Components.Schemas.Add(typeof(T).Name, this.openApiSchema);
    }
}

The endpoint and object:

[Function("Upsert")]
[OpenApiOperation(
    operationId: nameof(Upsert),
    Description = "Insert a new Direct Energy record or Update an existing one",
    Visibility = OpenApiVisibilityType.Important)]
[OpenApiRequestBody("application/json", typeof(EnergyRequest))]
[OpenApiParameter(name: "id?", In = ParameterLocation.Path, Required = false, Type = typeof(Guid?))]
public async Task<HttpResponseData> Upsert(
    [HttpTrigger(AuthorizationLevel.Anonymous, "patch", Route = "energy/{id?}")] HttpRequestData requestData,
    Guid? id,
    FunctionContext executionContext)
{
    var response = requestData.CreateResponse(HttpStatusCode.OK);
    response.WriteString(id.ToString());

    return response;
}

public class EnergyRequest
{

    public IQuantity Quantity { get; set; }

    public string Description { get; set; }

    public decimal Cost { get; set; }
}

Sample downloadable here: FunctionApp4.zip

Exception (I believe this occurs as it tries to walk through IQuantity properties instead of simply resolving custom schema that was added) System.InvalidOperationException: 'Sequence contains no elements'

    System.Linq.dll!System.Linq.ThrowHelper.ThrowNoElementsException()  Unknown
>   Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.dll!Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.ListObjectTypeVisitor.Visit(Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Abstractions.IAcceptor acceptor, System.Collections.Generic.KeyValuePair<string, System.Type> type, Newtonsoft.Json.Serialization.NamingStrategy namingStrategy, System.Attribute[] attributes) Line 115   C#
    Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.dll!Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.OpenApiSchemaAcceptor.Accept(Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.VisitorCollection collection, Newtonsoft.Json.Serialization.NamingStrategy namingStrategy) Line 75   C#
    Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.dll!Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.ObjectTypeVisitor.ProcessProperties(Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.IOpenApiSchemaAcceptor instance, string schemaName, System.Collections.Generic.Dictionary<string, System.Reflection.PropertyInfo> properties, Newtonsoft.Json.Serialization.NamingStrategy namingStrategy) Line 189   C#
    Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.dll!Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.ObjectTypeVisitor.Visit(Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Abstractions.IAcceptor acceptor, System.Collections.Generic.KeyValuePair<string, System.Type> type, Newtonsoft.Json.Serialization.NamingStrategy namingStrategy, System.Attribute[] attributes) Line 124   C#
    Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.dll!Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.OpenApiSchemaAcceptor.Accept(Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.VisitorCollection collection, Newtonsoft.Json.Serialization.NamingStrategy namingStrategy) Line 75   C#
    Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.dll!Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.ObjectTypeVisitor.ProcessProperties(Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.IOpenApiSchemaAcceptor instance, string schemaName, System.Collections.Generic.Dictionary<string, System.Reflection.PropertyInfo> properties, Newtonsoft.Json.Serialization.NamingStrategy namingStrategy) Line 189   C#
    Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.dll!Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.ObjectTypeVisitor.Visit(Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Abstractions.IAcceptor acceptor, System.Collections.Generic.KeyValuePair<string, System.Type> type, Newtonsoft.Json.Serialization.NamingStrategy namingStrategy, System.Attribute[] attributes) Line 124   C#
    Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.dll!Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.OpenApiSchemaAcceptor.Accept(Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.VisitorCollection collection, Newtonsoft.Json.Serialization.NamingStrategy namingStrategy) Line 75   C#
    Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.dll!Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.ObjectTypeVisitor.ProcessProperties(Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.IOpenApiSchemaAcceptor instance, string schemaName, System.Collections.Generic.Dictionary<string, System.Reflection.PropertyInfo> properties, Newtonsoft.Json.Serialization.NamingStrategy namingStrategy) Line 189   C#
    Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.dll!Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.ObjectTypeVisitor.Visit(Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Abstractions.IAcceptor acceptor, System.Collections.Generic.KeyValuePair<string, System.Type> type, Newtonsoft.Json.Serialization.NamingStrategy namingStrategy, System.Attribute[] attributes) Line 124   C#
    Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.dll!Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.OpenApiSchemaAcceptor.Accept(Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.VisitorCollection collection, Newtonsoft.Json.Serialization.NamingStrategy namingStrategy) Line 85   C#
    Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.dll!Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.DocumentHelper.GetOpenApiSchemas(System.Collections.Generic.List<System.Reflection.MethodInfo> elements, Newtonsoft.Json.Serialization.NamingStrategy namingStrategy, Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Visitors.VisitorCollection collection) Line 164   C#
    Microsoft.Azure.Functions.Worker.Extensions.OpenApi.dll!Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Document.Build(System.Reflection.Assembly assembly, Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums.OpenApiVersionType version)   Unknown
    Microsoft.Azure.Functions.Worker.Extensions.OpenApi.dll!Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Functions.OpenApiTriggerFunction.RenderSwaggerDocument(Microsoft.Azure.Functions.Worker.Http.HttpRequestData req, string extension, Microsoft.Azure.Functions.Worker.FunctionContext ctx)   Unknown
    Microsoft.Azure.Functions.Worker.Extensions.OpenApi.dll!Microsoft.Azure.Functions.Worker.Extensions.OpenApi.DefaultOpenApiHttpTrigger.RenderSwaggerDocument(Microsoft.Azure.Functions.Worker.Http.HttpRequestData req, string extension, Microsoft.Azure.Functions.Worker.FunctionContext ctx)  Unknown
    [Lightweight Function]  
    Microsoft.Azure.Functions.Worker.Core.dll!Microsoft.Azure.Functions.Worker.Invocation.TaskMethodInvoker<Microsoft.Azure.Functions.Worker.Extensions.OpenApi.DefaultOpenApiHttpTrigger, Microsoft.Azure.Functions.Worker.Http.HttpResponseData>.InvokeAsync(Microsoft.Azure.Functions.Worker.Extensions.OpenApi.DefaultOpenApiHttpTrigger instance, object[] arguments) Line 23  C#
    Microsoft.Azure.Functions.Worker.Core.dll!Microsoft.Azure.Functions.Worker.Invocation.DefaultFunctionInvoker<Microsoft.Azure.Functions.Worker.Extensions.OpenApi.DefaultOpenApiHttpTrigger, Microsoft.Azure.Functions.Worker.Http.HttpResponseData>.InvokeAsync(object instance, object[] arguments) Line 31    C#
    Microsoft.Azure.Functions.Worker.Core.dll!Microsoft.Azure.Functions.Worker.Invocation.DefaultFunctionExecutor.ExecuteAsync(Microsoft.Azure.Functions.Worker.FunctionContext context) Line 44    C#
    Microsoft.Azure.Functions.Worker.Core.dll!Microsoft.Azure.Functions.Worker.Pipeline.FunctionExecutionMiddleware.Invoke(Microsoft.Azure.Functions.Worker.FunctionContext context) Line 20    C#
    Microsoft.Azure.Functions.Worker.Core.dll!Microsoft.Extensions.Hosting.MiddlewareWorkerApplicationBuilderExtensions.UseFunctionExecutionMiddleware.AnonymousMethod__1_2(Microsoft.Azure.Functions.Worker.FunctionContext context) Line 57   C#
    Microsoft.Azure.Functions.Worker.Core.dll!Microsoft.Azure.Functions.Worker.OutputBindings.OutputBindingsMiddleware.Invoke(Microsoft.Azure.Functions.Worker.FunctionContext context, Microsoft.Azure.Functions.Worker.Middleware.FunctionExecutionDelegate next) Line 13 C#
    Microsoft.Azure.Functions.Worker.Core.dll!Microsoft.Extensions.Hosting.MiddlewareWorkerApplicationBuilderExtensions.UseOutputBindingsMiddleware.AnonymousMethod__3(Microsoft.Azure.Functions.Worker.FunctionContext context) Line 84    C#
    Microsoft.Azure.Functions.Worker.Core.dll!Microsoft.Azure.Functions.Worker.FunctionsApplication.InvokeFunctionAsync(Microsoft.Azure.Functions.Worker.FunctionContext context) Line 69   C#
    Microsoft.Azure.Functions.Worker.Grpc.dll!Microsoft.Azure.Functions.Worker.Handlers.InvocationHandler.InvokeAsync(Microsoft.Azure.Functions.Worker.Grpc.Messages.InvocationRequest request) Line 82 C#
    Microsoft.Azure.Functions.Worker.Grpc.dll!Microsoft.Azure.Functions.Worker.GrpcWorker.InvocationRequestHandlerAsync(Microsoft.Azure.Functions.Worker.Grpc.Messages.InvocationRequest request) Line 183  C#
    Microsoft.Azure.Functions.Worker.Grpc.dll!Microsoft.Azure.Functions.Worker.GrpcWorker.ProcessRequestCoreAsync(Microsoft.Azure.Functions.Worker.Grpc.Messages.StreamingMessage request) Line 138 C#
    Microsoft.Azure.Functions.Worker.Grpc.dll!Microsoft.Azure.Functions.Worker.GrpcWorker.ProcessRequestAsync.AnonymousMethod__0() Line 124 C#
    System.Private.CoreLib.dll!System.Threading.Tasks.Task<System.Threading.Tasks.Task>.InnerInvoke()   Unknown
    System.Private.CoreLib.dll!System.Threading.Tasks.Task..cctor.AnonymousMethod__272_0(object obj)    Unknown
    System.Private.CoreLib.dll!System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread threadPoolThread, System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state)   Unknown
    System.Private.CoreLib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot, System.Threading.Thread threadPoolThread)    Unknown
    System.Private.CoreLib.dll!System.Threading.Tasks.Task.ExecuteEntryUnsafe(System.Threading.Thread threadPoolThread) Unknown
    System.Private.CoreLib.dll!System.Threading.Tasks.Task.ExecuteFromThreadPool(System.Threading.Thread threadPoolThread)  Unknown
    System.Private.CoreLib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch()  Unknown
    System.Private.CoreLib.dll!System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart() Unknown
    System.Private.CoreLib.dll!System.Threading.Thread.StartCallback()  Unknown

Expected behavior Expect document generation to resolve the custom schema when looking at object type of IQuantity or a way to tell the generation to do this - in swagger this was done using SwaggerGenOptions:

options.MapType<UnitsNet.IQuantity>(() => new OpenApiSchema
    {
        Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = typeof(UnitsNet.IQuantity).Name }
    });

Is there something comparable here?

Screenshots N/A

Environment (please complete the following information, if applicable):

Additional context Think that's everything...

joan-grau commented 1 year ago

I have the same issue. Is there a way to map some type to others, as we do with SwaggerGenOptions? ie.

services.AddSwaggerGen(c =>
        {
            c.MapType<IPAddress>(() => new OpenApiSchema { Type = typeof(string).Name });
            // ...
        });
jacobjmarks commented 1 year ago

I've had some luck using the following approach:

Context: I want to customize the representation of DateOnly objects in the generated schema

  1. Define an IDocumentFilter to mutate the existing type which has been generated for the DateOnly object:

    public class DateOnlyDocumentFilter : IDocumentFilter
    {
        public void Apply(IHttpRequestDataObject req, OpenApiDocument document)
        {
            document.Components.Schemas["dateOnly"] = new()
            {
                Type = "string",
                Format = "date",
            };
        }
    }
  2. Simply add the filter during options setup. Note that I am using the static approach in which I inherit from DefaultOpenApiConfigurationOptions

    public class MyOpenApiConfigurationOptions : DefaultOpenApiConfigurationOptions
    {
        MyOpenApiConfigurationOptions()
        {
            DocumentFilters.Add(new DateOnlyDocumentFilter());
        }
    }

@joan-grau I suspect you can use an approach similar to the one above for your purposes of mutating the representation of the IPAddress object

jacobjmarks commented 1 year ago

The following more generic approach has also proved fruitful:

using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Abstractions;
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Extensions;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json.Serialization;

public class CustomTypeDocumentFilter<T> : IDocumentFilter
{
    private readonly static string _typeName = typeof(T).GetOpenApiTypeName(new CamelCaseNamingStrategy());
    private readonly OpenApiSchema _schema;

    public CustomTypeDocumentFilter(OpenApiSchema schema)
    {
        _schema = schema;
    }

    public void Apply(IHttpRequestDataObject req, OpenApiDocument document)
    {
        document.Components.Schemas[_typeName] = _schema;
    }
}
public class DateOnlyDocumentFilter : CustomTypeDocumentFilter<DateOnly>
{
    public DateOnlyDocumentFilter() : base(new()
    {
        Type = "string",
        Format = "date",
    })
    { }
}