Custom Model Binder for inheritance/typediscrimination

Viewed 500

Sorry if this has been asked an answered a million times already, but I can't seem to find a solution on Google or SO.

In ASP.NET Core, is it possible to write a custom model binder or some such, that will allow me to have some inheritance support in a web API?

Basically I have a class like:

public class Order {
   public Guid Id { get; set; }
   public PaymentContainer PaymentParameters { get; set; }
}

And a container class like

public class PaymentContainer 
{
    public string Type { get; set; }

    public IPaymentParameters Value { get; set; }
}

And some classes that implement the IPaymentParameters interface, and a controller method such as:

[HttpPost]
public IActionResult CreateOrder([FromBody]Order order)
{
}

I would very much like the client to be able to send in json such as:

{
  "id" : "1234-..-1234",
  "paymentParameters" : {
      "type": "SpecialParameters1",
      "value" : { /* instance of specialparameters1 here */ }
  }
}

And then have the "value" property be an instance of "SpecialParameters1" once it arrives in the controller method.

I would think it is possible to write a modelbinder, but i can't quite wrap my head around how it would be done.

Note: I am aware of the approach where one can change the Json.Net TypeNameHandling, but I would rather NOT mess with all the other stuff that relies on the json serializer, and just setting it to Auto or All will open up some avenues for remote code execution.

Clarification: A small update after the first answer The goal is to have multiple instance of the parameters, such that the following input also "works"

{
  "id" : "1234-..-1234",
  "paymentParameters" : {
      "type": "SpecialParameters2",
      "value" : { /* instance of specialparameters2 here */ }
  }
}

And of course I would have classes

public class SpecialParameters1 : IPaymentParameters{}
public class SpecialParameters2 : IPaymentParameters{}
2 Answers

Yes, it ASP.NET Core is quite good at model binding, I will setup example for you (just had refactored a little your classes - not sure in details what you have to do, but as a demonstration of a principle I think it will suit well)

In View you setup something like this:

<h2>orders with ajax</h2>
<dl>
    <dt>Order Id:</dt>
    <dd id="order-id">D3BCDEEE-AE26-4B20-9170-D1CA01B52CD4</dd>

    <dt>Payment container - Type</dt>
    <dd id="order-paymentcontainer-type">Card payment</dd>

    <dt>Payment Parameters - Name</dt>
    <dd id="order-paymentcontainer-paymentParameters-name">Card payment nr. 1</dd>

    <dt>Payment Parameters - Provider</dt>
    <dd id="order-paymentcontainer-paymentParameters-provider">Big Bank</dd>
</dl>
<a id="submit-button">Process order</a>


@section Scripts {
    <script src="~/js/paymentTest.js"></script>
}

Then you setup classes:

public class Order
{
    public Guid Id { get; set; }
    public PaymentContainer PaymentContainer { get; set; }
}

public class PaymentContainer
{
    public string Type { get; set; }
    public PaymentParameters PaymentParameters { get; set; }
}

public class PaymentParameters
{
    public string Name { get; set; }
    public string Provider { get; set; }
}

Then write in your paymentTest.cs

var order = {
        Id: "",
        PaymentContainer: {
            Type: "",
            PaymentParameters: {
                Name: "",
                Provider: ""
            }
        }
    };

order.Id = document.getElementById("order-id").innerHTML;
order.PaymentContainer.Type = document.getElementById("order-paymentcontainer-type").innerHTML;
order.PaymentContainer.PaymentParameters.Name = document.getElementById("order-paymentcontainer-paymentParameters-name").innerHTML;
order.PaymentContainer.PaymentParameters.Provider = document.getElementById("order-paymentcontainer-paymentParameters-provider").innerHTML;

$("#submit-button").click(function () {
    $.ajax({
        url: 'api/orders',
        type: 'POST',
        dataType: "json",
        contentType: 'application/json',
        data: JSON.stringify(order),
        success: function (data) {
            location.reload();
        }
    });
});

And finally in controller setup method for binding

[Route("api/orders")]
[HttpPost]
public async Task ProcessOrder([FromBody] Order order)
{
    var orders = new List<Order>();
    orders.Add(order);

    // and some other code ...
}

Please correct me If I'm wrong - the main problem here is that you cant bind data from View returned to the controller?

You can't bind because your Order class contains interface (indirectly), and MVC can't bind interfaces. That's O.K. since interfaces abstract behaviors not data.

Potential workaround for this problem is to use OrderViewModel instead of Order type in CreateOrder method. i.e.:

public IActionResult CreateOrder([FromBody]OrderViewModel order)

Then OrderViewModel will have PaymentContainerViewModel instead of PaymentContainer, and it would have concrete PaymentParameters Value instead of interface IPaymentParameters Value, and than binding is good to go.

Related