Url.Action returning incorrect URL for webapi action with Route attrubute - asp.net

I have a problem with the behaviour of Url.Action();
I have a webapi where all controllers require explicit route prefix attribute and all actions require a route attribute.
I register my routes in the WebApiConfig.cs
var constraintResolver = new DefaultInlineConstraintResolver()
{
ConstraintMap =
{
["apiVersion"] = typeof( ApiVersionRouteConstraint )
}
};
config.MapHttpAttributeRoutes(constraintResolver);
I have currently commented out the line below, but (because) it did not change the incorrect behaviour:
//config.Routes.MapHttpRoute(name: "DefaultApi",
//routeTemplate: "api/v{version:apiVersion}/{controller}/{action}/{id}", defaults: new {id = RouteParameter.Optional});
My controllers look as follows:
[RoutePrefix("api/v{version:apiVersion}/programs")]
public class ProgramsController : ApiController
{
[HttpGet, Route("{telemetryKey}/versions/latest")]
public async Task<LatestVersionResponse> GetLatestVersionInfo(Guid telemetryKey)
{
// serious business logic
}
}
I expect that '#Url.Action("GetLatestVersionInfo", "Programs", new { telemetryKey = Guid.Parse("43808405-afca-4abb-a92a-519489d62290") })'
should return /api/v1/programs/43808405-afca-4abb-a92a-519489d62290/versions/latest
however, I get /Programs/GetLatestVersionInfo?telemetryKey=43808405-afca-4abb-a92a-519489d62290 instead. So, my routeprefix and route attributes are ignored.
Swagger correctly discovers my routes and I can validate that requests to the expected routes work OK - it's only the Url.Action() that is confused.
What can be wrong...?

Well, it seems there were a few things wrong.
Wrong helper:
I should be using the Url.HttpRouteUrl for generating API links from a razor view (Url.Link is for generating link from within API controllers)
Conflict with aspnet-api-versioning library
For some reason (perhaps a bug?) the prefix that I have on the controller (apiVersion variable) breaks the URL helper mechanism.
For now, I have ditched the aspnet-api-versioning library, but created an issue on their github repo, in case its a bug.
Since I really hate the idea of creating and maintaing magic strings, so I took the following approach - each controller has a public static class which contains const values for the route names:
[RoutePrefix("api/v1/developers")]
public class DevelopersController : ApiController
{
[HttpGet, Route("{developerId}/programs", Name = Routes.GetPrograms)]
public async Task<IEnumerable<Program>> GetPrograms(Guid developerId){}
public static class Routes
{
public const string GetPrograms = nameof(DevelopersController) +"."+ nameof(DevelopersController.GetPrograms);
}
}
Now that can be used from a razor controller in a simple and relatively safe manner:
#Url.HttpRouteUrl(DevelopersController.Routes.GetPrograms, new { developerId = /* uniquest of guids */})
A bit better than magic strings. I've also added a bunch of unit tests for controllers where I validate that each route is unique and proper and that the routes class only contains routes for the action it contains.

Try the following:
Name your route:
[HttpGet, Route("{telemetryKey}/versions/latest", Name="LatestVersionInfoRoute")]
public async Task<LatestVersionResponse> GetLatestVersionInfo(Guid telemetryKey)
{
// serious business logic
}
Use Url.Link method:
#Url.Link("LatestVersionInfoRoute", new { telemetryKey = Guid.Parse("43808405-afca-4abb-a92a-519489d62290") })

Related

Set custom route using OData 8

Recently I updated to OData 8.0.10. I added this in my Startup.cs file:
services.AddRouting();
services.AddControllers().AddOData(opt =>
opt.AddRouteComponents("odata", GetEdmModel()).Filter().Select().OrderBy().Count());
where
private static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Project>("Project");
return builder.GetEdmModel();
}
I have this small controller
[Route("api/[controller]")]
[ApiController]
public class ProjectController : ControllerBase
{
[HttpGet]
[EnableQuery(PageSize = 20)]
public IQueryable<Project> GetAsync()
{
var projects = _projectRepository.GetAll();
return projects;
}
[HttpGet("{id}", Name = "GetProjectById")]
public async Task<ActionResult> GetAsyncById(long id)
{
var project = await _projectService.GetProjectByIDAsync(id);
return Ok(project);
}
[HttpPatch("{id}", Name = "PatchProjectById")]
public async Task<ActionResult> PatchProject(long id, [FromBody] ProjectPatchDetails projectPatch)
{
var project = await _projectRepository.GetAsync(id);
var updated = await _projectService.UpdateProjectAsync(id, project, projectPatch);
return Ok(updated);
}
}
that has three endpoints, one of them is annotated by [EnableQuery] and the rest aren't. When I access api/project?$count=true&$skip=0&$orderby=CreateDate%20desc, I get a paged info (20 records) but I don't get the #odata.context and #odata.count. If I access /odata/project?$count=true&$skip=0&$orderby=CreateDate%20desc, with odata/ prefix, it gives me #odata.context and #odata.count. I tried changing AddRouteComponents to AddRouteComponents("api", GetEdmModel()) but in this case I get the following error:
"The request matched multiple endpoints. Matches: MyApp.Api.Controllers.ProjectController.GetAsync (MyApp.Api) MyApp.Api.Controllers.ProjectController.GetAsync (MyApp.Api)"
I have multiple questions in this case:
Is there a way to reroute odata to api, make /odata prefix as /api and make it work?
Should I make another controller that will store all OData tagged actions and on this way maybe workaround this as a solution, if possible?
#anthino
Is there a way to reroute odata to api, make /odata prefix as /api and make it work?
if you add 'opt.AddRouteComponents("api", GetEdmModel())', remember to remove
[Route("api/[controller]")] and other attribute routings
Should I make another controller that will store all OData tagged actions and on this way maybe workaround this as a solution, if possible?
Basically, it's better to create two controllers, one for odata, the other for others. In your scenario, you mixed them together. You should be careful about this. You can use 'app.UseODataRouteDebug()' middleware to help you debug.
I think your ProjectController should be inheriting from ODataController not ControllerBase. With the ODataController, you should get the context url and the #odata.count

