Attribute routing - Enum not parsed - c#

I'm trying to make a route, but it's not working in this case:
If I call:
http://mysite.com/api/v1/product/Generic/1A
works fine.
If I call:
http://mysite.com/api/v1/product?=Generic
works too, but when I call:
http://mysite.com/api/v1/product/Generic
I get this error:
{
"Message":"The request is invalid.",
"MessageDetail":"The parameters dictionary contains a null entry for parameter 'type'
of non-nullable type 'GType' for method 'System.Net.Http.HttpResponseMessage
Get(GType)' in 'MySite.ControllersApi.V2.ProductController'. An optional parameter must
be a reference type, a nullable type, or be declared as an optional parameter."
}
The code of my route:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute
(
name: "DefaultApi",
routeTemplate: "api/{version}/{controller}/{type}/{id}",
defaults: new { id = RouteParameter.Optional, version = "v1", controller = "Product" }
)
}
}
public enum GType
{
Big,
Small,
Generic,
}
And the Controller:
public HttpResponseMessage Get(GType type)
{
...
}
public HttpResponseMessage Get(GType type, string id)
{
...
}
So, Web API isn't parsing the value in the URL. Did I forget anything?

Well, I still can't see how your first url actually works, but the issue with the second url is that you are trying to pass an invalid bit of data as a parameter.
To keep it simple, I'm just going to pretend for a minute that your route definition is this:
routeTemplate: "api/{controller}/{type}/{id}",
defaults: new { id = RouteParameter.Optional }
If you were to call a GET on this url http://mysite.com/product/Generic , then you will get the same error you are running into. This url will resolve a controller of ProductController, with a parameter called type that will have a value of Generic.
However, your type parameter has an actual Type of GType, which is an enum. Generic is not a valid value for that so an error occurs. It's the same as sending "abcd" as the value for a parameter that is an int.
If you tried to call a get to http://mysite.com/product/Big, it would work, as it would be able to parse that value (Big) and a member of the GType enum.

The problem was that I have two equals routes, but with diferents parameters name.
api/{version}/{controller}/{type}/{id}
and
api/{version}/{controller}/{id}
The second must be the last route declared.

The route you show us doesn't remotely match the URLs you're trying to hit. I have a feeling you're looking at the wrong route. Also, shouldn't "/Product/" be "/{controller}/"?
Oh, and you mis-spelled routeTamplate[sic] in the snippet. I assume that was a transcription error -- I'm pretty sure the compiler would bark on that.

Related

ASP.NET MVC enum argument in controller mapping

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.

Making a simple Web API post request

I'm really struggling with making a basic post request in a url to support a tutorial on web api.
I want to do something like this in browser: http://localhost:59445/api/group/post/?newvalue=test and get the post to register. However I don't seem to be able to form the request correctly. What is the correct way to do this?
The error I receive is:
{"Message":"The request is invalid.","MessageDetail":"The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method 'System.String Get(Int32)' in 'twin_groupapi.Controllers.GroupController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter."}
my model:
public class Group
{
public Int32 GroupID { get; set; }
public Int32 SchoolID { get; set; }
public string GroupName { get; set; }
}
routing:
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
GlobalConfiguration.Configuration.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
controller:
//[Route("api/Group/Post")]
[HttpPost]
public void Post([FromUri] string NewValue)
{
string newstring = NewValue;
}
Hitting a URL in your browser will only do a GET request.
You can either:
create a simple <form> with its method set to POST and form inputs to enter the values you want to send (like NewValue), OR
write some JavaScript to create an AJAX POST request using your favorite framework, OR
Use a tool like Postman to set up a POST request, invoke it, and examine the results.
The error message is most likely coming from your Get() method.
As #StriplingWarrior said you are making a GET request while the method is marked as [HttpPost]. You can see this if you use developer tools in your browser (F12 in most modern browsers to active them).
Have a look at How do I manually fire HTTP POST requests with Firefox or Chrome?
Note: the c# convention for parameter names is camelCase with first letter being common, not capital, e.g. string newValue.

Implicit Verbs from Method name

If I create a webApi controller, and populate it with methods prefixed with http verbs, the Api is able to correctly imply what verb should be used on that controller.
public class TestController : ApiController
{
public string GetData()
{
return "Called Get Method";
}
public string PostData()
{
return "Called Post Method";
}
public string PutData()
{
return "Called Put Method";
}
}
If I replace the Post with Update, the Post method continues to work implicitly.
public string UpdateData()
{
return "Called Updated Method";
}
Is there a list of the possible prefixes on a method and what verb they map to? Additionally, is it possible to define custom prefixes? For instance, if I wanted to always map a method starting with "Search" to a Post, can I define this?
If you put your Routing like the following :
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Consider the routeTemplate.You'll now be able to call the actions on the controller by name,and no prefix issue should exist. This approach is very helpful if you have multiple actions on your controller with similar HTTP Verbs(multiple GET or POST say).
Implicit Verbs are a function of the built in routing, and cannot appear to be manually extended.
This Asp.Net details the specific rules around the implicit routing.
HTTP Methods. The framework only chooses actions that match the HTTP method of the request, determined as follows:
You can specify the HTTP method with an attribute: AcceptVerbs, HttpDelete, HttpGet, HttpHead, HttpOptions, HttpPatch, HttpPost, or HttpPut.
Otherwise, if the name of the controller method starts with "Get", "Post", "Put", "Delete", "Head", "Options", or "Patch", then by convention the action supports that HTTP method.
If none of the above, the method supports POST.
The reason why the UpdateData method appears to work, is because any method not implicitly determined is automatically a Post

