3

I have a pretty standard WebApi that does some basic CRUD operations.

I'm trying to add some different kind of lookups but am not quite sure how it's suppose to be done.

Here's my FoldersController currently

public class FoldersController : ApiBaseController
{
    //using ninject to pass the unit of work in
    public FoldersController(IApiUnitOfWork uow)
    {
        Uow = uow;
    }

    // GET api/folders
    [HttpGet]
    public IEnumerable<Folder> Get()
    {
        return Uow.Folders.GetAll();
    }

    // GET api/folders/5
    public Folder Get(int id)
    {
        return Uow.Folders.GetById(id);
    }

    // POST api/folders
    public HttpResponseMessage Post(Folder folder)
    {
        Uow.Folders.Add(folder);
        Uow.Commit();

        var response = Request.CreateResponse(HttpStatusCode.Created, folder);

        // Compose location header that tells how to get this Folder
        response.Headers.Location = new Uri(Url.Link(WebApiConfig.DefaultRoute, new { id = folder.Id }));

        return response;
    }

    // PUT api/folders
    public HttpResponseMessage Put(Folder folder)
    {
        Uow.Folders.Update(folder);
        Uow.Commit();
        return new HttpResponseMessage(HttpStatusCode.NoContent);
    }

    // DELETE api/folders/5
    public HttpResponseMessage Delete(int id)
    {
        Uow.Folders.Delete(id);
        Uow.Commit();

        return new HttpResponseMessage(HttpStatusCode.NoContent);
    }
}

what I would like to do is add a method that looks something like this

public IEnumerable<Folder> GetChildFolders(int folderID)
{
     return Uow.Folders.GetChildren(folderID);
}

Since I already have the standard Get method in there, i'm not quite sure how to do so.

I initially thought I could just add a new route..something like

routes.MapHttpRoute(
        name: "ActionAndIdRoute",
        routeTemplate: "api/{controller}/{action}/{id}",
        defaults: null,
        constraints: new { id = @"^/d+$" } //only numbers for id
        );

And the just add something like an ActionName annotation to my method [ActionName("GetChildren")]

but that didn't fly.

Am I on the right track? How do I do something like this without adding another controller?

Kyle Gobel
  • 5,530
  • 9
  • 45
  • 68
  • Just google a little bit, check this SO answer: http://stackoverflow.com/questions/10121152/api-controller-declaring-more-than-one-get-statement and following article: [Web API: Mixing Traditional & Verb-Based](http://blog.appliedis.com/2013/03/25/web-api-mixing-traditional-verb-based-routing/). This should answer all your questions. – tpeczek May 17 '13 at 08:11

5 Answers5

11

You may not like this answer, but I feel it's the right one. WebAPI was designed to only have 5 calls, GET (one item / list items), POST, PUT and DELETE per entity type. This allows for REST URLs, such as Folders/Get/5, Folders/Get etc.

Now, in your scenario, you're wanting ChildFolders, which I can understand aren't different objects, but they are different entities in terms of REST (ChildFolders/Get) etc. I feel this should be another WebAPI controller.

There's ways of patching up the Http Routes to manage for this, but I don't feel it's how Web API was designed to work and it enforces you to follow REST data-by-entity-type protocols... otherwise why not just use .NET MVC Controllers for your AJAX calls?

Chris Dixon
  • 9,147
  • 5
  • 36
  • 68
  • Very nice answer Chris. However I'd like to expose an API where it serves different sets of data. Let's say for a Dashboard, my DashboardController should have different get methods where it will serve data for different widgets in the dashboard. My client side code will call these different get methods using AJAX. Does it make sense to use web API here? – Geethanga Sep 19 '15 at 06:59
  • I'd say that makes very good sense to use WebAPI, yup! However, have you checked out MVC 6 yet? Controllers and WebAPI are one of the same, so there's no difference between the two any more. – Chris Dixon Sep 19 '15 at 14:24
  • I will use MVC Controllers to create the REST API instead of using a WebAPI project. – Juan Zamora May 10 '17 at 04:28
