asp.net-mvcmodelbinders

MVC Controller handle POSTed JSON as a string


I have a 3rd party app making POST submissions to my site with application/json in the body. I can capture a strongly typed object as:

public string Postback([FromBody]MyResponse myResponse)

My problem is that after having gotten this much working I'm directed to support a 2nd type at the same endpoint. Therefore I need to accept a string value (instead of the result of the model binder) and then JsonConvert the string to one or the other of the 2 possible types.

So I figure I should change my method's signature to:

public string Postback([FromBody]string _sfResponse)

But the string is always showing up null (with or without the [FromBody] directive). Seems the ModelBinder insists on participating. Anyway to convince him not to?

Just in case there's anything about routing:

routes.MapRoute(
    "MyPostback",
    url: "rest/V1/sync/update",
    defaults: new { controller = "Admin", action = "MyPostback" }    
);

The controller action:

[System.Web.Mvc.HttpPost]
[System.Web.Mvc.AllowAnonymous]
public string MyPostback([System.Web.Http.FromBody][ModelBinder(typeof(MyResponseModelBinder))] MyResponseToProduct _sfResponse)
{
//stuff
}

The json being sent is more complex that average but keep in mind that when the controller's signature is referencing a strongly typed object that matches that json everything works. (and to repeat -- I'm having to accommodate 2 different incoming types which is why I'm needing to take a string at the beginning instead of a model).

 {
"results": [
    {
        "errors": {
            "error": "No Error"
        },
        "sku": "70BWUS193045G81",
        "status": "success",
        "productId": "123"
    },
    {
        "errors": {
            "error": "No Error"
        },
        "sku": "70BWUS193045G82",
        "status": "success",
        "productId": "123"
    }
],
"validationType": "products",
"messageId": "ac5ed64f-2957-51b4-8fbb-838e0480e7ad"
}

I've added a custom model binder just to be able to peek at values before the controller gets hit:

public class MyResponseModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        HttpRequestBase request = controllerContext.HttpContext.Request;
        int id = Convert.ToInt32(request.Form.GetValues("Id"));

        return new MyResponseToProduct()
        {
            results = new List<MyResponseToProduct.Result>()
        };
    }
}

When I inspect the values within the custom model binder the controllerContext.HttpContext.Request.Form is empty (this is a POST submission so the body really should be there, shouldn't it?)

I can see my bits data in the bindingContext.PropertyMetadata but I can't imagine I'm supposed to be going that deep.

Very strange why Request.Form is empty.


Solution

  • ASP.Net Core:

    You can declare your controller action argument as object and then call ToString() on it, like this:

    [HttpPost]
    public IActionResult Foo([FromBody] object arg)
    {
        var str = arg?.ToString();
        ...
    }
    

    In this example str variable will contain JSON string from a request body.

    ASP.Net MVC:

    Since the previous option does not work in old ASP.Net MVC, as an option you can read data directly from Request property in your controller's action. The following extension method will help you with it:

    public static string GetBody(this HttpRequestBase request)
    {
        var requestStream = request?.InputStream;
        requestStream.Seek(0, System.IO.SeekOrigin.Begin);
        using (var reader = new StreamReader(requestStream))
        {
            return reader.ReadToEnd();
        }
    }
    

    And your action will look like that:

    [HttpPost]
    [Route("test")]
    public ActionResult Foo()
    {
        string json = Request.GetBody();
        ...
    }