Whenever I write code using ASP.NET Web API, I invariably make my controllers “humble.” I use this term in the same sense as described in the Humble Object pattern. Basically, a controller should be a thin coordinator between the client’s request and the real business logic. (I suppose the business logic would be “arrogant” in this humble paradigm?)
This approach actually isn’t specific to ASP.NET; I have always done the same type of thing when writing WCF services. I like such service logic, regardless of technology, to consist of three simple parts, ideally placed on a single line of code:
- Extract/translate input(s) from request.
- Invoke domain object.
- Translate result(s) to response.
If this sounds similar to Ports-Adapters-Simulators style, that is no surprise. I am essentially treating the service/controller class as a “humble adapter.” Its main job is to translate client data into a form that my domain objects can understand and vice versa.
Here is an example of the type of humble controller I am talking about, using a simple file server example:
class FilesController : ApiController { private readonly DataFolder folder; public FilesController(DataFolder folder) { this.folder = folder; } public Task<string> PostAsync() { return this.folder.WriteAsync(s => this.Request.Content.CopyToAsync(s)); } public HttpResponseMessage Get(string id) { HttpResponseMessage message = new HttpResponseMessage(); message.Content = new StreamContent(this.folder.Open(id)); message.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); return message; } }
All the real work is delegated to the DataFolder
domain class, which handles the file/stream management logic. The only code left is that which must deal with the HTTP requests and responses, where it is thoroughly decoupled from the rest of our domain. For this pattern to work best, it is important that the domain class be optimized for the expected call pattern from its consumer. Rather than splitting the logic between multiple method calls, we should strive to create a single “one-liner” for the client to invoke. This explains the perhaps unusual choice of DataFolder.WriteAsync
accepting a lambda; if we had instead returned a write stream, it would have required a bit of extra client logic to manage the lifetime (i.e. Dispose()
it). As this would push us into not-as-humble territory, we should reject that choice.
What is not visible here are other policies and behaviors that would be controlled by the host, such as exception filters or route handlers. For exception handling, I typically define exception filters mapping a domain exception (e.g. FileNotFoundException
) to a sensible HTTP response (e.g. “404 Not Found”), and register them at the top-level configuration. Other behaviors may require stacks of attributes on the controllers themselves (e.g. specialized routing scenarios), which admittedly calls the controller’s humility into question.
All that said, it still is good to test even a humble controller — but not with isolated unit tests (twist ending!). This is a case where a system like Approval Tests might work better (it even has a small extension library for Web API). In these tests, you would verify all your routing, authorization, error handling policies, and so on — but not your complete domain logic (aside from what is necessary to cover the needed request/response scenarios) since it is already covered in your unit tests.