Kyle Gobel
Kyle Gobel

Reputation: 5750

WebApi adding another Get Method

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?

Upvotes: 3

Views: 16565

Answers (5)

Madeline Trotter
Madeline Trotter

Reputation: 380

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

Upvotes: 3

Chris Dixon
Chris Dixon

Reputation: 9167

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?

Upvotes: 11

Jon Susiak
Jon Susiak

Reputation: 4978

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.

Upvotes: 0

Liel
Liel

Reputation: 2437

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)
    {
    }

Upvotes: 0

Maggie Ying
Maggie Ying

Reputation: 10175

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" }
        );

Upvotes: 0

Related Questions