Misiu
Misiu

Reputation: 4919

WebAPI post JSON string and map it to model

I must create webhook endpoint that will consume JSON messages.
Messages is send as x-www-form-urlencoded in form:

key = json
value = {"user_Id": "728409840", "call_id": "1114330","answered_time": "2015-04-16 15:37:47"}

as shown in PostMan:

enter image description here

request looks like this:

json=%7B%22user_Id%22%3A+%22728409840%22%2C+%22call_id%22%3A+%221114330%22%2C%22answered_time%22%3A+%222015-04-16+15%3A37%3A47%22%7D

To get values from request as my class (model) I must create temporary object containing single string property:

public class Tmp
{
    public string json { get; set; }
}

and method inside my controller that consumes that request:

[AllowAnonymous]
[Route("save_data")]
[HttpPost]
public IHttpActionResult SaveData(Tmp tmp)
{
    JObject json2 = JObject.Parse(tmp.json);
    var details = json2.ToObject<CallDetails>();
    Debug.WriteLine(details);
    //data processing
    return Content(HttpStatusCode.OK, "OK", new TextMediaTypeFormatter(), "text/plain");
}

As You can see Tmp class is useless.

Is there a way to get request data as this class:

public class CallDetails
{
    public string UserId { get; set; }
    public string CallId { get; set; }
    public string AnsweredTime { get; set; }
}

I'm aware of IModelBinder class, but before I start I'd like to know if there is an easier way.

I can't change web-request format, by format I mean that is will always be POST containing single key - JSON yhat has json string as value.

Upvotes: 3

Views: 12686

Answers (3)

Aleksey L.
Aleksey L.

Reputation: 37918

You can use JsonProperty attribute for mapping json object properties to c# object properties:

public class CallDetails
{
    [JsonProperty("user_id")]
    public string UserId { get; set; }
    [JsonProperty("call_id")]
    public string CallId { get; set; }
    [JsonProperty("answered_time")]
    public string AnsweredTime { get; set; }
}

Then it can be used without temp class:

[AllowAnonymous]
[Route("save_data")]
[HttpPost]
public IHttpActionResult SaveData(CallDetails callDetails)

Update. Because the data is sent as x-www-form-urlencoded - I think the way you handled it is most straightforward and not so bad. If you want to check another options here're some of them:

Option 1 - custom model binder. Something like this:

public class CustomModelBinder : IModelBinder
{
    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        var body = actionContext.Request.Content.ReadAsStringAsync().Result;
        body = body.Replace("json=", "");
        var json = HttpUtility.UrlDecode(body);

        bindingContext.Model = JsonConvert.DeserializeObject<CallDetails>(json);

        return true;
    }
}

And usage: SaveData([ModelBinder(typeof(CustomModelBinder))]CallDetails callDetails). Downside - you'll lose validation and maybe other stuff defined in web api pipeline.

Option 2 - DelegatingHandler

public class NormalizeHandler : DelegatingHandler
{
    public NormalizeHandler(HttpConfiguration httpConfiguration)
    {
        InnerHandler = new HttpControllerDispatcher(httpConfiguration);
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var source = await request.Content.ReadAsStringAsync();
        source = source.Replace("json=", "");
        source = HttpUtility.UrlDecode(source);

        request.Content = new StringContent(source, Encoding.UTF8, "application/json");

        return await base.SendAsync(request, cancellationToken);
    }
}

Usage:

[AllowAnonymous]
[HttpPost]
public IHttpActionResult SaveData(CallDetails callDetails)

Downside - you'll need to define custom route for it:

config.Routes.MapHttpRoute(
            name: "save_data",
            routeTemplate: "save_data",
            defaults: new { controller = "YourController", action = "SaveData" },
            constraints: null,
            handler: new NormalizeHandler(config)
        );

Upvotes: 4

leetibbett
leetibbett

Reputation: 843

Json.NET by NewtonSoft can help you deserialize an object. If your json property names don't match your actual class names you can write a custom converter to help.

EDIT

You could try this if you are on MVC6. Change your parameter from type Tmp to type CallDetails and mark it with attribute [FromBody], like this:

public IHttpActionResult SaveData([FromBody]CallDetails details)

For example look at the section "Different model binding" in this post. However, I'm still thinking that you will need to deserialize manually because the property names of class CallDetails don't exactly match the incoming JSON properties.

Upvotes: 0

shiroma
shiroma

Reputation: 65

You don´t forget to decode the url encoded before use JObject.Parse ?, it´s maybe works. And the properties of the object don´t match the json atributes

Upvotes: 0

Related Questions