Vladyslav Kolodka
Vladyslav Kolodka

Reputation: 162

Model binding with inheritance in Asp.Core

I need to accept list of objects from user:

public async Task<IActionResult> CreateArticle(List<InformationBlockModel> informationBlocks)
    {
        ...
    }

ModelBinder should determine concrete types, but when I trying to cast InformationBlock to TextInformationBlock, exception throws.

Hierarchy:

public class InformationBlockModel
{
    public virtual InformationBlockType Type { get; set; }
}

public class TextInformationBlockModel : InformationBlockModel
{
    public string Text { get; set; }

    public override InformationBlockType Type { get; set; } = InformationBlockType.Text;
}

public class ImageInformationBlockModel : InformationBlockModel
{
    public override InformationBlockType Type { get; set; } = InformationBlockType.Image;
    public string Name { get; set; }
}

Upvotes: 5

Views: 4920

Answers (2)

Vladyslav Kolodka
Vladyslav Kolodka

Reputation: 162

Finally, I found a solution:

Startup.cs

services.AddMvc()
    .AddJsonOptions(options => options.SerializerSettings.Converters.Add(new InformationBlockConverter()));

JsonCreationConverter.cs

public abstract class JsonCreationConverter<T> : JsonConverter
{
    public override bool CanWrite { get; } = false;

    public override bool CanRead { get; } = true;

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
    }

    protected abstract T Create(Type objectType, JObject jObject);

    public override bool CanConvert(Type objectType)
    {
        return typeof(T) == objectType;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
        JsonSerializer serializer)
    {
        var jObject = JObject.Load(reader);

        var target = Create(objectType, jObject);

        serializer.Populate(jObject.CreateReader(), target);

        return target;
    }
}

InformationBlockConverter

public class InformationBlockConverter : JsonCreationConverter<InformationBlockModel>
{
    private readonly Dictionary<InformationBlockType, Type> _types = new Dictionary<InformationBlockType, Type>
    {
        {InformationBlockType.Text, typeof(TextInformationBlockModel)},
        {InformationBlockType.Image, typeof(ImageInformationBlockModel)},
        {InformationBlockType.Video, typeof(VideoInformationBlockModel)}
    };

    protected override InformationBlockModel Create(Type objectType, JObject jObject)
    {
        return (InformationBlockModel) jObject.ToObject(_types[Enum.Parse<InformationBlockType>(
            jObject.GetValue("type", StringComparison.InvariantCultureIgnoreCase).Value<string>(), true)]);
    }
}

InformationBlockType

public enum InformationBlockType
{
    Text,
    Image,
    Video
}

Upvotes: 10

David Jones
David Jones

Reputation: 3352

Asp.Net binding does not work like this by default. If you want to this sort of behaviour you will have to write your own custom model binding, which isn't too difficult.

Or, use a view model:

public class InformationBlockViewModel
{
    public string Type { get; set; }
    public string Text { get; set; }
    public string Name { get; set; }
}

Then handle the block type in the controller:

public async Task<IActionResult> CreateArticle(List<InformationBlockViewModel> informationBlocks)
{
    foreach (var block in informationBlocks) {        
        switch (block.Type)
        {
            case "Text":
                // Handle text
                break;
            case "Image":
                // Handle image
                break;
            case default:
                throw new Exception("Unknown information block type.");
        }
    }
}

Upvotes: 2

Related Questions