0
votes

I've been working on building an API for my company's website. I've got actions for all of my GETs working fine and returning JSON like I want.

I'm having problems when it comes to using POST to create new records though. I'm sending JSON to my controller. This works fine if my controller is accepting a string or object parameter, but doesn't work when it is expecting a Model.

Inside my api controller, I have this method:

[HttpPost]
public void POSTEstimate(Estimate estimate)
{
    //never makes it to the first line when expecting a Model (Estimate)
}

From what I've been reading it looks like Web API should be able to deserialize the JSON that is posted automatically (and put it into my Model), but instead I always get a 500 Internal Server Error. The method works fine when I tell it to accept a string.

Is there something obvious I'm doing wrong here?

Edit: Here's the stack trace from the test page I'm using to call the API Method:

[WebException: The remote server returned an error: (500) Internal Server Error.]
   System.Net.HttpWebRequest.GetResponse() +6120419
   PDR.Controllers.AdminController.test() in c:\PDR\PDR\PDR\Controllers\AdminController.cs:714
   lambda_method(Closure , ControllerBase , Object[] ) +96
   System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters) +17
   System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary`2 parameters) +205
   System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary`2 parameters) +27
   System.Web.Mvc.Async.<>c__DisplayClass42.<BeginInvokeSynchronousActionMethod>b__41() +28
   System.Web.Mvc.Async.<>c__DisplayClass8`1.<BeginSynchronous>b__7(IAsyncResult _) +12
   System.Web.Mvc.Async.WrappedAsyncResult`1.End() +57
   System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeActionMethod(IAsyncResult asyncResult) +50
   System.Web.Mvc.Async.<>c__DisplayClass39.<BeginInvokeActionMethodWithFilters>b__33() +58
   System.Web.Mvc.Async.<>c__DisplayClass4f.<InvokeActionMethodFilterAsynchronously>b__49() +237
   System.Web.Mvc.Async.<>c__DisplayClass37.<BeginInvokeActionMethodWithFilters>b__36(IAsyncResult asyncResult) +12
   System.Web.Mvc.Async.WrappedAsyncResult`1.End() +57
   System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeActionMethodWithFilters(IAsyncResult asyncResult) +50
   System.Web.Mvc.Async.<>c__DisplayClass2a.<BeginInvokeAction>b__20() +24
   System.Web.Mvc.Async.<>c__DisplayClass25.<BeginInvokeAction>b__22(IAsyncResult asyncResult) +126
   System.Web.Mvc.Async.WrappedAsyncResult`1.End() +57
   System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeAction(IAsyncResult asyncResult) +45
   System.Web.Mvc.<>c__DisplayClass1d.<BeginExecuteCore>b__18(IAsyncResult asyncResult) +14
   System.Web.Mvc.Async.<>c__DisplayClass4.<MakeVoidDelegate>b__3(IAsyncResult ar) +25
   System.Web.Mvc.Async.WrappedAsyncResult`1.End() +62
   System.Web.Mvc.Controller.EndExecuteCore(IAsyncResult asyncResult) +61
   System.Web.Mvc.Async.<>c__DisplayClass4.<MakeVoidDelegate>b__3(IAsyncResult ar) +25
   System.Web.Mvc.Async.WrappedAsyncResult`1.End() +62
   System.Web.Mvc.Controller.EndExecute(IAsyncResult asyncResult) +49
   System.Web.Mvc.Controller.System.Web.Mvc.Async.IAsyncController.EndExecute(IAsyncResult asyncResult) +10
   System.Web.Mvc.<>c__DisplayClass8.<BeginProcessRequest>b__3(IAsyncResult asyncResult) +28
   System.Web.Mvc.Async.<>c__DisplayClass4.<MakeVoidDelegate>b__3(IAsyncResult ar) +25
   System.Web.Mvc.Async.WrappedAsyncResult`1.End() +62
   System.Web.Mvc.MvcHandler.EndProcessRequest(IAsyncResult asyncResult) +49
   System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.EndProcessRequest(IAsyncResult result) +9
   System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +9042109
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +184
2
What happens when you debug the application? Debugging will tell you WHY the 500 Internal Server Error happens. - JLe
It never breaks anywhere in debug. It just goes straight to the error page. - Josh Blade
The server probably sends some information back about the 500. What does that say? - JLe
Then the error must be ocurring during the model binding...are you sure you don't have any errors in your model class (i.e a default ctor with some code that could throw or properties that might throw, etc)? Also, I'm not sure that you need to stringify your json on POST, can you try just setting your $.ajax call's data to "real" json i.e no escaping or quoting, just to see if this makes any difference? - Stephen Byrne
Don't think so - you should end up with ModelState.IsValid==false but not an Http 500. I'd be more inclined to think that the default model binder is being whacked with an exception originating from trying to either construct an instance of your model, or bind a particular property value. If you're really stuck try adding a custom exception filter to your API (asp.net/web-api/overview/web-api-routing-and-actions/…) which will at least give you some shot at seeing the "real" exception. - Stephen Byrne

2 Answers

2
votes

Web API, like other REST frameworks, is encouraging and supporting using a POCO serializable to JSON DTO. The serialization seems to be failing.

Without the client code, which should be creating a properly formatted JSON body to POST, properly setting the content type being posted as application/json, it is hard to tell which part of the client code isn't correct.

The error being reported as an internal server error isn't helpful. The fact that your code isn't being reached, without even looking at the StackTrace, clearly indicates that the issue is occurring in framework code. The framework is expecting a properly formatted DTO, so compare the DTO being POST'd vs the DTO's definition OR construct and serialize an instance of the DTO and compare the actual strings.

0
votes

I fell on something similar today, and my issue was that there were multiple possible actions in my controller that could be used by Web Api and it wasn't able to know which one to use. So in my case I used the longest form client-side and server-side for the action, instead of relying on default api route :

Router:

config.Routes.MapHttpRoute(
            name: "ApiWithAction",
            routeTemplate: "metadataApi/{controller}/{action}/{id}",
            defaults: new {id = RouteParameter.Optional});

Ajax call:

[...]
var options = {
    url: basePath + 'metadataapi/employee/FetchPage',
    type: 'POST',
    data: filter,
    dataType: 'json'
};

return $.ajax(options);