3

The idea behind WebAPI is to follow REST patterns, as Chris said. With that in mind, it's up to you to decide how your domain maps to that pattern. If child-folders are folders and use the same internal logic then maybe it makes perfect sense to put that Get in your FoldersController. If not, or if you want to perform all the REST methods on child folders, it may make more sense to create a ChildFoldersController.

Now that your app is organized sensibly, you can think about routing. WebAPI now supports attribute routing. If you add this line to your WebApiConfig.Register -- config.MapHttpAttributeRoutes(); -- like so:

config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "{controller}/{id}",
    defaults: new { id = RouteParameter.Optional },
);

You can then put your route on the action itself:

[HttpGet]
[Route("folders/{folderID}/children")] // <-- notice the route here
public IEnumerable<Folder> GetChildFolders(int folderID)
{
     return Uow.Folders.GetChildren(folderID);
}

Now all your REST calls to the path "folders/{folderID}" will work using the default route, while any gets on that route that include "/children" will hit that action. It also makes the call's behavior very clear to the caller of your API.

You can also still do this with the normal routing pattern:

// default route
config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

// folder children route
config.Routes.MapHttpRoute(
    name: "FolderChildrenApi",
    routeTemplate: "api/folders/{folderID}/children",
    defaults: new { controller = "Folders", action = "GetChildFolders" }
);

You could also leave controllers as a variable in that custom route, if you have other resources that have children, and you can still put that action in a separate controller that also handles calls to the route "childFolders/{folderID}".

Routing is very flexible. Just make sure to design it so that it makes sense at a glance for both the api callers and the people maintaining your software (including you).

Here's some attribute routing info: Attribute Routing in WebAPI 2

Madeline Trotter
  • 370
  • 4
  • 14
0

One way is to write a new route specific to the GetChildFolders action.

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        config.Routes.MapHttpRoute(
            name: "DefaultApi-GetChildFolders",
            routeTemplate: "api/{controller}/GetChildFolders/{id}",
            defaults: new { action = "GetChildFolders" }
        );
Maggie Ying
  • 10,095
  • 2
  • 33
  • 36
  • This still doesn't work because I have 2 Get methods with the same parameters. The GetChildFolders will work when I send it to that action, but then when i try to hit api/folders/5, It's confused as what GetMethod to use. Not sure what do here – Kyle Gobel May 17 '13 at 01:00
0

Actions are a great way to have two Get methods inside a WebApi controller.
Here is what I do in order to have different actions, and also an optional extra ID parameter for some of the actions:

Inside WebApiConfig.cs I have the following:

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}/{action}/{actionId}",
            defaults: new
            {
                id = RouteParameter.Optional,
                actionId = RouteParameter.Optional,
                action = "DefaultAction"
            }
       );

and in the controller:

    [ActionName("DefaultAction")]
    public AdGetResponse Get(int id)
    {
        ...
    }

    [ActionName("AnotherAction")]
    public HttpResponseMessage GetAnotherAction(int id, int actionId)
    {
    }
Liel
  • 2,407
  • 4
  • 20
  • 39
0

Make your routes as follows:

config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional },
    constraints: new { id = @"\d*" }
);

config.Routes.MapHttpRoute(
    name: "DefaultApiPlusActionAndFolderid",
    routeTemplate: "api/{controller}/{action}/{folderID}",
    defaults: null,
    constraints: new { action = @"[a-zA-Z]+", folderID = @"\d+" }
);

You can then have methods such as

[ActionName("GetChildren")]
public IEnumerable<Folder> GetChildFolders(int folderID)
{
     return Uow.Folders.GetChildren(folderID);
}

Which you can call with /api/folders/getchildren/123. Just make sure that the parameter value in the method is folderID rather than id.

Jon Susiak
  • 4,948
  • 1
  • 30
  • 38