Architecture of Web Applications (with Design Patterns)

Posted by: Damir Arh , on 1/21/2021, in Category ASP.NET Core
Views: 944273
Abstract: This article covers a selection of design patterns that are used in most web applications today.

There are two primary categories of web applications based on where the final HTML markup to be rendered in the browser, is generated:

  • In server-side rendered applications, this is done on the server. The final HTML is sent to the browser as a network response.
  • In client-side rendered applications (commonly known as SPAs – single page applications), this is done in the browser. The code to do that and the data required are requested from the server.

In my previous DNC Magazine article Developing Web Applications in .NET, I described the difference between the two in more detail and also listed the technology stacks for both of them that a .NET developer would most likely choose.


Figure 1: Data exchange between the browser and the server in web applications

This article will cover only the architecture of the server-side rendered applications. The architecture of client-side rendered applications has more in common with desktop and mobile applications which I have already covered in my previous article from this series: Architecture of desktop and mobile applications.

The way the application code is decoupled from the user interface in such applications depends on the technology stack used (Blazor or one of the JavaScript SPA frameworks), but it’s usually not the MVVM pattern.

The other patterns described in that article (dependency injection, remote proxy and validation using the visitor pattern) are just as applicable to single-page web applications as they are to desktop and mobile applications.

The term server-side rendering is sometimes also used in connection with modern JavaScript SPA frameworks. This is not to be confused with the more traditional meaning of the term I used. The server-side rendering (SSR) feature of JavaScript single-page applications only means that on the first request to the server, the page is rendered there, and sent to the browser in its final form. There, the single-page application is “rehydrated”, meaning that any further interaction with the page is handled on the client and only data is retrieved from the server from here on.



Figure 2: Data exchange between the browser and the server in server-side rendered single page applications

This can be achieved by running the same application code on the server and in the browser. That’s why such applications are also called isomorphic (as having the same form on the client and on the server) or universal.

That’s for the introduction. I’m not going to discuss such applications any further in this article.

Decoupling application code from UI

Most of the server-side rendered applications today follow the MVC (model-view-controller) pattern to decouple the user interface from the rest of the application code. As the name implies, there are three main building blocks in this pattern:

  • Models describe the current state of the application and serve as a mean of communication between the views and the controllers.
  • Views are the user interface of the application. They read the data to be rendered from the models. Since they are web pages, interaction with them can trigger new requests to the server.
  • Controllers handle requests sent to the server when users navigate to a URL or interact with a previously served page. In response, Controllers select a view to be rendered and generate a model that they pass to it.


Figure 3: MVC pattern building blocks

In the .NET ecosystem, the currently recommended MVC framework (or application model) is ASP.NET Core MVC. It’s a part of .NET Core. Although it has some similarities with ASP.NET MVC for .NET framework, it was rewritten from scratch and is different enough that applications can’t easily be migrated between the two.

The controller code in ASP.NET Core MVC is placed in the so-called action methods which are grouped together in controller classes:

public class HomeController : Controller
    public IActionResult Index()
        return View();

By default, HTTP requests invoke a corresponding action method based on a routing convention:

    name: "default",
    template: "{controller=Home}/{action=Index}/{id?}");

This template specifies that the first part of the URL selects the controller class, the second one selects the action method in it, and the rest is treated as an argument for the action method (arguments will be explained in more detail later in the article). It also specifies the default value for the controller class and the action method if they aren’t included in the URL. Of course, the routing convention can be extended and customized.

The sample action method simply triggers the rendering of the view. The selection of the view is again convention-based. By default, the view name will have to match the name of the action method and be placed in a folder matching the name of the controller class (i.e. Home/Index in this case). Alternatively, the name of the view to render can be passed as an argument to the View method.

The sample action method will always render an identical page. In most cases that’s not enough. To make the page content dynamic, the action method will need to pass data to the view using a model:

return View(personModel);

In the view, the model is then accessed through its Model property:

<h4>@Model.FirstName @Model.LastName</h4>

To respond to user interaction, the action method also needs inputs. In a web application, these will come from the URL (the path and the query string) or from the form fields in the case of a submitted HTML form. The model binding process in ASP.NET Core MVC maps all of them to the action method arguments.

