简体   繁体   中英

Complex Type Enum Model Binding

Background

In .NET Core, default controller model binding fails (produces null value for your action argument) if your model contains an enum anywhere in its hierarchy and the supplied values don't match EXACTLY with the names in the enum . White-space or odd capitalization breaks the binding, and that seems unfriendly for the consumer of my API endpoint.

My solution

I created a model binder provider, which uses reflection to determine if somewhere in the target binding type there exists an enum ; if this check is true, it returns a custom model binder (constructed by passing along the enum type) which uses Regex/string manipulation (gross) to scan the request body for the enum values and make an effort to resolve them to a name in that enum type, before passing JsonConvert for deserializing.

This solution is, in my opinion, far too complex and ugly for what I'm trying to achieve.

What I'd like is something like a JsonConvert attribute (for my enum fields) that makes this effort during binding/deserialization. Newtonsoft's out of the box solution ( StringEnumConverter ) doesn't try to adjust the string to fit the enum type (fair, I suppose), but I can't extend Newtonsoft's functionality here because it relies on a lot of internal classes (without copying and pasting a ton's of their code).

Is there a piece in the pipeline somewhere I'm missing that could be better leveraged to fit this need?

PS I placed this here rather than code review (it's too theoretical) or software engineering (too specific); please advise if it's not the right place.

I've used a Type Safe Enum pattern for this, I think it will work for you. With the TypeSafeEnum you can control what is mapped to the JSON using Newtonsoft's JsonConverter attribute. Since you have no code to post I've built up a sample.

Base class used by your application's TypeSafeEnums:

public abstract class TypeSafeEnumBase
{
    protected readonly string Name;
    protected readonly int Value;

    protected TypeSafeEnumBase(int value, string name)
    {
        this.Name = name;
        this.Value = value;
    }

    public override string ToString()
    {
        return Name;
    }
}

Sample type implemented as a TypeSafeEnum, it would have normally been a plain Enum, including Parse and TryParse methods:

public sealed class BirdType : TypeSafeEnumBase
{
    private const int BlueBirdId = 1;
    private const int RedBirdId = 2;
    private const int GreenBirdId = 3;
    public static readonly BirdType BlueBird = 
        new BirdType(BlueBirdId, nameof(BlueBird), "Blue Bird");
    public static readonly BirdType RedBird = 
        new BirdType(RedBirdId, nameof(RedBird), "Red Bird");
    public static readonly BirdType GreenBird = 
        new BirdType(GreenBirdId, nameof(GreenBird), "Green Bird");

    private BirdType(int value, string name, string displayName) :
        base(value, name)
    {
        DisplayName = displayName;
    }

    public string DisplayName { get; }

    public static BirdType Parse(int value)
    {
        switch (value)
        {
            case BlueBirdId:
                return BlueBird;
            case RedBirdId:
                return RedBird;
            case GreenBirdId:
                return GreenBird;
            default:
                throw new ArgumentOutOfRangeException(nameof(value), $"Unable to parse for value, '{value}'. Not found.");
        }
    }

    public static BirdType Parse(string value)
    {
        switch (value)
        {
            case "Blue Bird":
            case nameof(BlueBird):
                return BlueBird;
            case "Red Bird":
            case nameof(RedBird):
                return RedBird;
            case "Green Bird":
            case nameof(GreenBird):
                return GreenBird;
            default:
                throw new ArgumentOutOfRangeException(nameof(value), $"Unable to parse for value, '{value}'. Not found.");
        }
    }

    public static bool TryParse(int value, out BirdType type)
    {
        try
        {
            type = Parse(value);
            return true;
        }
        catch
        {
            type = null;
            return false;
        }
    }

    public static bool TryParse(string value, out BirdType type)
    {
        try
        {
            type = Parse(value);
            return true;
        }
        catch
        {
            type = null;
            return false;
        }
    }
}

Container to handle type safe conversion, so you don't need to create a converter for every type safe implemented, and to prevent changes in the TypeSafeEnumJsonConverter when new type safe enums are implemented:

public class TypeSafeEnumConverter
{
    public static object ConvertToTypeSafeEnum(string typeName, string value)
    {
        switch (typeName)
        {
            case "BirdType":
                return BirdType.Parse(value);
            //case "SomeOtherType": // other type safe enums
            //    return // some other type safe parse call
            default:
                return null;
        }
    }
}

Implements Newtonsoft's JsonConverter which in turn calls our TypeSafeEnumConverter

public class TypeSafeEnumJsonConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        var types = new[] { typeof(TypeSafeEnumBase) };
        return types.Any(t => t == objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        string name = objectType.Name;
        string value = serializer.Deserialize(reader).ToString();
        return TypeSafeEnumConversion.ConvertToTypeSafeEnum(name, value); // call to our type safe converter
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (value == null && serializer.NullValueHandling == NullValueHandling.Ignore)
        {
            return;
        }
        writer.WriteValue(value?.ToString());
    }
}

Sample object that uses our BirdType and sets the converter to use:

public class BirdCoup
{
    [JsonProperty("bird-a")]
    [JsonConverter(typeof(TypeSafeEnumJsonConverter))] // sets the converter used for this type
    public BirdType BirdA { get; set; }

    [JsonProperty("bird-b")]
    [JsonConverter(typeof(TypeSafeEnumJsonConverter))] // sets the converter for this type
    public BirdType BirdB { get; set; }
}

Usage example:

// sample #1, converts value with spaces to BirdTyp
string sampleJson_1 = "{\"bird-a\":\"Red Bird\",\"bird-b\":\"Blue Bird\"}";
BirdCoup resultSample_1 = 
JsonConvert.DeserializeObject<BirdCoup>(sampleJson_1, new JsonConverter[]{new TypeSafeEnumJsonConverter()});

// sample #2, converts value with no spaces in name to BirdType
string sampleJson_2 = "{\"bird-a\":\"RedBird\",\"bird-b\":\"BlueBird\"}";
BirdCoup resultSample_2 = 
JsonConvert.DeserializeObject<BirdCoup>(sampleJson_2, new JsonConverter[] { new TypeSafeEnumJsonConverter() });

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM