Is there a way to pass querystring parameters to an ASP.NET MVC4 Web Api controller without using the OData conventions outlined here?
http://www.asp.net/web-api/overview/web-api-routing-and-actions/paging-and-querying
I have some repository methods built using Dapper that don't support IQueryable and want to be able to manually paginate them without using the OData conventions, but whenever I try doing it the traditional ASP.NET way I get "route not found" errors.
For instance, here's a route:
context.Routes.MapHttpRoute(
name: "APIv1_api_pagination",
routeTemplate: "api/v1/{controller}/{id}",
defaults: new { area = AreaName, controller = "category", offset = 0, count = 100});
And here's the signature to match
public class CategoryController : ApiController
{
// GET /api/<controller>
public HttpResponseMessage Get(int id, int offset = 0, int count = 0)
And whenever I pass the following query:
http://localhost/api/v1/category/1?offset=10
I get the following error:
No action was found on the controller 'Category' that matches the
request.
Any suggestions on how to work with querystrings sanely in ASP.NET MVC4 Web Api?
When you start to use querystring you actually call exact method of controller with its parameters. What I prefer you to change your router like :
context.Routes.MapHttpRoute(
name: "APIv1_api_pagination",
routeTemplate: "api/v1/{controller}/{action}/{id}",
defaults: new { area = AreaName, controller = "category", offset = 0, count = 100});
and then change your method into
public HttpResponseMessage Items(int id, int offset = 0, int count = 0);
From now on whenever you query like
http://localhost/api/v1/category/Items?id=1&offset=10&count=0
it will run.
Another method came to my mind while writing this. I don't know if it works but try to change your router like
context.Routes.MapHttpRoute(
name: "APIv1_api_pagination",
routeTemplate: "api/v1/{controller}/{id}/{offset}/{count}",
defaults: new { area = AreaName, controller = "category", offset = RouteParameter.Optional, count = RouteParameter.Optional});
In this instance, the issue I was running into was the fact that I had multiple overloads for GET on my WebApi controller instance. When I removed those (and condensed everything down to one Get method with more optional parameters and control-flow inside the method itself) everything worked as expected.
Related
So I'm having a little problem here with routing.
There are two parts to this web application:
1. Brochure / Display Website
2. Internal Site / Client Application
We wanted a way to release changes for the brochure without having to do a whole release of said Web application.
Visiting existing named views will take the user to a brochure page, however if it doesn't exist, it will act like they are a client and will redirect them to their company's login screen.
Global.asax:
//if view doesnt exist then url is a client and should be redirected
routes.MapRoute(
name: "Brochure",
url: "{id}",
defaults: new { controller = "brochure", action = "Brochure", id = "Index" },
namespaces: new[] { "Web.Areas.Brochure.Controllers" }
);
//This is home page
routes.MapRoute(
name: "HomeDefault",
url: "{client}/{action}",
defaults: new { controller = "home", action = "index" },
namespaces: new string[] { "Web.Controllers" }
);
Controller:
/// <summary> Check if the view exists in our brochure list </summary>
private bool ViewExists(string name) {
ViewEngineResult result = ViewEngines.Engines.FindView(ControllerContext, name, null);
return (result.View != null);
}
/// <summary> Generic action result routing for all pages.
/// If the view doesn't exist in the brochure area, then redirect to interal web
/// This way, even when we add new pages to the brochure, there is no need to re-compile & release the whole Web project. </summary>
public ActionResult Brochure(string id) {
if (ViewExists(id)) {
return View(id);
}
return RedirectToRoute("HomeDefault", new { client = id });
}
This code works fine up until we log in and go to the landing page. It seems to keep the Brochure action in the route and doesn't want to go to the subsequent controller which results in a 500 error.
e.g. 'domain/client/Brochure' when it needs to be: 'domain/client/Index'
Things tried but not worked:
Changing RedirectToRoute() to a RedirectToAction() - this results in a
finite loop of going back to the ActionResult Brochure(). So
changing controllers through that didn't work.
Create an ActionResult called Brochure() inside the 'HomeController'. It
doesn't even get hit.
Passed in namespaces for RedirectToRoute() as an attribute. I knew this would probably not work, but it was worth a try.
So the question is:
How can I get the route to act properly?
If you can restrict id to some subset of all values you can add that constraints to route (i.e. numbers only) to let default handle the rest.
routes.MapRoute(
name: "Brochure",
url: "{id}",
defaults: new { controller = "brochure", action = "Brochure", id = "Index" },
namespaces: new[] { "Web.Areas.Brochure.Controllers" }
constraints : new { category = #"\d+"}
);
If you can't statically determine restrictions - automatically redirecting in your BrochureController similar to your current code would work. The only problem with sample in the question is it hits the same route again and goes into infinite redirect loop - redirect to Url that does not match first rule:
// may need to remove defaults from second route
return RedirectToRoute("HomeDefault", new { client = id, action = "index" });
If standard constraints do not work and you must keep single segment in url - use custom constraints - implement IRouteConstraint and use it in first route. See Creating custom constraints.
There are several issues with your configuration. I can explain what is wrong with it, but I am not sure I can set you on the right track because you didn't provide the all of the URLs (at least not all of them from what I can tell).
Issues
Your Brouchure route, which has 1 optional URL segment named {id}, will match any URL that is 0 or 1 segments (such as / and /client). The fact that it matches your home page (and you have another route that is named HomeDefault that will never be given the chance to match the home page) leads me to believe this wasn't intended. You can make the {id} value required by removing the default value id = "Index".
The Brouchure route has a namespace that indicates it is probably in an Area. To properly register the area, you have to make the last line of that route ).DataTokens["area"] = "Brochure"; or alternatively put it into the /Areas/Brouchure/AreaRegistration.cs file, which will do that for you.
The only way to get to the HomeDefault route is to supply a 2 segment URL (such as /client/Index, which will take you to the Index method on the HomeController). The example URLs you have provided have 3 segments. Neither of the routes you have provided will match a URL with 3 segments, so if these URLs are not getting 404 errors they are obviously matching a route that you haven't provided in your question. In other words, you are looking for the problem in the wrong place.
If you provide your entire route configuration including all Area routes and AttributeRouting routes (including the line that registers them), as well as a complete description of what URL should go to what action method, I am sure you will get more helpful answers.
So the question is:
How can I get the route to act properly?
Unknown. Until you describe what properly is.
Related: Why map special routes first before common routes in asp.net mvc?
Two ways I could have solved this issue:
Way 1
I reviewed the redirect and just passed in an action in order to get a route that has 2 segments in the url. i.e. client/Index. The Index action now handles logins - going past a custom controller.
public class HomeController : CustomController
public ActionResult Brochure(string id, string action) {
if (ViewExists(id)) {
return View(id);
}
return RedirectToAction("Index", "Home", new { client = id, action = "Index" });
}
Way 2
(from #Alexei_Levenkov)
Create a custom Route constraint so the route will be ignored if the view cannot be found.
namespace Web.Contraints {
public class BrochureConstraint : IRouteConstraint {
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) {
//Create our 'fake' controllerContext as we cannot access ControllerContext here
HttpContextWrapper context = new HttpContextWrapper(HttpContext.Current);
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "brochure");
ControllerContext controllerContext = new ControllerContext(new RequestContext(context, routeData), new BrochureController());
//Check if our view exists in the folder -> if so the route is valid - return true
ViewEngineResult result = ViewEngines.Engines.FindView(controllerContext, "~/Areas/Brochure/Views/Brochure/" + values["id"] + ".cshtml", null);
return result.View != null;
}
}
}
namespace Web {
public class MvcApplication : System.Web.HttpApplication {
//If view doesnt exist then url is a client so use the 'HomeDefault' route below
routes.MapRoute(
name: "Brochure",
url: "{id}",
defaults: new { controller = "brochure", action = "Brochure", id = "Index" },
namespaces: new[] { "Web.Areas.Brochure.Controllers" },
constraints: new { isBrochure = new BrochureConstraint() }
);
//This is home page for client
routes.MapRoute(
name: "HomeDefault",
url: "{client}/{action}",
defaults: new { controller = "home", action = "index" },
namespaces: new string[] { "Web.Controllers" }
);
}
}
I hope this helps someone else out there.
Very new to ASP.net MVC and C# in general. Experience with PHP/NodeJS mostly, a little Java.
I have a method in a controller like so:
public ActionResult ImageProcess(string fileName){
string url = "http://myurl.com/images/" + fileName + ".jpg";
//Code to stream the file
}
And when I navigate to it as "http://myurl.com/Home/ImageProcess/12345" I get thrown a 404 error by the process as it's trying to fetch the file.
If I hard-code it like so...
public ActionResult ImageProcess(string fileName){
string url = "http://myurl.com/images/12345.jpg";
//Code to stream the file
}
...It works just fine, returns my processed image as expected.
Why is this happening?
If you're using the default routes provided for ASP.NET MVC, the fix is simple: change fileName to id.
Example:
public ActionResult ImageProcess(string id) {
string url = "http://myurl.com/images/" + id + ".jpg";
}
In the file RouteConfig.cs you should see something like this:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "YourProject.Controllers" }
);
This is the configuration that tells the framework how to interpret URL strings and map them to method calls. The parameters for these method calls need to be named the same as in the route.
If you want the parameter to be named fileName, just rename {id} to {fileName} in RouteConfig.cs, or create a new route with a new name and defaults above the default route. But, if that's all you're doing, you might as well stick with the default route and name the parameter id in your action.
Your other option would be to use a query parameter, which would not require any route or variable changes:
link text
Look here for a nice tutorial on routing.
Either change the routing value as #johnnyRose already suggested or change the url to a get parameter, that will let the model binding find the fileName attribute. Like this:
http://myurl.com/Home/ImageProcess?fileName=12345
I am currently playing around with some things...According to this link, I need to construct a route that is open to the following format
webServiceURL/version/devices/deviceLibraryIdentifier/registrations/passTypeIdentifier?passesUpdatedSince=tag
so I defined the route like so
config.Routes.MapHttpRoute(
name: "DefaultApi3",
routeTemplate: "{version}/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{passesUpdatedSince}",
defaults: new { controller = "SerialNumbers", action = "GET", passesUpdatedSince = RouteParameter.Optional }
);
However, the following route fails for the url
http://localhost/v1/devices/24358235235loji200/registrations/pass.com.mypass?passesUpdatedSince=12a512
How can I configure the route so that the above url can reach my controller?
My controller looks like
[HttpGet]
public HttpResponseMessage Get(string passesUpdatedSince ="")
{
//do stuff
}
UPDATE
Thanks to the comments, I've made the following changes.
the route
config.Routes.MapHttpRoute(
name: "DefaultApi3",
routeTemplate: "v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}",
defaults: new { controller = "SerialNumbers", action = "GET" }
);
My controller is as follows
public HttpResponseMessage Get(string deviceLibraryIdentifier,
string passTypeIdentifier,
string passesUpdatedSince = "")
{
//do stuff
}
According to the Apple docs, is it right to assume the following the webservice calls could look like
http://localhost:31472/v1/devices/23lk5235232oijlk/registrations/pass.com.mypass
http://localhost:31472/v1/devices/23lk5235232oijlk/registrations/pass.com.mypass?passesUpdatedSince=159025
as these are returning 404.
These, however, do work.
http://localhost:31472/v1/devices/23lk5235232oijlk/registrations/pass.com.mypass/?passesUpdatedSince=1415l
http://localhost:31472/v1/devices/23lk5235232oijlk/registrations/pass.com.mypass/
So would there be a way to get it to work without the presence of the / near the end of the url?
It does look like the device is unable to recognize the route. I get the following message
Get serial #s task (for device 2523ff2fswtsfdh6544, pass type pass.com.mypass, last updated (null); with web service url https://weburl) encountered error: Unexpected response code 404
Because part of the URI had periods in it (pass.com.mypass), this always returned a 404
I had to add the
<modules runAllManagedModulesForAllRequests="true" />
in my web.config. And after that, everything worked as expected
For the route, try:
config.Routes.MapHttpRoute(
name: "DefaultApi3",
routeTemplate: "{version}/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}",
defaults: new { controller = "SerialNumbers", action = "GET" }
);
Note that you should actually have a hard-coded value where {version} is, according to the link you gave us (https://developer.apple.com/library/ios/documentation/PassKit/Reference/PassKit_WebService/WebService.html#//apple_ref/doc/uid/TP40011988-CH0-SW4).
A hard-coded version would look like this:
config.Routes.MapHttpRoute(
name: "DefaultApi3",
routeTemplate: "v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}",
defaults: new { controller = "SerialNumbers", action = "GET" }
);
Your controller action also needs to be able to accept all parameters of the route:
[HttpGet]
public HttpResponseMessage Get(string deviceLibraryIdentifier,
string passTypeIdentifier,
string passesUpdatedSince ="")
{
//do stuff
}
I've looked for a solution to this but even the simplest examples aren't working properly. Passing a single parameter {id} works successfully but that's the only parameter that is working. Changing the single parameter to anything else fails. In the example below multiple parameters also fail. It seems as the only workable parameter is "id".
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Servers",
url: "{controller}/{action}/{id}/{a}",
defaults: new
{
controller = "Test"
}
);
}
public class TestController : Controller
{
[HttpGet]
public ActionResult Monster(string id, string a)
{
return Json(new { success = id }, JsonRequestBehavior.AllowGet);
}
}
The url localhost/Test/Monster/hi Successfully reads the parameter as "hi". Specifying localhost/Test/Monster/hi/hello fails and gives a 404.
Try this:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Servers",
url: "{controller}/{action}/{id}/{a}",
defaults: new
{
controller = "Test",
id = UrlParameter.Optional,
a = UrlParameter.Optional
}
);
}
Also, is this your only route?
The order that the routes are set-up in is important, it's very easy to overwrite a route with a later route. I have done that mistake countless times.
In case action is not optional, you should specific the default value for it. Please try :
routes.MapRoute(
name: "Servers",
url: "{controller}/{action}/{id}/{a}",
defaults: new
{
controller = "Test",
action = "Monster"
}
);
In your method you have specified the parameter string a so when you pass the URl localhost/Test/Monster/hi/hello MVC will look for the parameter a in the url as it matches the form post parameters with the parameters in the function
So this link might help you as it helped me
http://www.codeproject.com/Articles/299531/Custom-routes-for-MVC-Application
This is a very late response but the issue with this was that there was an area being registered further downstream that was causing the routing issues. The area being registered had an optional url parameter that was taking the routes over. Utilizing this registered area fixed the issue.
sorry to say but as you saying
localhost/Test/Monster/Hi
working mean only one parameter routing is configured... did you try by restarting the IISExpress as routing get loaded on very first call and one time only..
after making the changes in routing you have to stop the IIS Express from the Icon Tray and re-run you project then with one parameter it should throw error.. as you have not set these option it will work only when u specify both the parameters.
Please excuse my ignorance in this area. I have read many threads and still cannot get my routing correct.
I have a ProductsController like this:
public class ProductsController : ApiController
{
[ActionName("GetListOfStudents")]
public static List<Structures.StudentInfo> GetListOfStudents(string Username, string Password)
{
List<Structures.StudentInfo> si = StudentFunctions.GetListOfStudents(Username, Password);
return si;
}
}
I have a console test program where I have defined the route:
config.Routes.MapHttpRoute(
name: "ApiByAction",
routeTemplate: "api/products/GetListOfStudents",
defaults: new { controller = "products", action = "GetListOfStudents" });
But when I run call
GET http://localhost:8080/api/Products/GetListOfStudents
I get the error message:
MessageDetail=No action was found on the controller 'Products' that matches the name 'GetListOfStudents'.
I have been pulling my hair out and cannot work out what the correct route should be.
Would any kind person care to help me out?
Ok- thanks for the help peeps!
This what I did to get it working:
Removed the "static" from the GetListOfStudents function.
Added the route below.
config.Routes.MapHttpRoute(
name: "ApiByAction",
routeTemplate: "api/products/GetListOfStudents/{username}/{password}",
defaults: new { controller = "products", action = "GetListOfStudents" }
);
Thanks everyone for your help!
When registering your global api access point, you should tell the config which route to use in the following manner:
config.Routes.MapHttpRoute(
name: "ApiByAction",
routeTemplate: "api/{controller}/{action}
defaults: new { controller = "products", action = "GetListOfStudents" });
In this sample you explicitly tell the controller it should only go to the "products" controller, you can make it generic without specifying the control or the action, just omit the defaults, like this:
config.Routes.MapHttpRoute(
name: "ApiByAction",
routeTemplate: "api/{controller}/{action}
That should do the job :)
Your GetListOfStudents action requires two parameters, username and password. Yet, the route definition contains neither specification in the route template where the values for those parameters should come from, nor specification for those parameter defaults in the defaults: parameter definition.
So when request comes in, routing is able to find your controller, but it is unable to find the action that it can call with the request and route context that it has because it has no information for the username and password parameters.
The most important is:
ASP.Net's mvc not only seek action by name, also it will check method's signature, only the method is non-static, name matches and parameters matches, the action will be executed.
for your case, there are two ways to correct it.
one way is declare default value, mvc will use default value when parametr not found.
public List<Structures.StudentInfo> GetListOfStudents(string Username = null, string Password = null)
{
List<Structures.StudentInfo> si = StudentFunctions.GetListOfStudents(Username, Password);
return si;
}
the second way is use override
public List<Structures.StudentInfo> GetListOfStudents()
{
return GetListOfStudents(null, null);
}
public List<Structures.StudentInfo> GetListOfStudents(string Username, string Password)
{
List<Structures.StudentInfo> si = StudentFunctions.GetListOfStudents(Username, Password);
return si;
}
I had this problem and solved it by including the verb as part of the action (i.e. GetThis, GetThat) and manually creating routes. I was attempting to create routes using attributes, but that did not work. This SO question may be the answer as to why the attributes aren't working, I haven't gotten that straightened out yet. As an additional note for anyone else having the same problem, when debugging it locally, IE was crashing when the "no action found" xml was returned. Once I gave up and switched to Chrome, the message detail was returned, and it was obvious that my controller at least was being found, it was just a matter of getting the action to work...
If you want to call GetListOfStudents method without parameter you must set default value for parameter. such as
GetListOfStudents(string Username=null, string Password=null)
Otherwise you must call method with Parameters.
GET http://localhost:8080/api/Products/GetListOfStudents/Username/Password
One issue could be the order of the route declarations in your WebApiConfig.cs file. Have a look here about the precedence of routes. If you have two routes with the same amount of parameters, you may need to reorder the routes, or -- depending on how specific the route is -- hardcode the controller or action name
When sending, encode the password with base64.
Then when you about to use it decode it.
byte[] numArray = Convert.FromBase64String(password);
string Pass = Encoding.UTF8.GetString(numArray);
List<Structures.StudentInfo> si = StudentFunctions.GetListOfStudents(Username, Pass);
Works fine for me.