Search code examples
c#azure-functionsswaggerswashbuckleautorest

Why autorest is replacing my custom struct by an object in Swagger?


I have created a custom readonly struct to define an immutable value type that I called TenantId:

[DebuggerDisplay("ID={m_internalId.ToString()}")]
[JsonConverter(typeof(TenantIdJsonConverter))]
public readonly struct TenantId : IEquatable<TenantId>
{
    private readonly Guid m_internalId;

    public static TenantId New => new(Guid.NewGuid());

    private TenantId(Guid id)
    {
        m_internalId = id;
    }

    public TenantId(TenantId otherTenantId)
    {
       m_internalId = otherTenantId.m_internalId;
    }

    ...
}

I have also defined a contract called PurchaseContract that is part of an HTTP response:

[JsonObject(MemberSerialization.OptIn)]
public sealed class PurchaseContract
{
    [JsonProperty(PropertyName = "tenantId")]
    public TenantId TenantId { get; }
        
    [JsonProperty(PropertyName = "total")]
    public double Total { get; }
}

Finally, I have set up an HTTP triggered function that will return an instance of PurchaseContract. For now, it has been described in the ProducesResponseTypeAttribute:

[ApiExplorerSettings(GroupName = "Purchases")]
[ProducesResponseType(typeof(PurchaseContract), (int) HttpStatusCode.OK)]
[FunctionName("v1-get-purchase")]
public Task<IActionResult> RunAsync
(
    [HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = "v1/purchases")]
    HttpRequest httpRequest,
    [SwaggerIgnore] 
            ClaimsPrincipal claimsPrincipal
)
{
    //  Stuff to do.
    return Task.FromResult((IActionResult)new OkResult());
}

In my Startup class, I am setting up swagger like this:

private static void ConfigureSwashBuckle(IFunctionsHostBuilder functionsHostBuilder)
{
    functionsHostBuilder.AddSwashBuckle(Assembly.GetExecutingAssembly(), options =>
            {
                options.SpecVersion = OpenApiSpecVersion.OpenApi3_0;
                options.AddCodeParameter = true;
                options.PrependOperationWithRoutePrefix = true;
                options.XmlPath = "FunctionApp.xml";
                options.Documents = new []
                {
                    new SwaggerDocument
                    {
                        Title = "My API,
                        Version = "v1",
                        Name = "v1",
                        Description = "Description of my API",
                    }
                };
            });
        }

In the swagger UI page, I can see that it looks good:

Swagger page

Question

There is an unexpected outcome when creating a C# client using Autorest. Somehow, the TenantId struct is dropped and replaced by an object instead:

TenantId is dropped and replaced by an Object

Why is that and what should I do to have an autogenerated TenantId, like the PurchaseContract in the client as well?

Details

Here is the version information.

  • Function App V3 running on netcore3.1;
  • OpenApi 3.0;
  • Autorest Core 3.0.6274, autorest.csharp' (~2.3.79->2.3.91) and autorest.modeler' (2.3.55->2.3.55);
  • NuGet package AzureExtensions.Swashbuckle;

Solution

  • I started investigating the source code of Swashbuckle.AspNetCore.SwaggerGen to find out how my readonly struct was interpreted. It all happens in the class JsonSerializerDataContractResolver, in the method GetDataContractForType which determines the DataContract for the provided type:

    public DataContract GetDataContractForType(Type type)
    {
        if (type.IsOneOf(typeof(object), typeof(JsonDocument), typeof(JsonElement)))
        {
            ...
        }
    
        if (PrimitiveTypesAndFormats.ContainsKey(type))
        {
            ...
        }
    
        if (type.IsEnum)
        {
            ...
        }
    
        if (IsSupportedDictionary(type, out Type keyType, out Type valueType))
        {
            ...
        }
    
        if (IsSupportedCollection(type, out Type itemType))
        {
            ...
        }
    
        return DataContract.ForObject(
            underlyingType: type,
            properties: GetDataPropertiesFor(type, out Type extensionDataType),
            extensionDataType: extensionDataType,
            jsonConverter: JsonConverterFunc);
    }
    

    My custom struct TenantId does not match any of these conditions and, consequently, it falls back to being considered an object (last statement).

    I then moved on to look at the existing tests to see how the class was used and see if I could change anything. Surprisingly, I found a test called GenerateSchema_SupportsOption_CustomTypeMappings (line 356) which shows a way to provide a custom mapping (see the first statement of the method):

    [Theory]
    [InlineData(typeof(ComplexType), typeof(ComplexType), "string")]
    [InlineData(typeof(GenericType<int, string>), typeof(GenericType<int, string>), "string")]
    [InlineData(typeof(GenericType<,>), typeof(GenericType<int, int>), "string")]
    public void GenerateSchema_SupportsOption_CustomTypeMappings(
                Type mappingType,
                Type type,
                string expectedSchemaType)
    {
        var subject = Subject(configureGenerator: c => c.CustomTypeMappings.Add(mappingType, () => new OpenApiSchema { Type = "string" }));
    
        var schema = subject.GenerateSchema(type, new SchemaRepository());
    
        Assert.Equal(expectedSchemaType, schema.Type);
        Assert.Empty(schema.Properties);
    }
    

    In my case, I would like to have my TenantId to be mapped to a string. To do so, I edited the configuration of SwashBuckle when my Function App starts:

    private static void ConfigureSwashBuckle(IFunctionsHostBuilder functionsHostBuilder)
    {
        functionsHostBuilder.AddSwashBuckle(Assembly.GetExecutingAssembly(), options =>
        {
            ...
            options.ConfigureSwaggerGen = (swaggerGenOptions) => swaggerGenOptions.MapType<TenantId>(() => new OpenApiSchema {Type = "string"});
        });
    }
    

    And here it is, the TenantId is now considered a string in Swagger.

    TenantId as a string