3

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.

6
  • why can't you change the content type to application/json when it is json that you are passing around? Commented Jul 18, 2016 at 14:29
  • 2
    Unrelated to the question, but I am bothered by the fact that you're using the async keyword just to do Task.Delay(1) Commented Jul 18, 2016 at 14:35
  • @MatiasCicero I've removed code responsible for processing data and saving it to database, I'm doing everything async so instead of changing method declaration I simply added Task.Delay(1) sorry if it is confusing. Commented Jul 18, 2016 at 18:31
  • @CallumLinington I've edited my question. By format I mean that it always will be POST request with single key-value pair. key is JSON and value contains json string Commented Jul 18, 2016 at 19:02
  • @Misiu So your question is how to get rid of Tmp class? Commented Jul 19, 2016 at 8:15

3 Answers 3

4

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)
        );
Sign up to request clarification or add additional context in comments.

7 Comments

Thank You for reply, but what about request format? Data is send using POST with key=JSON and value={"user_Id": "728409840", "call_id": "1114330","answered_time": "2015-04-16 15:37:47"}. I want to map that string to CallDetails
Thank You for update, as You wrote my initial option isn't bad. What I really want is to be able to use ModelState.IsValid inside methods. DelegatingHandler looks like the best option - I'll get my model right away, probably ModelState.IsValid will work (not sure on that). The downside is custom route. Maybe handler can be specified inside attribute? Do You know if this is possible?
@Misiu, I know that it is not possible to specify handler with attribute routing. And validation will work
In which cases validation should work? Only with DelegatinHandler or with ModelBinder or initial solution (JObject.ToObject<>)? Sorry for all the questions but I'd like to implement best solution possible.
Only with DelegatinHandler, in two other cases you'll need to run it manually (which is not so bad).
|
0

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

1 Comment

I've added some more details to my question. Basically I want to remove usage of Tmp class and instead mapping request to Tmp and then to CallDetails I want to map my request to CallDeails from start.
0

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.

5 Comments

I've added some more details to my question. Basically I want to remove usage of Tmp class and instead mapping request to Tmp and then to CallDetails I want to map my request to CallDeails from start.
Could You please post some code, and this would help a lot. Isn't there more generic solution? I have four methods that have similar problem. I'd like to have more reusable solution.
MVC5 also has FromBody attribute, but it didn't helped. WHen deserializing from XML we can use attributes for element names. Can similar thing be done with JSON?
Try to make sure that the posting form is including the contentType: "application/json" header and change your CallDetails properties to match the names in json. You may be stuck with the Tmp class since your page is posting json wrapped in a key/value pair.
As I wrote in my question I can't modify request, because it is done by external service, all I'm able to set is URL to which request is done.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.