Returning JSON Errors from Asp.Net MVC 6 API - json

I'm trying out building a web API with MVC 6. But when one of my controller methods throws an error, the content of the response is a really nicely formatted HTML page that would be very informative were this an MVC app. But since this is an API, I'd rather have some JSON returned instead.
Note: My setup is super basic right now, just setting:
app.UseStaticFiles();
app.UseIdentity();
// Add MVC to the request pipeline.
app.UseMvc();
I want to set this up universally. Is there a "right/best" way to set this up in MVC 6 for an API?
Thanks...

One way to achieve your scenario is to write an ExceptionFilter and in that capture the necessary details and set the Result to be a JsonResult.
// Here I am creating an attribute so that you can use it on specific controllers/actions if you want to.
public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
var exception = context.Exception;
context.Result = new JsonResult(/*Your POCO type having necessary details*/)
{
StatusCode = (int)HttpStatusCode.InternalServerError
};
}
}
You can add this exception filter to be applicable to all controllers.
Example:
app.UseServices(services =>
{
services.AddMvc();
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new CustomExceptionFilterAttribute());
});
.....
}
Note that this solution does not cover all scenarios...for example, when an exception is thrown while writing the response by a formatter.

Related

EmbedIO with a list of unknown web APIs

I want to have a modular EmbedIO setup with a dynamic list of unknown web API controller types. I thought it'd be easy :( But at the moment I'm stuck at registering the web APIs:
// Some APIs to setup at the EmbedIO webserver in "Server"
Dictionary<string, WebApiController> apis = ...;
// Register the APIs at the webserver
foreach(KeyValuePair<string, WebApiController> kvp in apis)
{
// Exception: "Controller type must be a subclass of WebApiController."
Server.WithWebApi(kvp.Key, m => m.WithController(() => kvp.Value));
}
The problem is: The factory method needs to return the final type of the controller object. Everything else seems to fail.
I tried with dynamic instead of WebApiController or returning object and giving the type as first parameter for WithController - whatever I tried, it resulted in an exception; Or when I use a class WebApiControllerWrapper : WebApiController and a Dictionary<string, WebApiControllerWrapper>, the exported controller methods of the final type are missing, because they're not defined in WebApiControllerWrapper.
It seems the only way is to use reflection for the generic call of WithController - or does anyone know another working solution (I'm in .NET Standard 2.1)?
I was able to solve it with an expression tree that calls a generic method to create the factory function:
public class ModularWebApiController : WebApiController
{
public Func<T> CreateFactoryMethod<T>() where T : WebApiController => () => (T)this;
}
public static class Extensions
{
public static WebApiModule WithController(this WebApiModule webApiModule, ModularWebApiController api)
{
Delegate factoryFunc = Expression
.Lambda(Expression.Call(
Expression.Constant(api),
typeof(ModularWebApiController).GetMethod("CreateFactoryMethod").MakeGenericMethod(api.GetType())
))
.Compile();
return (WebApiModule)typeof(WebApiModuleExtensions)
.GetMethods(BindingFlags.Static | BindingFlags.Public)
.Single(mi => mi.IsGenericMethod & mi.Name == "WithController" && mi.GetParameters().Length == 2)
.MakeGenericMethod(api.GetType())
.Invoke(null, new object[] { webApiModule, factoryFunc.DynamicInvoke(Array.Empty<object>()) });
}
}
I only had to ensure that all web API controller types extend the ModularWebApiController type, and I had to change the modular web API setup for EmbedIO:
Dictionary<string, ModularWebApiController> apis = ...;
foreach(KeyValuePair<string, ModularWebApiController> kvp in apis)
{
Server.WithWebApi(kvp.Key, m => m.WithController(kvp.Value));
}
After browsing the EmbedIO source I think this seems to be the only way to have a modular web API setup, where the code doesn't know which web API controller types are going to be used.
Now I'm able to load and instance any web API controller type configured in a JSON configuration file like this:
[
{
"Type": "name.space.WebApiControllerTypeName",
"Path": "/webapipath/"
},
{
"Type": "name.space.AnotherWebApiControllerTypeName",
"Path": "/anotherwebapipath/"
}
]
Just for example. I wonder why it seems that nobody else want to do this ;)

