WebApiConfig & what is the difference between routes - c#

I have a WebApiController that implements two Get method: one that does not require a parameter and the other method requires an interger parameter...
//Get api/<controller>
public IEnumerable<EmployeeVM> Get()
{
List<EmployeeVM> list = new List<EmployeeVM>()
{
new EmployeeVM(){
FullName = "Milton Waddams"
},
new EmployeeVM(){
FullName = "Andy Bernard"
}
};
return list;
}
//Get api/<controller>
public string Get(int id)
{
return "value";
}
If I use the following configuration in my WebApiConfig class,
configuration.Routes.MapHttpRoute("API Default", "api/{controller}/{id}", new { id = RouteParameter.Optional });
then I would get the following error:
"The parameters dictionary contains a null entry for parameter 'id' of
non-nullable type 'System.Int32' for method 'System.String Get(Int32)'
in 'AngularForMVC.Controllers.EmployeeWebApiController'. An optional
parameter must be a reference type, a nullable type, or be declared as
an optional parameter."
Now if I use the this following configuration:
configuration.Routes.MapHttpRoute("API Default", "api/{controller}/{action}/{id}", new { id = RouteParameter.Optional });
then it works. I can execute the Get() method without any errors.
What is the difference? Why does the second code reference work? I know that I added an {action} into the url path, but even if I did not include the {action} path to the url this should still work.

You should give the id parameter as a nullable int. Get(int? id)

I always prefer using [Route] attribute to define my routes configurations than add new roles in route.config
https://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2

A custom [Route] would be better for several reasons
1-Is to identify what each GET request is supposed to do.
for example if you have [Route("api/getlist")] and [Route("api/getitem/{id}")]
this would be more descriptive.
2-You won't face the problem you're facing now.

Related

URL has Id specified, but still getting the parameters dictionary contains a null entry for parameter 'Id' error

I understand what this error is stating and the typical cause, but in this case, I'm not sure why it is being thrown.
Here's the full error message:
System.ArgumentException: The parameters dictionary contains a null
entry for parameter 'Id' of non-nullable type 'System.Int32' for
method 'System.Threading.Tasks.Task`1[System.String]
AppUninstalled(Int32)' in
'Storefront.Controllers.ShopifyWebhooksController'. An optional
parameter must be a reference type, a nullable type, or be declared as
an optional parameter. Parameter name: parameters
The url being called against my app is: /storefront/wh/AppUninstalled/88564. So it is passing the Id as int.
Here's the route definition:
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Storefront_default",
"Storefront/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
Here's the action signature that is being called: public async Task<string> AppUninstalled(int id)
Now when I test this locally or against my staging server using Postman, I don't get this error. But when Shopify calls it, I do get the error. And I can verify via the Elmah error generated, that the url that was called is just as I posted above, with the trailing Id value.
UPDATE: 1
I also tried having shopify call the url with the id explicitly named: /storefront/wh/AppUninstalled?id=88564 but get the same error.
Could it be something in the encoding that MVC can't convert the id to an int?
UPDATE 2
This works, but it doesn't explain why the above did not work.
Changing the action method in MVC to: public async Task<string> AppUninstalled(string strId)
Changing the Shopify call back url to: /storefront/wh/AppUninstalled?strId=88564
I suspect that since id is declared optional in MapRoute you should declare your action like this:
public async Task<string> AppUninstalled(int? id)
And check if id has a value and take action if not.
Could you try RouteParameter.Optional and MapHttpRoute instead of UrlParameter.Optional and MapRoute
routes.MapHttpRoute( // <-- this
name: "Storefront_default",
routeTemplate: "Storefront/{controller}/{action}/{id}",
defaults: new {action ="Index", id = RouteParameter.Optional // <-- this
});

'The parameters dictionary contains a null entry' error, Web API

