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 IGeometry
also 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;
}
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 :-))