The web API controller action name can not be determined by the type of the request - asp.net

Doing a REST styled API I would like to know how I deal with duplicate action names:
public SchoolyearBrowseResponse Get(int userId)
{
return _service.GetSchoolyears(userId);
}
public SchoolyearOpenResponse Get(int id)
{
return _service.OpenSchoolyear(id);
}
It is said that the action`s name should be the request type. Now I have TWO Get methods with an int parameter which is not possible in C#.
Should I name the 2nd Get: Open(int id) ?

Doing a REST styled API I would like to know how I deal with duplicate action names
In a RESTful styled API you should never have to deal with such duplicates. In a RESTful styled API you are dealing with resources.
So in your particular case you have 2 resources:
a user
a school year
So you would have the following routes:
/users/{userId}/schoolyears -> which corresponds to your first action
/shoolyears/{id} -> which corresponds to your second action
So:
public class UsersController : ApiController
{
public SchoolyearBrowseResponse GetSchoolYears(int userId)
{
return _service.GetSchoolyears(userId);
}
}
and:
public class SchoolYearsController : ApiController
{
public SchoolyearOpenResponse Get(int id)
{
return _service.OpenSchoolyear(id);
}
}
and the final step is your routes:
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "UserSchoolYears",
routeTemplate: "api/{controller}/{userId}/schoolyears",
defaults: new { controller = "Users", action = "GetSchoolYears" }
);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
As a side note I would like to mention that a school year for a user (your SchoolyearBrowseResponse) class hardly makes sense. Normally for a given user you have a list of school years. And if you wanted to get a specific school year for a user you would use /users/{userId}/schoolyears/{schoolyearid}.

a typical solution would be naming then in the following order :)
_service.OpenSchoolyearByYear(id);
_service.OpenSchoolyearByUserId(id);

Related

can we pass null in the url parameter?

I have a Asp.Net webApi controller as below:
[RoutePrefix("business/api/v1")]
public class BusinessController : ApiController
{
[Route("GetDetails/{id}")]
public HttpResponseMessage Get(string id)
{
// get business details code.
}
}
Is there anyway that client can hit this api with id null??
It depends on your configuration of Web API routes in App_Start/WebApiConfig.cs.
If route is something like:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "business/api/v1/GetDetails/{id}",
defaults: new { id = RouteParameter.Optional }
);
then user can reach resource use http://localhost.business/api/v1/GetDetails or http://localhost.business/api/v1/GetDetails/1.
When you remove defaults: new { id = RouteParameter.Optional } then user have to pass an id otherwise, it will return 404
Please try:
[RoutePrefix("business/api/v1")]
public class BusinessController : ApiController
{
[Route("GetDetails/{id:int?}")]
public HttpResponseMessage Get(int id)
{
// get business details code.
}
}

Routing in ASP .NET Web API

I created a web service using WEB API.
I'm using this routing configuration
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
And my solution include two controller (ProductController and DetailController)
So when I want to call a WS that refers to the GetDetails method(is located inside DetailController) I have to use a URL like this:
http://localhost/api/Details/GetDetails/?id=4
Is there a way for use, for the same request, this URL instead:
http://localhost/api/Product/GetDetails/?id=4
letting the GetDetails method inside the DetailController?
Actually your urls should be:
http://localhost/api/Details/4
http://localhost/api/Products/4
and your controllers:
public class DetailsController: ApiController
{
public HttpResponseMessage Get(int id)
{
...
}
}
and:
public class ProductsController: ApiController
{
public HttpResponseMessage Get(int id)
{
...
}
}
Now that's RESTful.

Abstract Generic ODataController Class Leads To 'No HTTP resource was found'

I am trying to abstract the auto-generated ODataController class in VS 2013 because the code looks identical across different controllers except the name of the POCO, so, I did the following:
public abstract class ODataControllerBase<T,DB> : ODataController
where T : class, IIdentifiable, new()
where DB : DbContext, new()
{
protected DB _DataContext;
public ODataControllerBase() : base()
{
_DataContext = new DB();
}
// only one function shown for brevity
[Queryable]
public SingleResult<T> GetEntity([FromODataUri] int key)
{
return SingleResult.Create(_DataContext.Set<T>().Where(Entity => Entity.Id.Equals(key)));
}
}
IIdentifiable is an interface that forces the T parameter to have a readable/writable Id integer property.
The implementation looks like this (POCOs and DataContexts should've already been created)
public class MyObjectsController : ODataControllerBase<MyObject,MyDbContext>
{
public MyObjectsController() : base()
{
}
// That's it - done because all the repetitive code has been abstracted.
}
Now, my WebApiConfig's Register function contains the following only:
public static void Register(HttpConfiguration config)
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<MyObject>("MyObjects");
config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
}
I run the project, http://localhost:10000/odata/MyObjects and I get the response:
<m:error>
<m:code/>
<m:message xml:lang="en-US">No HTTP resource was found that
matches the request URI `http://localhost:10000/odata/MyObjects.`
</m:message>
<m:innererror>
<m:message>No routing convention was found to select an action
for the OData path with template '~/entityset'.
</m:message>
<m:type/>
<m:stacktrace/>
</m:innererror>
</m:error>
What is missing? What should I remove? Is this something we can't do, i.e. are we really required to inherit ODataController directly with no intermediate parent class?
In one of our projects We also use a generic ODataController base class where we actually use GetEntity for retrieving single entities and GetEntitySet for retrieving a list of entities.
According to your supplied URL and the resulting error message, the ODATA framework cannot find an ODataAction for ~/entityset. As you have given http://localhost:10000/odata/MyObjects as the example, the action in question cannot be public SingleResult<T> GetEntity([FromODataUri] int key) as this only corresponds to a query like this http://localhost:10000/odata/MyObjects(42).
Our code for a generic controller looks like this:
public abstract class OdataControllerBase<T> : ODataController
where T : class, IIdentifiable, new()
{
protected OdataControllerBase(/* ... */)
: base()
{
// ...
}
public virtual IHttpActionResult GetEntity([FromODataUri] long key, ODataQueryOptions<T> queryOptions)
{
// ...
return Ok(default(T));
}
public virtual async Task<IHttpActionResult> GetEntitySet(ODataQueryOptions<T> queryOptions)
{
// ...
return Ok<IEnumerable<T>>(default(List<T>));
}
public virtual IHttpActionResult Put([FromODataUri] long key, T modifiedEntity)
{
// ...
return Updated(default(T));
}
public virtual IHttpActionResult Post(T entityToBeCreated)
{
// ...
return Created(default(T));
}
[AcceptVerbs(HTTP_METHOD_PATCH, HTTP_METHOD_MERGE)]
public virtual IHttpActionResult Patch([FromODataUri] long key, Delta<T> delta)
{
// ...
return Updated(default(T));
}
public virtual IHttpActionResult Delete([FromODataUri] long key)
{
// ...
return Updated(default(T));
}
}
The code for a specific controller then is as short as this:
public partial class KeyNameValuesController : OdataControllerBase<T>
{
public KeyNameValuesController(/* ... */)
: base()
{
// there is nothing to be done here
}
}
However we found out that both Get methods (for single result and enumerable result) actually have to start with Get. First we tried List instead of GetEntitySet and this did not work, as the framework then expects a POST for the List action).
You can actually verify and diagnose the resolving process by supplying a custom IHttpActionSelector as described in Routing and Action Selection in ASP.NET Web API (ahving a look at ASP.NET WEB API 2: HTTP Message Lifecycle might also be worth it).
So actually it is possible to use GetEntity as your method name as you originally tried in your example and there is no need to rename it to simple Get. In addition, there is no need for any modification in your ODATA configuration.
To determine which action to invoke, the framework uses a routing table. The Visual Studio project template for Web API creates a default route:
routes.MapHttpRoute(
name: "API Default",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Routing by Action Name
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 }
);
I configured config as follows:
config.Routes.MapHttpRoute(
name: "GetMessage",
routeTemplate: "api/{controller}/{action}/{quoteName}",
defaults: new { quoteName = RouterParameters.Optional }
);
Access your URI like this:
http://localhost:42201/api/Extract/GetMessage/Q3
OR
http://localhost:42201/api/Extract/GetMessage/?quotename=Q3