On my Web API I have a Document Controller with two simple actions:
[AllowAnonymous]
public class DocumentController : ApiController
{
public String Get(int id)
{
return "test";
}
public String Get(string name)
{
return "test2";
}
}
The following URL (executes the first function) works fine:
http://localhost:1895/API/Document/5
But this URL (should execute the second function):
http://localhost:1895/API/Document/test
Throws this error:
{
"message": "The request is invalid.",
"messageDetail": "The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method 'xx.yy.Document Get(Int32)' in 'API.Controllers.DocumentController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter."
}
This is the MapHttpRoute in the WebApiConfig:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
What am I doing wrong? Please advise.
Your second function has a parameter name, and the default parameter is called id. With your current setup, you can access the second function on
http://localhost:1895/API/Document/?name=test
To make the URLs work as you have specified previously, I would suggest using attribute routing and route constraints.
Enable attribute routing:
config.MapHttpAttributeRoutes();
Define routes on your methods:
[RoutePrefix("api/Document")]
public class DocumentController : ApiController
{
[Route("{id:int}")]
[HttpGet]
public String GetById(int id) // The name of this function can be anything now
{
return "test";
}
[Route("{name}"]
[HttpGet]
public String GetByName(string name)
{
return "test2";
}
}
In this example, GetById has a constraint on the route ({id:int}) which specifies that the parameter must be an integer. GetByName has no such constraint so should match when the parameter is NOT an integer.

call to web api with string parameter

I have a web api where I have 2 methods, one without parameter and two with different types of parameter (string and int). When calling the string method it doesnt work...what am I missing here?
public class MyControllerController : ApiController
{
public IHttpActionResult GetInt(int id)
{
return Ok(1);
}
public IHttpActionResult GetString(string test)
{
return Ok("it worked");
}
}
WebApiConfig.cs:
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
My Call:
/api/MyController/MyString //Doesnt work
/api/MyController/1 //work
I get following error:
The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method 'System.Web.Http.IHttpActionResult GetInt(Int32)' in 'TestAngular.Controllers.MyControllerController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.
What am I missing in my request?
Here is my solution:
without changing default route in webapiconfig.cs file
add just a route to your string function:
[Route("Api/MyController/GetString/{test}")]
public IHttpActionResult GetString(string test)
http://localhost:49609/api/MyController/GetString/stringtest
Also this uri's should work:
api/MyController/GetAll
api/MyController/GetString?param=string
api/MyController/GetInt?param=1
I think this is much clearer and should always work.
You use the routing behavior.
See here:
http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-and-action-selection
You have to change your uri's
/api/MyController
/api/MyController/string
/api/MyController/1
You don't have to specify the methods.
You could take look at this tutorial on asp.net for further clarification.
It's been a while since you posted this, but I think I have the answer.
First off, there are two issues. First, as Pinback noted, you can't use the same route for two different endpoints.
However if you just eliminate the int method, you'll still run into the problem.
Remember: the default route looks like this: api/{controller}/{id}
In order to bind the parameter, it has to be called "id", and not "test".
Change the signature to this:
public IHttpActionResult GetString(string id)
and it will work.
(you can also change {id} to {test} in the webapiconfig.cs file).
You can also take string parameter in body
string body;
using (var sr = new StreamReader(Request.Body))
body = sr.ReadToEnd();

Query string not working while using attribute routing

I'm using System.Web.Http.RouteAttribute and System.Web.Http.RoutePrefixAttribute to enable cleaner URLs for my Web API 2 application. For most of my requests, I can use routing (eg. Controller/param1/param2) or I can use query strings (eg. Controller?param1=bob&param2=mary).
Unfortunately, with one of my Controllers (and only one), this fails. Here is my Controller:
[RoutePrefix("1/Names")]
public class NamesController : ApiController
{
[HttpGet]
[Route("{name}/{sport}/{drink}")]
public List<int> Get(string name, string sport, string drink)
{
// Code removed...
}
[HttpGet]
[Route("{name}/{drink}")]
public List<int> Get(string name, string drink)
{
// Code removed...
}
}
When I make a request to either using routing, both work fine. However, if I use a query string, it fails, telling me that that path does not exist.
I have tried adding the following to my WebApiConfig.cs class' Register(HttpConfiguration config) function (before and after the Default route), but it did nothing:
config.Routes.MapHttpRoute(
name: "NameRoute",
routeTemplate: "{verId}/Names/{name}/{sport}/{drink}",
defaults: new { name = RouteParameter.Optional, sport = RouteParameter.Optional, drink = RouteParameter.Optional },
constraints: new { verId = #"\d+" });
So for clarity, I would like to be able to do both this:
localhost:12345/1/Names/Ted/rugby/coke
localhost:12345/1/Names/Ted/coke
and,
localhost:12345/1/Names?name=Ted&sport=rugby&drink=coke
localhost:12345/1/Names?name=Ted&drink=coke
but sadly the query string versions don't work! :(
Updated
I've removed the second Action altogether and now trying to use just a singular Action with optional parameters. I've changed my route attribute to [Route("{name}/{drink}/{sport?}")] as Tony suggested to make sport nullable, but this now prevents localhost:12345/1/Names/Ted/coke from being a valid route for some reason. Query strings are behaving the same way as before.
Update 2
I now have a singular action in my controller:
[RoutePrefix("1/Names")]
public class NamesController : ApiController
{
[HttpGet]
[Route("{name}/{drink}/{sport?}")]
public List<int> Get(string name, string drink, string sport = "")
{
// Code removed...
}
}
but still, using query strings does not find a suitable path, while using the routing method does.
I was facing the same issue of 'How to include search parameters as a query string?', while I was trying to build a web api for my current project. After googling, the following is working fine for me:
Api controller action:
[HttpGet, Route("search/{categoryid=categoryid}/{ordercode=ordercode}")]
public Task<IHttpActionResult> GetProducts(string categoryId, string orderCode)
{
}
The url I tried through postman:
http://localhost/PD/search?categoryid=all-products&ordercode=star-1932
http://localhost/PD is my hosted api
After much painstaking fiddling and Googling, I've come up with a 'fix'. I don't know if this is ideal/best practice/plain old wrong, but it solves my issue.
All I did was add [Route("")] in addition to the route attributes I was already using. This basically allows Web API 2 routing to allow query strings, as this is now a valid Route.
An example would now be:
[HttpGet]
[Route("")]
[Route("{name}/{drink}/{sport?}")]
public List<int> Get(string name, string drink, string sport = "")
{
// Code removed...
}
This makes both localhost:12345/1/Names/Ted/coke and localhost:12345/1/Names?name=Ted&drink=coke valid.
With the Attribute routing you need to specify default values so they would be optional.
[Route("{name}/{sport=Football}/{drink=Coke}")]
Assigning a value will allow it to be optional so you do not have to include it and it will pass the value to specify.
I have not tested the query string for this but it should work the same.
I just re-read the question and I see that you have 2 Get verbs with the same path, I believe this would cause conflict as routing would not know which one to utilize, perhaps using the optional params will help. You can also specify one can be null and do checking in the method as to how to proceed.
[Route("{name}/{sport?}/{drink?}")]
Then check the variables in the method to see if they are null and handle as needed.
Hope this helps, some? lol
If not perhaps this site will, it has more details about attribute routing.
http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2
Clip from that site:
Optional parameters and default values You can specify that a
parameter is optional by adding a question mark to the parameter, that
is:
[Route("countries/{name?}")]
public Country GetCountry(string name = "USA") { }
Currently, a default value must be specified on the optional parameter
for action selection to succeed, but we can investigate lifting that
restriction. (Please let us know if this is important.)
Default values can be specified in a similar way:
[Route("countries/{name=USA}")]
public Country GetCountry(string name) { }
The optional parameter '?' and the default values must appear after
inline constraints in the parameter definition.
Just a side note from my part as well. In order for queryString params to work, you need to provide a default value for your method parameters to make it optional. Just as you would also do when normally invoking a C# method.
[RoutePrefix("api/v1/profile")]
public class ProfileController : ApiController
{
...
[HttpGet]
[Route("{profileUid}")]
public IHttpActionResult GetProfile(string profileUid, long? someOtherId)
{
// ...
}
...
}
This allows me to call the endpoint like this:
/api/v1/profile/someUid
/api/v1/profile/someUid?someOtherId=123
Using Route("search/{categoryid=categoryid}/{ordercode=ordercode}") will enable you to use both Querystrings and inline route parameters as answered by mosharaf hossain. Writing this answer as this should be top answer and best way. Using Route("") will cause problems if you have multiple Gets/Puts/Posts/Deletes.
Here's a slight deviant of #bhargav kishore mummadireddy's answer, but an important deviation. His answer will default the querystring values to an actual non-empty value. This answer will default them to empty.
It allows you to call the controller through path routing, or using the querystring. Essentially, it sets the default value of the querystring to empty, meaning it will always be routed.
This was important to me, because I want to return 400 (Bad Request) if a querystring is not specified, rather than having ASP.NET return the "could not locate this method on this controller" error.
[RoutePrefix("api/AppUsageReporting")]
public class AppUsageReportingController : ApiController
{
[HttpGet]
// Specify default routing parameters if the parameters aren't specified
[Route("UsageAggregationDaily/{userId=}/{startDate=}/{endDate=}")]
public async Task<HttpResponseMessage> UsageAggregationDaily(string userId, DateTime? startDate, DateTime? endDate)
{
if (String.IsNullOrEmpty(userId))
{
return Request.CreateResponse(HttpStatusCode.BadRequest, $"{nameof(userId)} was not specified.");
}
if (!startDate.HasValue)
{
return Request.CreateResponse(HttpStatusCode.BadRequest, $"{nameof(startDate)} was not specified.");
}
if (!endDate.HasValue)
{
return Request.CreateResponse(HttpStatusCode.BadRequest, $"{nameof(endDate)} was not specified.");
}
}
}
I use FromUri attribute as solution
[Route("UsageAggregationDaily")]
public async Task<HttpResponseMessage> UsageAggregationDaily([FromUri] string userId = null, [FromUri] DateTime? startDate = null, [FromUri] DateTime? endDate = null)
Since you have [Route("{name}/{drink}/{sport?}")] as attribute routing, this code will never be hit.
config.Routes.MapHttpRoute(
name: "NameRoute",
routeTemplate: "{verId}/Names/{name}/{sport}/{drink}",
defaults: new { name = RouteParameter.Optional, sport = RouteParameter.Optional, drink = RouteParameter.Optional },
constraints: new { verId = #"\d+" });
So only the attribute route [Route("{name}/{drink}/{sport?}")] is going to be honored here. Since your request localhost:12345/1/Names?name=Ted&sport=rugby&drink=coke, doesn't have name, sport or drink in the URL it is not going to match this attribute route. We do not consider the query string parameters when matching the routes.
To solve this, you need to make all 3 optional in your attribute route. Then it will match the request.

Web API 2 controller does not accept QueryString parameters

I have a GET() controller to retrieve a list of entities. I want to pass a parameter to the action to filter the list of objects returned as follows:
Mysite.com/Users?nameContains=john
This is my action definition:
public IEnumerable<object> Get(string nameContains)
{
// I want to use nameContains here
}
I get an error:
The requested resource does not support http method 'GET'.
If I revert the method to not get that parameter, it works.
Try this
public IEnumerable<object> Get([FromUri] string nameContains)
{
// I want to use nameContains here
}
Also since you are working in Web Api 2, you can make use of attribute routing
[Route("users")]
public IEnumerable<object> Get([FromUri] string nameContains)
{
Sorry, it was my mistake, I used 2 parameters and I didn't pass one of them (nor assigned it a default value) so it returned an error. Cheers.
You can add a new route to the WebApiConfig entries.
For instance, your method definition:
public IEnumerable<object> Get(string nameContains)
{
// I want to use nameContains here
}
add:
config.Routes.MapHttpRoute(
name: "GetSampleObject",
routeTemplate: "api/{controller}/{nameContains}"
);
Then add the parameters to the HTTP call:
GET //<service address>/Api/Data/test
or use HttpUtility.ParseQueryString in your method
// uri: /Api/Data/test
public IEnumerable<object> Get()
{
NameValueCollection nvc = HttpUtility.ParseQueryString(Request.RequestUri.Query);
var contains = nvc["nameContains"];
// BL with nameContains here
}

Categories