Home > Enterprise >  ASP.NET Core - StringEnumConverter - ONLY Allow Strings
ASP.NET Core - StringEnumConverter - ONLY Allow Strings

Time:12-23

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();
        }

Test Result enter image description here

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());
  • Related