ASP.NET Web API get child list (hierarchical resources) - c#

I have the following rest schema that I'd like to implement using the ASP.NET Web Api:
http://mydomain/api/students
http://mydomain/api/students/s123
http://mydomain/api/students/s123/classes
http://mydomain/api/students/s123/classes/c456
I've got the first two links working properly using the ApiController and the following two methods:
public class StudentsController : ApiController {
// GET api/students
public IEnumerable<Student> GetStudents() {
}
// GET api/students/5
public IEnumerable<Student> GetStudent(string id) {
}
}
In this same controller, (or do I need a different controller called ClassesController?), how would I implement the last two links? Also, what would the routing for the 'classes' part look like (if necessary)?
Here's my WebApiConfig (which I'd like to keep as dynamic, rather than hard-coding the route to the /classes if possible:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
// EDIT - I'm getting 404's when trying to use this
context.Routes.MapHttpRoute(
name: "JobsApi",
routeTemplate: this.AreaName + "/Students/{id}/Classes/{classId}",
defaults: new { classId = RouteParameter.Optional }
);
EDIT
Here's my newly created ClassesController:
public class ClassesController : ApiController {
// GET api/classes
public IEnumerable<TheClass> Get(string id) {
return null;
}
}
I'm getting 404 Errors when attempting to go to this URL:
http://mydomain/api/students/s123/classes

Routing in ASP.NET can express these more complex rules but needed to be explicitly set up. For example in this case you would have to define 2 routes:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/students/{studentId}/{controller}/{classId}",
defaults: new { classId = RouteParameter.Optional }
);
And you would have a controller for it:
public class ClassesController
{
public TheClass Get(int studentId, int classId)
{
....
}
}
This is perhaps not ideal but the main option.
I was working on a hierarchical routing which was not possible due to an implementation issue in Web API but this issue has been fixed now so I might start working on it again.

With this nice hierarchical approach, you have more concerns that routing internally. There is a good sample application which adopts the hierarchical resource structure: PingYourPackage. Check that out.
Note: I have a blog post about this issue which explains the below concerns and gives solutions to those with a few code samples. You can
check it out for more details:
Hierarchical Resource Structure in ASP.NET Web API
Let me explain the concerns here briefly by setting up a sample scenario. This may not be the desired approach for these type of situations but lays out the concerns very well. Let's say you have the below two affiliates inside your database for a shipment company:
Affiliate1 (Id: 100)
Affiliate2 (Id: 101)
And then assume that these affiliates has some shipments attached to them:
Affiliate1 (Key: 100)
Shipment1 (Key: 100)
Shipment2 (Key: 102)
Shipment4 (Key: 104)
Affiliate2 (Key: 101)
Shipment3 (Key: 103)
Shipment5 (Key: 105)
Finally, we want to have the following resource structure:
GET api/affiliates/{key}/shipments
GET api/affiliates/{key}/shipments/{shipmentKey}
POST api/affiliates/{key}/shipments
PUT api/affiliates/{key}/shipments/{shipmentKey}
DELETE api/affiliates/{key}/shipments/{shipmentKey}
Routing Concerns
#Ali already explained it but I've a different approach here. Assume that we are sending a GET request against /api/affiliates/105/shipments/102. Notice that the affiliate key is 105 here which doesn't exist. So, we would want to terminate the request here ASAP. We can achieve this with a per-route message handler.
Authorization Concerns
If you have some type of authentication in place, you would want to make sure (in our scenario here) that the authenticated user and the requested affiliate resource is related. For example, assume that Affiliate1 is authenticated under the Affiliate role and you have the AuthorizeAttribute registered to check the "Affiliate" role authorization. In this case, you will fail miserably because this means that Affiliate1 can get to the following resource: /api/affiliates/101/shipments which belongs to Affiliate2. We can eliminate this problem with a custom AuthorizeAttribute.
Ownership Concerns
Now, the following URI should get me the correct data:
GET /api/affiliates/100/shipments/102
However, what would happen for the below URI:
GET /api/affiliates/100/shipments/103
This should get you "404 Not Found" HTTP response because affiliate whose Id is 100 doesn't own the shipment whose id is 103.

Related

API URL requires "/api/api/" after being published

