mgiurni
mgiurni

Reputation: 165

C# - Post a model with generic property (Web API)

I have this situation:

public class Dto 
{
    public int TypeId { get; set; }
    public IType Type { get; set; }
}

public class Type1 : IType
{
   public string PropertyA { get; set; }
}

public class Type2 : IType
{
   public int PropertyB { get; set; }
   public bool PropertyC { get; set; }
}

public class MyController : ApiController
{
   [HttpPost]
   public IHttpActionResult Post(Dto dto) 
   {
   }
}

How can I deserialize for the correct implementation of IType interface, depending on the value of the TypeId property?

I tried using JsonConverter (following this example: https://gist.github.com/teamaton/bba69cf95b9e6766f231), but I can only specify one concrete type:

public class Dto 
{
    public int TypeId { get; set; }

    [JsonConverter(typeof(ConcreteTypeConverter<Type1>)]
    public IType Type { get; set; }
}

Upvotes: 0

Views: 962

Answers (1)

weichch
weichch

Reputation: 10035

JsonConverter is the correct way to go, however the ConcreteTypeConverter isn't for your case.

Assume you need to determine which concrete type to create at runtime based on TypeId, you will need a JsonConverter on Dto not on Type property.

Try this:

[JsonConverter(typeof(DtoJsonConverter))]
public class Dto
{
    public IType Type { get; set; }
    public int TypeId { get; set; }
}

class DtoJsonConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Dto);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
        {
            return null;
        }

        // Load this to a JObject so that we can read TypeId
        var obj = JObject.Load(reader);
        var typeId = obj["TypeId"].Value<int>();

        // Figure out JSON covnerter for type property based on TypeId
        JsonConverter converter = null;
        switch (typeId)
        {
            // Assuming 1 means Type1
            case 1:
                converter = new CreateITypeJsonConverter(() => new Type1());
                break;
            case 2:
                converter = new CreateITypeJsonConverter(() => new Type2());
                break;
        }

        if (converter != null)
        {
            serializer.Converters.Add(converter);
        }

        try
        {
            // Now create Dto and populate the object.
            // This will call the JsonConverter we just added for Type property.
            var dto = new Dto();
            serializer.Populate(obj.CreateReader(), dto);
            return dto;
        }
        finally
        {
            if (converter != null)
            {
                serializer.Converters.Remove(converter);
            }
        }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotSupportedException();
    }
}

class CreateITypeJsonConverter : CustomCreationConverter<IType>
{
    private readonly Func<IType> _factory;

    public CreateITypeJsonConverter(Func<IType> factory)
    {
        _factory = factory;
    }

    public override IType Create(Type objectType)
    {
        return _factory();
    }
}

The DtoJsonConverter works out the concrete type of IType as per the value of TypeId, and use another CreateITypeJsonConverter to instantiate the concrete type, then populate the Dto.

It is also possible that you could move TypeId into IType, then use one of the methods in this question: JsonConverter with Interface

Upvotes: 2

Related Questions