asp.net-corecustom-model-binder

Custom model binding through body in ASP.Net Core


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;
    }

Solution

  • 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 :-))