Supporting GET *and* POST in WebApi - c#

Let's have a test model.
public class TestRequestModel
{
public string Text { get; set; }
public int Number { get; set; }
}
I would like this service to be able to accept the following requests:
GET /test?Number=1234&Text=MyText
POST /test with header: Content-Type: application/x-www-form-urlencoded and body: Number=1234&Text=MyText
POST /test with header: Content-Type: application/json and body: {"Text":"Provided!","Number":9876}
The routes are configured the following way:
_config.Routes.MapHttpRoute(
"DefaultPost", "/{controller}/{action}",
new { action = "Post" },
new { httpMethod = new HttpMethodConstraint(HttpMethod.Post) });
_config.Routes.MapHttpRoute(
"The rest", "/{controller}/{action}",
defaults: new { action = "Get" });
My controller looks like this:
public class TestController : ApiController
{
[HttpGet]
public TestResponseModel Get([FromUri] TestRequestModel model)
{
return Do(model);
}
[HttpPost]
public TestResponseModel Post([FromBody] TestRequestModel model)
{
return Do(model);
}
(...)
This seems like an acceptable amount of boiler plate code, but I still would like to avoid it if possible.
Having the extra route is not ideal too. I have a fear of MVC/WebAPi routes and I believe they are evil.
Is there a way to avoid having two methods and/or the DefaultPost route?

What you are asking for is not typical with ASP.NET Web API. In ASP.NET MVC, it is common to have the same action method handling the initial GET and the subsequent post back (POST). ASP.NET Web API is intended for building HTTP services and GET is used for retrieving a resource without changing anything in the system, while POST is for creating a new resource, as pointed by Matthew.
Anyway, it is not impossible to have one action method in Web API to accomplish this. But you want the same action method to not only handle GET and POST and also do the model binding and formatter binding. Model binding (similar to MVC) binds request URI, query string, etc to parameters while formatter binding (unique to web API) binds the body content to parameter. By default, simple types are bound from URI, query string and complex types from body. So, if you have an action method with parameters of string text, int number, TestRequestModel model, you can have web API bind from URI or body and in this case, you will need to check what is not empty and use that. But, such a solution will look more like a hack, unfortunately. Or if you want the same complex type to be populated from both URI/query string and body, you will need to write your own parameter binder that checks for request parts and populate the parameter accordingly.
Also, you don't need to have two route mappings. The default one like this will do.
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);

Related

MVC WebApi advanced custom routing