ASP.NET Web API Routing with PUT

I'm trying to setup Web API routing for what I thought would be a very simple thing. However, it seems that routing in Web API is not consistent with different HTTP verbs. Suppose I have this controller with these actions...
public class AvalancheController : ApiControllerBase
{
// GET api/avalanche
public IEnumerable<Avalanche> Get() {}
// GET api/avalanche/5
public Avalanche Get(int id) {}
// GET api/avalanche/ActionTest/5
[ActionName("ActionTest")]
public Avalanche GetActionTest(int id) {}
// GET api/avalanche/ActionTest/2
[ActionName("ActionTest2")]
public Avalanche GetActionTest2(int id) {}
// POST api/avalanche
public void Post([FromBody]Avalanche value) {}
// PUT api/avalanche/5
public void Put(int id, [FromBody]Avalanche value) {}
// PUT api/avalanche/test/5
[ActionName("Test")]
public void PutTest(int id, [FromBody]Avalanche value) {}
// DELETE api/avalanche/5
public void Delete(int id) {}
}
and I have the following routes defined...
config.Routes.MapHttpRoute(
name: "ActionRoutes",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new
{
controller = "Avalanche",
action = "(ActionTest|ActionTest2|Test)"
}
);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Then I end up with the following routes being defined...
GET api/Avalanche/ActionTest/{id}
GET api/Avalanche/ActionTest2/{id}
PUT api/Avalanche/Test/{id}
GET api/Avalanche
POST api/Avalanche
DELETE api/Avalanche/{id}
Why doesn't the default PUT route get picked up? What's different between the routing of the default GET and the default PUT? I've tried decorating the functions in every imaginable way but I get the same results.
Mainly I want to know how to get the default PUT route to be picked up. If you have any suggestions on how to modify these routes so that I don't have to have a route for each controller to specify action names that would be fantastic also.
Thanks!
Ian
EDIT: I noticed this morning that the following route is also not being defined..
GET api/Avalanche/{id}
Glad you've found solution for your problem. But I would provide my feedback based on my learning with REST services. Idea for REST webservice, is to resolve each url to a resource (or maybe entity) and depending upon HttpVerb, operation is decided. In this case, you've three GET operations, which works fine with your modification.
But I think controllers can also be re-arranged to have single GET operation and have single responsibility thus better maintainability. For ex:
AvalancheController
public class AvalancheController : ApiControllerBase
{
public IEnumerable<Avalanche> GET()
{
}
public void POST(Avalanche avalanche)
{
}
}
It can be assumed to deal with all avalanche (s) on top level, below are the operations to be defined.
GET : returns all avalanche
POST: inserts new avalanche
PUT: not used
DELETE: not used
AvalancheDetailsController
public class AvalancheDetailsController : ApiControllerBase
{
public Avalanche GET(int id)
{
}
public int PUT(int id)
{
}
public int DELETE(int id)
{
}
}
It can be assumed to deal with single avalanche, below are the operations to be defined.
GET : returns single avalanche
POST: not used
PUT: updates single avalanche
DELETE: deletes single avalanche
Now I assume we have clear distinction of between controllers. In the OP you've mentioned, there can be different GET operations, but it returns only single Avalanche. So, I would change GET method to take object as input and check for values i.e,
public class AvalanceRequest
{
public int? Id {get;set;}
public string Name {get;set;}
}
public class AvalancheDetailsController : ApiControllerBase
{
public Avalanche GET(AvalanceRequest request)
{
//write business logic based on parameters
if(request.Id.HasValue)
//return avalanche;
if(request.Name.IsNullOrEmpty())
//return avalanche
}
//other methods
}
Dealing with URL, I didn't really work with WebAPI but was trying ServiceStack to develop REST services. It allows to attach url's independent of controller names.
Url
api/Avalanche --> AvalancheController (Operations are called based on HttpVerb)
api/Avalanche/Id --> AvalancheDetailsController (Operations are called based on HttpVerb)
I don't know whether url's can be attached likewise in WebAPI, otherwise you end up having default config and call via. api/Avalanche and api/AvalancheDetails/id.
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
I am sorry for long post, hope it makes sense.

