Microsoft's ASP.NET Web Forms was first released way back in 2002. Even though Web Forms has been around for nigh on 20 years, that doesn't mean that it's gone away. In fact, plenty of .NET shops are sure to still have thousands of lines of Web Forms codes in production today.
18 years in software time is roughly the equivalent of an eternity; and as such, Web Forms design philosophy doesn't really mesh well with modern software development. Today's widely adopted practices, things like dependency injection and unit tests, were not common tools in the software engineer's metaphorical tool belt back in '02.
Today we will discuss 3 issues with the traditional approach to Web Forms developments:
- Ease of testing
Let's look at a very simple page to search board games by title. The code behind might look something like this:
The markup for this page looks like this:
So far, in this very simple example, we have two methods on our page that are a little tough to write unit tests for; in the case of
Search in particular, it's pretty much impossible as we have a dependence on our
You can probably imagine, and may have experienced, the chaos of a sizable enterprise application built this way. Before long, domain logic is duplicated throughout the code, and possibly implemented in a slightly different way in each place; code becomes inflexible and brittle, and everyone is afraid to make changes.
Let's look at an example of this by adding a second page to our application that searches games by year released:
As you can see, the code is almost identical, with the search condition being the only difference.
The markup is nearly identical to our initial game search page as well:
This example is quite obviously contrived and could be condensed into a single page with search filters, but it's easy to imagine a scenario where Developer A implements a feature in a page, and then Developer B re-implements that same feature (more than likely in a slightly different manner) elsewhere.
Another problem that we often face when working in Web Forms is a lack of testable code - in these two very simple pages, we have multiple bits of domain logic that are difficult to test. This isn't ideal.
The final problem, that we will address today anyways, is the fact that this design is not easy to change.
For example, take a hypothetical scenario where the stakeholders decide that we must exclude a given publisher's games from our search features; we must now update every spot where we are searching games to include this new business rule. In our small example this is trivial, but in a real world application with much more code this could be a task of gigantic proportion.
One way to alleviate these issues is to refactor our pages a bit, and abstract some functionality out into services and view models.
Service classes are nothing new, and can have their own set of tradeoffs (we won't worry about those for today), but the approach fits our problems well.
We can create a service to handle our game fetching functionality by doing the following:
First, create an interface for our game service:
Then, create a concrete implementation of that interface:
Now we have a service that can be shared across pages, and any domain specific logic, like our publisher example from earlier, can be encapsulated and validated in this one place
One other thing you can see here is that we've abstracted our
GameRepository behind an interface (
IGameRepository) and then inject it in our
GameService class's constructor. This is important, as it will allow us to mock that dependency when we write our unit tests.
The next type we will define is what I'll call a 'Page Model', which will handle our page logic:
As you may have noticed, any dependencies the page model has are declared in its constructor. Like I mentioned earlier, this will allow us to mock those dependencies when the time to test comes.
The purpose of our view model type is to encapsulate any view related functionality for the page. In our case, this will be any dynamically generated html:
Now that we've defined these abstractions, let's create a new page to search for games by title that uses them:
And the markup changes slightly to look like this:
The instantiation of the
GamesPageModel is a little dirty, but this could be handled by some type of
DependencyResolver class, or Dependency Injection container. Here is an excellent article by Nathanael Marchand on the topic as it relates to Web Forms in particular - https://www.natmarchand.fr/aspnet-dependency-injection-net472/
As you can see in the code behind, our page code is now very thin, doing nothing more than passing data through to our page model. By taking this approach, we've now made our code testable, easier to change, and centralized any domain level logic in our service types.
One thing to keep in mind with this approach is that it adds architectural overhead, and is not as straight forward as simply writing logic in page events.
Also, as an application grows in complexity (for example a page with many components sharing data), this level of abstraction can mean that communication between those components is not trivial.
Let's look at an example test using MSTest:
You can see in our test that we are passing a
GameServiceMock into the constructor of our
GamesPageModel. This is a type defined in our test project that implements
IGameService and just returns some static data:
This is one small test, but since we've factored our code well, we can also write tests against our domain level services, and even our view models if we so desire.
We've looked at addressing a few problems typically faced in a Web Forms application; namely, ease of testing, maintainability, and duplication.
The solution we looked at was to make our code behind nothing more than a thin event handler, simple passing data to our page model. We also abstracted domain logic into a service class, and created interfaces for dependencies that need to be mocked.
There are a few more things we could do, such as adding some type of inversion of control, re-factoring our search functionality into a shared component, and so on, but those are topics for another day.
What design patterns have you found useful when building and maintaining Web Forms application?