Web API routing and a Web API Help Page: how to avoid repeated entries - asp.net-mvc-routing

I am getting repeat entries rendered in my Web API Help Page with different parents, such as these, that refer to the same method:
GET api/{apiVersion}/v1/Products - Gets all products
...
GET api/v1/Products - Gets all products
...
I have a Web API page with some routing like this:
config.Routes.MapHttpRoute (
name: "DefaultVersionApi",
routeTemplate: "api/{apiVersion}/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Routes.MapHttpRoute (
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
I had thought that this routing would make the "v1" optional, so the derived documentation above is not expected.
(sidebar: Going to api/products certainly doesn't work, so I am not sure what is wrong with this. What am I missing?)
It seems the real problem is that Web API Help Page is reading the routes improperly, as I thought v1 and {apiVersion} should not both appear in the same action. What am I missing here?

Try using Attribute Routing, install nuget package
Install-Package Microsoft.AspNet.WebApi.WebHost
Enable Attribute Routing in the WebApiConfig.cs
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Attribute routing.
config.MapHttpAttributeRoutes();
// Convention-based routing.
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
Then use the attribute Route in the methods of your Controller
[Route("~/api/v1/Products")]
[HttpGet]
public List<Product> Products()
{}
[Route("~/api/v2/Products")]
[HttpGet]
public List<Product> V2Products()
{}
in the documentation you will get
GET api/v1/Products - Gets all products
GET api/v2/Products - Gets all products

It seems like this is a shortcoming of the ASP.NET Web API help pages. To workaround, I changed the view to exclude these invalid routes from the rendered document. For the above example, I added this Where clause to the loop in ApiGroup.cshtml, changing
#foreach (var api in Model){
to
#foreach (var api in Model.Where(m => !m.Route.RouteTemplate.Contains(#"{apiVersion}"))){

Related

WebApi HelpPage api detail page 404, when "api" prefix removed?

.net4.7 + WebApi5.23 + HelpPage5.23.
My WebApiConfig.Register:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
...
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "{controller}/{action}/{id}", //note: there is no "api/" prefix
defaults: new { id = RouteParameter.Optional }
);
}
}
And the index page is worked:
But the api detail page fail(Page not found):
Please help, thank you.
Routing is bound to be getting confused between routing to your MVC controller or your WebApi controller since the are now sharing the same path.
If you need a web page to show, create a new method within the HelpController that returns a new view.
If you need Json returned, you can still create a new method within the HelpController to do that, just change the return type to JsonResult.
Hopefully this gives you enough to understand what's going wrong, and therefore what to google next.

How to use an Area in ASP.NET Core