Attribute routes missing from routing table when using multiple namespaces

I am using namespace versioning in my web api (4.0) project. I'm using Attribute routing to define custom routes for our actions. This works just fine when a controller only exists in one namespace/version.
But when a controller exists in both namespaces/versions, the attribute route is ignored completely. I am using the solution here to look at what routes are in the routing table. And it only contains one route, for my "single" controller. If I delete/comment out/change the name of the controller in V2, then suddenly the "double" controller's route appears in the routing table as well.
Is there any reason that an attribute route would be ignored because the same class/controller name exists in 2 different namespaces? Here is my code, broken down to the simplest failing example:
namespace API.v1
{
public class SingleController : ApiController
{
[HttpGet]
[Route("api/{version}/Single")]
public string Test()
{
return "Response";
}
}
}
This class/route works just fine. http://localhost:57560/api/v1/Single returns "Response".
namespace API.v1
{
public class DoubleController : ApiController
{
[HttpGet]
[Route("api/{version}/Double")]
public string Test()
{
return "Response";
}
}
}
namespace API.v2
{
public class DoubleController : ApiController
{
[HttpGet]
[Route("api/{version}/Double")]
public string Test()
{
return "Response";
}
}
}
These 2 classes fail; the attribute route is missing from the routing table, and http://localhost:57560/api/v1/Double returns 404, as well as http://localhost:57560/api/v2/Double. If I just change the name of the v2.Double class, then it works just fine.
In this example, I have no default routes set up at all; only the Attribute routing. When using default routes, the versioning and routing work just fine:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{version}/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
This route works perfectly across multiple versions. But I need it to work with attribute routing.
I have been able to solve the issue using the solution here: http://abhinawblog.blogspot.com/2014/12/web-api-versioning-using.html. The problem appears to be related to the way that attribute routing differs from using the default routes table. It is required that the routes have unique names, and that the version numbers in the routes have custom constraints put on them.
In order to make the namespace versioning possible, you need to override the default behavior of routing. Check this MSDN Link for more details
Also check this sample code to see how it's done

Complex routing in ASP.NET MVC5