public ActionResult Edit(int id, PersonModel person)
    // ...

As in the case of routing, ASP.NET Core MVC includes conventions that are used to automatically bind values from different sources to method arguments based on the names of arguments (id in the example above) and the property names of complex types in the role of arguments (PersonModel in the example above). These can be further customized using attributes and even custom model binders.

It might be tempting to reuse existing internal entities as models to be passed to the view and to be received as arguments from the model. However, this isn’t considered a good practice because it increases the coupling between the internal application business logic and the user interface.

Having specialized models brings additional benefits:

  • When they are passed to the view, they can already be structured according to the needs of that view. This can reduce the amount of code that would be needed in the view if more generic models were used.
  • When they are received as arguments, they can allow the model binder to do a better job at validating them. If a model doesn’t have a property that’s not expected in the request, then there’s no danger that it would be parsed from a malicious request and used in an unexpected manner. Also, there’s no potential for conflicting validation attributes if the model is only used for a single request.

A common argument against using dedicated models is the need for mapping the properties between the entity classes and the models. While writing these manually can be time consuming and error prone, there are libraries available to make this job easier. Among the most popular ones is AutoMapper.


Dependency injection

The described architectural approach makes the view completely unaware of the controller and the rest of the business logic in the application. Each view knows only about the model received from the controller and the application routes it interacts with.

On the other hand, the controller is fully aware of the view that it wants to render and its corresponding model, as well as the model it can optionally receive as input.


Figure 4: Interaction between MVC building blocks

However, there’s another big part to action methods that I haven’t talked about yet.

They will need to execute some business logic in response to their input in order to generate the model required for the view. While this business logic could be implemented directly in the action method, it’s best to minimize the amount of code inside it and implement the actual business logic in dedicated service classes:

public ActionResult Edit(int id, PersonModel person)
    var personService = new PersonService();
    var updatedPerson = personService.UpdatePerson(person);
    return View(updatedPerson);

To avoid instantiating those service classes inside the controller class and thus coupling the controller class to a specific service class implementation, dependency injection can be used. This allows the controller class to depend only on the public interface of the service class and get the instance as a constructor parameter:

public class PersonController : Controller
    private readonly IPersonService personService;

    public PersonController(IPersonService personService)
        this.personService = personService;

    public ActionResult Edit(int id, PersonModel person)
        var updatedPerson = this.personService.UpdatePerson(person);
        return View(updatedPerson);

ASP.NET Core has built-in support for dependency injection!

Even its own internals take advantage of it extensively. Hence, it’s very easy to use that same dependency injection implementation for your own dependencies. They can be configured in the ConfigureServices method of the Startup class:

services.AddScoped<IPersonService, PersonService>();

The configuration consists of the interface, the implementing service and the service lifetime. In a web application, the service lifetime is commonly defined as scoped, meaning that the services are created for each request. This ensures isolation of state between multiple requests which are being processed in parallel.

Data Access

By removing the business logic from the controller, the services become another actor in the MVC architecture. The controllers must be aware of them. Or to be more exact: thanks to dependency injection, they only need to be aware of their public interface, but not of their implementation and internal dependencies.


Figure 5: Interaction between MVC building blocks when using a service

Of all the typical service dependencies, data access deserves special attention. It is most likely going to be implemented using an ORM (object-relational mapper) like Entity Framework Core or a micro ORM like Dapper.

But how do data access libraries fit into a web application architecture?

The architecture patterns that are most often mentioned in connection with data access are the repository pattern and the unit of work pattern. So, let’s start by exploring these.

The repository pattern isolates the business logic from the implementation details of the data access layer. Its interface exposes the standard data access operations over a specific resource (or entity), often referred to as CRUD (Create, Read, Update, and Delete):

public interface IPersonRepository
    IEnumerable<Person> List();
    Person Get(int id);
    Person Insert(Person person);
    Person Update(Person person);
    void Delete(Person person);

Any other type of queries that might need to be performed over the same entity, should be exposed from its repository in a similar manner. This makes the repository the sole container for the remote store queries and any mapping code that might not be done automatically by the ORM used for its implementation.


Figure 6: Data access using the repository pattern