I've got an API app up and running on my localhost. It works great at en endpoint such as:
http://localhost:26307/api/ModelName/12345
However, after deploying to the Test server, the same url needs to be:
http://v10test.mydomain.com/api/api/ModelName/12345
I'm trying to get rid of the double /api/.
I adjusted my routes FROM THIS (for example):
config.Routes.MapHttpRoute(
name: "route2",
routeTemplate: "api/{controller}/{organizationSys}"
);
TO THIS.....
config.Routes.MapHttpRoute(
name: "route2",
routeTemplate: "{controller}/{organizationSys}"
);
NOTE: I REMOVED "api/" FROM THE ROUTES.
But that seemed to have no effect. In IIS, the api's are set up as an Application within the domain:
I'm probbaly overlooking something obvious, but I'm just not sure what. How do I get rid of that double /api/?
There are several ways to specify routes to a controllers actions, and the order of precedence matters.
Firstly, it can be done from a general configuration, e.g. as you've done it...
config.Routes.MapHttpRoute(
name: "route2",
routeTemplate: "{controller}/{organizationSys}"
);
However this can be overridden by specifying a Route attribute on the controller or by specifying the route in the verb attribute. For example in the code below...
[Route("api/[controller]/[action]")]
public class TestController : Controller
{
[HttpGet]
[Route("/api")] // url... /api
[Route("/api/test")] // url... /api/test
[Route("testalso")] // url... /api/test/get/testalso
public string Get()
{
return "Alive";
}
[HttpGet("/api/echo/{id}")] // url... /api/echo/{id}
public string Echo(string id)
{
return $"Get Echo: {id}";
}
[HttpPost("{id}")] // url... /api/test/postit/{id}
public string PostIt(string id)
{
return $"Thanks for {id}";
}
}
The declaration on the controller specifies a default for all methods within the controller and any methods that specify attributes can either override the controller by starting the route with a '/' or append to the controller's route. See the comments next to each attribute above for examples.
Beyond that the routes will be relative to the base application hosted within iis in your case which starts at...
http://v10test.mydomain.com/api/
Hope that's enough information for you.
It is because of the way it is deployed in IIS. For IIS your application is at address
http://v10test.mydomain.com/api/
And after that comes all the controllers and etc. If you deploy it to (for example)
http://v10test.mydomain.com/myapp/
the calls will work like
http://v10test.mydomain.com/myapp/api/ModelName/12345
EDIT
Also - check the routes of your controllers or methods inside the controllers. This is the other place where the route may be modified/extended.

WebAPI ActionName routing half working

