ServiceStack 4 Cookbook
上QQ阅读APP看书,第一时间看更新

Splitting HTTP request methods

When you're building a REST service, it's good practice to make efficient use of the HTTP protocol to assist you in your design. While full coverage of what makes for good RESTful design is outside the scope of this book, we will cover how to use ServiceStack's built-in tools to help you reuse code.

Note

I recommend REST in Practice, O'Reilly, and APIs: A Strategy Guide, O'Reilly, for more on designing effective RESTful web services.

One project I am acquainted with started out with an RPC-style design. The service owner realized that they had well over 65 endpoints, such as /createNewGroup, /renameGroup, and so on, making it hard for developers to remember the exact name of the endpoint for the function they need. This was overhauled, and the RESTful approach was applied. The end result had only four endpoints and still served the same user base with the same functionality. The end result was a much simpler service that was easier to learn, deploy, and debug—but the process created a lot of churn for the teams that consumed that service to get to that end goal.

Basically, this redesign consisted of replacing endpoints that facilitated only small operations to a more RESTful design based on resources. The R in REST stands for Resource. The idea is to design an API that provides different resources and then allows consuming teams to perform different operations on that resource. This cleans up our API considerably—instead of /getMessagesFromGroup, /createNewGroup, and /renameGroup, you would just have /groups. Our service can infer from the type of request being sent to the resource what the client is requesting. This has many benefits—less code, greater code reuse, and importantly, an easy-to-use API for clients.

Getting ready

Let's take the ReidsonMessenger service that we built up in the Routing using data transfer object attributes recipe in Chapter 1, Configuration and Routing. It currently expects to find HTTP GET requests that contain a Group object from the ServiceModel project. Currently, this specific endpoint returns all the messages from the specified group. What if you also wanted to allow a New Group functionality? We could create a /newGroup endpoint that accepts HTTP POST messages with a new ServiceModel type, but then we're starting to have a proliferation of endpoints and message types. It's better to design our services to avoid this proliferation.

How to do it…

We'll start out with the ReidsonMessenger service where we left off, but we'll make some changes to facilitate the new group creation functionality that we need. Since we're refactoring some of our existing code, let's start with a test, as follows:

[Test]
public void ShouldCreateNewGroupsOnRequest()
{
   var service = new MessengerService();
   var response = service.Post(
   new Group
   {
      GroupName = "NewGroup",
      Creator = "Me"
   });
   Assert.That(response.Name.Equals("NewGroup"));
}

We're specifying that we'd like the service to expect a Group message arriving as an HTTP POST. It will contain a new Creator property that's a string containing the user who wants to create the group. At the moment, if we try to run this test, it won't even compile—we need to create the Creator property, and we need to handle the Post method on MessengerService. Let's do those two things next.

Changing the Group model is a simple refactor; we should end up with code that looks like the following:

[Route("/groups/{GroupName}")]
public class Group
{
  public string GroupName { get; set; }
  public string Creator { get; set; }
}

Next up, we'll add a new Post method to MessengerService that accepts Group objects. We can implement this fairly simply for now by just placing a message in our _messages list with GroupName set to that of the request and Sender set to the string specified in the Creator property.

Let's take a look at both of the methods that deal with Group requests in MessengerService after providing this implementation:

public object Get(Group request)
{
  return new GroupResponse {
      Messages = _messages.Where(message => message.GroupName.Equals(request.GroupName))
      .ToList()
  };
}
public object Post(Group request)
{
  _messages.Add(new Message
  {
    Sender = request.Creator,
    GroupName = request.GroupName,
    Body = request.Creator + " created " + request.GroupName + " group."
  });
  return new GroupResponse
  {
    Name = request.GroupName,
    Messages = _messages.Where(message => message.GroupName.Equals(request.GroupName))
    .ToList()  
    };
  }
}

Our tests should pass now, and our new functionality is ready. However, if we check the metadata that ServiceStack generates for this new functionality, we'll see the following for the Group message type:

How to do it…

What's happened is that the service will now accept the GET or POST messages at /groups/{GroupName}—an unintended side effect. We wanted GET /groups/bestfriends to get all the messages on the bestfriends group, but we didn't want to enable posting to /groups/newGroupName necessarily. This is easy to fix—we simply need to fix the [Route] annotation on the Group object so that it only works on GET methods and add another route to handle both, like this:

[Route("/groups/")]
[Route("/groups/{GroupName}","GET")]
public class Group
{
  public string GroupName { get; set; }
  public string Creator { get; set; }
}

Now, if we check our service metadata, we'll see what we were expecting—the /group endpoint accepts all HTTP verbs, but /groups/{GroupName} only accepts HTTP GET, which is depicted in the following screenshot:

How to do it…

There's more...

We could easily make use of this pattern to create new service methods that would allow the deletion of a group by calling the /groups endpoint with an HTTP DELETE call.