Dynamic parameter as part of request URI with Apache HttpCore

I am looking for existing solutions to match dynamic parameters with HttpCore. What I have in mind is something similar to constraints in ruby on rails, or dynamic parameters with sails (see here for example).
My objective is to define a REST API where I could easily match requests like GET /objects/<object_id>.
To give a little bit of context, I have an application that creates an HttpServer using the following code
server = ServerBootstrap.bootstrap()
.setListenerPort(port)
.setServerInfo("MyAppServer/1.1")
.setSocketConfig(socketConfig)
.registerHandler("*", new HttpHandler(this))
.create();
And the HttpHandler class that matches the requested URI and dispatches it to the corresponding backend method:
public void handle(final HttpRequest request, final HttpResponse response, final HttpContext context) {
String method = request.getRequestLine().getMethod().toUpperCase(Locale.ROOT);
// Parameters are ignored for the example
String path = request.getRequestLine().getUri();
if(method.equals("POST") && path.equals("/object/add") {
if(request instanceof HttpEntityEnclosingRequest) {
addObject(((HttpEntityEnclosingRequest)request).getEntity())
}
[...]
For sure I can replace path.equals("/object/add") by something more sophisticated with RegEx to match these dynamic parameters, but before doing so I'd like to know if I am not reinventing the wheel, or if there is an existing lib/class I didn't see in the docs that could help me.
Using HttpCore is a requirement (it is already integrated in the application I am working on), I know some other libraries provide high-level routing mechanisms that support these dynamic parameters, but I can't really afford switching the entire server code to another library.
I am currently using httpcore 4.4.10, but I can upgrade to a newer version of this might help me.
At present HttpCore does not have a fully featured request routing layer. (The reasons for that are more political than technical).
Consider using a custom HttpRequestHandlerMapper to implement your application specific request routing logic.
final HttpServer server = ServerBootstrap.bootstrap()
.setListenerPort(port)
.setServerInfo("Test/1.1")
.setSocketConfig(socketConfig)
.setSslContext(sslContext)
.setHandlerMapper(new HttpRequestHandlerMapper() {
#Override
public HttpRequestHandler lookup(HttpRequest request) {
try {
URI uri = new URI(request.getRequestLine().getUri());
String path = uri.getPath();
// do request routing based on the request path
return new HttpFileHandler(docRoot);
} catch (URISyntaxException e) {
// Provide a more reasonable error handler here
return null;
}
}
})
.setExceptionLogger(new StdErrorExceptionLogger())
.create();

passing json data of webapi method from controller to view

I have data in my apicontroller in following way-
public class OutletPOCController : ApiController
{
OutletPOCContext db = new OutletPOCContext();
[System.Web.Http.ActionName("GetTabText")]
public TabTextModel GetTabText(int bizId)
{
var outlet = db.Info.Where(t => t.BizId == bizId).SingleOrDefault();
return new TabTextModel
{
HomeTab = outlet.BizHomeTabText,
AboutTab = outlet.BizAboutTabText,
TimingsTab = outlet.BizTimingsTabText,
};
}
And now i want to retrieve this data into my view. How shall i create view for this controller and pass the above data? What will be my action method? I am new to webapi and json. Any help is appreciable! Thanks in advance!
The API controller dosent really have views in the sense that you create a cshtml page that takes care of how you display your data. The purpose of the ApiController is simply to return data in the format that you want to consume it.
Basically the API exposes raw data to the web, you consume it in some way, and then display it..
I use something similar to this to load data dynamically into a web page.
Just a simple web api that returns data to the client.
public class APIController : ApiController
{
[HttpGet]
[HttpPost] // allow both post and get requests
public IEnumerable<String> GetData()
{
return new List<string>() { "test1", "test2" };
}
}
When you browse to the API method above it returns this xml data
<ArrayOfstring xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<string>test1</string>
<string>test2</string>
</ArrayOfstring>
Which I get using Jquery and do what I please with (http://api.jquery.com/jQuery.get/):
$.get("/api/GetData", function(data) {
alert("Data Loaded: " + data);
});
Examples of XML parsing with JS/Jquery:
http://tech.pro/tutorial/877/xml-parsing-with-jquery
http://www.kawa.net/works/js/jkl/parsexml-e.html
If you are simply looking to get data into a regular view and work with it there without going through javascript I wouldent use a webapi, but instead get the data in the controller and send it to the view for displaying (ASP MVC4 - Pass List to view via view model).
You can also check out the ViewBag container for passing random odd data to the view http://goo.gl/03JTR
On the off chance you really do want to render your data in a view, check this out: Web API - Rendering Razor view by default?

How do I get ASP.NET Web API to return JSON instead of XML using Chrome?

This question's answers are a community effort. Edit existing answers to improve this post. It is not currently accepting new answers or interactions.
Using the newer ASP.NET Web API, in Chrome I am seeing XML - how can I change it to request JSON so I can view it in the browser? I do believe it is just part of the request headers, am I correct in that?
Note: Read the comments of this answer, it can produce a XSS Vulnerability if you are using the default error handing of WebAPI
I just add the following in App_Start / WebApiConfig.cs class in my MVC Web API project.
config.Formatters.JsonFormatter.SupportedMediaTypes
.Add(new MediaTypeHeaderValue("text/html") );
That makes sure you get JSON on most queries, but you can get XML when you send text/xml.
If you need to have the response Content-Type as application/json please check Todd's answer below.
NameSpace is using System.Net.Http.Headers.
If you do this in the WebApiConfig you will get JSON by default, but it will still allow you to return XML if you pass text/xml as the request Accept header.
Note: This removes the support for application/xml
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes.FirstOrDefault(t => t.MediaType == "application/xml");
config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
}
}
If you are not using the MVC project type and therefore did not have this class to begin with, see this answer for details on how to incorporate it.
Using RequestHeaderMapping works even better, because it also sets the Content-Type = application/json in the response header, which allows Firefox (with JSONView add-on) to format the response as JSON.
GlobalConfiguration.Configuration.Formatters.JsonFormatter.MediaTypeMappings
.Add(new System.Net.Http.Formatting.RequestHeaderMapping("Accept",
"text/html",
StringComparison.InvariantCultureIgnoreCase,
true,
"application/json"));
I like Felipe Leusin's approach best - make sure browsers get JSON without compromising content negotiation from clients that actually want XML. The only missing piece for me was that the response headers still contained content-type: text/html. Why was that a problem? Because I use the JSON Formatter Chrome extension, which inspects content-type, and I don't get the pretty formatting I'm used to. I fixed that with a simple custom formatter that accepts text/html requests and returns application/json responses:
public class BrowserJsonFormatter : JsonMediaTypeFormatter
{
public BrowserJsonFormatter() {
this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
this.SerializerSettings.Formatting = Formatting.Indented;
}
public override void SetDefaultContentHeaders(Type type, HttpContentHeaders headers, MediaTypeHeaderValue mediaType) {
base.SetDefaultContentHeaders(type, headers, mediaType);
headers.ContentType = new MediaTypeHeaderValue("application/json");
}
}
Register like so:
config.Formatters.Add(new BrowserJsonFormatter());
MVC4 Quick Tip #3–Removing the XML Formatter from ASP.Net Web API
In Global.asax add the line:
GlobalConfiguration.Configuration.Formatters.XmlFormatter.SupportedMediaTypes.Clear();
like so:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
BundleTable.Bundles.RegisterTemplateBundles();
GlobalConfiguration.Configuration.Formatters.XmlFormatter.SupportedMediaTypes.Clear();
}
In the WebApiConfig.cs, add to the end of the Register function:
// Remove the XML formatter
config.Formatters.Remove(config.Formatters.XmlFormatter);
Source.
In the Global.asax I am using the code below. My URI to get JSON is http://www.digantakumar.com/api/values?json=true
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
GlobalConfiguration.Configuration.Formatters.JsonFormatter.MediaTypeMappings.Add(new QueryStringMapping("json", "true", "application/json"));
}
Have a look at content negotiation in the WebAPI. These (Part 1 & Part 2) wonderfully detailed and thorough blog posts explain how it works.
In short, you are right, and just need to set the Accept or Content-Type request headers. Given your Action isn't coded to return a specific format, you can set Accept: application/json.
As the question is Chrome-specific, you can get the Postman extension which allows you to set the request content type.
This code makes json my default and allows me to use the XML format as well. I'll just append the xml=true.
GlobalConfiguration.Configuration.Formatters.XmlFormatter.MediaTypeMappings.Add(new QueryStringMapping("xml", "true", "application/xml"));
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
Thanks everyone!
One quick option is to use the MediaTypeMapping specialization. Here is an example of using QueryStringMapping in the Application_Start event:
GlobalConfiguration.Configuration.Formatters.JsonFormatter.MediaTypeMappings.Add(new QueryStringMapping("a", "b", "application/json"));
Now whenever the url contains the querystring ?a=b in this case, Json response will be shown in the browser.
Don't use your browser to test your API.
Instead, try to use an HTTP client that allows you to specify your request, such as CURL, or even Fiddler.
The problem with this issue is in the client, not in the API. The web API behaves correctly, according to the browser's request.
Most of the above answers makes perfect sense.
Since you are seeing data being formatted in XML format ,that means XML formatter is applied,SO you can see JSON format just by removing the XMLFormatter from the HttpConfiguration parameter like
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Formatters.Remove(config.Formatters.XmlFormatter);
config.EnableSystemDiagnosticsTracing();
}
since JSON is the default format
Returning the correct format is done by the media-type formatter.
As others mentioned, you can do this in the WebApiConfig class:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
...
// Configure Web API to return JSON
config.Formatters.JsonFormatter
.SupportedMediaTypes.Add(new System.Net.Http.Headers.MediaTypeHeaderValue("text/html"));
...
}
}
For more, check:
Media Formatters in ASP.NET Web API 2.
Content Negotiation in ASP.NET Web API.
In case your actions are returning XML (which is the case by default) and you need just a specific method to return JSON, you can then use an ActionFilterAttribute and apply it to that specific action.
Filter attribute:
public class JsonOutputAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
ObjectContent content = actionExecutedContext.Response.Content as ObjectContent;
var value = content.Value;
Type targetType = actionExecutedContext.Response.Content.GetType().GetGenericArguments()[0];
var httpResponseMsg = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
RequestMessage = actionExecutedContext.Request,
Content = new ObjectContent(targetType, value, new JsonMediaTypeFormatter(), (string)null)
};
actionExecutedContext.Response = httpResponseMsg;
base.OnActionExecuted(actionExecutedContext);
}
}
Applying to action:
[JsonOutput]
public IEnumerable<Person> GetPersons()
{
return _repository.AllPersons(); // the returned output will be in JSON
}
Note that you can omit the word Attribute on the action decoration and use just [JsonOutput] instead of [JsonOutputAttribute].
I used a global action filter to remove Accept: application/xml when the User-Agent header contains "Chrome":
internal class RemoveXmlForGoogleChromeFilter : IActionFilter
{
public bool AllowMultiple
{
get { return false; }
}
public async Task<HttpResponseMessage> ExecuteActionFilterAsync(
HttpActionContext actionContext,
CancellationToken cancellationToken,
Func<Task<HttpResponseMessage>> continuation)
{
var userAgent = actionContext.Request.Headers.UserAgent.ToString();
if (userAgent.Contains("Chrome"))
{
var acceptHeaders = actionContext.Request.Headers.Accept;
var header =
acceptHeaders.SingleOrDefault(
x => x.MediaType.Contains("application/xml"));
acceptHeaders.Remove(header);
}
return await continuation();
}
}
Seems to work.
In the latest version of ASP.net WebApi 2, under WebApiConfig.cs, this will work:
config.Formatters.Remove(GlobalConfiguration.Configuration.Formatters.XmlFormatter);
config.Formatters.Add(GlobalConfiguration.Configuration.Formatters.JsonFormatter);
I found the Chrome app "Advanced REST Client" excellent to work with REST services. You can set the Content-Type to application/json among other things:
Advanced REST client
config.Formatters.Remove(config.Formatters.XmlFormatter);
It's unclear to me why there is all of this complexity in the answer. Sure there are lots of ways you can do this, with QueryStrings, headers and options... but what I believe to be the best practice is simple. You request a plain URL (ex: http://yourstartup.com/api/cars) and in return you get JSON. You get JSON with the proper response header:
Content-Type: application/json
In looking for an answer to this very same question, I found this thread, and had to keep going because this accepted answer doesn't work exactly. I did find an answer which I feel is just too simple not to be the best one:
Set the default WebAPI formatter
I'll add my tip here as well.
WebApiConfig.cs
namespace com.yourstartup
{
using ...;
using System.Net.Http.Formatting;
...
config.Formatters.Clear(); //because there are defaults of XML..
config.Formatters.Add(new JsonMediaTypeFormatter());
}
I do have a question of where the defaults (at least the ones I am seeing) come from. Are they .NET defaults, or perhaps created somewhere else (by someone else on my project). Anways, hope this helps.
You can use as below:
GlobalConfiguration.Configuration.Formatters.Clear();
GlobalConfiguration.Configuration.Formatters.Add(new JsonMediaTypeFormatter());
Here is a solution similar to jayson.centeno's and other answers, but using the built-in extension from System.Net.Http.Formatting.
public static void Register(HttpConfiguration config)
{
// add support for the 'format' query param
// cref: http://blogs.msdn.com/b/hongyes/archive/2012/09/02/support-format-in-asp-net-web-api.aspx
config.Formatters.JsonFormatter.AddQueryStringMapping("$format", "json", "application/json");
config.Formatters.XmlFormatter.AddQueryStringMapping("$format", "xml", "application/xml");
// ... additional configuration
}
The solution was primarily geared toward supporting $format for OData in the early releases of WebApi, but it also applies to the non-OData implementation, and returns the
Content-Type: application/json; charset=utf-8 header in the response.
It allows you to tack &$format=json or &$format=xml to the end of your uri when testing with a browser. It does not interfere with other expected behavior when using a non-browser client where you can set your own headers.
Just add those two line of code on your WebApiConfig class
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
//add this two line
config.Formatters.Clear();
config.Formatters.Add(new JsonMediaTypeFormatter());
............................
}
}
You just change the App_Start/WebApiConfig.cs like this:
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
//Below formatter is used for returning the Json result.
var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes.FirstOrDefault(t => t.MediaType == "application/xml");
config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
//Default route
config.Routes.MapHttpRoute(
name: "ApiControllerOnly",
routeTemplate: "api/{controller}"
);
}
Some time has passed since this question was asked (and answered) but another option is to override the Accept header on the server during request processing using a MessageHandler as below:
public class ForceableContentTypeDelegationHandler : DelegatingHandler
{
protected async override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var someOtherCondition = false;
var accHeader = request.Headers.GetValues("Accept").FirstOrDefault();
if (someOtherCondition && accHeader.Contains("application/xml"))
{
request.Headers.Remove("Accept");
request.Headers.Add("Accept", "application/json");
}
return await base.SendAsync(request, cancellationToken);
}
}
Where someOtherCondition can be anything including browser type, etc. This would be for conditional cases where only sometimes do we want to override the default content negotiation. Otherwise as per other answers, you would simply remove an unnecessary formatter from the configuration.
You'll need to register it of course. You can either do this globally:
public static void Register(HttpConfiguration config) {
config.MessageHandlers.Add(new ForceableContentTypeDelegationHandler());
}
or on a route by route basis:
config.Routes.MapHttpRoute(
name: "SpecialContentRoute",
routeTemplate: "api/someUrlThatNeedsSpecialTreatment/{id}",
defaults: new { controller = "SpecialTreatment" id = RouteParameter.Optional },
constraints: null,
handler: new ForceableContentTypeDelegationHandler()
);
And since this is a message handler it will run on both the request and response ends of the pipeline much like an HttpModule. So you could easily acknowledge the override with a custom header:
public class ForceableContentTypeDelegationHandler : DelegatingHandler
{
protected async override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var wasForced = false;
var someOtherCondition = false;
var accHeader = request.Headers.GetValues("Accept").FirstOrDefault();
if (someOtherCondition && accHeader.Contains("application/xml"))
{
request.Headers.Remove("Accept");
request.Headers.Add("Accept", "application/json");
wasForced = true;
}
var response = await base.SendAsync(request, cancellationToken);
if (wasForced){
response.Headers.Add("X-ForcedContent", "We overrode your content prefs, sorry");
}
return response;
}
}
Here is the easiest way that I have used in my applications. Add given below 3 lines of code in App_Start\WebApiConfig.cs in the Register function:
var formatters = GlobalConfiguration.Configuration.Formatters;
formatters.Remove(formatters.XmlFormatter);
config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
Asp.net web API will automatically serialize your returning object to JSON and as the application/json is added in the header so the browser or the receiver will understand that you are returning JSON result.
From MSDN Building a Single Page Application with ASP.NET and AngularJS (about 41 mins in).
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// ... possible routing etc.
// Setup to return json and camelcase it!
var formatter = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
formatter.SerializerSettings.ContractResolver =
new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
}
It should be current, I tried it and it worked.
Using Felipe Leusin's answer for years, after a recent update of core libraries and of Json.Net, I ran into a System.MissingMethodException:SupportedMediaTypes.
The solution in my case, hopefully helpful to others experiencing the same unexpected exception, is to install System.Net.Http. NuGet apparently removes it in some circumstances. After a manual installation, the issue was resolved.
WebApiConfig is the place where you can configure whether you want to output in json or xml. By default, it is xml. In the register function, we can use HttpConfiguration Formatters to format the output.
System.Net.Http.Headers => MediaTypeHeaderValue("text/html") is required to get the output in the json format.
I'm astonished to see so many replies requiring coding to change a single use case (GET) in one API instead of using a proper tool what has to be installed once and can be used for any API (own or 3rd party) and all use cases.
So the good answer is:
If you only want to request json or other content type install Requestly or a similar tool and modify the Accept header.
If you want to use POST too and have nicely formatted json, xml, etc. use a proper API testing extension like Postman or ARC.