How do I use an Area in ASP.NET Core?
I have an app that needs an Admin section. This section requires its Views to be placed in that area. All requests that start with Admin/ will need to be redirected to that area.
In order to include an Area in an ASP.NET Core app, first we need to include a conventional route in the Startup.cs file (It's best to place it before any non-area route):
In Startup.cs/Configure method:
app.UseMvc(routes =>
{
routes.MapRoute("areaRoute", "{area:exists}/{controller=Admin}/{action=Index}/{id?}");
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Then make a folder named Areas in the app root and make another named Admin inside the former, also make these folders inside Admin (ViewComponent is optional):
Now we create a controller inside the Controllers folder named AdminController, the content can be like:
[Area("Admin")]
[Route("admin")]
public class AdminController : Controller
{
public AdminController()
{
// do stuff
}
public IActionResult Index()
{
return View();
}
[Route("[action]/{page:int?}")]
public IActionResult Orders()
{
return View();
}
[Route("[action]")]
public IActionResult Shop()
{
return View();
}
[Route("[action]/newest")]
public IActionResult Payments()
{
return View();
}
}
Now in order for that to work, you'll need to create Views for all actions that return one. The hierarchy for views is just like what you have in a non-area Views folder:
Now, you should be good to go!
Question:
What if I want to have another controller inside my Area?
Answer:
Just add another controller beside AdminController and make sure the routes are like the following:
[Area("Admin")]
[Route("admin/[controller]")]
public class ProductsController : Controller
{
public ProductsController()
{
//
}
[Route("{page:int?}")]
public IActionResult Index()
{
return View();
}
}
The important part is [Route("admin/[controller]")]. With that you can keep the style of routing to admin/controller/action/...
In ASP.NET Core 3.0. If you are working with Endpoint patterns, after adding the Area (Right click over project, Add, New Scaffolded Item, Area), you have to add manually routing pattern on startup.cs Configure method. (At this point the generated ScaffoldingReadMe.txt is out of date).
app.UseEndpoints(endpoints =>
{
endpoints.MapAreaControllerRoute(
"Admin",
"Admin",
"Admin/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
In the Microsoft docs to migrate from ASP.NET CORE 2.2 to 3.0 the suggestion is to:
Replace UseMvc with UseEndpoints.
I encountered some challenges while trying to fix my Area's while simultaneously having Identity to keep working - but the solution below seems to be working for ASP.NET CORE 3.0 :
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllerRoute("areas", "{area:exists}/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
Hopefully I could also help you out and reduce the research time :-)
Scaffolding has generated all the files and added the required dependencies.
However the Application's Startup code may required additional changes for things to work end to end.
Add the following code to the Configure method in your Application's Startup class if not already done:
app.UseMvc(routes =>
{
routes.MapRoute(
name : "areas",
template : "{area:exists}/{controller=Home}/{action=Index}/{id?}");
});
Areas Implementation in Routing
First Create Area(Admin) using VS and add the following code into Startup.cs
First Way to Implement:-
Add Controller Login and Index Action and add Following Code, [Area(“Admin”)] is compulsory to add on controller level to perform asp.net areas Routing.
Startup.cs
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areas",
template: "{area:exists}/{controller=Login}/{action=Index}/{id?}"
);
});
Note: Area routing must be placed first with non area routing, area: exists is compulsory to add area routing.
Controller Code:
[Area("Admin")]
public class LoginController : Controller
{
public IActionResult Index()
{
return Content("Area Admin Login Controller and Index Action");
}
}
This route may be called using http://localhost:111/Admin
Second Way to Implement Area Routing:-
Add Following code into startup.cs.
app.UseMvc(routes =>
{
routes.MapAreaRoute(
name: "default",
areaName: "Guest",
template: "Guest/{controller}/{action}/{id?}",
defaults: new { controller = "GuestLogin", action = "Index" });
});
Create an Area “Guest”, Add “GuestLogin” Controller and “Index” Action and add the following code into the newly created controller.
[Area("Guest")]
public class GuestLoginController : Controller
{
public IActionResult Index()
{
return Content("Area Guest Login Controller and Index Action");
}
}
This route may be called using http://localhost:111/Guest
Use this pattern in Configure method in Startup.Cs, as its full routing manner:
app.UseMvc(routes =>{
routes.MapRoute(
name: "MyArea",
template: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");});
In Core 3.1 you should use below code in ConfigureServices method:
services.AddMvc(option => option.EnableEndpointRouting = false);
With .net core, following is needed to be added in the startup file if you are adding an area:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areas",
template: "{area:exists}/{controller=Home}/{action=Index}/{id?}"
);
});
After that you can just simply mark your area and route in the controller, i.e
[Area("Order")]
[Route("order")]
it works for me.
Use this pattern in Configure method in Startup.Cs, as its full routing manner:
app.UseMvc(routes =>{
routes.MapRoute(
name: "MyArea",
template: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");});
In Core 3.1 you should use below code in ConfigureServices method:
services.AddMvc(option => option.EnableEndpointRouting = false);

Web API 2.2 Content Negotiation with file extensions

I am working on a Web API and I want to use Content Negotiation with file extensions to allow browser clients to specify the content they want to receive. For instance
http://localhost:54147/data.xslx.
According to this article (http://msdn.microsoft.com/en-us/magazine/dn574797.aspx) I should be able to setup routing with something like this
//setup default routes
config.Routes.MapHttpRoute(
name: "Default",
routeTemplate: "{controller}/{id}",
defaults: new {id = RouteParameter.Optional}
);
//setup routes with extensions
config.Routes.MapHttpRoute(
name: "Url extension",
routeTemplate: "{controller}/{action}.{ext}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Here is my simple controller
public class TestController : ApiController
{
public HttpResponseMessage Get()
{
var items = new[] {"test1", "test2", "test3"};
return Request.CreateResponse(HttpStatusCode.OK, items);
}
}
using this url
http://localhost:54147/test/get.xlsx
I always get the browser default (xml in chrome, json in IE11).
or possibly
http://localhost:54147/test.xlsx
to which I get the error
No HTTP resource was found that matches the request URI 'http://localhost:54147/test.xlsx'.
I should be able to use my custom formatter. But it's not happening. Here is the constructor of my custom formatter.
public ExcelFormatter()
{
MediaTypeMappings.Add(new UriPathExtensionMapping("xlsx", ContentType.Excel));
SupportedMediaTypes.Add(new MediaTypeHeaderValue(ContentType.Excel));
}
Again according to the article this should help the API Content Negotiator use my custom formatter. I appreciate any help.
As the question is old, but is still without an answer:
Generally this links should help:
How to build media formatter
Microsofts words about content negotiation
To the code in the question:
it seems you need to extend from BufferedMediaTypeFormatter(sync) or MediaTypeFormatter`(async)
you need to make your formatter known to HttpConfiguration.Formatters (link)
You probably want to do this in an config for the complete application.
For testing you could add in to a single ApiController like following.
untested example
public class TestController : ApiController
{
TestController() {
Configuration.Formatters.Add(new ExcelFormatter());
}
public HttpResponseMessage Get()
{
var items = new[] {"test1", "test2", "test3"};
return Request.CreateResponse(HttpStatusCode.OK, items);
}
}
```

Properly handling nested resources in ASP.net MVC 4 WebApi routing

I'd like to provide REST API in this way:
GET /api/devices
POST /api/devices
PUT /api/devices/1
DELETE /api/devices/1
This is my configuration:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
And these are the actions:
public IEnumerable<Device> Get()
{
//return all devices
}
public Devices Get(id)
{
//return a specific devices
}
and so on.
The issue appears when I want to handle nested resources:
GET /api/devices/1/readings
POST /api/devices/1/readings
GET /api/devices/1/readings/1
PUT /api/devices/1/readings/1
DELETE /api/devices/1/readings/1
This is my configration for these:
config.Routes.MapHttpRoute(
name: "NestedApi",
routeTemplate: "api/{controller}/{parentResourceId}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
The issue shows up when trying to GET and POST to the nested resource:
[HttpGet]
public String Readings(int parentResourceId)
{
//return a list of readings for the device
}
[HttpPost]
public String Readings(int parentResourceId)
{
//create and return the id of a reading for the device
}
This is, of course, failing because there are two actions with the same signature.
I'd like to hear from a way of accomplishing this with the most RESTful approach
Microsoft is adding Attribute Routing to increase the flexibility of the routing system.
Have a look at their documentation on Scenario 3
There is also some answers on Stack Overflow like:
How to handle hierarchical routes in ASP.NET Web API?
There are solutions based on specifying route mappings but if you want more a more generic solution, this is by far the best solution I have seen related to this topic. Of course, Web API 2 has attribute routing.

RESTful Web API with Associations. Is it possible?

I have written a REST service using Web API and after reading sections of this Web API Design from Brian Mulloy, was trying to figure out how I could implement associations with Web API.
Web API Design Extract:
Associations
Resources almost always have relationships to other
resources. What's a simple way to express these relationships in
aWebAPI?
Let's look again at the API we modeled in nouns are good,
verbs are bad -theAPI that interacts with our dogs resource.
Remember, we had two base URLs: /dogs and dogs/1234.
We're using HTTP
verbs to operate on the resources and collections. Our dogs belong to
owners. To get all the dogs belonging to a specific owner, or to
create a new dog for that owner, do a GET or a POST:
GET /owners/5678/dogs
POST /owners/5678/dogs
Now, the relationships can be
complex. Owners have relationships with veterinarians, who have
relationships with dogs, who have relationships with food, and so on.
It's not uncommon to see people string these together making a URL 5
or 6 levels deep. Remember that once you have the primary key for one
level, you usually don't need to include the levels above because
you've already got your specific object. In other words, you shouldn't
need too many cases where a URL is deeper than what we have above
/resource/identifier/resource.
So I tried to add a controller method for the association like follows:
public class EventsController : ApiController
{
// GET api/events
public IEnumerable<Event> Get()
{
// get list code
}
// GET api/events/5
public Event Get(int id)
{
// get code
}
// POST api/events
public void Post([FromBody]Event evnt)
{
// add code
}
// POST api/events/5
public void Post(int id, [FromBody]Event evnt)
{
// update code
}
// DELETE api/events/5
public void Delete(int id)
{
// delete code
}
// GET api/events/5/guests
public IEnumerable<Guest> Guests(int id)
{
// association code
}
}
I also modified my route templates to the following:
config.Routes.MapHttpRoute("ApiWithAssociations",
"api/{controller}/{id}/{action}");
config.Routes.MapHttpRoute("DefaultApi",
"api/{controller}/{id}",
new { id = RouteParameter.Optional });
Unfortunately, when I do an update/post of the event resource I now get a HTTP 500 Internal Server Error with a response body stating
Multiple actions were found that match the request
I've tried modifying the route templates in conjunction with adding System.Web.Http.HttpPostAttribute (and other HTTP verbs) as well but to no avail.
Has anyone tried this and got it working? Any help would be appreciated. If it is absolutely not possible to have multiples for an http verb then I guess I'll have to abandon associations with my REST service.
EDIT: SOLUTION
Using Radim Köhler's answer, I was able to get this working. Add the HttpGetAttribute to the Guests method like so:
// GET api/event/5/guests
[HttpGet]
public IEnumerable<Guest> Guests(int id)
{
// association code
}
And added an addition route to cater for the default GET action like follows:
config.Routes.MapHttpRoute("DefaultGet",
"api/{controller}/{id}",
new {action = "Get"},
new {httpMethod = new HttpMethodConstraint(HttpMethod.Get)});
config.Routes.MapHttpRoute("ApiWithAssociations",
"api/{controller}/{id}/{action}");
config.Routes.MapHttpRoute("DefaultApi",
"api/{controller}/{id}",
new {id = RouteParameter.Optional});
The solution, could be in an explicit POST mapping
Just add new definition, which will be used for events/5 POST
// explicit Post() mapping
config.Routes.MapHttpRoute(
name: "DefaultPost",
routeTemplate: "api/{controller}/{id}",
defaults: new { action = "Post" }
, constraints: new { httpMethod = new HttpMethodConstraint(HttpMethod.Post) }
);
// existing
config.Routes.MapHttpRoute("ApiWithAssociations",
"api/{controller}/{id}/{action}");
config.Routes.MapHttpRoute("DefaultApi",
"api/{controller}/{id}",
new { id = RouteParameter.Optional });