Think with Enlab

Diving deep into the ocean of technology

Stay Connected. No spam!

How to build and deploy a three-layer architecture application with C#

 

 

What’s three-layer architecture?

Layer indicates the logical separation of components. Layered architecture concentrates on grouping related functionality within an application into distinct layers that are stacked vertically on top of each other. Each layer has unique namespaces and classes.

An N-layer application may reside on the same physical computer (same tier); each layer's components communicate with another layer’s components by well-defined interfaces.

In C#, each layer is often implemented in a Dynamic Link Libraries (DLL) file.

To help you understand more about the n-layer architecture, I’ve put together all pieces of information about three-layer with a practical example in this article as follows:

  • Presentation layer
    It is the first and topmost layer present in the application where users can interact with the application.
  • Business Logic layer
    This is the middle layer - the heart of the application. It contains all business logic of the application, describes how business objects interact with each other, where the Presentation layer and Data Access layer can indirectly communicate with each other.
  • Data Access layer
    The Data Access layer enforces rules regarding accessing data, providing simplified access to data stored in persistent storage, such as SQL Server.
    It is noteworthy that this layer only focuses on data access instead of data storage. It may create a bit of confusion with the Data-tier in a three-tier architecture.

 

Advantages of three-layer architecture

  • Explicit code: The code is separated into each layer. Each one is dedicated to a single responsibility such as interface, business processing, and querying instead of bundling all the code in one place.
  • Easy to maintain: As its roles separate each layer, it would be easier to change something. The modification can be isolated in a single layer or affected only the nearest layer without affecting the whole program.
  • Easy to develop, reuse: When we want to modify a function, we can easily do that as we already have a standard architecture. In case we want to alter a complete layer such as from Winform to Web form, we only need to implement to replace the Presentation layer; other layers can be reused completely.
  • Easy to transfer: We could save time on moving the application to others as they have a standard architecture to follow and apply.
  • Easy to distribute the workloads: By organizing the code into different layers based on its responsibility, each team/member can write their code on each layer independently, which in turn helps developers control their workload.

 

How does it work?

In three-layer architecture, the Presentation layer doesn’t communicate directly with the Data Access layer. The Business Logic layer works as a bridge between the Presentation layer and the Data Access layer. The three-layer architecture works as follows:

  • The Presentation layer is the only class that is directly interacted with the user. It is mainly used for presenting/collecting data from users, then passes them to the Business Logic layer for further processing.
  • After receiving information from the Presentation layer, the Business Logic layer does some business logic on the data. During this process, this layer may retrieve or update some data from the application database. However, this layer doesn’t take responsibility for accessing the database; it sends requests to the next layer, the Data Access layer.
  • The Data Access layer receives requests from the Business Logic layer and builds some queries to the database to handle these requests. Once the execution gets done, it sends the result back to the Business Logic layer.
  • The Business Logic layer gets responses from the Data Access layer, then completes the process and sends the result to the Presentation Layer.
  • The Presentation layer gets the responses and presents them to users via UI components.

The diagram below illustrates how three-layer architecture works.

 

How to build and deploy 3-layer architecture application with C#

 

How to build and deploy a three-layer application in C#?

To guide you build and deploy a three-layer application, I have prepared a demo with the following components:

  • Business Objects - Entity(Data Transfer Objects) layer: .NET Core class library
  • Data Access layer: .NET Core class library
  • Business Logic layer: .NET Core class library
  • Presentation Layer: ASP.NET Core 5.0 Razor pages

The Business Objects layer includes objects that are used in the application and common helper functions (without logic) used for all layers. In a three-layer architecture, this layer is optional. However, as we follow the OOP with C#, we should reduce the duplicate codes as much as possible. Therefore, using a layer to keep common codes instead of holding them in each layer is essential.

To easily follow the article, you can download the demo of the three-layer architecture sample. It is built based on Repository + UnitOfWork pattern, which’s a well-designed pattern in C#. Here are the demo and practical examples of the Repository + UnitOfWork pattern.

The diagram below explains how the application was designed.

 

How is a 3-layer application designed?

 

3-layer sample.web

 

Database

Firstly, create a database with the design below to store the application’s data. You can execute the ThreeLayerSample.Database.sql, which was placed inside the Data Access layer to create it.

 

3-layer database

 

The demo will focus on getting the Work table items before representing them to users.

 

Business Objects/Entity layer

ThreeLayerSample.Domain class project in this example.

1. Connect to the database with Entity Framework Core