I have been building a WebAPI, trying to route to the right methods with ActionName. It works with one of my methods I try to call, but the other one gets a 404 error.
My WebAPI Config file:
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
My WebAPI Controller methods are formatted as such:
This first one is the working one:
[ActionName("postdb")]
public IEnumerable<string[]> postDB(string id)
{ ...
This second one does not:
[ActionName("getquery")]
public IEnumerable<string[]> getQuery(string tables)
{ ...
I'm calling both of them the same way from angular (Temp is a string that is being passed as the argument):
$http.post('api/Test/postdb/' + temp).then(function (response) { ...
and
$http.get('api/Test/getquery/' + temp).then(function (response) { ...
I have tried changing names of both actions, the first one works no matter the name, the second one doesn't work no matter the name. I have also tried reordering them, changing between GET and POST, and changing arguments.
Any suggestions?
Not sure why you are using ActionName to setup routing?
You should probably be looking at Route attribute. eg.
[HttpPost]
[Route("postdb")]
// Action doesn't have to be called 'postdb'
public IEnumerable<string[]> postDB(string id)
ActionName is usually used for a different purpose (Purpose of ActionName)
Nevertheless, I think something odd is going on in your example - I'd think setting ActionName shouldn't have affected routing there. To debug I'd suggest to set up Failed Request Tracing to see at which point the request fails to reach the action.
These are the basic rules for Action selection in WebAPI (http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-and-action-selection)
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.
So, in your example postdb method may map to the POST method. But may be because it's in lower case ASP.NET didn't like that and applied Rule 3 - try with ActionName("PostDB") and [ActionName("GetQuery")] if you really want to use ActionName (for whatever reason) instead of Route.
The name of the parameter tables in the second action
[ActionName("getquery")]
public IEnumerable<string[]> getQuery(string tables)
{ ...
does not match the name of the parameter id in the route:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }

Extend MVC Api Controller Name

Right now I have two controllers, one for MVC another one for API.
// API
public class UsersController : ApiController
{
// GET: api/Users <- works great
public IQueryable<User> GetUsers() {}
// **Not sure if this one is correct!**
public IHttpActionResult GetUsersChart([FromURI]int id)
{
return Ok(repo.GetUsersChart(id));
}
}
I am trying to understand on how to call my Charts generation api within C# code with HttpWebRequest?
Something like this:
HttpWebRequest createChartRequest =
WebRequest.Create(String.Format("{0}/api/Users/Chart/?id={1}", ServerUrl, 1));
would give me HTTP 500 error.
Update:
Here is routing configuration:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
A response code of 500 means that you have some error in your API action. It has nothing to do with your route or how you're requesting it. If this is in development, inspect the response in your browser's developer tools and you should be able to see a description of the actual server error there. If this is in production, you'll need to log the error somehow such as with ELMAH so you can see what's causing the issue.

ASP.NET Web Api 2 - Attribute Based Routing - How do I force a parameter to query string only?

I am working on a Web Api 2 project and I am using attribute based routing. Here is a sample route:
[Route("{id:int}", Name = "GetEmployeeById")]
[HttpGet]
public IHttpActionResult GetEmployee(int id)
{
...
}
This works with the following URLs:
://host/employee/12345
://host/employee?id=12345
What I would prefer is that the first form (the parameter in the URI), would not be allowed, and only the second form (query string) would work.
What I've Tried
Mostly, I've tried searching the web for a way to make this work, and I'm not finding much.
This page talks about route constraints but this syntax doesn't seem to work (anymore?).
This page doesn't actually prevent the URI form from working.
There is an attribute called "[FromUri]" that you can use to decorate a method parameter, and the model binder will try to look for that parameter from the Querystring, it may not help you with this scenario but it is good to know about it, so in case you want to pass a search options for example to a Get method.
http://msdn.microsoft.com/en-us/library/system.web.http.fromuriattribute(v=vs.118).aspx
http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api
Hope that helps.
Couple of ways to achieve this. Here are some options
Rename parameter to something else than id (eg. employeeId).
Change the default routing configuration in WebApiConfig:
//Default configuration, you can see here the "id" parameter which enables action/id matching
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
//It should look like this
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}"
);
Also you can do it with custom attributes.
Actually, I was wrong about my original code. The query string parameter did not work with the route I specified. Instead, I could do this:
[Route("", Name = "GetEmployeeById")]
[HttpGet]
public IHttpActionResult GetEmployee(int id)
{
...
}
And this will do what I want. It must be getting the name id from the function's parameter list.
Unfortunately, this means I can't put a constraint on it anymore, but I guess I can just validate within the function.

Prevent ASP.NET Web API route engine from deducting custom method names

I have the Web API controller with 2 methods - let's say the first method returns the plain project list and the second one returns all projects assigned to the specific user.
public class ProjectController: ApiController
{
public IQueryable<Project> Get() { ... }
[HttpGet]
public IQueryable<Project> ForUser(int userId) { ... }
}
The method implementation is not important in this case.
Web API route config is also adjusted to support the custom method names.
config.Routes.MapHttpRoute(
"DefaultApi",
"api/v1/{controller}/{id}",
new { id = RouteParameter.Optional }
);
config.Routes.MapHttpRoute(
"DefaultApiWithAction",
"api/v1/{controller}/{action}");
It works fine, I can access both /api/v1/projects/ and /api/v1/projects/forUser/ endpoints, but seems that the route engine is too smart, so it decides that /api/v1/projects?userId=1 request may match the ForUser(..) method (due to the userId argument name, I guess) and ignores the {action} part of the route.
Is there any way to avoid this behavior and require the action part to be explicitly specified in the URL?
Couple things. First of all this route:
config.Routes.MapHttpRoute(
"DefaultApiWithAction",
"api/v1/{controller}/{action}",
new { id = RouteParameter.Optional });
Does not have "action" as an optional parameter. You have included id as optional (I assume as a typo), but as it does not exist in the route, you will not get a match with only one supplementary segment. Only URLs containing two parts, a controller and an action, will pass through this route. This url:
/api/v1/projects?userId=1
...contains a single segment and will not. This route, and any other which lacks a second component will default to this route:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/v1/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
...which only takes a controller and an optional ID. You need to either reformat the given URL to take an action parameter, or rewrite your route to make the action optional and set the default as you desire. This will all depend on your application architecture, but always err on the side of simplicity. Routes can get very complicated--simpler is generally better.
As for required/optional route components, keep in mind the following two things:
All route segments are required unless they are set as optional in the anonymous object.
Segments can also be excluded if they have a default value, set by providing one in the anonymous object in the form of placeholder = value.
I don't understand your problem completely.
Shouldn't /api/v1/projects?userId=1 indeed call the ForUser action?
Anyway, to make the action required, make your HttpRoute like this:
name: "DefaultApi",
routeTemplate: "api/v1/{controller}/{action}/{id}",
defaults: new { id = System.Web.Http.RouteParameter.Optional });
Now you can call like this:
/api/v1/projects/ForUser/2
I've finally come up with the solution that satisfies my requirements. I've combined this answer and ideas suggested by levib and user1797792 into the following config:
config.Routes.MapHttpRoute(
"DefaultApiWithActionAndOptionalId",
"api/v1/{controller}/{action}/{id}",
new {id = RouteParameter.Optional});
config.Routes.MapHttpRoute(
"DefaultApiGet",
"api/v1/{controller}",
new { action = "Get" },
new { httpMethod = new HttpMethodConstraint(HttpMethod.Get) });
Note that the config order matters a lot here.
First of all, the /api/v1/projects request with any query string (even with arguments whose names match the other action's parameters) is dispatched to the Get() method via the second route. This is important because in the real project I've got a custom action filter attached to this action that filters the returned IQueryable based on the provided request arguments.
api/v1/projects/forUser/1-like requests are dispatched to ForUser(int id) method by the first route. Renaming userId parameter into id allowed to construct cleaner URLs.
Obviously, this approach has some limitations, but it is all I need in my specific case.

Categories