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;
}
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[FromBody]
with a certain class, you cannot instantiate an interface. What if you want to receive e.g. an geometic shape (interface) with a methodgetArea
. 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. – RingoCircle : IGeometry
, you need a method which supports that. And if you receive aRectangle : IGeometry
, you need another method which received that. Then you can pass it to your serviceservice.DoSomething(IGeometry)
. As I read between the lines, you want to abuse the model binder to do business logic (service layer). – Lugeawait 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. – Lugeshapes
I add withIGeometry
). 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