asp.net-coreenumssystem.text.jsonjsonserializercustom-model-binder

Deserialize enums using the EnumMember value in AspNet [FromQuery] model binding


I have an endpoint in .NET 6 Microsoft.NET.Sdk.Web project that deserialize query strings into a .NET object by using the standard [FromQuery]

[Route("[controller]")]
public class SamplesController
    : ControllerBase
{
    [HttpGet]
    public IActionResult Get([FromQuery]QueryModel queryModel)
    {
        if (!queryModel.Status.HasValue)
        {
            return BadRequest("Problem in deserialization");
        }
        
        return Ok(queryModel.Status.Value.GetEnumDisplayName());
    }
}

The model contains an enum

public class QueryModel
{
    /// <summary>
    /// The foo parameter
    /// </summary>
    /// <example>bar</example>
    public string? Foo { get; init; } = null;
    
    /// <summary>
    /// The status
    /// </summary>
    /// <example>on-hold</example>
    public Status? Status { get; set; } = null;
}

And the enum has EnumMember attributes which value I want to use to deserialize from.

public enum Status
{
    [EnumMember(Value = "open")]
    Open,
    
    [EnumMember(Value = "on-hold")]
    OnHold
}

By default, .NET 6 does not take into consideration the EnumMember when deserializing.

The goal is to be able to send requests such as

http://localhost:5000/Samples?Foo=bar&Status=on-hold 

and have the controller's action deserialize the QueryModel with the proper Status.OnHold value by using its EnumMember

I have tried without luck an extensions library that contains a converter, but the converter is not getting triggered when using [FromQuery]. See https://github.com/Macross-Software/core/issues/30

I have added a project to reproduce problem and as a sandbox to provide a solution** https://gitlab.com/sunnyatticsoftware/issues/string-to-enum-mvc/-/tree/feature/1-original-problem

NOTE: I would need a solution where the Enum and the does not require any external dependency (just .NET sdk).


Solution

  • A custom Enum converter might be your choice. By leveraging the existing EnumConverter class what we need is to have a customized ConvertFrom method:

    public class CustomEnumConverter : EnumConverter
    {
        public CustomEnumConverter([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] Type type) : base(type)
        {
        }
    
        public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
        {
            if (value is string strValue)
            {
                try
                {
                    foreach (var name in Enum.GetNames(EnumType))
                    {
                        var field = EnumType.GetField(name);
                        if (field != null)
                        {
                            var enumMember = (EnumMemberAttribute)(field.GetCustomAttributes(typeof(EnumMemberAttribute), true).Single());
                            if (strValue.Equals(enumMember.Value, StringComparison.OrdinalIgnoreCase))
                            {
                                return Enum.Parse(EnumType, name, true);
                            }
                        }
                    }
                }
                catch (Exception e)
                {
                    throw new FormatException((string)value, e);
                }
            }
    
            return base.ConvertFrom(context, culture, value);
        }
    }
    

    And then decorate the converter to your Model class:

    [TypeConverter(typeof(CustomEnumConverter))]
    public enum Status
    {
        [EnumMember(Value = "open")]
        Open,
        
        [EnumMember(Value = "on-hold")]
        OnHold
    }
    

    then we can get the "on-hold" parsed. You might also want to override the ConverTo() for printing the EnumMember value to swagger. It is a bit hacky, but if you want a pure .NET solution this should be one of the minimal viable solutions.

    enter image description here