Custom model binding through body in ASP.Net Core
Asked Answered
R

1

12

I would like to bind an object in a controller through the body of a HTTP Post.

It works like this

public class MyModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
                throw new ArgumentNullException("No context found");

            string modelName = bindingContext.ModelName;
            if (String.IsNullOrEmpty(modelName)) {
                bindingContext.Result = ModelBindingResult.Failed();
                return Task.CompletedTask;
            }

            string value = bindingContext.ValueProvider.GetValue(modelName).FirstValue;
...

The modelName is viewModel (honestly, I don't know why, but it works...)

My controller looks like this

    [HttpPost]
    [Route("my/route")]
    public IActionResult CalcAc([ModelBinder(BinderType = typeof(MyModelBinder))]IViewModel viewModel)
    {
   ....

i.e. it works, when I make this HTTP-Post request

url/my/route?viewModel=URLparsedJSON

I would like however to pass it through the body of the request, i.e.

public IActionResult Calc([FromBody][ModelBinder(BinderType = typeof(MyModelBinder))]IViewModel viewModel)

In my Modelbinder then, the modelName is "" and the ValueProvider yields null... What am I doing wrong?

UPDATE

Example; Assume you have an interface IGeometry and many implementations of different 2D shapes, like Circle: IGeometry or Rectangle: IGeometry or Polygon: IGeometry. IGeometry itself has the method decimal getArea(). Now, my URL shall calculate the area for any shape that implements IGeometry, that would look like this

    [HttpPost]
    [Route("geometry/calcArea")]
    public IActionResult CalcArea([FromBody]IGeometry geometricObject)
    {
         return Ok(geometricObject.getArea());
         // or for sake of completness
         // return Ok(service.getArea(geometricObject));
    }

the problem is, you cannot bind to an interface, that yields an error, you need a class! That's where the custom model binder is used. Assume your IGeometryalso has the following property string Type {get; set;} the in the custom model binding you would simply search for that Type in the passed json and bind it to the correct implementation. Something like

if (bodyContent is Rectangle) // that doesn't work ofc, but you get the point
var boundObject = Newtonsoft.Json.JsonConvert.DeserializeObject<Rectangle>(jsonString);

ASP.Net EF

In ASP.Net EF the custom model binding looks like this

 public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)

here you get the body of the HTTPPost request like this

 string json = actionContext.Request.Content.ReadAsStringAsync().Result;

in ASP.Net Core you don't have the actionContext, only the bindingContext where I can't find the body of the HTTP Post.

UPDATE 2

Ok, I found the body, see accepted answer. Now inside the controller method I really have an object from type IGeometry (an interface) that is instantiated inside the custom model binder! My controller method looks like this:

    [HttpPost]
    [Route("geometry/calcArea")]
    public IActionResult CalcArea([FromBody]IGeometry geometricObject)
    {
         return Ok(service.getArea(geometricObject));
    }

And my injected service like this

    public decimal getArea(IGeometry viewModel)
    {
        return viewModel.calcArea();
    }

IGeometry on the other hand looks like this

public interface IGeometry
{
    string Type { get; set; } // I use this to correctly bind to each implementation
    decimal calcArea();

...

Each class then simply calculates the area accordingly, so

public class Rectangle : IGeometry
{
    public string Type {get; set; }
    public decimal b0 { get; set; }
    public decimal h0 { get; set; }

    public decimal calcArea()
    {
        return b0 * h0;
    }

or

public class Circle : IGeometry
{
    public string Type {get; set; }
    public decimal radius { get; set; }

    public decimal calcArea()
    {
        return radius*radius*Math.Pi;
    }
Ringo answered 22/5, 2019 at 15:27 Comment(13)
I'm confused. Why are you trying to use a ModelBinder for this? Why not just put the model you're trying to bind directly into the method as a parameter?Fossa
Because I need it to be an interface. Imagine HTTP Post IAnimal, and the user can post a Cat, or a Dog or whatnot. I really need the custom model bindingRingo
Honestly, I would never use interfaces as parameters for methods exposed over the wire like that.. a) It causes far more problems than it solves b) you will be forever trying to track down odd bugs.. IMO, stick to simple POCO’s for transferring across the wire and you wont need modelbinding at all.. Its built inFossa
Thanks for the opinion. I share that, but in very rare occasion, this is betterRingo
Which is this rare occasion? I mean seriously, you receive just data. Nothing holds you back from inheriting from this interface in your ViewModel (e.g. class ViewModel : IViewModel. Interfaces allow you to have different implementations. I can't imagine a way, how transfered data can have different implementations. They are just data. Probably you need a second method, which can receive another data model. Then pass it to your service layer and use interfaces there, if necessary.Luge
Or in other words: If you need methods on a data transfer model, you are doing something wrong. A data transfer model should only be responsible for data transfer. If you need to operate on this data, you operate on this data in your service layer. If your data transfer object does not use methods, why is it an interface (except directly inherited by your model)?Luge
@ChristianGollhardt In a controller method you define [FromBody] with a certain class, you cannot instantiate an interface. What if you want to receive e.g. an geometic shape (interface) with a method getArea. You would then allow to send a circle (only property is radius) or rectangle (only properties are height and width) or a polygon (array of x/y data). If you had an URL HTTP Post /object/ you would have to have an URL for each single geometic object. Using a custom model binder you can check the incoming data and map it to the specific class.Ringo
A model binders job is to bind a model. What you describe is business logic. And yes, if you need to receive a Circle : IGeometry, you need a method which supports that. And if you receive a Rectangle : IGeometry, you need another method which received that. Then you can pass it to your service service.DoSomething(IGeometry). As I read between the lines, you want to abuse the model binder to do business logic (service layer).Luge
@ChristianGollhardt Thank you for your consideration. My Business Logic is indeed inside the service, however I need to pass the IGeometry to it! Therefore I need to BIND the incoming data to a class that implements IGeometry. Typically, in the FromBody binding of ASP.NET Core you can only bind to a specific class, not to an interface. If you want to bind to an interface to pass it to the service, you need a model binder (which I described in the OP)Ringo
@ChristianGollhardt I extended the OP with an exampleRingo
@ChristianGollhardt Dear Christian, I found the missing part and extended the OP with an example. I would appreciate some feedback on the implementaiton. if you see another way to pass different types of data (we're talking about json objects) that can be bound to different classes with the same interface implementation, I'd be happy to hear about it.Ringo
I would probably make it async await reader.ReadToEndAsync(), but yeah, that's the way you read the body. Anyway, a seperate method foreach Geometry Object is still prefered (imho). It's a cleaner contract, and shorter than a custom model binder. Think about how you want to document a request, if it can have any value? Again a Model Binder is responsible to Bind a Model. That's not what your Binder is doing. Your binder performs business logic, and after that creates an instance, depending of the result. This sounds wrong.Luge
This is a toy example, if you have hundrets of different implementations, that would result in that many url's... custom model binding is massively shorter (right now, I have about 30 lines of code in the CMB that don't increase, no matter how many more shapes I add with IGeometry). I don't see any business logic in my model binder. It's very similar (IMO) to the official example learn.microsoft.com/en-us/aspnet/core/mvc/advanced/…Ringo
R
10

I found a solution. The body of a HTTP Post request using ASP.NET Core can be obtained in a custom model binder using this lines of code

string json;
using (var reader = new StreamReader(bindingContext.ActionContext.HttpContext.Request.Body, Encoding.UTF8))
   json = reader.ReadToEnd();

I found the solution after looking at older EF projects. There the body is inside the ActionContext which is passed separately as an argument in the BindModel method. I found that the same ActionContext is part of the ModelBindingContext in ASP.Net Core, where you get an IO.Stream instead of a string (easy to convert :-))

Ringo answered 28/5, 2019 at 11:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.