Is there a way to forbid integers passed in as enums without resorting to a string property type?
I already do the following:
return builder.AddJsonOptions(options =>
{
var namingPolicy = JsonNamingPolicy.CamelCase;
// omitted...
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(namingPolicy, allowIntegerValues: false));
// omitted...
});
And I have even tried making my own JsonConverterFactory
and JsonConverter<TEnum>
but the deserialization from integer to enum still happens.
I only want users to be able to the call the API with the documented STRING enums, not integers.
CodePudding user response:
If you want to restrict api to only accept String Enum, I think you could try to check action parameters with ActionFilter
or middlewares.
Here is a simple demo for checking Enum from Query, you could add more checks like from Request Body.
ValidateActionParametersAttribute
public class ValidateActionParametersAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var descriptor = context.ActionDescriptor as ControllerActionDescriptor;
if (descriptor != null)
{
var parameters = descriptor.MethodInfo.GetParameters();
foreach (var parameter in parameters)
{
var argument = context.ActionArguments[parameter.Name];
var argumentType = argument.GetType();
if (argumentType.IsEnum)
{
var value = context.HttpContext.Request.Query[parameter.Name];
if (int.TryParse(value, out int argumentValue))
{
context.Result = new BadRequestObjectResult($"{(parameter.Name)} Shold be String");
}
}
}
}
base.OnActionExecuting(context);
}
}
Useage
[HttpGet(Name = "GetWeatherForecast")]
[ValidateActionParameters]
public IEnumerable<WeatherForecast> Get(Season season)
{
//var options = new JsonSerializerOptions { Converters = { new JsonStringEnumConverter(allowIntegerValues: false) } };
//var r1 = JsonSerializer.Deserialize<Season>("\"42\"", options); // does not fail
//var r2 = JsonSerializer.Deserialize<Season>("42", options); // fails as expected
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
CodePudding user response:
As commented by Jeremy Lakeman
, this is a known issue that is planned to be fixed in a future release.
See https://github.com/dotnet/runtime/issues/58247
I was able to work it out this morning with a custom model binder:
internal class StringOnlyEnumTypeModelBinderProvider : IModelBinderProvider
{
public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.UnderlyingOrModelType.IsEnum)
{
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
return new StringOnlyEnumTypeModelBinder(suppressBindingUndefinedValueToEnumType: true, context.Metadata.UnderlyingOrModelType, loggerFactory);
}
return null;
}
private class StringOnlyEnumTypeModelBinder : EnumTypeModelBinder
{
public StringOnlyEnumTypeModelBinder(bool suppressBindingUndefinedValueToEnumType, Type modelType, ILoggerFactory loggerFactory)
: base(suppressBindingUndefinedValueToEnumType, modelType, loggerFactory)
{
}
protected override void CheckModel(ModelBindingContext bindingContext, ValueProviderResult valueProviderResult, object? model)
{
if (bindingContext is null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var paramName = bindingContext.ModelName;
var paramValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).FirstValue;
if (int.TryParse(paramValue, out var _))
{
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, bindingContext.ModelMetadata.ModelBindingMessageProvider.AttemptedValueIsInvalidAccessor(paramValue, paramName));
}
else
{
base.CheckModel(bindingContext, valueProviderResult, model);
}
}
}
}
// options are MvcOptions
options.ModelBinderProviders.Remove(options.ModelBinderProviders.Single(x => x.GetType() == typeof(EnumTypeModelBinderProvider)));
options.ModelBinderProviders.Insert(0, new StringEnumTypeModelBinderProvider());