I have 2 get methods in my API controller, one which accepts no arguments and one which accepts an integer argument.
The path for the API page is /api/contact. When I navigate here, the page displays as expected.
However, when I change the path to /api/contact/4 to try to invoke the get method which accepts an integer argument, the code instead just invokes the same get method without any arguments. I know this by putting in breakpoints and debugging. What is going wrong?
public PhoneInfo[] Get()
{
return contactRepository.GetAllContacts();
}
public PhoneInfo[] Get(int phn)
{
return contactRepository.GetMessages(phn.ToString());
}
WebApi works based on reflection this means that your curly braces {vars} in your global.asax/routing configuration must match with the same name in your methods.
By default your global.asax will look like this:
RouteTable.Routes.MapHttpRoute(name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = System.Web.Http.RouteParameter.Optional });
Therefore to match the above where id exists as a var the url is api/contact/{id} the method needs to be declare like this:
public string Get(int id)
return "test";
}
While in your example your are changing the default name of the parameter from id to phn causing WebApi not being able to find your method.
Another option can be to use the RouteAttribute:
[Route("api/contact/{phn}"), HttpGet]
public string Get(int phn)
return "another value";
}
Related
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") })
When I want to call a new page in .net, say the "About.cshtml" page, I use the following code in the HomeController:
public ActionResult About()
{
ViewBag.Title = "About";
return View();
}
To call it I'd use a link to "/Home/About". And if I wanted to create a new page called "Contact.cshtml", for example, I'd copy the above and replace About with Contact.
I know that the route in "/Home" calls the HomeController. But how, exactly, does that controller know to return the About.cshtml page? I assume it's based on the name of the function. But this doesn't sound right to me. About() isn't an HTTP verb like Get() or Post(), and the name of the function normally shouldn't define what it does, unless it already existed.
Also, when exactly is View() defined, and when is it assigned to the About.cshtml page?
Finally, is there an attribute that would allow me to return the About.cshtml page with a different function name (as I can set a function to respond to Get with the [HttpGet] attribute)?
But how, exactly, does that controller know to return the About.cshtml page?
Because the action method name is About:
public ActionResult About()
The route found that method by the URL:
/Home/About
If the URL didn't include the action:
/Home
Then it would look for a default action. Normally this is Index(), as configured by the default route mapping:
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
Note how a default value is defined for both controller and action if none is provided on the URL.
the name of the function normally shouldn't define what it does
Why on Earth not? A function name should exactly define what that function does.
Also, when exactly is View() defined
It's in the base controller class.
Finally, is there an attribute that would allow me to return the About.cshtml page with a different function name
Not an attribute per se, but you can specify the view name when calling View():
return View("SomeOtherView");
only to explain a few more (the David's response is so good), View() is an object of type ViewResultBase, in class Controller;
protected internal ViewResult View()
{
return View(viewName: null, masterName: null, model: null);
}
ViewResultBase has a method ExecuteResult() that receives a parameter of type ControllerContext (this parameter has the info about the request) and inside this method, if the name of the view is null, the view name is established based on the url (read the explain of David about the routing) that is called accesing to the RouteData:
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (String.IsNullOrEmpty(ViewName))
{
ViewName = context.RouteData.GetRequiredString("action");
}
....
}
Here, if you watch the RouteData you can view that the called action is Index, and this value is set to the ViewName property:
Regards,
What component(s) do I need to implement and how can I hook it into the framework in order to have urls where the query parameters with names containing 2 or more words separated by hyphens?
For example:
I would have this url:
www.mysite.com/resource/i-am-looking-for?parameterOne=foo¶meterTwo=bar
I would like to have it like this:
www.mysite.com/resource/i-am-looking-for?parameter-one=foo¶meter-two=bar
My action would be something like this:
ActionResult DoSomething(string parameterOne, string parameterTwo)
The reason: user friendly urls and consistency
I need to have:
the component to integrate seamlessly with the framework URL helpers (Url.Action, Url.RouteUrl etc)
the incoming data to be bound to the action parameters (or model)
Is there a diagram where I can see the framework extensibility point in this regard?
Thank you!
public ActionResult SomeAction( string Some-Var ) {} is invalid because in C# variable names can not contain hyphens. Underscores ARE allowed, however so this IS valid public ActionResult SomeAction( string Some_Var ) {}
Now, if you relax your need to bind to strongly typed input vars to the action, you can accomplish your goal with Request.QueryString["some-var"] but you will have to handle the type conversion and error handling associated with it.
You can add Custom Value Provider Factory as shown below,
public class MyQueryStringValueProvider : NameValuePairsValueProvider
{
public QueryStringValueProvider(
HttpActionContext actionContext,
CultureInfo culture)
: base(
() =>{
var request = actionContext.ControllerContext;
foreach(var pair in request
.GetQueryNameValuePairs()){
yield return new KeyValuePair<String,String)(
Transform(pair.Key), pair.Value
);
}, culture)
{
}
private String Transform(String key){
// just removing all - , as it is case insensitive
//
return key.Replace("-","");
}
}
And you have to register your provider as shown below,
ValueProviderFactories.Factories.Add(
new MyQueryStringValueProvider());
For safe side, you can remove existing QueryStringValueProvider to avoid name conflicts for keys that does not have dash.
For dashes in Action Name, you can refer https://github.com/AtaS/lowercase-dashed-route
How about url encoding on client's side?
This way you can call controllers using general way. Please have a look one of the answers regarding that: Encode URL in JavaScript?
for example: this is your action.
public ActionResult GetNew(int id)
{
return View(news.Where(i => i.Id == id).SingleOrDefault());
}
First Step
Open App_Start>RouteConfig.cs open in your project.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "GetNew",
url: "news/new-detail/{id}",
defaults: new { controller = "News", action = "GetNew", id = ""}
);
}
}
Now run your project and write your browser http://localhost:….(different for you)/news/new-detail/1
New ID No. 1 will open.
I hope it's been helpful
I haven't used the ASP.NET WebAPI much although I have used routes in straight MVC. Obviously I'm doing something wrong, perhaps someone can help?
I have one controller called UsersController and two methods on it, Register and Details, both take two string parameters and return a string, both marked as HttpGet.
Originally I started with two route mappings in WebApiConfig.cs:
config.Routes.MapHttpRoute(
name: "TestApi",
routeTemplate: "api/{controller}/{action}/{userId}/{key}"
);
config.Routes.MapHttpRoute(
name: "Test2Api",
routeTemplate: "api/{controller}/{action}/{userId}/{class}"
);
With this set up only the first route is found for a URL such as:
http://<domain>/api/users/register/a123/b456/
If I call:
http://<domain>/api/users/details/a123/b456/
I get a 404. If I swap the two routes around then I can call the Details method but not the Register method, again getting a 404. The workaround I have in place is to be more specific with the routes:
config.Routes.MapHttpRoute(
name: "TestApi",
routeTemplate: "api/{controller}/register/{userId}/{key}"
);
config.Routes.MapHttpRoute(
name: "Test2Api",
routeTemplate: "api/{controller}/details/{userId}/{class}/"
);
The UsersController looks like:
namespace HelloWebAPI.Controllers
{
using System;
using System.Web.Http;
using HelloWebAPI.Models;
public class UsersController : ApiController
{
[HttpGet]
public string Register(string userId, string key)
{
return userId + "-" + key;
}
[HttpGet]
public string Enrolments(string userId, string #class)
{
return userId + "-" + #class
}
}
}
So what I don't understand is why the {action} component of the route registered second is not being associated with the correct method.
Thanks
Joe
The routing in ASP.NET Web API works in three steps:
Matching the URI to a route template.
Selecting a controller.
Selecting an action.
The framework selects the first route in the route table that matches the URI, there is no "second guessing" in case when 2nd or 3rd step fails - just 404. In your case both URI are always matching the first route so the second is never used.
For further analysis let's assume that the first route is:
api/{controller}/{action}/{userId}/{key}
And you call it with following URI:
http://<domain>/api/users/enrolments/a123/b456/
In order to choose action framework is checking three things:
The HTTP method of the request.
The {action} placeholder in the route template, if present.
The parameters of the actions on the controller.
In this case the {action} part will be resolved correctly to enrolments, but framework will be looking for Enrolments method with userId and key parameters. Your method has a class parameter which is no match. This will result in 404.
To avoid the issue you have to make more specific routes (like you did) or unify parameters names.
You only need to define the one route:
config.Routes.MapHttpRoute(
name: "TestApi",
routeTemplate: "api/{controller}/{action}/{userId}/{key}"
);
Then change your controller methods to the following:
[HttpGet]
public string Register(string userId, string key)
{
return userId + "-" + key;
}
[HttpGet]
public string Details(string userId, string key)
{
return userId + "-" + key
}
I would like to create a simple route which will allow me to have ONLY one item listed after the base URL (other than when it's a controller), and for that item to be passed into a controller as a parameter. for example:
www.mydomain.com/user1
www.mydomain.com/user2
www.mydomain.com/user3
www.mydomain.com/user3
where user1, user2 etc are usernames and are being passed dynamically, ie i don't want to have to create a controller for each username.
naturally i would want to make sure if something like this is possible that it wont cause conflicts with other genuine controller names, thus i guess the other controllers would have to have rules created specifically for them and listed above the wildcard route
i'm not sure how to do this as i guess the first item after the slash is usually a controller.
any ideas how to tackle this?
the examples i provided may seem ambiguous, when i put www.mydomain.com/user1 etc it represents that it could be anything (ie a username),for example, www.mydomain.com/jsmith, www.mydomain.com/djohnson, www.mydomain.com/sblake, www.mydomain.com/fheeley
the idea is that a users profile can be looked up simply by entering the domain name then a fwd slash and the username.
ASP.Net MVC routes are process from the top down, and as soon as a match is found it won't look any further for a match. So put your most specific routes first, and your wildcard route last. If none of the specific routes match, control will be passed to the wildcard route.
Use a route definition such as
routes.MapRoute("{username}", new { controller = "User", action = "View"});
in your global.asax.cs file but put it last in your list of route definitions.
MVC processes your route mappings from top to bottom, so put your most general route mappings at the top, and your more specific ones at the bottom.
After you do this you will need the corresponding controller/action so create a new Controller named "UsersController" with an action named "View", like as follows:
using System.Web.Mvc;
namespace Routes.Controllers
{
public class UsersController : Controller
{
//
// GET: /{username}
public ActionResult List(string username)
{
// Get User from database/repository and pass to view
var user = ...;
return View(user);
}
}
}
You have to do following thing.
Create one controller or Action for Handle above scenario. For example controller="Home" and Action="GetUser"
public ActionResult GetUser(string username){
// do your work
}
In Global.asax ( Top route)
// Look at point 3 for UserNameConstraint
route.MapRoute("UserRoute" , "{username}" , new { controller="Home" , action="GetUser" } , new { username = new UserNameConstraint() });
// Put your default route after above here
Now for Create Route constraint.
Hope this help you.
public class UserNameConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
List<string> userName = GetUserName from DB
userName.Contain(values[parameterName].ToString());
}
}
Create a route like as follows,
routes.MapRoute(
"DefaultWithUser", // Route name
"{user}/{controller}/{action}/{id}", // URL with parameters
new { user=UrlParameter.Optional, controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);