I am having some issues with Web API, and standard documentation didn't help me much..
I have a ProductsController, with a default method GetAllProducts(), which accepts several GET parameters (it was easier to implement that way) for querying.
Now, in another part of the application, I use a jQuery autocomplete plugin, which has to query my webservice and filter the data. Problem is, it expects results in a custom format, which is different than that returned by Web API. I procedeed creating another method, GetProductsByQuery(string query), which should return the data in that format.
Is there any way I can enforce WebAPI to return the data as I want it, without making another Controller?
I am also having problems with the routing table, because all the GETs go straight to the first method, even if I routed the second one to url: "{controller}/query/{query}"
Here is some code:
public class ProductsController : ApiController
{
public IEnumerable<Product> GetAllProducts()
{
NameValueCollection nvc = HttpUtility.ParseQueryString(Request.RequestUri.Query);
// Querying EF with the parameters in the query string
return returnQuery;
}
[System.Web.Mvc.HttpGet]
public dynamic GetProductsByQuery(string query)
{
return SomeCustomObject;
}
And the routing:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Query",
url: "{controller}/query/{query}");
You need to swap your routes around - any request that matches your second route will match your first route first.
Secondly, look into custom media formatters if you need specific return formats for your data:
http://www.asp.net/web-api/overview/formats-and-model-binding/media-formatters
Related
I am using ASP.NET MVC and I am trying to create a new route for a parameter like so:
config.Routes.MapHttpRoute(
name: "MarkOnline",
routeTemplate: "api/{controller}/{id}",
defaults: new { offline = RouteParameter.Optional }
);
and here is my method call I am trying to use inside my API Controller
public void MarkOnline(string offline)
{
}
however what gets returned is my Entity Framework GetData method in the API Controller, which is this:
public IQueryable<VistaLCPreview> GetData()
{
return db.Data;
}
What am I doing wrong?
In this context, GetData is being called due to the fact that it has a prefix of Get. There's a convention that maps HTTP GET to functions prefixed with Get, HTTP POST to PostXXX, etc. GetData is being resolved by the default HTTP route, which specifies an optional id parameter and is not present in your expected GetData URL example (which is what you want there).
The MapHttpRoute from your example is not going to match, due to the id parameter in the routeTemplate, which has not been defaulted to RouteParameter.Optional. This route is actually unnecessary - You do not need to include query-string parameters in this route definition. Query-string parameters are simply mapped into the arguments passed into the actions (offline in your case).
Because MarkOnline is not prefixed with one of the HTTP Verbs as I mentioned above, it is not being matched by the default HTTP route. To fix your problem you simply need to do two things:
Remove the MapHttpRoute that you added. This is not needed as the default HTTP route I've already mentioned will cover your use-case.
Add the HttpGet attribute to your MarkOnline method. This will cause the routing to pick up MarkOnline when an offline query-string parameter is found, but call GetData when it is not.
Your route is not configured correctly, you are not specifying the default action on your controller.
It should be something like this:
routes.MapRoute(
name: "MarkOnlineRoute",
url: "api/{controller}",
defaults: new { action = "MarkOnline" }
);
But also notice that the order which you configure your routes is important, it should be located before the default route configuration:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
And as a reference, this is my test controller:
public class AvailabilityController : Controller
{
// GET: MarkOnline
public void MarkOnline(string offline)
{
//return Json(new { isOnline = true, name=offline }, JsonRequestBehavior.AllowGet);
}
}
And it is called with: http://localhost/api/availability?offline=xxx#xxx.com
ASP.NET MVC provides simple templates for controller methods such as Details, and can have something like:
public ActionResult Details(int id)
{
// do something
}
This can be accessed by: http://localhost:port/Controller/Details/id
What I'm trying to do is instead provide a different type like:
public enum MyEnum
{
All,
Pending,
Complete
}
And then I setup my controller method like:
public ActionResult MyMethod(MyEnum myEnum = MyEnum.Pending)
{
// do something
}
This works fine for: http://localhost:port/Controller/MyMethod/ because it uses the default argument.
To specify a different argument I have to do http://localhost:port/Controller/MyMethod?myEnum=All and that works.
I'm wondering, is it possible for me to be able to do http://localhost:port/Controller/MyMethod/All instead of using ?myEnum=All?
Upon trying to do it that way I get a 404 exception which is understandable, but why doesn't this happen for id in Details?
Can I change the MapRoute which is currently: url: "{controller}/{action}/{id}" to allow me to achieve it with my own type?
What I've tried so far:
I only want this route enforcement for one of my schemes such as http://localhost:port/Controller/MyMethod/{ViewType}, I tried this but it doesn't seem to do anything:
routes.MapRoute(
"MyRoute",
"MyController/Index/{MyEnum}",
new { controller = "MyController", action = "Pending" }
);
/Controller/MyMethod/All will actually work. The problem is with the default route, which will consider All to be the id route parameter, which doesn't line up with what your action is using as a parameter. It would actually work fine if your action signature was:
public ActionResult MyMethod(MyEnum id = MyEnum.Pending)
Since it will then bind All to the right thing.
You could add another route for this use-case, but you'll need to be careful that you don't just create another "default" route, which will take over. In other words, you'll have to fix part of the URL:
routes.MapRoute(
"MyCustomRoute",
"Controller/{action}/{myEnum}",
new { controller = "Controller", action = "MyMethod", myEnum = MyEnum.Pending }
);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Then, by the mere presence of the /Controller/ prefix to the route, it will use your custom route instead, and fill in All for the myEnum param, rather than hitting the default route and try to fill in id.
However, be advised that when using enums as route params, they must be exact matches. So, while /Controller/MyMethod/All will work, /Controller/MyMethod/all will not. To get around this, you'll have to create a custom model binder. I did a quick search and found the following article which may help you in that regard.
You can indeed. Do not change the default route "{controller}/{action}/{id}", but rather add one before the default. This new one needs to be fairly specific:
routes.MapRoute(
"EnumRoute",
"Controller/MyMethod/{myEnum}",
new { controller = "Controller", action = "MyMethod", myEnum = UrlParameter.Optional }
);
What that basically says is "when you see request to literally Controller/MyMethod/whatever, use this controller and that method and pass whatever as parameter of the request". Note that actual controller does not necessary have to be what route says in the url, although it is a good idea to stick to that.
I have a controller with multiple actions and I set up the following routes:
routes.MapRoute(
name: "MyCustomRoute",
url: "MyTarget/{option}",
defaults: new { controller = "MyTarget", action = "Index", option = "" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
The main idea here is to call the Index action of the MyTarget controller as default, passing only the argument in the URL.
The lightweight controller looks like this:
public class MyTargetController : Controller
{
public ActionResult Index(string option)
{ ... }
public ActionResult FirstAction()
{ ... }
public ActionResult SecondAction(param list)
{ ... }
}
The MyCustomRoute is set to map MyWebsite/MyTarget/randomOption to the Index action, passing randomOption as the option parameter. The problem is that this route catches all the other actions too: MyWebsite/MyTarget/FirstAction and MyWebsite/MyTarget/SecondAction (ignore the lack of parameters) are mapped to the Index action and their names are routed as the option parameter.
I don't want to change the URL into something like MyWebsite/MyTarget/Index/randomOption. Is there a clear way for distinguishing between a default action with one parameter and other actions, which may or may not have parameter?
EDIT: the following workarounds can be implemented, balancing the advantages and disadvantages:
all actions except Index can be moved to a helper controller: creates two separate controllers processing the same logic;
a custom route constraint can be created, that checks if the parameter value corresponds to the name of an existing action (except Index): needs a way of storing the names of the actions and needs the list to be updated every time a new action is added to the controller (Reflection might be a better approach).
None of the above workarounds seam to be elegant and without any "special" caring.
So I know google can penalize a site if you have the same content on multiple urls... unfortunately, in MVC this is too common i can have example.com/, example.com/Home/ and example.com/Home/Index and all three urls would take me to the same page... so how do I make sure that whenever Index is in the url, that it redirects to the same without the Index and of course the same thing with the Home
Perhaps this little library may be useful for you.
This library is not very convinient in your case, but it should work.
var route = routes.MapRoute(name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });
routes.Redirect(r => r.MapRoute("home_index", "/home/index")).To(route);
routes.Redirect(r => r.MapRoute("home", "/home")).To(route);
The way I handle this is for default pages like Index is to simply create an explicit route for only one of them. I.e. "example.com/People" would be the route for People/Index, and there would be no valid page at the url "/example.com/People/Index".
The Home example is unique in that it has potentially three different URLs. Again in this case I'd simply create a route for "example.com" for that Index action, and not support the other two urls. In other words, you would never link to the other forms of the URL, so their absence should never cause a problem.
We use a Nuget package called AttributeRouting to support this. When you specifiy a GET route for a page, it overrides the defaults for MVC.
Using AttributeRouting usually you'd map the index to [GET("")] but for the special case of Home where you also want to also support the root URL that omits the controller name , I think you'd also add an additional attribute with IsAbsoluteUrl:
public class HomeController : BaseController
{
[GET("")]
[GET("", IsAbsoluteUrl = true)]
public ActionResult Index()
{...
So I found a way to do it without any external Library...
In my RouteConfig I had to add these two routes at the top, just below the IgnoreRoute
routes.MapRoute(
"Root",
"Home/",
new { controller = "Redirect", action = "Home" }
);
routes.MapRoute(
"Index",
"{action}/Index",
new { controller = "Redirect", action = "Home" }
);
Then I had to create a new Controller called Redirect and I created a method for each of my other Controllers like this:
public class RedirectController : Controller
{
public ActionResult Home()
{
return RedirectPermanent("~/");
}
public ActionResult News()
{
return RedirectPermanent("~/News/");
}
public ActionResult ContactUs()
{
return RedirectPermanent("~/ContactUs/");
}
// A method for each of my Controllers
}
That's it, now my site looks legit. No more Home, no more Index in my URLs, this of course has the limitation of not being able to accept parameters to any of the Index methods of your Controllers though if it was really necessary, you should be able to tweak this to achieve what you want.
Just an FYI, if you want to pass an argument to your Index Action, then you can add a third route like this:
routes.MapRoute(
name: "ContactUs",
url: "ContactUs/{id}/{action}",
defaults: new { controller = "ContactUs", action = "Index", id = UrlParameter.Optional }
);
This will create a URL like this: /ContactUs/14
Using the new Api Controller in MVC4, and I've found a problem. If I have the following methods:
public IEnumberable<string> GetAll()
public IEnumberable<string> GetSpecific(int i)
This will work. However, if I want to retrieve some different data of a different type, it defaults to the GetAll method, even though the $.getJSON is set to the GetAllIntegers method:
public IEnumberable<int> GetAllIntergers()
(bad naming conventions)
Is it possible for me to be able to do this?
Can I only have a single GetAll method in the Web API controller?
I think it's easier to visualise what I'm trying to achieve. Here is a snippet of code to show what I'd like to be able to do, in a single ApiController:
public IEnumerable<string> GetClients()
{ // Get data
}
public IEnumerable<string> GetClient(int id)
{ // Get data
}
public IEnumerable<string> GetStaffMember(int id)
{ // Get data
}
public IEnumerable<string> GetStaffMembers()
{ // Get data
}
This is all in the routing. The default Web API route looks like this:
config.Routes.MapHttpRoute(
name: "API Default",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
With the default routing template, Web API uses the HTTP method to select the action. In result it will map a GET request with no parameters to first GetAll it can find. To work around this you need to define a route where the action name is included:
config.Routes.MapHttpRoute(
name: "ActionApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
After that you can star making requests with following URL's:
api/yourapicontroller/GetClients
api/yourapicontroller/GetStaffMembers
This way you can have multiple GetAll in Controller.
One more important thing here is that with this style of routing, you must use attributes to specify the allowed HTTP methods (like [HttpGet]).
There is also an option of mixing the default Web API verb based routing with traditional approach, it is very well described here:
Web API: Mixing Traditional & Verb-Based Routing
In case someone else faces this problem. Here's how I solved this. Use the [Route] attribute on your controller to route to a specific url.
[Route("api/getClient")]
public ClientViewModel GetClient(int id)
[Route("api/getAllClients")]
public IEnumerable<ClientViewModel> GetClients()