I am trying to standardize an extension model for our REST API development team. We need to provide default implementation of routes, while allowing for custom implementations of routes that replace the default as well.
As a simple example if we have a GET route api/users like this:
public class DefaultUsersController : ApiController
{
[HttpGet]
[Route("api/users", Order = 0)]
public IEnumerable<string> DefaultGetUsers()
{
return new List<string>
{
"DefaultUser1",
"DefaultUser2"
};
}
}
We expect the default work like this:
Now a developer wants to change the behavior of that route, he should be able to simply define the same route with some mechanism to imply their implementation should be the one used, instead of the default. My initial thinking was to use the Order property on the Route attribute since that's what it appears to be there for, as a way to provide a priority (in ascending order) when an ambiguous route is discovered. However it's not working that way, consider this custom implementation that we want to override the default api/users route:
public class CustomUsersController : ApiController
{
[HttpGet]
[Route("api/users", Order = -1)]
public IEnumerable<string> CustomGetUsers()
{
return new List<string>
{
"CustomUser1",
"CustomUser2"
};
}
}
Notice the Order property is set to -1 to give it a lower priority value than the default, which is set to 0. I would have thought this would be used by the DefaultHttpControllerSelector, but it isn't. From the DefaultHttpControllerSelector:
And we end up with this exception being returned in the response:
Is it possible Microsoft just missed the logic/requirement to use Order as a route disambiguator and this is a bug? Or is there another simple way to override a route, hopefully with an attribute?
I have pretty much the same problem. I am creating a starter site, but I want users to be able to redefine to behaviour of a Controller, especially if there is a bug.
I use Autofac to resolve the Controller, but even when I register the new controller as the old one, the original one gets selected.
What I'll do is probably go with URL Rewriting. Especially since this issue is temporary in my case. However, I would be interested if someone has a better option.
Related
My issue is similar to Web Api Routing : Multiple controller types were found that match the URL but I want to keep them in separate controllers.
From the comments, 2 preexisting answers are good workarounds but do not solve the actual issue I'm trying to resolve.
The URLs I'm making up are similar to nested directories in a file system OR are very similar to Firebase URLs.
/BiggestSet/{BiggestSetCode}/Subset1/{Subset1Code}/SubsetOfSubset1/{SubsetOfSubset1}
... etc all the way down to where ever the tree stops. Think of it as a tree of data.
/Collection/{Instance}/Collection/{Instance}
The issue I have is that at the /Collection level I want to also provide specific collection level operations. Like Add and search and other collection specific Operations Collection/ProccessData
Collection Controller:
/Collection/Add
/Collection/ProcessDataOnTheColleciton
Instance Controller:
/Collection/{InstanceCode}
/Collection/{InstanceCode}/ProcessOnTheInstance
The problem I'm having is the Collection/ProcessData clashes with the instance Collection/{InstanceCode}
NOTE: 1 is an parameter and the other is a constant.
If you setup the controllers so that collection and Instance are in the same controller. the /{InstanceCode} doesn't clash with the /ProcessData
BUT
If you setup so the controllers are split into logical functions WebAPI gives the error Multiple controller types were found that match the URL.
Does anyone know how to modify attribute routing to somehow behave as if they are in the same controller OR to prioritize the constant over the parameter across controllers?
To keep two separate controllers and still have such routes you can use regular expression route constraints. This way you can specify for the instanceCode you accept everything except the actions from the other controller.
Here is a sample of how to configure routes like that:
public class CollectionController : ApiController
{
[HttpGet]
[Route("Collection/Add")]
public string Add()
{
return $"CollectionController = Collection/Add";
}
[HttpGet]
[Route("Collection/Process")]
public string Process()
{
return $"CollectionController = Collection/Process";
}
}
public class InstanceController : ApiController
{
[HttpGet]
[Route("Collection/{instanceCode:regex(^(?!Add$|Process$).*)}")]
public string Get(string instanceCode)
{
return $"InstanceController = Collection/{instanceCode}";
}
[HttpGet]
[Route("Collection/{instanceCode:regex(^(?!Add$|Process$).*)}/Process")]
public string Process(string instanceCode)
{
return $"InstanceController = Collection/{instanceCode}/Process";
}
}
Here is also a link to the post that explains the regular expression used in the sample.
An even better option would be if you have a specific format for the instanceCode and set the regular expression to accept only this specific format. Then you would not need to modify the regular expression for every new action added. I include also a link to the documentation for all available Route constraints. There you can see all the available options. For example if your instance code is a number you don't even need a regular expression you can just restrict with the int constraint like this [Route("Collection/{instanceCode:int}")].
I wanted to ask you guys if is it possible, to make some routing like this for my project /{action}/{title}?
I was wondering if that is possible, does this url has to be a primary key too? Since there is no ID passed to know which blog post is this.
Thank you.
You can do this quite easily with attribute routing:
[Route("blogs")]
public class BlogController
{
[AcceptVerbs("GET", "HEAD", Route = "{slug}")]
public IActionResult View(string slug)
{
}
}
This maps all requests to /blogs/whatever to that action, and sets slug to the value after "/blogs/".
I want to be able to have additional GetXYZ methods in my EntitySetController derived controller class. For example:
[HttpGet]
[Queryable]
public string GetAirportsWithinRadius(int airportId, int radius)
{
var resultAirports = GetAirportsWithinRadius2(airportId, radius);
return resultAirports;
}
This is what I have for config:
ActionConfiguration getAirportsWithinRadius = modelBuilder.Entity<Airport>().Collection.Action("GetAirportsWithinRadius");
getAirportsWithinRadius.Parameter<int>("airportId");
getAirportsWithinRadius.Parameter<int>("radius");
getAirportsWithinRadius.ReturnsCollectionFromEntitySet<Airport>("Airports");
I want this action to be composable just like the default Get Queryable action, but this would be an alternative that supports all odata parameters but additionally an airportId and radius. This would first filter airports by a radius search (this I know how to do - it's irrelevant to the question) and then return the Queryable so that it can be further filtered by odata params.
Everything I read says this would be an odata action and therefore must be a POST, but Get is also an action and that is a GET, so why not allow extended getters with additional parameters? Am I missing something? How do I accomplish what I want to get done?
I would call this from an ajax client as such:
GET /odata/Airports?$inlinecount=allpages&$top=25&airportId=2112&radius=50
as opposed to a regular odata GET:
GET /odata/Airports?$inlinecount=allpages&$top=25
Thanks
EDIT:
I understand now that this is an odata "function" and it is under consideration as a future feature. Let's forget for second the odata meaning of this. It is essentially a WebApi HttpGet that returns a Queryable, right? So, as long as I don't care about the metadata advertising of this "function", how can I make sure that it is a reachable HttpGet form a route perspective inside of an ODataController? The ODataController needs the MapODataRoute and can I additionally add non odata routes using additional MapHttpRoutes?
I ask this because it seems to me that I should be able to, but all my tries have failed (trying to hit the HttpGet through fiddler). I can find no examples on extending an ODataController with additional non-odata GETs. Can someone help me understand if and how this can be done with the example?:
[Queryable]
public IQueryable<Airport> Get()
{
return db.Airports;
}
[HttpGet]
[Queryable]
public string GetAirportsWithinRadius(int airportId, int radius)
{
var resultAirports = GetAirportsWithinRadius2(airportId, radius);
return resultAirports;
}
You are looking for OData Functions, which is not yet supported out of the box. We have an issue over here. You can up-vote it.
http://aspnetwebstack.codeplex.com/workitem/881
I have an ApiController, quite simple, like this:
public class AssetController : ApiController
{
// removed for brevity
}
When I insert a route to it from a view, the url created is something like:
http://host/Asset
but I would like to customize the name, so that it becomes this:
http://host/assets
How can I specify a custom name for my controller, without resorting to a complete custom routing table?
When I insert a route to it from a view, the url created is something like: http://host/Asset
You haven't really shown how you are doing this inserting but the following should work fine:
#Url.RouteUrl("DefaultApi", new { httproute = "false", controller = "assets" })
and if you want an absolute url you could specify the protocol scheme as third argument:
#Url.RouteUrl("DefaultApi", new { httproute = "false", controller = "assets" }, "http")
And in order to obey RESTFul conventions you should rename your controller to AssetsController.
I'd recommend looking at the https://github.com/mccalltd/AttributeRouting library. It handles this aspect quite well by putting an attribute right on each function and giving it a specific route (which can be anything).
I've had to resolve this issue so I've opted to adjust my routing table to reflect the API that I really want.
We have an a PHP application that we are converting to MVC. The goal is to have the application remain identical in terms of URLs and HTML (SEO and the like + PHP site is still being worked on). We have a booking process made of 3 views and in the current PHP site, all these view post back to the same URL, sending a hidden field to differentiate which page/step in the booking process is being sent back (data between pages is stored in state as the query is built up).
To replicate this in MVC, we could have a single action method that all 3 pages post to, with a single binder that only populates a portion of the model depending on which page it was posted from, and the controller looks at the model and decides what stage is next in the booking process. Or if this is possible (and this is my question), set up a route that can read the POST parameters and based on the values of the POST parameters, route to a differen action method.
As far as i understand there is no support for this in MVC routing as it stands (but i would love to be wrong on this), so where would i need to look at extending MVC in order to support this? (i think multiple action methods is cleaner somehow).
Your help would be much appreciated.
I have come upon two solutions, one devised by someone I work with and then another more elegant solution by me!
The first solution was to specify a class that extends MVcRouteHandler for the specified route. This route handler could examine the route in Form of the HttpContext, read the Form data and then update the RouteData in the RequestContext.
MapRoute(routes,
"Book",
"{locale}/book",
new { controller = "Reservation", action = "Index" }).RouteHandler = new ReservationRouteHandler();
The ReservationRouteHandler looks like this:
public class ReservationRouteHandler: MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var request = requestContext.HttpContext.Request;
// First attempt to match one of the posted tab types
var action = ReservationNavigationHandler.GetActionFromPostData(request);
requestContext.RouteData.Values["action"] = action.ActionName;
requestContext.RouteData.Values["viewStage"] = action.ViewStage;
return base.GetHttpHandler(requestContext);
}
The NavigationHandler actually does the job of looking in the form data but you get the idea.
This solution works, however, it feels a bit clunky and from looking at the controller class you would never know this was happening and wouldn't realise why en-gb/book would point to different methods, not to mention that this doesn't really feel that reusable.
A better solution is to have overloaded methods on the controller i.e. they are all called book in this case and then define your own custome ActionMethodSelectorAttribute. This is what the HttpPost Attribute derives from.
public class FormPostFilterAttribute : ActionMethodSelectorAttribute
{
private readonly string _elementId;
private readonly string _requiredValue;
public FormPostFilterAttribute(string elementId, string requiredValue)
{
_elementId = elementId;
_requiredValue = requiredValue;
}
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
if (string.IsNullOrEmpty(controllerContext.HttpContext.Request.Form[_elementId]))
{
return false;
}
if (controllerContext.HttpContext.Request.Form[_elementId] != _requiredValue)
{
return false;
}
return true;
}
}
MVC calls this class when it tries to resolve the correct action method on a controller given a URL. We then declare the action methods as follows:
public ActionResult Book(HotelSummaryPostData hotelSummary)
{
return View("CustomerDetails");
}
[FormFieldFilter("stepID", "1")]
public ActionResult Book(YourDetailsPostData yourDetails, RequestedViewPostData requestedView)
{
return View(requestedView.RequestedView);
}
[FormFieldFilter("stepID", "2")]
public ActionResult Book(RoomDetailsPostData roomDetails, RequestedViewPostData requestedView)
{
return View(requestedView.RequestedView);
}
[HttpGet]
public ActionResult Book()
{
return View();
}
We have to define the hidden field stepID on the different pages so that when the forms on these pages post back to the common URL the SelectorAttributes correctly determines which action method to invoke. I was suprised that it correctly selects an action method when an identically named method exists with not attribute set, but also glad.
I haven't looked into whether you can stack these method selectors, i imagine that you can though which would make this a pretty damn cool feature in MVC.
I hope this answer is of some use to somebody other than me. :)