ASP.NET MVC: Specify value provider on a per-action or per-route basis?

I'm trying to set up an action in ASP.NET MVC 3 to handle the payload of a mercurial webhook request - in this case, generated by Kiln.
The payload is JSON, but unfortunately it is sent as a URL encoded form value with the content type application/x-www-form-urlencoded, because apparently using application/json and sending it unencoded with no parameter name would make it too easy and um... standard.
This means that I can't just use the new JsonValueProviderFactory because it only picks up requests using the standard application/json content type. And of course I can't just kludge the factory to also pick up application/x-www-form-urlencoded requests too, because I need those requests to use the form data value provider everywhere else in my app that's actually receiving form data and not JSON.
So, is there a way I can specify that a ValueProvider or ValueProviderFactory should only be used for a specific action or route?
If you create a specific controller to handle these webhook requests, you can assign your unique ValueProvider when you instantiate your controller.
public class KilnController : Controller
{
public KilnController()
{
this.ValueProvider = MyCustomValueProvider;
}
...
}
This should fulfill your need for a custom ValueProvider for these requests.
Turns out that IValueProvider was not the particular bit of extensibility I was looking for - I just needed to use a quick IModelBinder implementation I found courtesy of James Hughes. It needed a little tweaking to cover pulling something out of the form:
public class JsonFormModelBinder : IModelBinder
{
#region [ ModelBinder Members ]
Object IModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
HttpRequestBase request = controllerContext.HttpContext.Request;
var jsonStringData = request.Form[bindingContext.ModelName];
if (jsonStringData != null) return JsonConvert.DeserializeObject(jsonStringData, bindingContext.ModelType);
else return null;
}
#endregion
}
And the usage:
[HttpPost]
public ActionResult WebHook([ModelBinder(typeof(JsonFormModelBinder))] WebHookMessage payload)
{
return Content("OK");
}