Web API Routing - api/{controller}/{action}/{id} "dysfunctions" api/{controller}/{id}

I have the default Route in Global.asax:
RouteTable.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = System.Web.Http.RouteParameter.Optional }
);
I wanted to be able to target a specific function, so I created another route:
RouteTable.Routes.MapHttpRoute(
name: "WithActionApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = System.Web.Http.RouteParameter.Optional }
);
So, in my controller, I have:
public string Get(int id)
{
return "object of id id";
}
[HttpGet]
public IEnumerable<string> ByCategoryId(int id)
{
return new string[] { "byCategory1", "byCategory2" };
}
Calling .../api/records/bycategoryid/5 will give me what I want.
However, calling .../api/records/1 will give me the error
Multiple actions were found that match the request: ...
I understand why that is - the routes just define what URLs are valid, but when it comes to function matching, both Get(int id) and ByCategoryId(int id) match api/{controller}/{id}, which is what confuses the framework.
What do I need to do to get the default API route to work again, and keep the one with {action}? I thought of creating a different controller named RecordByCategoryIdController to match the default API route, for which I would request .../api/recordbycategoryid/5. However, I find that to be a "dirty" (thus unsatisfactory) solution. I've looked for answers on this and no tutorial out there on using a route with {action} even mentions this issue.
The route engine uses the same sequence as you add rules into it. Once it gets the first matched rule, it will stop checking other rules and take this to search for controller and action.
So, you should:
Put your specific rules ahead of your general rules(like default), which means use RouteTable.Routes.MapHttpRoute to map "WithActionApi" first, then "DefaultApi".
Remove the defaults: new { id = System.Web.Http.RouteParameter.Optional } parameter of your "WithActionApi" rule because once id is optional, url like "/api/{part1}/{part2}" will never goes into "DefaultApi".
Add an named action to your "DefaultApi" to tell the route engine which action to enter. Otherwise once you have more than one actions in your controller, the engine won't know which one to use and throws "Multiple actions were found that match the request: ...". Then to make it matches your Get method, use an ActionNameAttribute.
So your route should like this:
// Map this rule first
RouteTable.Routes.MapRoute(
"WithActionApi",
"api/{controller}/{action}/{id}"
);
RouteTable.Routes.MapRoute(
"DefaultApi",
"api/{controller}/{id}",
new { action="DefaultAction", id = System.Web.Http.RouteParameter.Optional }
);
And your controller:
[ActionName("DefaultAction")] //Map Action and you can name your method with any text
public string Get(int id)
{
return "object of id id";
}
[HttpGet]
public IEnumerable<string> ByCategoryId(int id)
{
return new string[] { "byCategory1", "byCategory2" };
}
You can solve your problem with help of Attribute routing
Controller
[Route("api/category/{categoryId}")]
public IEnumerable<Order> GetCategoryId(int categoryId) { ... }
URI in jquery
api/category/1
Route Configuration
using System.Web.Http;
namespace WebApplication
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API routes
config.MapHttpAttributeRoutes();
// Other Web API configuration not shown.
}
}
}
and your default routing is working as default convention-based routing
Controller
public string Get(int id)
{
return "object of id id";
}
URI in Jquery
/api/records/1
Route Configuration
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Attribute routing.
config.MapHttpAttributeRoutes();
// Convention-based routing.
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
Review article for more information Attribute routing and onvention-based routing here & this
Try this.
public class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
var json = config.Formatters.JsonFormatter;
json.SupportedMediaTypes.Add(new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
config.Formatters.Remove(config.Formatters.XmlFormatter);
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional , Action =RouteParameter.Optional }
);
}
}
The possible reason can also be that you have not inherited Controller from ApiController.
Happened with me took a while to understand the same.
To differentiate the routes, try adding a constraint that id must be numeric:
RouteTable.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
constraints: new { id = #"\d+" }, // Only matches if "id" is one or more digits.
defaults: new { id = System.Web.Http.RouteParameter.Optional }
);

Resources