I'm struggling with this for some time now. I've searched all over the internet, didn't find any solution.
I'd like to create a webapi project with somewhat custom routing. Using VS 2019, project is of type ASP.NET WebApi on .NET Core 2.2. Routing should be like this:
Basic application must reside on url similar to "https://my.server.com/myapi". URLs which will be called are in form "https://my.server.com/myapi/{InstanceName}/{CommandName}?{customParams}"
I have one controller defined in my project and I would like to redirect all requests to that controller, where instanceName could be parameter of all the methods contained in a controller, so I would get a value for that parameter. CommandName is basicly the same as "action" RouteData by MVC principles. As you can see there is no controller specified, since all is handled by one controller.
So far I've tried setup routing like this:
Startup.cs
app.UseMvc(routes =>
{
routes.MapRoute(
name: "MyRoute",
template: "{instance}/{action}",
defaults: new { controller = "MyController" });
});
}
MyController.cs
[Route("/")]
[ApiController]
public class MyController : ControllerBase
{
[HttpGet("{instance}/info")]
public JsonResult Info(string instance, InfoCommand model)
{
// Just return serialized model for now.
var result = new JsonResult(model);
return result;
}
}
But this does not work. I get 415 response from (I think) web server when I call for example
https://my.server.com/myapi/MYINSTANCE/info?param1=value1&param2=value2
While debugging from VS this URL looks like this:
https://localhost:12345/MYINSTANCE/info?param1=value1&param2=value2
but I think it shouldn't matter for routing.
In best case scenario (putting [Route("{instance}")] above controller and [HttpGet("info")] above Info method) I get 404 response, which is also what I do not want.
I've even tried creating my own ControllerFactory, but that didn't work either (changing controller inside ControllerFactory's create method and adding another parameter to RouteData).
How to setup routing like that? Is it even possible? I would still like to use all other MVC features (model binding, proper routing, auth features, etc.), it's just this routing I cannot figure it out.
Your attempt resulting a in 415 Unsupported Media Type error was your best one.
You were only missing the FromQuery as shown below.
The error indicates that the complex type InfoCommand could not be resolved.
You must specify that it must be parsed from the querystring.
Note that the route defined via MapRoute doesn't have effect, since you are using attribute-based routing; it's only one or the other.
[Route("/")]
[ApiController]
public class MyController : ControllerBase
{
[HttpGet("{instance}/info")]
public JsonResult Info(string instance, [FromQuery] InfoCommand model)
{
var result = new JsonResult(model);
return result;
}
}
public class InfoCommand
{
public InfoCommand()
{}
public string Param1 { get; set; }
public string Param2 { get; set; }
}

Dash in url (mapped to the action)

I try to implement an ASP.NET Web Api Controller that supports the JsonApi standard (http://jsonapi.org/ primarily used by Ember.js)
The URL's may contain a dash. But the corresponding method name in the C# code may not contain a dash.
My javascript try to POST to
http://localhost:50000/jsonapi/activity-exercises
But I am not able to implement an endpoint that can receive that request.
I have tried:
[HttpPost]
public HttpResponseMessage ActivityExercises([FromBody] ActivityExerciseEntry value)
{
// ...
Ideally there should be an attribute to add to the method to specify the mapped action name in the URL. Does such attribute exist?
My route mapping looks like this:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapHttpRoute("EmberJsonApi", "jsonapi/{action}/{id}", new { controller = "JsonApi", id = RouteParameter.Optional });
routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}", new { id = RouteParameter.Optional });
}
I figured it out. It took a lot of googling, because this is not very well documented....
As I suspected, there exists an attribute I could use: [ActionName]
So the endpoind definition just looks looks like this:
[HttpPost]
[ActionName("activity-exercises")]
public HttpResponseMessage ActivityExercises([FromBody] ActivityExerciseEntry value)
{
...

Content-based action selection Web API using FiveLevelsOfMediaType (5LMT) not working

I'm trying to implement a RESTful API that uses commands to alter the state of the system (based on CQRS strategy). By default Web API's routing will have difficulty matching to actions based on examining the object parameter's type. To get around this I've been using the following guide:
Content Based Action Selection Using Five Levels of Media Type
After following the instructions it still results in an ambiguous match exception, caused by overloaded Put methods in my controller.
My WebApiConfig is as follows:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.AddFiveLevelsOfMediaType();
}
}
My controller looks like:
public class ProductsController : ApiController
{
public ProductDTO Get(int id)
{
var query = new ProductByIdQuery { Id = id };
ProductDTO product = _queryBus.Dispatch(query);
return product;
}
public void Put(ChangeProductCodeCommand command)
{
_commandBus.Dispatch(command);
}
public void Put(SetProductParentCommand command)
{
_commandBus.Dispatch(command);
}
public ProductsController(IQueryBus queryBus, ICommandBus commandBus)
{
_queryBus = queryBus;
_commandBus = commandBus;
}
IQueryBus _queryBus;
ICommandBus _commandBus;
}
On the client side, the http header I send is:
PUT /api/products HTTP/1.1
Content-Type: application/json;domain-model=ChangeProductCodeCommand
And the JSON:
{
ProductId: 758,
ProductCode: "TEST"
}
Results in:
{
"Message": "An error has occurred.",
"ExceptionMessage": "Ambiguous Match",
"ExceptionType": "System.InvalidOperationException",
"StackTrace": " at ApiActionSelection.System.Web.Http.Controllers.ApiActionSelector.ActionSelectorCacheItem.SelectAction(HttpControllerContext controllerContext)\r\n at ApiActionSelection.System.Web.Http.Controllers.ApiActionSelector.SelectAction(HttpControllerContext controllerContext)\r\n at System.Web.Http.ApiController.ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)\r\n at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()"
}
Any ideas why this wouldn't work?
With two Put methods that differ only by parameter type, Web API is attempting to resolve to the correct action based on the type of the incoming parameter.
Unless the default model binder can map the incoming JSON to a complex type you have implemented, and that type matches the parameter type of one of your Put methods, then Web API will respond with the "Ambiguous Match" exception you are seeing because it can't determine which method to call. Note that by default Web API will not use your Content-Type header in action resolution.
I suggest reading Routing and Action Selection in ASP.NET Web API so you understand how routing is working internally. Then you might want to create you own implementation of IHttpActionSelector where you examine your Content-Type and you can route the request to the action of your choice. You can find a sample implementation here.
You can also read more on model binding here to see how your incoming JSON can get resolved to one of your types.

