It’s not uncommon for ASP.NET MVC developers for find things missing out of the box. Now that MVC is open source, the missing puzzle pieces might actually make it into future releases.

Problem

Sending and model binding JSON with an enum is one of the missing pieces. For example, let’s assume we have the following models:

public enum Suit
{
    Spades = 1,
    Hearts = 2,
    Clubs = 3,
    Diamonds = 4
}

public class Card
{
    public Suit Suit { get; set; }
    public int Value { get; set; }
}

And assume that we’ve sent the following JSON back to our CardsController:

// Six of hearts.
{
  'Suit': 2,
  'Value': 6
}

This doesn’t work as expected:

invalid enum model binding

Why is the Suit is 0? Well, it turns out that the default model binder only handles enums by the string representation of their name. Therefore, sending the following JSON works as expected:

{
  'Suit': 'Hearts',
  'Value': 6
}

Sending this type of enum representation back to a controller is counter intuitive (especially if you’re also representing the enum in JavaScript!).

Solution

The following model binder can be used to model bind enum values like { 'Suite': 2 }. You can specify a default enum value if necessary.

public class EnumModelBinder<T> : IModelBinder
        where T : struct
{
    private readonly T defaultValue;
    private readonly bool hasDefaultValue;

    public EnumModelBinder(T defaultValue)
    {
        this.defaultValue = defaultValue;
        this.hasDefaultValue = true;
    }

    public EnumModelBinder()
    {
        this.hasDefaultValue = false;
    }

    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        var modelState = new ModelState() { Value = valueResult };

        object actualValue = null;

        if (valueResult == null && this.hasDefaultValue)
        {
            actualValue = this.defaultValue;
        }
        else if (valueResult == null)
        {
            modelState.Errors.Add("No default representation of enum " + typeof(T).Name + " could be found.");
        }
        else
        {
            string value = valueResult.AttemptedValue;
            T enumValue;

            if (Enum.TryParse<T>(value, out enumValue) && Enum.IsDefined(typeof(T), enumValue))
            {
                actualValue = enumValue;
            }
            else if (this.hasDefaultValue)
            {
                actualValue = this.defaultValue;
            }
            else
            {
                modelState.Errors.Add("Could not parse " + value + 
                                      " as a valid numerical value of enum " + typeof(T).Name + ".");
            }
        }

        bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
        return actualValue;
    }
}

Wire this up inside your Global.asax:

ModelBinders.Binders.Add(typeof(Suit), new EnumModelBinder<Suit>());

And now we can go home happy campers:

fixed enum model binding

Conclusion

Because model binding enum values isn’t supported out of the box in ASP.NET MVC, we have to create our own binder. However, I still think this type of binder should be baked into the framework.

Enjoy!


Appendix

Here are the associated tests for this model binder:

[TestClass]
public class EnumModelBinderTest
{
    private enum Foo
    {
        A = 1,
        B = 2,
        C = 3
    }

    [TestMethod]
    public void Bind_to_default_value_if_value_provider_has_no_value()
    {
        // Arrange
        var b = TestableEnumModelBinder<Foo>.Create("X", null, Foo.B);

        // Act
        var result = b.BindModel(b.ControllerContext, b.BindingContext);

        // Assert
        Assert.AreEqual(Foo.B, result);
        Assert.IsTrue(b.BindingContext.ModelState.IsValid);
    }

    [TestMethod]
    public void Bind_to_default_value_if_parsing_fails()
    {
        // Arrange
        var b = TestableEnumModelBinder<Foo>.Create("X", "adsf", Foo.C);

        // Act
        var result = b.BindModel(b.ControllerContext, b.BindingContext);

        // Assert
        Assert.AreEqual(Foo.C, result);
        Assert.IsTrue(b.BindingContext.ModelState.IsValid);
    }

    [TestMethod]
    public void Add_error_if_value_provider_has_no_value_and_no_default_value_is_set()
    {
        // Arrange
        var b = TestableEnumModelBinder<Foo>.Create("X", null);

        // Act
        var result = b.BindModel(b.ControllerContext, b.BindingContext);

        // Assert
        Assert.IsNull(result);
        Assert.IsFalse(b.BindingContext.ModelState.IsValid);
    }

    [TestMethod]
    public void Add_error_if_value_could_not_be_parsed_as_enum_value_and_no_default_value_is_set()
    {
        // Arrange
        var b = TestableEnumModelBinder<Foo>.Create("X", "999");

        // Act
        var result = b.BindModel(b.ControllerContext, b.BindingContext);

        // Assert
        Assert.IsNull(result);
        Assert.IsFalse(b.BindingContext.ModelState.IsValid);
    }

    [TestMethod]
    public void Bind_correct_value()
    {
        // Arrange
        var b = TestableEnumModelBinder<Foo>.Create("X", "2");

        // Act
        var result = b.BindModel(b.ControllerContext, b.BindingContext);

        // Assert
        Assert.AreEqual(Foo.B, result);
        Assert.IsTrue(b.BindingContext.ModelState.IsValid);
    }

    private class TestableEnumModelBinder<T> : EnumModelBinder<T>
        where T : struct
    {
        public ControllerContext ControllerContext;
        public ModelBindingContext BindingContext;

        private TestableEnumModelBinder(T defaultValue)
            : base(defaultValue)
        {

        }

        private TestableEnumModelBinder()
            : base()
        {

        }

        public static TestableEnumModelBinder<T> Create(string modelName, string modelValue, object defaultValue = null)
        {
            var controllerContext = new ControllerContext();
            var valueProvider = new Mock<IValueProvider>();

            if (modelValue != null)
            {
                valueProvider.Setup(p => p.GetValue(modelName))
                    .Returns(new ValueProviderResult(modelValue, modelValue, CultureInfo.CurrentCulture));
            }
            else
            {
                valueProvider.Setup(p => p.GetValue(modelName)).Returns((ValueProviderResult)null);
            }

            var bindingContext = new ModelBindingContext()
            {
                ModelName = modelName,
                ValueProvider = valueProvider.Object
            };

            TestableEnumModelBinder<T> binder;

            if (defaultValue != null)
            {
                binder = new TestableEnumModelBinder<T>((T)defaultValue);
            }
            else
            {
                binder = new TestableEnumModelBinder<T>();
            }

            binder.ControllerContext = controllerContext;
            binder.BindingContext = bindingContext;

            return binder;
        }
    }
}