When we have the database, we need to create a mapping between the database and the application.
Let’s open your Package Manager console in this layer, run a Scaffolding command, then replace SERVER, DATABASE, USER, and PASSWORD with suitable values based on your SQL Server settings.

Scaffold-DbContext "Data Source=SERVER;Initial Catalog=DATABASE;Persist Security Info=True;User ID=USER;Password=PASSWORD;MultipleActiveResultSets=True" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Entities -ContextDir Context -Context DemoContext -f

Once the command has finished, the database tables should be created into the code and name entities. As the entities could be used in all layers without any database or business logic, we should keep them in this layer.

 

3-layer sample

 

A DataContext class named “DemoContext” is also created. This class provides database access, so it should be placed in the Data Access layer.

 

democontext

 

2. Create Generic interfaces for Repository and UnitOfWork

All data entities should have CRUD actions, so let’s create a generic interface named IRepository, and then the repository of each entity should implement this interface.

 

ThreeLayerDomain/Interfaces/IRepository.cs

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
 
namespace ThreeLayerSample.Domain.Interfaces
{
   	public interface IRepository<T> where T : class
	{
    	DbSet<T> Entities { get; }
    	DbContext DbContext { get; }
    	/// <summary>
    	/// Get all items of an entity by asynchronous method
    	/// </summary>
    	/// <returns></returns>
    	Task<IList<T>> GetAllAsync();
    	/// <summary>
    	/// Fin one item of an entity synchronous method
    	/// </summary>
    	/// <param name="keyValues"></param>
    	/// <returns></returns>
    	T Find(params object[] keyValues);
    	/// <summary>
    	/// Find one item of an entity by asynchronous method
    	/// </summary>
    	/// <param name="keyValues"></param>
    	/// <returns></returns>
    	Task<T> FindAsync(params object[] keyValues);
    	/// <summary>
    	/// Insert item into an entity by asynchronous method
    	/// </summary>
    	/// <param name="entity"></param>
    	/// <param name="saveChanges"></param>
    	/// <returns></returns>
    	Task InsertAsync(T entity, bool saveChanges = true);
    	/// <summary>
    	/// Insert multiple items into an entity by asynchronous method
    	/// </summary>
    	/// <param name="entities"></param>
    	/// <param name="saveChanges"></param>
    	/// <returns></returns>
    	Task InsertRangeAsync(IEnumerable<T> entities, bool saveChanges = true);
    	/// <summary>
    	/// Remove one item from an entity by asynchronous method
    	/// </summary>
    	/// <param name="id"></param>
    	/// <param name="saveChanges"></param>
    	/// <returns></returns>
    	Task DeleteAsync(int id, bool saveChanges = true);
    	/// <summary>
    	/// Remove one item from an entity by asynchronous method
    	///
    	/// </summary>
    	/// <param name="entity"></param>
    	/// <param name="saveChanges"></param>
    	/// <returns></returns>
    	Task DeleteAsync(T entity, bool saveChanges = true);
    	/// <summary>
    	/// Remove multiple items from an entity by asynchronous method
    	/// </summary>
    	/// <param name="entities"></param>
    	/// <param name="saveChanges"></param>
    	/// <returns></returns>
    	Task DeleteRangeAsync(IEnumerable<T> entities, bool saveChanges = true);
	}
}

We need another interface named IUnitOfWork as follows:

 

ThreeLayerDomain/Interfaces/IUnitOfWork.cs

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
 
namespace ThreeLayerSample.Domain.Interfaces
{
	public interface IUnitOfWork : IDisposable
	{
    	DbContext DbContext { get; }
 
    	/// <summary>
    	/// Get repository instance of an entity inside UnitOfWork scope
    	/// </summary>
    	/// <typeparam name="T"></typeparam>
    	/// <returns></returns>
    	IRepository<T> Repository<T>() where T : class;
    	/// <summary>
    	/// Saves changes to database, previously opening a transaction
    	/// only when none exists. The transaction is opened with isolation
    	/// level set in Unit of Work before calling this method.
    	/// </summary>
    	Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
    	Task BeginTransaction();
 
    	/// <summary>
    	/// Commits the current transaction (does nothing when none exists).
    	/// </summary>
    	Task CommitTransaction();
 
    	/// <summary>
    	/// Rolls back the current transaction (does nothing when none exists).
    	/// </summary>
    	Task RollbackTransaction();
	}
}

They are interfaces without business/database logic here, so I put them in this layer.

 

Data Access layer

ThreeLayerSample.Infrastructure class project in this example.

