Hi, back to the architecture patterns, in the last article I explained what the three-layer architecture is and how to apply it to a project. Today we will continue to explore the three-tier architecture which can be considered as a distributed system architecture.
Good products are often built with a multi-tier architecture, a.k.a n-tiers. The most popular form of n-tier is the three-tier application. Although the three-tier application is a very well-known and prevalent architecture, it's not entirely apparent to developers who are new in software project development. This article will explain each tier of this architecture and share the best practices in our projects with C#.
What is three-tier architecture?
Three-tier architecture is client-server architecture. The application is separated into physical computing tiers, which means the business logic, data storage, data access, and user interface are developed and maintained as single modules on separate platforms.
Similarly to the three-layer architecture, the three-tier architecture classifies an application into three main logical components but deploy them on different physical computing tiers:
- Presentation tier
The tier is the user interface of the application, where users interact with the application. Its main purpose is to display information to and collect data from users. This is the top-most level in the architecture and can be run on the web browser or a desktop/mobile application. - Application tier
This is the heart of the application, better known as the logic tier or middle tier. It coordinates all business logic of the application, prescribes how business objects interact with each other. It handles collected information from the Presentation tier. During the process, this tier may need to access the Data-tier to retrieve or modify the data. In a three-tier application, all communication goes smoothly through the Application tier. The Presentation tier and the Data-tier can not communicate directly with one another. - Data-tier
The Data-tier is sometimes referred to as the database tier, where it stores and manages the data processed by the Application tier.
What are the differences between layer and tier?
There are divergent opinions on this issue. Layer and tier are often used interchangeably - this is a mistake.
The critical difference between layer and tier is about how it is organized. A multi-layer architecture refers to separating the application into multiple logical units that run on the same infrastructure. In contrast, a multi-tier architecture refers to separating the application into numerous physical units that run on separated infrastructures.
Another difference comes from the responsibility of the lowest component in each architecture:
- Data-tier in three-tier architecture: stores and retrieves data.
- Data Access layer in three-layer architecture: enforces rules regarding accessing data, providing simplified access to data stored in persistent storage.
The Data Access layer does not provide data, so in most cases, it will be in the Application tier (some designs separate it into a tier).
From that point of view, a tier can contain multiple layers. For example, a camera application on your phone is n-layer; it also can be called a single-tier application as all of the processes are executed on your phone:
- Presentation layer: a user interacts with the app to capture an image
- Business Logic layer: the app handles the captured image by converting it to binary, then sends the handled information to the Data Access layer.
- Data Access layer: the app accesses the memory on your device to store the handled information.
Why is three-tier architecture the most common use?
Because of the logical and physical separation of functionality, each tier can run on a separate hosting environment. Typical servers to deploy a three-tier application are web server, application server, and database server, each serves appropriate functional requirements. Each tier operates separately, so its services can be customized and optimized without affecting the other tiers.
- Speed up the development: As each tier can be enhanced simultaneously by different teams, so time to market of the product is optimized, and developers can use the latest tools and the best languages for each tier.
- Improve the scalability: By deploying the application on different tiers, you are able to scale any tier independently of the others at any given time.
- Improve the reliability: Because it has different tiers, you can also boost reliability and availability by running disparate parts of your application on distinct servers and employing cached results.
- Improve the security: By using a well-designed application tier, it functions as a sort of internal firewall, which will help to prevent SQL injections and other malicious exploits.
How does it work?
The Presentation tier - the individual level where the user can interact with the application, collect data from the user, validate them if needed and then send it to the Application tier via an HTTP request.
The Application tier receives the (validated) data from the Presentation tier, then processes it by relevant business logic based on the Presentation requested. During the process, the Application tier may access the Data-tier to query data (retrieve/modify/store data) if necessary.
The Data-tier receives commands from the Application-tier, executes them, then sends a response back to the Application tier.
The Application tier receives the executed result and processes it. Once the process is completed, the Application tier sends the data back processed to the Presentation tier.
The Presentation tier receives the processed data and then presents them to the user.
How to build and deploy a three-tier application with C#?
To guide you build and deploy a three-tier application, I have prepared a demo with the following components:
Presentation Tier: Angular
Application tier: Applying the three-layer architecture for the Application tier. It is explained in How to build and deploy a three-layer architecture application with C#.
Data-tier: MS SQL Server
To easily follow the article, you can download the demo of the three-tier architecture sample.
The diagram below explains how the application was designed.
Data-tier with SQL Server
Create database project
Let’s use the database project template in Visual Studio 2019 and create a database project.
Design database
Once the project is created, design your tables, views, and stored procedures for this database. Also, add a script to initialize the data when the project is published to SQL Server.
Deployment
Right-click on the project and select Publish.
Next, specify your SQL Server connection and database name, then click on Publish.
Testing
Open your SSMS to check for the result.
Application tier with C#
This is the heart of the three-tier architecture, which is also the most complicated and challenging tier of implementation, so we need a good design to manage and organize the code. That is why I used three-layer architecture for this tier.
This tier does not interact with users; it will interact with the other tiers/applications. This means the Presentation layer (of the Application tier) is not a User Interface; it is an Application Interface, more commonly known as Application Programming Interface (API).
Please follow How to build and deploy a three-layer architecture application with C#, the Data Access layer (Infrastructure), Domain layer, and Business Logic layer (Service) can be reused from this article. To accomplish the Presentation layer (of the Application tier), I will use ASP.NET Core Web API as an interface to interact with the Presentation tier (in three-tier architecture).
Create WebAPI layer - Asp.Net Core Web API Application
Let’s follow this tutorial to create an ASP.NET Core Web API application.
Once the application is developed, create a ServiceCollectionExtensions class under the Extensions folder.
ApplicationTier.API/Extensions/ServiceCollectionExtensions.cs
- AddDatabase: register the database instances
- AddServices: register service instances
- AddCORS: access from external applications to this application(via API layer)
using System;
using ApplicationTier.Domain.Interfaces;
using ApplicationTier.Domain.Interfaces.Services;
using ApplicationTier.Domain.Models;
using ApplicationTier.Infrastructure;
using ApplicationTier.Service;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace ApplicationTier.API.Extensions
{
public static class ServiceCollectionExtensions
{
/// <summary>
/// Add needed instances for database
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddDatabase(this IServiceCollection services)
{
// Configure DbContext with Scoped lifetime
services.AddDbContext<DemoContext>(options =>
{
options.UseSqlServer(AppSettings.ConnectionString,
sqlOptions => sqlOptions.CommandTimeout(120));
options.UseLazyLoadingProxies();
}
);
services.AddScoped<Func<DemoContext>>((provider) => () => provider.GetService<DemoContext>());
services.AddScoped<DbFactory>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
return services;
}
/// <summary>
/// Add instances of in-use services
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddServices(this IServiceCollection services)
{
return services.AddScoped<IWorkService, WorkService>();
}
/// <summary>
/// Add CORS policy to allow external accesses
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddCORS(this IServiceCollection services)
{
return // CORS
services.AddCors(options => {
options.AddPolicy("CorsPolicy",
builder => {
builder.WithOrigins(AppSettings.CORS)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
}
}
}
Open your AppSettings.cs class in the Domain layer, then update the following codes.
ApplicationTier.Domain/Models/AppSettings.cs
namespace ApplicationTier.Domain.Models
{
public class AppSettings
{
public static string ConnectionString { get; private set; }
public static string[] CORS { get; private set; }
}
}
Update some values to your appsettings.json file.
ApplicationTier.API/appsettings.js
- ConnectionString: the connection string to the database on Data-tier.
- CORS: the domain list of applications that you allow access to the application.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AppSettings": {
"ConnectionString": "Data Source=(local);Initial Catalog=DataTier.SqlServer;Persist Security Info=True;User ID=sa;Password=PASSWORD;MultipleActiveResultSets=True",
"CORS": [ "http://localhost:4200" ]
},
"AllowedHosts": "*"
}
Open the Startup.cs file, then add the following codes.
ApplicationTier.API/StartUp.cs
- StartUp(constructor): read data from appsettings.json, then store it in the AppSettings class.
- ConfigureServices: register instances for DataContext, its Factory, UnitOfWork, WorkService, and CORS policy to the application (using extension methods in the ServiceCollectionExtensions class).
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using System;
using ApplicationTier.API.Extensions;
using ApplicationTier.Domain.Models;
namespace ApplicationTier.API
{
public class Startup
{
public Startup(IWebHostEnvironment env)
{
Configuration = InitConfiguration(env);
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services
.AddDatabase()
.AddServices()
.AddCORS();
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "ApplicationTier.API", Version = "v1" });
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
// Move swagger out of this if block if use want to use it on production
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ApplicationTier.API v1"));
}
// Auto redirect to https
//app.UseHttpsRedirection();
// Allow external access;
app.UseCors("CorsPolicy");
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
private IConfiguration InitConfiguration(IWebHostEnvironment env)
{
// Config the app to read values from appsettings base on current environment value.
var configuration = new ConfigurationBuilder()
.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true)
.AddEnvironmentVariables().Build();
//
// Map AppSettings section in appsettings.json file value to AppSetting model
configuration.GetSection("AppSettings").Get<AppSettings>(options => options.BindNonPublicProperties = true);
return configuration;
}
}
}
Create a WorkController, add the following code to inject the interface of WorkService into the controller, then use it to implement some basic APIs.
ApplicationTier.API/Controllers/WorkController.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using ApplicationTier.Domain.Entities;
using ApplicationTier.Domain.Interfaces.Services;
using Microsoft.AspNetCore.Mvc;
namespace ApplicationTier.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class WorkController : ControllerBase
{
private readonly IWorkService _workService;
public WorkController(IWorkService workService)
{
_workService = workService;
}
#region CRUD
[HttpGet]
public async Task<IList<Work>> GetAll()
{
return await _workService.GetAll();
}
[HttpPut]
public async Task Update(Work work)
{
await _workService.Update(work);
}
[HttpGet("{id:int}")]
public async Task<Work> GetOne([FromRoute] int id)
{
return await _workService.GetOne(id);
}
[HttpPost]
public async Task Add(Work work)
{
await _workService.Add(work);
}
[HttpDelete("{id}")]
public async Task Delete([FromRoute] int id)
{
await _workService.Delete(id);
}
#endregion
}
}
Testing
Run your application to test the API via Swagger UI (already integrated when creating the ASP.NET Core API app).
Or by a specific API endpoint.
Deployment
In the machine where you want to store the Application tier, create a folder to store the source code.
Notes: Make sure this machine has installed .NET 5 Bundle.
On the same machine, open your IIS to create a website as below.
Open your solution in your Visual Studio, then follow these steps:
Right-click on ApplicationTier.API, select Publish.
Select “Folder”.
Enter “Folder location” following the path of the created source code folder.
Click to publish the project.
Testing
Notes: Please test on a specific URL with the suffix “api/work” as the app in this example has been configured for this route only.
Presentation tier with Angular
Create an Angular application
Open your UI folder, then run this command to create a new Angular project. “enlab-software” is the project’s name; you can name whatever you want.
ng new enlab-software |
After creating, some files should be added to your project as below, or you can download a hero sample via the Angular portal. Then extract the file to your UI folder.
Next, move cmd to the created project folder and install all UI packages by running “npm install” command. Please ensure you have installed Node.js before running this command.
Wait until all packages have been installed; you can see the following illustration.
Next, run “npm start” to launch the project.
Open your browser on http://localhost:4200; the result is shown below.
Modify the application
Now open your editor to modify the app (I am using Webstorm).
src/environments/environment.ts (localhost)
src/environments/environment.prod.ts (production)
Set the URL of the deployed Application tier to baseUrl value.
src/app/app.component.ts
import {Component} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {environment} from '../environments/environment';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'Enlab Sofware Demo';
works: any;
constructor(private httpClient: HttpClient) {
this.httpClient.get(`${environment.baseUrl}/api/work`)
.subscribe(works => {
this.works = works;
});
}
}
src/app/app.component.html
<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
<h1>
Welcome to <a href="https://enlabsoftware.com" target="_blank">{{ title }}!</a>
</h1>
</div>
<table>
<tr>
<th style="width: 50px; text-align: left">Id</th>
<th style="width: 100px; text-align: left">Name</th>
</tr>
<tr *ngFor="let work of works">
<td>
{{work.id}}
</td>
<td>
{{work.name}}
</td>
</tr>
</table>
Testing
Save all codes, then double-check your page on the browser.
Deployment
Publish the site
On the same machine, open IIS, then create a site to host the UI app. In this demo, I created the site on the same machine as the Application machine. Despite the separation of this tier, you can deploy it to any machine you want. I set the port to 1234.
Open your cmd at the Angular project folder, then run “npm run build”.
Afterward, open the “dist” folder inside the angular project, copy all files, and paste it to the AngularUI site’s source folder in IIS.
Then reload the UI site. Oops. Something went wrong!
Config CORS policy
This happened because we have not told you about the Application tier that it should accept requests from the Presentation tier. To solve this problem, please follow these steps:
Open the appsettings.json file in the source code folder of the Application tier, then add the domain URL of the Presentation tier to the CORS parameter.
On the Application machine, open IIS and restart the Application site.
Finally, reload the Presentation site on the browser. The site works now.
Conclusion
The three-tier architecture is a well-designed architecture widely used in the software industry because of its scalability, reliability, and high security. It is suitable for building large applications, which handle heavy business logic, and require high load, high security, and availability, such as e-commerce websites, SaaS (Software-as-a-Service), online payment gateway, etc.
Well, that is all I want to share about three-tier architecture. Hopefully, it will be helpful to you if you are going to start building a three-tier architecture application.
Thank you for reading, and happy coding!
References
- IBM Cloud Education, What is Three-Tier Architecture, www.ibm.com, 2020.
- Scott Hanselman, A reminder on "Three/Multi-Tier/Layer Architecture/Design" brought to you by my late-night frustrations, www.hanselman.com, 2004.
- Multitier architecture, www.en.wikipedia.org.