I want to know the best approach to create my controllers structure.
Let's say I have several events and for each event I can have several devices.
My idea would be to have something like:
http://mydomain/event/1/device/4
So I can access the deviceId 4 (belonging to eventId 1).
Should I have two different controllers? One for Event and for Device or device info has to be in EventController?
How can I have this routing in my RouteConfig?
It's entirely up to you how you want to set this up. You can use separate controllers or the same controller. It doesn't matter.
As far as routing goes, if you're using standard MVC routing, you'll need to create a custom route for this:
routes.MapRoute(
"EventDevice",
"event/{eventId}/device/{deviceId}",
new { controller = "Event", action = "Device" }
);
Which would correspond with something like this:
public class EventController : Controller
{
public ActionResult Device(int eventId, int deviceId)
{
...
}
}
Just make sure you place that before the default route, so it will catch first. For more about custom routes see: http://www.asp.net/mvc/overview/older-versions-1/controllers-and-routing/creating-custom-routes-cs
Alternatively, in MVC5+ you can use attribute routing, which makes defining custom routes much easier if you're doing a lot of stuff like this. In RouteConfig.cs, uncomment the line:
// routes.MapMvcAttributeRoutes();
Then, on your action define the route like:
[Route("event/{eventId}/device/{deviceId}")]
public ActionResult Device(int eventId, int deviceId)
{
...
You can also use [RoutePrefix] on your controller class to move part of the route to apply to the whole controller. For example:
[RoutePrefix("event")]
public class EventController : Controller
{
[Route("{eventId}/device/{deviceId}")]
public ActionResult Device(int eventId, int deviceId)
{
...
}
}
For more about attribute routing see: http://blogs.msdn.com/b/webdev/archive/2013/10/17/attribute-routing-in-asp-net-mvc-5.aspx

WebAPI service design

I'm pretty comfortable with how Asp.NET MVC controllers worked when designing services.
However the new WebAPI controllers. how am I supposed to design my services here?
Lets say we have 3 different ways to list e.g. Users.
Get 10 latest , Get all, Get inactive or whatever.
none of these might need parameters. so how would you solve this in WebAPI
IEnumerable<User> Get10Latest()
IEnumerable<User> GetAll()
IEnumerable<User> GetInactive()
That won't work since they have the same param signature.
So what is the correct way to design this here?
You can support multiple methods in one controller for a single HTTP method by using the action parameter.
E.g.
public class UsersController : ApiController
{
[ActionName("All")]
public HttpResponseMessage GetAll()
{
return new HttpResponseMessage();
}
[ActionName("MostIQ")]
public HttpResponseMessage GetMostIQ()
{
return new HttpResponseMessage();
}
[ActionName("TenLatest")]
public HttpResponseMessage GetTenLatest()
{
return new HttpResponseMessage();
}
}
Unfortunately, I have not found a way to get a single controller to handle both with and without the action at the same time.
e.g.
public class UsersController : ApiController
{
[ActionName("")] // Removing this attribute doesn't help
public HttpResponseMessage Get()
{
return new HttpResponseMessage();
}
[ActionName("All")]
public HttpResponseMessage GetAll()
{
return new HttpResponseMessage();
}
[ActionName("MostIQ")]
public HttpResponseMessage GetMostIQ()
{
return new HttpResponseMessage();
}
[ActionName("TenLatest")]
public HttpResponseMessage GetTenLatest()
{
return new HttpResponseMessage();
}
}
Being able to use a single controller for a collection resource and all of its subsets would be nice.
Someone will probably be along and wrap me on the knuckles for this, but you need to configure your routing to handle the Gets. This is how I got it working with the above operations:
config.Routes.MapHttpRoute(
name: "CustomApi",
routeTemplate: "api/{controller}/{action}",
defaults: new { id = RouteParameter.Optional }
);
So now your requests are mapped to the correct controller -> action via the route template. Note that the new route needs to be registered first in WebApiConfig.cs. If you keep the old, default one.
EDIT
Having re-read the question I realize I wasn't quite answering the design question. I would think that one way to go about it, from a REST perspective, would be to use a separate resource to expose the proper collections (Get10Latest for example) since I assume that there is a business reason for exposing that exact subset of data through the service. In that case you'd expose that resource though a single Get in its own Controller (if that is the desired behaviour).
Well why not have urls like this:
GET /users
GET /users/latest
GET /users/inactive
Using routing you could route them to
public classs UserController : ApiController
{
public IEnumerable<User> Get(string mode)
{
// mode is in routing restricted to be either empty, latest, or inactive
}
}
Otherwise use multiple controllers. The use of action names in Web API is kind of a wrong way to about it.

Infinite URL Parameters for ASP.NET MVC Route

I need an implementation where I can get infinite parameters on my ASP.NET Controller. It will be better if I give you an example :
Let's assume that I will have following urls :
example.com/tag/poo/bar/poobar
example.com/tag/poo/bar/poobar/poo2/poo4
example.com/tag/poo/bar/poobar/poo89
As you can see, it will get infinite number of tags after example.com/tag/ and slash will be a delimiter here.
On the controller I would like to do this :
foreach(string item in paramaters) {
//this is one of the url paramaters
string poo = item;
}
Is there any known way to achieve this? How can I get reach the values from controller? With Dictionary<string, string> or List<string>?
NOTE :
The question is not well explained IMO but I tried my best to fit it.
in. Feel free to tweak it
Like this:
routes.MapRoute("Name", "tag/{*tags}", new { controller = ..., action = ... });
ActionResult MyAction(string tags) {
foreach(string tag in tags.Split("/")) {
...
}
}
The catch all will give you the raw string. If you want a more elegant way to handle the data, you could always use a custom route handler.
public class AllPathRouteHandler : MvcRouteHandler
{
private readonly string key;
public AllPathRouteHandler(string key)
{
this.key = key;
}
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var allPaths = requestContext.RouteData.Values[key] as string;
if (!string.IsNullOrEmpty(allPaths))
{
requestContext.RouteData.Values[key] = allPaths.Split('/');
}
return base.GetHttpHandler(requestContext);
}
}
Register the route handler.
routes.Add(new Route("tag/{*tags}",
new RouteValueDictionary(
new
{
controller = "Tag",
action = "Index",
}),
new AllPathRouteHandler("tags")));
Get the tags as a array in the controller.
public ActionResult Index(string[] tags)
{
// do something with tags
return View();
}
That's called catch-all:
tag/{*tags}
Just in case anyone is coming to this with MVC in .NET 4.0, you need to be careful where you define your routes. I was happily going to global.asax and adding routes as suggested in these answers (and in other tutorials) and getting nowhere. My routes all just defaulted to {controller}/{action}/{id}. Adding further segments to the URL gave me a 404 error. Then I discovered the RouteConfig.cs file in the App_Start folder. It turns out this file is called by global.asax in the Application_Start() method. So, in .NET 4.0, make sure you add your custom routes there. This article covers it beautifully.
in asp .net core you can use * in routing
for example
[HTTPGet({*id})]
this code can multi parameter or when using send string with slash use them to get all parameters

Resources