1. Implement generic classes

By applying Generic Repository and Unit of Work design patterns, the data access layer classes are implemented as follows:

 

ThreeLayerSample.Infrastructure/Repository.cs

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using ThreeLayerSample.Domain.Interfaces;
 
namespace ThreeLayerSample.Infrastructure
{
   	public class Repository<T> : IRepository<T> where T : class
   	{
          	public DbSet<T> Entities => DbContext.Set<T>();
 
    	public DbContext DbContext { get; private set; }
 
          	public Repository(DbContext dbContext)
          	{
                 	DbContext = dbContext;
    	}
 
          	public async Task DeleteAsync(int id, bool saveChanges = true)
          	{
                 	var entity = await Entities.FindAsync(id);
                 	await DeleteAsync(entity);
 
                 	if (saveChanges)
                 	{
                       	await DbContext.SaveChangesAsync();
                 	}
          	}
 
          	public async Task DeleteAsync(T entity, bool saveChanges = true)
          	{
                 	Entities.Remove(entity);
                 	if (saveChanges)
                 	{
                       	await DbContext.SaveChangesAsync();
                 	}
          	}
 
          	public async Task DeleteRangeAsync(IEnumerable<T> entities, bool saveChanges = true)
          	{
        	var enumerable = entities as T[] ?? entities.ToArray();
        	if (enumerable.Any())
                 	{
                       	Entities.RemoveRange(enumerable);
                 	}
 
                 	if (saveChanges)
                 	{
                       	await DbContext.SaveChangesAsync();
                 	}
          	}
 
    	public async Task<IList<T>> GetAllAsync()
          	{
                 	return await Entities.ToListAsync();
          	}
 
          	public T Find(params object[] keyValues)
          	{
                 	return Entities.Find(keyValues);
          	}
 
          	public virtual async Task<T> FindAsync(params object[] keyValues)
          	{
                 	return await Entities.FindAsync(keyValues);
          	}
 
          	public async Task InsertAsync(T entity, bool saveChanges = true)
          	{
                 	await Entities.AddAsync(entity);
 
                 	if (saveChanges)
                 	{
                       	await DbContext.SaveChangesAsync();
                 	}
          	}
 
          	public async Task InsertRangeAsync(IEnumerable<T> entities, bool saveChanges = true)
          	{
                 	await DbContext.AddRangeAsync(entities);
 
                 	if (saveChanges)
                 	{
                       	await DbContext.SaveChangesAsync();
                 	}
          	}
   	}
}

 

ThreeLayerSample.Infrastructure/UnitOfWork.cs

using System.Collections.Generic;
using System.Data;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using ThreeLayerSample.Domain.Interfaces;
 
namespace ThreeLayerSample.Infrastructure
{
   	public class UnitOfWork : IUnitOfWork
   	{
          	public DbContext DbContext { get; private set; }
    	private Dictionary<string, object> Repositories { get; }
    	private IDbContextTransaction _transaction;
    	private IsolationLevel? _isolationLevel;
 
    	public UnitOfWork(DbFactory dbFactory)
          	{
                 	DbContext = dbFactory.DbContext;
        	Repositories = new Dictionary<string, dynamic>();
    	}
 
    	public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
          	{
                 	return await DbContext.SaveChangesAsync(cancellationToken);
          	}
 
          	private async Task StartNewTransactionIfNeeded()
    	{
        	if (_transaction == null)
        	{
            	_transaction =  _isolationLevel.HasValue ?
                	await DbContext.Database.BeginTransactionAsync(_isolationLevel.GetValueOrDefault()) : await DbContext.Database.BeginTransactionAsync();
  	      }
    	}
 
    	public async Task BeginTransaction()
    	{
        	await StartNewTransactionIfNeeded();
    	}
 
    	public async Task CommitTransaction()
    	{
        	/*
         	do not open transaction here, because if during the request
         	nothing was changed(only select queries were run), we don't
         	want to open and commit an empty transaction -calling SaveChanges()
         	on _transactionProvider will not send any sql to database in such case
        	*/
        	await DbContext.SaveChangesAsync();
 
        	if (_transaction == null) return;
        	await _transaction.CommitAsync();
 
        	await _transaction.DisposeAsync();
        	_transaction = null;
    	}
 
