Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use enums with EnumMember attribute in FromBody ViewModel in Web API Request?

I'm trying to implement an HttpPost method in an ASP.NET Core Web API project with a [FromBody] view model and enums. In the past, binding view models with the [FromBody] attribute worked well.

In my particular scenario, I want to offer a JSON endpoint where I convert a given value into a C# enum with different names. This example should explain what I want to achieve:

    public enum WeatherEnum
    {
        [EnumMember(Value = "good")]
        Good,

        [EnumMember(Value = "bad")]
        Bad
    }

Internally, I want to use WeatherEnum.Good and WeatherEnum.Bad and the consumer of my endpoint wants to use lowercase values. Therefore, I'm trying to map the values which will be passed in the JSON body to my Enum representation.

I've read about the EnumMember attribute and StringEnumConverter. I've created a minimal example from the new ASP.NET Core Web API 3.0 template (You need to add these NuGet packages Microsoft.Extensions.DependencyInjection, Microsoft.AspNetCore.Mvc.NewtonsoftJson, and Newtonsoft.Json)

ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
    }).SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
    .AddNewtonsoftJson(json =>
    {
        json.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
        json.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
    });
    services.AddControllers();
}

WheatherForecastController:

using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System.Runtime.Serialization;

namespace WebAPITestEnum.Controllers
{
    [ApiController]
    [Produces("application/json")]
    [Consumes("application/json")]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        [HttpPost]
        [Route("method")]
        public ActionResult<QueryResponseClass> TestMethod([FromBody] QueryRequestClass request)
        {
            // do something with the request ...

            return new QueryResponseClass()
            {
                Foo = "bar"
            };
        }
    }

    public class QueryRequestClass
    {
        public WeatherEnum Weather { get; set; }
    }

    public class QueryResponseClass
    {
        public string Foo { get; set; }
    }


    [JsonConverter(typeof(StringEnumConverter))]
    public enum WeatherEnum
    {
        [EnumMember(Value = "good")]
        Good,

        [EnumMember(Value = "bad")]
        Bad
    }
}

My endpoint is being called from Postman with the following body

{
  "Weather": "good"
}

which results in this error:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "|245d862e-4ab01d3956be5f60.",
    "errors": {
        "$.Weather": [
            "The JSON value could not be converted to WebAPITestEnum.Controllers.WeatherEnum. Path: $.Weather | LineNumber: 1 | BytePositionInLine: 18."
        ]
    }
}

It feels like I'm only missing one line somewhere. It is possible to use Enums in view models with the FromBody attribute?

like image 566
citronas Avatar asked Oct 07 '19 13:10

citronas


1 Answers

The code from the question that I posted is indeed valid. In my minimal sample, I forgot to set the [Required] attribute on the enum. However, then I had the problem how the method should react if then value is not set. It correctly(?) assumed the default value of the enum which is not what I wanted.

I searched around and found this solution https://stackoverflow.com/a/54206737/225808 The enum is nullable which is not ideal but at least I have the validation and I get an error message if the value is missing

Update/Warning: You can use the solution referenced above, but! It seems that the code will compile but instead throws the error message from question. I further compared my own project with the test project and noticed that I also needed two include 2 NuGet packages in order to make everything work:

  • Microsoft.AspNetCore.Mvc.NewtonsoftJson
  • Newtonsoft.Json

It seems that Microsoft.AspNetCore.Mvc.NewtonsoftJson overrides from default behavior? If someone can shed some light on this, I'd glady appreciate it.

Update 2: I also updated the referenced so solution to parse the enum value based on the EnumMemberAttribute:

[JsonConverter(typeof(CustomStringToEnumConverter<WeatherEnum>))]
public enum WeatherEnum
{
    [EnumMember(Value = "123good")]
    Good,

    [EnumMember(Value = "bad")]
    Bad
}

public class CustomStringToEnumConverter<T> : StringEnumConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (string.IsNullOrEmpty(reader.Value?.ToString()))
        {
            return null;
        }
        try
        {
            return EnumExtensions.GetValueFromEnumMember<T>(reader.Value.ToString());
        }
        catch (Exception ex)
        {
            return null;
        }
    }
}

public static class EnumExtensions
{
    public static T GetValueFromEnumMember<T>(string value)
    {
        var type = typeof(T);
        if (!type.IsEnum) throw new InvalidOperationException();
        foreach (var field in type.GetFields())
        {
            var attribute = Attribute.GetCustomAttribute(field,
                typeof(EnumMemberAttribute)) as EnumMemberAttribute;
            if (attribute != null)
            {
                if (attribute.Value == value)
                    return (T)field.GetValue(null);
            }
            else
            {
                if (field.Name == value)
                    return (T)field.GetValue(null);
            }
        }
        throw new ArgumentException($"unknow value: {value}");
    }
}
like image 85
citronas Avatar answered Nov 11 '22 06:11

citronas