BaseController input param Object results in Multiple actions were found that match the request

I've got this intresting situation where I have a baseController for webApi and have tracked the issue down to this. If I have a base controller that has an input parameter type Object it will end up with http 500 Multiple actions where found...
And this is in a default empty mvc 4.0 project for vs 2012 using IISExpress
public class BaseSecureApi : ApiController
{
public string Wtf(object ohMyGodThisIsSilly)
{
return null;
}
}
And the controller
public class Default1Controller : BaseSecureApi
{
// GET api/default1
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// POST api/default1
[HttpPost]
public string Post([FromBody]string value)
{
return "Postmyvalue:" + value;
}
}
And a route
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Now why is the routing going haywire.
IRL the base class returns a httpResponse based on who is logged on and if the result should be encrypted or not.
From the routing-and-action-selection article on the ASP.NET site:
HTTP Methods. The framework only chooses actions that match the HTTP method of the request, determined as follows:
You can specify the HTTP method with an attribute: AcceptVerbs, HttpDelete, HttpGet, HttpHead, HttpOptions, HttpPatch, HttpPost, or HttpPut.
Otherwise, if the name of the controller method starts with "Get", "Post", "Put", "Delete", "Head", "Options", or "Patch", then by convention the action supports that HTTP method.
If none of the above, the method supports POST.
This means that the Wtf method will support POST because it is a public method, with no attributes and it does not start with any of the conventional words.
If you make the Wtf method protected and it will not be called by the Web api, you can still call it from the methods of your derived classes where you need to.

Routing with Multiple Parameters using ASP.NET MVC

Our company is developing an API for our products and we are thinking about using ASP.NET MVC. While designing our API, we decided to use calls like the one below for the user to request information from the API in XML format:
http://ws.audioscrobbler.com/2.0/?method=artist.getimages&artist=cher&api_key=b25b959554ed76058ac220b7b2e0a026
As you can see, multiple parameters are passed (i.e. artist and api_key). In ASP.NET MVC, artist would be the controller, getImages the action, but how would I pass multiple parameters to the action?
Is this even possible using the format above?
Parameters are directly supported in MVC by simply adding parameters onto your action methods. Given an action like the following:
public ActionResult GetImages(string artistName, string apiKey)
MVC will auto-populate the parameters when given a URL like:
/Artist/GetImages/?artistName=cher&apiKey=XXX
One additional special case is parameters named "id". Any parameter named ID can be put into the path rather than the querystring, so something like:
public ActionResult GetImages(string id, string apiKey)
would be populated correctly with a URL like the following:
/Artist/GetImages/cher?apiKey=XXX
In addition, if you have more complicated scenarios, you can customize the routing rules that MVC uses to locate an action. Your global.asax file contains routing rules that can be customized. By default the rule looks like this:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
If you wanted to support a url like
/Artist/GetImages/cher/api-key
you could add a route like:
routes.MapRoute(
"ArtistImages", // Route name
"{controller}/{action}/{artistName}/{apikey}", // URL with parameters
new { controller = "Home", action = "Index", artistName = "", apikey = "" } // Parameter defaults
);
and a method like the first example above.
Starting with MVC 5, you can also use Attribute Routing to move the URL parameter configuration to your controllers.
A detailed discussion is available here:
http://blogs.msdn.com/b/webdev/archive/2013/10/17/attribute-routing-in-asp-net-mvc-5.aspx
Summary:
First you enable attribute routing
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
}
}
Then you can use attributes to define parameters and optionally data types
public class BooksController : Controller
{
// eg: /books
// eg: /books/1430210079
[Route("books/{isbn?}")]
public ActionResult View(string isbn)
You can pass arbitrary parameters through the query string, but you can also set up custom routes to handle it in a RESTful way:
http://ws.audioscrobbler.com/2.0/?method=artist.getimages&artist=cher&
api_key=b25b959554ed76058ac220b7b2e0a026
That could be:
routes.MapRoute(
"ArtistsImages",
"{ws}/artists/{artist}/{action}/{*apikey}",
new { ws = "2.0", controller="artists" artist = "", action="", apikey="" }
);
So if someone used the following route:
ws.audioscrobbler.com/2.0/artists/cher/images/b25b959554ed76058ac220b7b2e0a026/
It would take them to the same place your example querystring did.
The above is just an example, and doesn't apply the business rules and constraints you'd have to set up to make sure people didn't 'hack' the URL.

Categories