skip to Main Content

I have an ASP.NET Core Web API end point which takes (FromBody) The Search object defined below

public class Search {
    public int PageSize {get;set;}
    public Expression Query{get;set;}
}
    
public class Expression {
    public string Type {get;set;}
}
    
public class AndExpression {
    public IList<Expression> Expressions {get;set;}
}
    
public class MatchesExpression {
    public string FieldId {get;set;}
    public string Value {get;set;}
    public string Operator {get;set;}
}

So… if I post the following JSON to my endpoint

{ "pageSize":10, "query": { "fieldId": "body", "value": "cake", "operator": "matches" } }

I successfully get a Search Object, but the Query property is of type Expression, not MatchesExpression.

This is clearly a polymorphic issue.

This article (towards the end) gives a good example of a how to deal with this issue when your entire model is polymorphic.

https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-5.0

In my case, the property of my Model "Query" is polymorphic, so Im unsure how to build a ModelBinder for my Search object that will allow me to handle the Query Property

I Imagine, I need to write a model binder to construct the search object and then follow the pattern described for the property, however I cannot locate any examples of how to implement a model binder that isnt utterly trivial.

Any suggestions on how to achieve this? Good sources of information?

2

Answers


  1. Chosen as BEST ANSWER

    So.. I gave up with ModelBInders (because Im using the FromBody attribute which isnt compatible with my aims).

    Instead I wrote a System.Text.Json JsonConvertor to handle the polymorphism (see shonky code below)

    using Searchy.Models;
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using System.Text.Json;
    using System.Text.Json.Serialization;
    using System.Threading.Tasks;
    
    namespace Searchy
    {
    
        public class ExpressionJsonConverter : JsonConverter<Expression>
        {
            public override Expression Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            {
    
                Utf8JsonReader readerClone = reader;
    
                using (var jsonDocument = JsonDocument.ParseValue(ref readerClone)) 
                { 
                    if (!jsonDocument.RootElement.TryGetProperty("type", out var typeProperty))
                     {
                     throw new JsonException();
                    }
    
                    switch (typeProperty.GetString()) 
                    {
                        case "comparison":
                            return JsonSerializer.Deserialize<Comparison>(ref reader, options);
                        case "and":
                            return JsonSerializer.Deserialize<And>(ref reader, options);
                    }
                }
    
                return null;
            }
    
            public override void Write(
                Utf8JsonWriter writer,
                Expression expression,
                JsonSerializerOptions options)
            {
    
            }
        }
    }
    

    My Expression class also had the following attribue

    [JsonConverter(typeof(ExpressionJsonConverter))]
    

  2. I recently ran into the same issue, but was using Newtonsoft.Json, here is iasksillyquestions’ solution using Newtonsoft.Json:

    public class ConnectionJsonConverter : JsonConverter<Connection>
        {
            public override Connection? ReadJson(JsonReader reader, Type objectType, Connection? existingValue, bool hasExistingValue, JsonSerializer serializer)
            {
                // if this is the base type, deserialize it into the child type
                if (objectType == typeof(Connection))
                {
                    JObject parsedObject = JObject.Load(reader);
    
                    if (!parsedObject.TryGetValue("connectionType", out var connectionType))
                    {
                        throw new Exception("Unable to parse ConnectionType");
                    }
    
                    switch (connectionType.ToObject<ConnectionType>())
                    {
                        case ConnectionType.Database:
                            return parsedObject.ToObject<DatabaseConnection>();
                        case ConnectionType.LDAP:
                            return parsedObject.ToObject<LDAPConnection>();
                        default:
                            throw new Exception("Unrecognised ConnectionType");
                    }
                }
    
                // if it is a child type, just deserialize it normally
                else
                {
                    serializer.ContractResolver.ResolveContract(objectType).Converter = null;
                    return serializer.Deserialize(reader, objectType) as Connection;
                }
            }
    
            public override void WriteJson(JsonWriter writer, Connection? value, JsonSerializer serializer)
            {
                throw new NotImplementedException();
            }
        }
    

    In my case I had an enum ConnectionType to specify what derived type the object was.

    Connection.cs:

        [JsonConverter(typeof(ConnectionJsonConverter))]
        public abstract class Connection : Entity
        {
            #region Properties
            // what type of system are we connecting to? database / api / ldap ...
            public ConnectionType ConnectionType { get; set; }
    
            // an optional description to explain where the connection is going
            public string Description { get; set; } = "";
            #endregion
    
    
            #region Helper Methods
            // test if the connection is good. Will throw an error if the connection is not good.
            public abstract Task Test();
            #endregion
        }
    

    LDAPConnection.cs:

        public class LDAPConnection : Connection
        {
            #region Properties
            public string Endpoint { get; set; }
            public string? Username { get; set; }
            public string? Password { get; set; }
            #endregion
    
            #region Helper Methods
            public override async Task Test()
            {
                DirectoryEntry directoryEntry = new DirectoryEntry(this.Endpoint, this.Username, this.Password);
    
                // check if the native object exists. If it does, then we are connected
                if (directoryEntry.NativeObject == null)
                {
                    throw new Exception("Unable to bind to server");
                }
    
                // assume that if we got here without any exceptions, we are connected.
            }
            #endregion
        }
    

    Then you can have one controller for all derived types:

    // POST: /api/connections/create
    [HttpPost, ActionName("Create")]
    public async Task<IActionResult> Create([Bind] Connection connection)
    {
        connection.Test();
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search