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:
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:
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.
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.