ASP.NET – Thinking In Resources
A few months ago I wrote Tinyweb, an ASP.NET web framework which dictates an SRP approach to building web applications. Since then I’ve used Tinyweb for a few internal things and I’ve been happy with the direction it leads you in.
So, we’ve all heard enough times by now the benefits of the SOLID principles. In particular, we know that we should minimise the responsibility of a class, with the aim of having just one responsibility per class and consequently only one reason for that class to change.
That’s an interesting word – change. We often refer to new work on an existing system as change, but unless the new work involves modifying the way something worked before, it shouldn’t be change, it should be extension.
As you know, another SOLID principle tells us that code should be open for extension, but closed for modification. Usually this refers to being able to extend behaviour by subclassing and using polymorphism to effectively replace the old behaviour – but can’t OCP be more generally applied?
What about our architecture, project layout, working conventions? Have these been arranged to support extension or will a change require that we have to rethink these things in order to accommodate future changes?
How Tinyweb Helps
Tinyweb takes a rest-style approach to building web applications by enforcing that a handler can only handle the standard HTTP methods for a single URL. For example, if we have a UsersHandler (liken this to a controller in MSMVC parlance) we will have a /Users URL and will only be able to respond to the HTTP GET, POST, PUT and DELETE methods for this resource.
Where in MSMVC you might have an AccountController with all kinds of actions, Tinyweb dictates that every URL is a separate resource independent of all others and requires its own handler.
I think this helps in enforcing a certain level of granularity and responsibility. While you can’t really argue that a handler has single responsibility (since it might handle both GET and POST), it more broadly has the single responsibility of controlling interactions with that resource.
This is why the web has evolved so successfully – everything is ultimately just an addressable resource that can send you to other resources in some coherent way to achieve a goal. Adding new functionality simply involves deciding which new resource to introduce and deciding how it will integrate with the existing system.
User Signup Example
Let’s see this workflow in action by using a simple example. Let’s pretend we’re building some web based system that requires users to sign up for accounts. We’ll start with a generic user registration system.
public class UserRegisterHandler
{
public IResult Get()
{
return View.Spark("Signup.spark");
}
}
By creating a handler named UserRegisterHandler, I’m in effect creating a resource addressable at /User/Register which can support any of the standard HTTP methods. In this particular case, I’ve implemented the HTTP GET method to allow the resource to display a view for a potential user to submit their account details.
Still thinking in terms of resources, we should now consider which resource will handle receiving a user’s details and creating the account. For the purposes of this example, we’ll use the same resource. So, a GET request to /User/Register will get the user registration view and a POST request to /User/Register will create the user.
public class UserRegisterHandler
{
IAccountService _accountService;
public UserRegisterHandler(IAccountService accountService)
{
_accountService = accountService;
}
public IResult Get()
{
return View.Spark("Signup.spark");
}
public IResult Post(User user)
{
_accountService.Create(user);
return Result.String("Done");
}
}
I’ve made a couple of changes to the handler here. I’ve introduced a dependency on an IAccountService which will handle the user registration. I’ve also implemented the HTTP POST method which binds the POST data to a User object and passes that down to the account service to do its magic.
Let’s continue this pretentious silliness and imagine that we’ve just been informed of our next requirement. Apparently, our current handling of new users leaves a lot to be desired. We’ve been told that when new users sign up, they should be taken to a page that welcomes them and confirms their details.
Because we’re thinking exclusively in terms of the resources our system is composed of, the next question becomes: which resource do we need to introduce to support this extension?
We’ll create a UserWelcomeHandler, giving us a new resource addressable at /User/Welcome. Let’s assume that a client will pass us the ID of the user we’re welcoming when issuing a GET request to this resource.
public class UserWelcomeHandler
{
IAccountService _accountService;
public UserWelcomeHandler(IAccountService accountService)
{
_accountService = accountService;
}
public IResult Get(int id)
{
var model = _accountService.GetWelcomeDetails(id);
return View.Spark(model, "Welcome.spark");
}
}
The user ID is now being taken from the GET request and being passed to the account service to fetch a model that represents the welcome details for that user. The model can then be rendered to a view as normal.
The new requirement has been implemented by introducing a new resource and extending the original system. But there’s still a missing link between the two resources, and we’ll have to return to the UserRegisterHandler and modify it. Instead of just displaying “done”, we want it to delegate responsibility to our new /User/Welcome resource.
public class UserRegisterHandler
{
IAccountService _accountService;
public UserRegisterHandler(IAccountService accountService)
{
_accountService = accountService;
}
public IResult Get()
{
return View.Spark("Signup.spark");
}
public IResult Post(User user)
{
var userID = _accountService.Create(user);
var url = Url.For<UserWelcomeHandler>(new { UserID = userID });
return Result.Redirect(url);
}
}
With this change, the responsibility of UserRegisterHandler has been contained to dealing with the new account creation only. Satisfying our new requirement of welcoming the user has been implemented by introducing a new resource. We now simply move between the separate resources as required.
The Point
Although you could easily work this way with any framework, Tinyweb makes it more explicit with its opinionated approach to what a handler can do. You’re forced to think about separate resources and how they should collaborate to form a coherent system – and ultimately, this feels like a very natural and intentional way to build applications.


2 Comments
leave a commentTrackbacks and Pingbacks