MVC Alternative routing is failing with optional parameter

I am trying to implement alternative routing in my MVC 5 web app.
I have the controller code:
namespace MyMvcProject.Web.Controllers
{
[RoutePrefix("account")]
public class AccountController : Controller {
[Route("{param}")]
[Authorize]
public ActionResult Index(string param = null) {
...
which works great when hitting the url: http://example.com/account/testparam.
However, I would like to be able to have the value param as an optional parameter.
I have tried changing [Route("{param}")] to [Route("{param}*")] but then the Index() method is never entered. I have also tried changing it to [Route("{param:string=Test}")] but I get a routing runtime error with the term string.
My RoutConfig.cs contains:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{param}",
defaults: new { controller = "Home", action = "Index", param = UrlParameter.Optional }
);
}
Does anyone have any idea how I could force Index() to have an optional parameter using this alternative routing syntax? It's quite useful in other parts of my app, so I'd like to keep the RoutePrefix and Route decorators.
UPDATE
I'm still trying to figure this out, so I changed my route decorator to [Route("{param:int=0}")] and my constructor to public ActionResult Index(int id) and it works as expected (i.e., http://example.com/account behaves as if http://example.com/account/0 was entered. This is exactly what I want, only using string datatypes.
When I change the decorator to: [Route("{id:string=\"\"}")] and the constructor to public ActionResult Index(string id) and I see the runtime error:
The inline constraint resolver of type 'DefaultInlineConstraintResolver' was unable to resolve the following inline constraint: 'string'.
Found the answer here. I need to make param nullable using ?.
[Route("{param?}")]
[Authorize]
public ActionResult Index(string param = null) {
...
}
Hopefully this will help someone in the future.
Not many references that I could find on the topic.
#Brett's answer is great: adding ? to the parameter in the Route attribute and then providing a default value in the Action signature allows that parameter to be optional.
And here's an excellent article from Mike Wasson on Attribute Routing in Web API 2, which includes a piece on optional parameters.
Mike also talks about applying multiple constraints, and says:
You can apply multiple constraints to a parameter, separated by a colon.
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { ... }
which also works great...until you want multiple constraints and an optional parameter.
As we know, applying ? allows the parameter to be optional. So taking the above example, one might assume that
[Route("users/{id:int?:min(1)}")]
public User GetUserById(int id = 1) { ... }
constrains the id parameter to be integers greater than 0, or empty (in which case the default of 1 is used). In reality, we get the error message:
The inline constraint resolver of type 'DefaultInlineConstraintResolver' was unable to resolve the following inline constraint: 'int?'.
It turns out that the order of the constraints matters! Simply putting the optional constraint ? after all other constraints gives the intended behavior
[Route("users/{id:min(1):int?}")]
public User GetUserById(int id = 1) { ... }
This works for more than 2 constraints as well, e.g.
[Route("users/{id:min(1):max(10):int?}")]
public User GetUserById(int id = 1) { ... }

Defining two get function in WebAPI

I am getting to following exception when i am trying to call a GET function in MVC WebAPI
{"$id":"1","Message":"An error has occurred.",
"ExceptionMessage":"Multiple actions were found that match the request:
\r\nSystem.Xml.XmlNode Get(Int32, System.String)
I think the problem is cause due to two get function
I have defined two functions:
One:
[HttpGet]
public XmlNode Get(int id, string Tokken)
{
//Do something
}
Second One
[HttpGet]
public List<UsersAnswers> GetUsersInteractions(int? activityID, string Tokken)
{
// Do Something
}
The route configuration
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Now i am getting the exception when i try to call to the second function:
{SiteURL}/api/Activities/GetUsersInteractions?activityID=32&Tokken=r54e54353
As you can see the route engine sent the request to the first function instead of the second.
How can i define two get operation and to call each one separately?
With the default routing template, Web API uses the HTTP method to select the action. However, you can also create a route where the action name is included in the URI:
routes.MapHttpRoute(
name: "ActionApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
In this route template, the {action} parameter names the action method on the controller. With this style of routing, use attributes to specify the allowed HTTP methods. For example, suppose your controller has the following method:
public class ProductsController : ApiController
{
[HttpGet]
public string Details(int id);
}
In this case, a GET request for “api/products/details/1” would map to the Details method. This style of routing is similar to ASP.NET MVC, and may be appropriate for an RPC-style API.
You can override the action name by using the ActionName attribute. In the following example, there are two actions that map to "api/products/thumbnail/id. One supports GET and the other supports POST:
public class ProductsController : ApiController
{
[HttpGet]
[ActionName("Thumbnail")]
public HttpResponseMessage GetThumbnailImage(int id);
[HttpPost]
[ActionName("Thumbnail")]
public void AddThumbnailImage(int id);
}
You are not calling the second function - the second function is named InsertUserRecord and is a POST method. The function you're calling via GET is GetUserInteractions. As there's no such function for GET, the engine may map this to the only GET function there is, but actually it should throw a "no such function" error.

Categories

Resources