Multiple PUT methods in ASP.NET Web API

11,465

Solution 1

What you have now
To utilize your methods as above you'll need to go RPC. That is because your example is already half way steeped in the RPC style of doing things. Default WebAPI routes encourage RESTful setups, but if you made a minor alteration to your routes everything would start working. For example you could change your default route to something like a typical MVC route:

routes.MapRoute( name    : "Default",       
                 url     : "{controller}/{action}/{id}",
                 defaults: new { controller = "Home", 
                                 action     = "Index", 
                                 id         = UrlParameter.Optional });

After adding the route, call things in typical MVC fashion where you use the controller name & action. From your question, however, I suspect you actually want to be RESTful, instead of just getting it to work so read on...

Being RESTful
REST doesn't require HTTP, although the two are often discussed together. REST is really about every resource having a semantically accurate representation. When using HTTP that means unique URI's that respect HTTP semantics. So for example, a call using HTTP GET should never modify data because that violates HTTP's definition of GET and confused HTTP infrastructure like caches.

POST/PUT vs MERGE/PATCH
We're all familiar with GET, POST, PUT, HEAD, etc.. as HTTP methods. Generally, GET is for retrieving, POST is for adding, and PUT is for modifying (although subject to lots of debate). Yet, you have two types of modifications: adding items and removing items from a collection. So are those both PUT or something else? The community hasn't quite settled on how to do this.

  • Option 1: Custom media type - The HTTP spec really allows for all sorts methods, it's the browsers that really restrict us to the familiar subset. So you can create MERGE (a Roy Fielding work around) or PATCH (an oData work around) methods and define the behavior for this new media type -- maybe one for adding and one for removing.

  • Option 2: Use POST/PUT - Use PUT for both adding and removing contacts. Just treat the list of ID's like a toggle (if exists remove, if missing add) or alternatley include enough information to know what to do. Then return an HTTP 303 indicating to the client it has a stale state and refresh.

  • Option 3: Full List - If your set is a reasonable size, you can always pass a complete list of contacts every time you want to update. This way the logic is a super simple wipe and replace.

What really matters from a RESTful perspective is that your application behaves in a consistent way across all methods. So if MERGE means add, it should always mean add. If you expect a complete set of ID's passed to PUT then always pass a complete set.

Controller Design
If it were me, I would break your controller into multiple controllers. One controller deals with Groups another deals Contacts (as a group) and a third deals with one contact within a group. Something like ...

//api/Group/
public List<GroupModel> Get()
public GroupModel Get(int ID)
public GroupModel Post(GroupModel model)  //add a group
public GroupModel Put(GroupModel model)   //update a group (see comments above)
public void Delete(int ID)


//api/GroupContacts/
public ContactsModel Get()                    //gets complete list
public void PostContacts(ContactsModel model) //pushes a COMPLETE new state
public void Delete()                          //delete entire group of contacts


//api/GroupContact/354/
public ContactModel Get(int id)             //get contact id #354
public void PostContact(ContactModel model) //add contact (overwrite if exits)
public void Delete(int id)                  //delete contact if exists

If you want your URL's to appear nested (eg: /api/Group/Contacts, /api/Group/Contact), you can look at this other post I wrote. IMHO, asp.net's routing needs a tune up to support nesting a bit easier ...but that's a different issue;-)

Solution 2

To echo what EBarr said, doing hierarchical routing in Web API can be a bit of a pain. I use it a lot in my services so I built a replacement routing service for Web API. The Nuget is here and the source is on GitHub

This approach to routing requires you to build up your URI namespace as a hierarchy of path segments. Rather than matching complete URI patterns to controllers, you attach controllers to arbitrary points in the tree of path segments.

Just to give you an idea of what it would look like I created a small self-host sample with URIs similar to what you are trying to do:

internal class Program
{
   private static void Main(string[] args)
   {
    var baseAddress = new Uri("http://oak:8700/");

    var configuration = new HttpSelfHostConfiguration(baseAddress);
    var router = new ApiRouter("api", baseAddress);

    // /api/Contacts
    router.Add("Contacts", rcs => rcs.To<ContactsController>());

    // /api/Contact/{contactid}
    router.Add("Contact", rc =>
                          rc.Add("{contactid}", rci => rci.To<ContactController>()));

    // /api/Group/{groupid}
    // /api/Group/{groupid}/Contacts
    router.Add("Group", rg =>
                        rg.Add("{groupid}", rgi => rgi.To<GroupController>() 
                                                       .Add("Contacts", rgc => rgc.To<GroupContactsController>())));


    configuration.MessageHandlers.Add(router);

    var host = new HttpSelfHostServer(configuration);
    host.OpenAsync().Wait();

    Console.WriteLine("Host open.  Hit enter to exit...");

    Console.Read();

    host.CloseAsync().Wait();
  }
}

public class GroupController : TestController { }
public class ContactsController : TestController { }
public class ContactController : TestController { }
public class GroupContactsController : TestController { }


public class TestController : ApiController
{
    public HttpResponseMessage Get()
    {
        var pathRouteData = (PathRouteData) Request.GetRouteData();

        var paramvalues = new StringBuilder();

        foreach (KeyValuePair<string, object> keyValuePair in pathRouteData.Values)
        {
            paramvalues.Append(keyValuePair.Key);
            paramvalues.Append(" = ");
            paramvalues.Append(keyValuePair.Value);
            paramvalues.Append(Environment.NewLine);
        }

        var url = pathRouteData.RootRouter.GetUrlForController(this.GetType());

        return new HttpResponseMessage()
                   {
                       Content = new StringContent("Response from " + this.GetType().Name + Environment.NewLine
                                                   + "Url: " + url.AbsoluteUri
                                                   + "Parameters: " + Environment.NewLine
                                                   + paramvalues.ToString())
                   };
    }
}

You should be able to just paste this code into a console app and add a reference to the Microsoft.AspNet.WebApi.SelfHost and Tavis.WebApiRouter nugets and try it out. If you are curious as to how far you can go with this kind of routing, there is a more complex sample here.

Share:
11,465
jcvandan
Author by

jcvandan

Developer. Technical Director / Co-Founder of Withoomph.com.

Updated on June 06, 2022

Comments

  • jcvandan
    jcvandan almost 2 years

    I have a controller Groups with the following actions:

    public GroupModel Get(int ID)
    
    public GroupModel Post(CreateGroupModel model)
    
    public void Put(PublicUpdateGroupModel model)
    
    public void PutAddContacts(UpdateContactsModel model)
    
    public void PutRemoveContacts(UpdateContactsModel model)
    
    public void Delete(int ID)
    

    And what I would like to do is use standard REST routing to call the standard get, post, put, delete mehods. But call the PutAddContacts and PutRemoveContacts if the action names are appended to the url, for example:

    GET groups/ - calls Get method

    POST groups/ - calls Post method

    PUT groups/ - calls Put method

    DELETE groups/ - calls Delete method

    PUT groups/addcontacts - calls PutAddContacts method

    PUT groups/removecontacts - calls PutRemoveContacts method

    Is it possible to set up routing to do this or do I need to go down the RPC route for routing if I want to use action names in my URL's?