    	public async Task RollbackTransaction()
    	{
        	if (_transaction == null) return;
 
        	await _transaction.RollbackAsync();
 
        	await _transaction.DisposeAsync();
    	    _transaction = null;
    	}
 
 
          	public void Dispose()
          	{
                 	if (DbContext == null)
                       	return;
                 	//
                 	// Close connection
                 	if (DbContext.Database.GetDbConnection().State == ConnectionState.Open)
                 	{
                       	DbContext.Database.GetDbConnection().Close();
                 	}
                 	DbContext.Dispose();
 
                 	DbContext = null;
          	}
 
    	public IRepository<TEntity> Repository<TEntity>() where TEntity : class
          	{
                 	var type = typeof(TEntity);
                 	var typeName = type.Name;
 
                 	lock (Repositories)
                 	{
                       	if (Repositories.ContainsKey(typeName))
            	{
                	return (IRepository<TEntity>) Repositories[typeName];
            	}
 
            	var repository = new Repository<TEntity>(DbContext);
 
                       	Repositories.Add(typeName, repository);
                       	return repository;
                 	}
          	}
	}
}

 

And another DbFactory class that will initialize a DbContext when we use it.

ThreeLayerSample.Infrastructure/DbFactory.cs

using System;
using Microsoft.EntityFrameworkCore;
 
namespace ThreeLayerSample.Infrastructure
{
	public class DbFactory : IDisposable
	{
    	private bool _disposed;
    	private Func<DemoContext> _instanceFunc;
    	private DbContext _dbContext;
    	public DbContext DbContext => _dbContext ?? (_dbContext = _instanceFunc.Invoke());
 
        public DbFactory(Func<DemoContext> dbContextFactory)
    	{
        	_instanceFunc = dbContextFactory;
    	}
 
    	public void Dispose()
    	{
        	if (!_disposed && _dbContext != null)
        	{
            	_disposed = true;
            	_dbContext.Dispose();
        	}
    	}
	}
}

 

Business Logic layer

ThreeLayerSample.Service class project in this example.

1. Create an interface for the service

Let’s create the interface for the WorkService that the Presentation layer depends on as we don’t want to tighten the Presentation layer and Business Logic layer with a concrete implementation.

 

ThreeLayerSample.Domain/Interfaces/Services/IWorkService.cs

using System.Collections.Generic;
using System.Threading.Tasks;
using ThreeLayerSample.Domain.Entities;
 
namespace ThreeLayerSample.Domain.Interfaces.Services
{
	public interface IWorkService
	{
    	/// <summary>
    	/// Get all items of Work table
    	/// </summary>
    	/// <returns></returns>
    	Task<IList<Work>> GetAll();
    	Task<Work> GetOne(int workId);
    	Task Update(Work work);
    	Task Add(Work work);
    	Task Delete(int workId);
	}
}

 

2. Implement code for the service

Next, implement business logic processing the Work service in a class named WorkService as follows. This is the business logic processing code, so we put this class in the Business Logic layer. As you can see, the service requests to get all items of the Work table via the Repository instance of the Work entity, which was implemented in the generic repository.

 

ThreeLayerSample.Service/WorkService.cs

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using ThreeLayerSample.Domain.Entities;
using ThreeLayerSample.Domain.Interfaces;
using ThreeLayerSample.Domain.Interfaces.Services;
 
namespace ThreeLayerSample.Service
{
	public class WorkService: IWorkService
	{
    	private readonly IUnitOfWork _unitOfWork;
    	public WorkService(IUnitOfWork unitOfWork)
    	{
        	_unitOfWork = unitOfWork;
    	}
 
    	public async Task<IList<Work>> GetAll()
    	{
        	return await _unitOfWork.Repository<Work>().GetAllAsync();
    	}
 
    	public async Task<Work> GetOne(int workId)
    	{
        	return await _unitOfWork.Repository<Work>().FindAsync(workId);
    	}
 
    	public async Task Update(Work workInput)
    	{
        	try
        	{
            	await _unitOfWork.BeginTransaction();
 
            	var workRepos = _unitOfWork.Repository<Work>();
            	var work = await workRepos.FindAsync(workInput.Id);
            	if (work == null)
                	throw new KeyNotFoundException();
 
            	work.Name = work.Name;
 
          	  await _unitOfWork.CommitTransaction();
        	}
        	catch (Exception e)
        	{
            	await _unitOfWork.RollbackTransaction();
            	throw;
        	}
    	}
 
    	public async Task Add(Work workInput)
    	{
        	try
        	{
            	await _unitOfWork.BeginTransaction();
 
            	var workRepos = _unitOfWork.Repository<Work>();
            	await workRepos.InsertAsync(workInput);
 
            	await _unitOfWork.CommitTransaction();
        	}
        	catch (Exception e)
        	{
            	await _unitOfWork.RollbackTransaction();
            	throw;
        	}
    	}
 
