Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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;
    }
like image 680
rst Avatar asked May 22 '19 15:05

rst


People also ask

What is custom model binder in MVC?

In the MVC pattern, Model binding maps the HTTP request data to the parameters of a Controllers action method. The parameter can be of a simple type like integers, strings, double etc. or they may be complex types. MVC then binds the request data to the action parameter by using the parameter name.

How do I bind a model to view in MVC core?

How does model binding work in ASP.NET Core MVC. In an empty project, change Startup class to add services and middleware for MVC. Add the following code to HomeController, demonstrating binding of simple types. Add the following code to HomeController, demonstrating binding of complex types.

Do you know what is model binding in asp net core How is it useful for developers?

Model binding is a well-designed bridge between the HTTP request and the C# action methods. It makes it easy for developers to work with data on forms (views), because POST and GET is automatically transferred into a data model you specify. ASP.NET MVC uses default binders to complete this behind the scene.


1 Answers

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

like image 77
rst Avatar answered Oct 23 '22 20:10

rst