Problem Description
I'm utilizing NSwag to generate the OpenAPI specification for my API. The project I'm currently engaged with was initially constructed using ASP.NET 5.0, and recently I made the decision to upgrade it to ASP.NET 8.0. Following the upgrade of all associated libraries, I promptly encountered an issue with one of the endpoints.
This specific endpoint is designed to accept a DTO containing an IFormFile
and an enumeration property named FileType
. In the controller code, the DTO parameter containing both these properties is annotated with the [FromForm]
attribute. However, upon transitioning to ASP.NET 8.0, I observed that this enum property is generated as an integer, whereas in ASP.NET 5.0 it behaved differently.
The OpenAPI specification describes FileType
as integer type and also generates enum
and x-enumNames
attributes:
After I migrated project to ASP.NET 8.0 the enum property suddenly became string and enum
and x-enumNames
don't get generated anymore:
Diving Deeper
I spent some time digging into NSwag issues trying to find a solution, but unfortunately, I didn't find anything useful. So, I decided to do some debugging. Turns out, NSwag uses the ASP.NET implementation of IApiDescriptionGroupCollectionProvider
and IApiDescriptionProvider
to describe the API.
After looking into it further, I noticed something interesting. The ApiDescription
object contains a bunch of ApiParameterDescription
instances that describe the API parameters. What caught my attention was the difference in the Type
property between ASP.NET 5.0 and 8.0. In the older version, it showed the enum type like FileType
, but in the newer one, it just said string
.
As I kept digging, I found out that the DefaultApiDescriptionProvider.CreateResult
method is where things differ. Let me break down the changes in this method below:
.net 5.0
private ApiParameterDescription CreateResult(
ApiParameterDescriptionContext bindingContext,
BindingSource source,
string containerName)
{
return new ApiParameterDescription()
{
...
Type = bindingContext.ModelMetadata.ModelType,
...
};
}
.net 8.0
private ApiParameterDescription CreateResult(
ApiParameterDescriptionContext bindingContext,
BindingSource source,
string containerName)
{
return new ApiParameterDescription()
{
...
Type = GetModelType(bindingContext.ModelMetadata),
...
};
}
private static Type GetModelType(ModelMetadata metadata)
{
// IsParseableType || IsConvertibleType
if (!metadata.IsComplexType)
{
return EndpointModelMetadata.GetDisplayType(metadata.ModelType);
}
return metadata.ModelType;
}
public static Type GetDisplayType(Type type)
{
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
return underlyingType.IsPrimitive
// Those additional types have TypeConverter or TryParse and are not primitives
// but should not be considered string in the metadata
|| underlyingType == typeof(DateTime)
|| underlyingType == typeof(DateTimeOffset)
|| underlyingType == typeof(DateOnly)
|| underlyingType == typeof(TimeOnly)
|| underlyingType == typeof(TimeSpan)
|| underlyingType == typeof(decimal)
|| underlyingType == typeof(Guid)
|| underlyingType == typeof(Uri) ? type : typeof(string);
}
As it can be seen, things have changed in the implementation, especially in .NET 8.0, where any enum is now treated as a string parameter. But I really need to keep this parameter as an integer
because switching it to a string
would mess up my API and cause problems. So, if you've got any ideas on how to deal with this, I'm all ears!
I solved the issue by providing a custom implementation of the IApiDescriptionGroupCollectionProvider
where I adjusted the code of the GetCollection
method:
var context = new ApiDescriptionProviderContext(actionDescriptors.Items);
foreach (var provider in _apiDescriptionProviders)
{
provider.OnProvidersExecuting(context);
}
// Added this block to change the parameter type for enumeration parameters back to their original type
foreach (var param in context.Results.SelectMany(r => r.ParameterDescriptions))
{
if (param.ModelMetadata.ModelType.IsEnum)
{
param.Type = param.ModelMetadata.ModelType;
}
}
for (var i = _apiDescriptionProviders.Length - 1; i >= 0; i--)
{
_apiDescriptionProviders[i].OnProvidersExecuted(context);
}
var groups = context.Results
.GroupBy(d => d.GroupName)
.Select(g => new ApiDescriptionGroup(g.Key, g.ToArray()))
.ToArray();
return new ApiDescriptionGroupCollection(groups, actionDescriptors.Version);