    	public async Task Delete(int workId)
    	{
            try
        	{
            	await _unitOfWork.BeginTransaction();
 
            	var workRepos = _unitOfWork.Repository<Work>();
            	var work = await workRepos.FindAsync(workId);
            	if (work == null)
         	       throw new KeyNotFoundException();
 
            	await workRepos.DeleteAsync(work);
 
            	await _unitOfWork.CommitTransaction();
        	}
        	catch (Exception e)
        	{
            	await _unitOfWork.RollbackTransaction();
            	throw;
        	}
    	}
	}
}

 

Presentation layer

ThreeLayerSample.Web(Razor) ASP.NET Core Web App in this example.

Follow this tutorial to create an ASP.NET Core Razor page application.
Once the application is created, create a ServiceCollectionExtensions class under the Extensions folder.

 

ThreeLayerSample.Web_Razor_/Extensions/ServiceCollectionExtensions .cs

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ThreeLayerSample.Domain.Interfaces;
using ThreeLayerSample.Domain.Interfaces.Services;
using ThreeLayerSample.Domain.Models;
using ThreeLayerSample.Infrastructure;
using ThreeLayerSample.Service;
 
namespace ThreeLayerSample.Web_Razor_.Extensions
{
   	public static class ServiceCollectionExtensions
	{
    	/// <summary>
    	/// Add needed instances for database
    	/// </summary>
    	/// <param name="services"></param>
    	/// <param name="configuration"></param>
    	/// <returns></returns>
    	public static IServiceCollection AddDatabase(this IServiceCollection services, IConfiguration configuration)
    	{
        	// 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>();
    	}
	}
}

 

Add your connection string into the appsettings.json file, and it’s the connection string that the Data Access layer will employ to establish a connection to the database (same value as the connection string in the Scaffold command).

 

ThreeLayerSample.Web_Razor_/appsettings.json

{
  "Logging": {
	"LogLevel": {
  	"Default": "Information",
  	"Microsoft": "Warning",
  	"Microsoft.Hosting.Lifetime": "Information"
	}
  },
  "AppSettings": {
	"ConnectionString": "Data Source=(local);Initial Catalog=ThreeLayerSample.Database;Persist Security Info=True;User ID=sa;Password=PASSWORD;MultipleActiveResultSets=True"
  },
  "AllowedHosts": "*"
}

 

Add an AppSettings class to the Entity layer.

 

ThreeLayerSample.Domain/Models/AppSettings.cs

namespace ThreeLayerSample.Domain.Models
{
	public class AppSettings
	{
    	public static string ConnectionString { get; private set; }
	}
}

 

Open the Startup.cs file, then add the following codes.

At its constructor: read data from appsettings.json, then store it in the created AppSettings class.

In the ConfigureServices: register instances for DataContext, its Factory, UnitOfWork, and WorkService to the application (using extension methods in ServiceCollectionExtensions class).

 

ThreeLayerSample.Web_Razor_/Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using ThreeLayerSample.Domain.Models;
using ThreeLayerSample.Web_Razor_.Extensions;
 
namespace ThreeLayerSample.Web_Razor_
{
	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(Configuration)
            	.AddServices();
        	//
        	services.AddRazorPages();
    	}
 
    	// 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();
        	}
        	else
        	{
            	app.UseExceptionHandler("/Error");
            	// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            	app.UseHsts();
        	}
 
        	app.UseHttpsRedirection();
        	app.UseStaticFiles();
 
        	app.UseRouting();
 
        	app.UseAuthorization();
 
        	app.UseEndpoints(endpoints =>
        	{
            	endpoints.MapRazorPages();
        	});
    	}
 
    	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;
    	}
	}
}

Open Index.cshtml.cs file, add the following code to inject WorkService, then get data from WorkService and set it to Works property.

 

ThreeLayerSample.Web_Razor_/Pages/Index.cshtml.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ThreeLayerSample.Domain.Entities;
using ThreeLayerSample.Domain.Interfaces.Services;
 
namespace ThreeLayerSample.Web_Razor_.Pages
{
	public class IndexModel : PageModel
	{
    	private readonly ILogger<IndexModel> _logger;
    	private readonly IWorkService _workService;
 
    	public IList<Work> Works { get; private set; }
 
    	public IndexModel(ILogger<IndexModel> logger, IWorkService workService)
    	{
        	_logger = logger;
        	_workService = workService;
    	}
 