In a simple implementation, any operation on the repository (including data modification) will immediately be executed on the underlying data store. However, this will be insufficient in more complex scenarios because it doesn’t allow multiple operations to be executed within a single transaction to ensure their atomicity (i.e. that either all operations are performed successfully or none).

One option would be to add a separate Save method to the repository which could be called to apply any pending changes caused by calling other methods in it. But this still doesn’t completely solve the problem. What if changes across multiple repositories need to be applied inside a single transaction?

This is where the unit of work pattern comes into play. It exposes a common Save method for multiple repositories. Typically, these repositories will now be accessed through the unit of work instead of directly:

public interface IUnitOfWork
    IPersonRepository PersonRepository { get;  }
    IOrderRepository OrderRepository { get;  }
    // ...

    void Save();


Figure 7: Atomic data modification using the unit of work pattern

If you’re familiar with Entity Framework Core, you probably recognized the similarity between the IUnitOfWork interface above and a typical DbContext class in Entity Framework Core:

public class SalesContext : DbContext
    public DbSet<Person> Persons { get; set; }
    public DbSet<Order> Orders { get; set; }

In it, the DbSet<TEntity> properties have the role of repositories, allowing simple execution of CRUD operation which is evident from the following (subset of) members of the class:

public abstract class DbSet<TEntity> : IEnumerable<TEntity>
    where TEntity : class
    public virtual EntityEntry<TEntity> Add(TEntity entity);
    public virtual EntityEntry<TEntity> Remove(TEntity entity);

Reading of entities can be done using the IEnumerable<TEntity> interface and editing is done by simply modifying properties of a TEntity instance. The DbContext class has a SaveChanges method which does the job of IUnitOfWork’s Save method.

This similarity is no coincidence.

Entity Framework Core follows the concepts of the repository and unit of work patterns. This raises the question:

Does it make sense to implement these two patterns in a web application when using Entity Framework Core for data access? (Dapper and other micro ORMs don’t implement these patterns themselves).

To answer this question, we must look at the benefits that these patterns bring and to what extent they are still applicable when the business logic code uses Entity Framework Core directly:

1. When using Entity Framework Core directly, it’s not abstracted away from the rest of the application anymore. This will make it much more difficult to replace it as the data access library later.

But are you ever going to do that?

Also, if Entity Framework Core is not (going to be) used for all data stores, exposing it directly will make it impossible to always use the same interface and hide this implementation detail from the rest of the code.

2. The concrete unit of work and repository pattern implementations can be replaced with mocked ones in unit tests. Again, that’s not possible when using Entity Framework Core directly.

However, there’s an in-memory database provider available for Entity Framework Core which can partially replace the mocks by making it possible to run the tests without accessing the underlying data store.

3. The repositories serve as containers for custom queries for their entity. This can still be achieved to some extent even when using Entity Framework Core directly. For example, the queries can be implemented as extension methods of the generic DbSet class for that specific entity.

To conclude, as often in software architecture, there’s no definitive answer to the question. It depends on the value the above points bring in for the application.

It makes more sense in implementing your own repositories and unit of work if your application is large and complex.


In the article, I have described some of the architecture patterns that are often used in web applications.

I started with the overarching MVC pattern that is used for decoupling the application code from the user interface in most of the server-side rendered web applications today. I continued with the application of dependency injection for further decoupling the business logic implementation from the controller.

In the final part, I covered the usage of repository and unit of work patterns in the data access layer.

In comparison to the application models for desktop and mobile applications covered in the previous article from the series, these patterns are already implemented by ASP.NET Core. In most cases, they can be used without installing third party libraries or implementing them yourself.

This article was technically reviewed by Ravi Kiran.

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Damir Arh has many years of experience with software development and maintenance; from complex enterprise software projects to modern consumer-oriented mobile applications. Although he has worked with a wide spectrum of different languages, his favorite language remains C#. In his drive towards better development processes, he is a proponent of Test-driven development, Continuous Integration, and Continuous Deployment. He shares his knowledge by speaking at local user groups and conferences, blogging, and writing articles. He is an awarded Microsoft MVP for .NET since 2012.

Page copy protected against web site content infringement 	by Copyscape

Feedback - Leave us some adulation, criticism and everything in between!