    	public async Task OnGetAsync()
    	{
        	Works = await _workService.GetAll();
    	}
	}
}

 

In the Index.cshtml file, add the following code to present data to the UI.

 

ThreeLayerSample.Web_Razor_/Pages/Index.cshtml.

@page
@model IndexModel
@{
	ViewData["Title"] = "Home page";
}
 
<div class="text-center">
	<h1 class="display-4">Welcome To Enlab Demo</h1>
	<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
	
	@foreach (var item in Model.Works)
	{
    	<div>@item.Name</div>
	}
</div>

 

 

Testing

Run the application to check for the result.

Testing 3-layer

 

Deployment

Create a folder to store your source code.

deployment 3-layer

 

Open your IIS, then choose Create Website to host your application. Provide a name, a specific port, and a physical path to the source code folder for the Content Directory section. Make sure “Start website immediately” is ticked.

 

3-layer deployment 2

 

Open your solution in your Visual Studio, then follow these steps:

Right-click on ThreeLayerSample.Web(Razor), select Publish.

 

publish 3-layer

 

Select “Folder”.

 

Publish a 3-layer architecture application

 

Enter “Folder location” by creating the source code folder path below.

 

publish location

 

Click to publish the project.

 

Publish

 

Once you’ve done publishing, open the application on your browser to check for the result.

 

Publish a 3-layer architecture application

 

Conclusion

The layered architecture is well-designed for software development that helps organize your code with high maintainability, reusability, and readability. However, even though codes are well-organized in layers, they are deployed and run on the same physical machine, even on the same process. This may not be the right choice for complex applications, which require high availability and stability. We need to upgrade this architecture to another higher level named three-tier architecture, which I will share in my next article.

Thank you for reading, and happy coding!

 

CTA Enlab Software

 

 

References

About the author

Uyen Luu

As a former full-stack developer at Enlab Software, he is fluent in C# and VB programming languages. He also specializes in ASP.NET, .NET Core, Angular, Devextreme UI, Telerik Web controls, Kendo UI, etc. He has helped clients to develop several software applications, which contributed to his working experience in IIS, SQL Server, Power BI, Azure Development.
Frequently Asked Questions (FAQs)
What is the 3 Layer Architecture in C# and How Does It Work?

The 3 layer architecture in C# is a design pattern that divides an application into three main layers: Presentation, Business Logic, and Data Access. This architecture promotes separation of concerns, making the application easier to manage and scale. The Presentation layer handles the UI and user interactions, the Business Logic layer processes data and business rules, and the Data Access layer manages database interactions.

How to Implement 3 Layer Architecture in C# Applications?

To implement 3 layer architecture in C#, start by creating three separate projects within your solution: one for each layer. The Presentation layer (often a web or desktop application) references the Business Logic layer, which in turn references the Data Access layer. Each layer should only interact with the layer directly below it, maintaining a clear separation.

What are the Benefits of Using 3 Layer Architecture in C#?

The benefits of using 3 layer architecture in C# include improved code organization and readability, easier maintenance and testing, and better separation of concerns. It allows different layers to be developed and updated independently and makes the application more scalable and robust.

Can 3 Layer Architecture in C# be Integrated with Other Design Patterns?

Yes, the 3 layer architecture in C# can be integrated with other design patterns like Repository, Unit of Work, and Dependency Injection. These patterns can enhance the architecture by promoting code reusability, simplifying data access, and making the layers more loosely coupled.

What are the Common Pitfalls in Implementing 3 Layer Architecture in C#?

Common pitfalls include tightly coupling between layers, making changes in one layer affect others, neglecting to enforce layer boundaries, and overcomplicating the design. It’s important to adhere to the principles of the architecture and keep the layers independent to avoid these issues.

Up Next

How to Choose the Right Tech Stack for Your Custom Software Project
December 27, 2024 by Dat Le
The Critical Role of Software Development Technologies in Custom Projects Choosing the right software development technologies...
Big Data Technologies Transforming Software Development
July 05, 2024 by Dat Le
In the rapidly evolving world of software development, Big Data stands out as a transformative force....
June 27, 2024 by Dat Le
In today's rapidly evolving digital landscape, secure coding practices are paramount to safeguarding applications from a...
June 20, 2024 by Dat Le
In the rapidly evolving digital landscape, the role of User Interface (UI) and User Experience (UX)...
Roll to Top

Can we send you our next blog posts? Only the best stuffs.

Subscribe