In a previous blog, I talked about the 3-tier architecture. I admit that it was boring and text only. Hence, in this tutorial, I will explain how to use a 3-tier architecture with C#. If you don’t know what a 3-tier architecture is, I advise you to read the blog first. Or just scroll through it.
Table Of Contents
How To Start?
One of the questions I got most when I was teaching was “How do you start a project?”. There isn’t a direct answer. There are many ways to start a project. Some people just start writing code and the architecture evolves with time. Other people like to write everything out on (digital) paper with strategies, diagrams, and whatnot.
And then there are people like me. I set up the basic structure and architecture and I just start coding. But it all depends on the project. Small projects are easy to start and finish. Bigger projects are a bit more tricky. These usually have a person specialized in architecture, who helps the developers start.
In this tutorial, I will be showing how I usually start a small or semi-big project. I will be creating an API for a movie database. Something simple, consisting of different layers (projects).
Think For A Second
Although you can just start creating your projects in the solution, you should stop and think for a second about the elements you need. Here are a few things you usually need:
- A presentation layer
This can be an API, console application, web app, etc. - An application layer
Also called a business layer that contains all your logic. - A data layer
Containing everything that has to do with the data. - An ORM
If you want to use a database. So you might also want to create, install, or set up a database environment.
The layers are class libraries with their own responsibility. Each class within the class libraries has its own responsibility too.
Another thing to think about is naming. It’s important to think about the names of your projects/layers before you create them. Yes, you can rename them after you created them, but it can create bigger problems in the future.
If you create a small, quick project to try something you don’t have to create all these layers. Just create a console application and build it. Then extract the code you want and add it to your main project.
Creating the projects
Time to create the projects so we can create a 3-tier architecture with C#. To begin you start Visual Studio and select a template. Since I want to create an API I start with that template. I am going to create the layers when I need them.
The presentation layer (API)
I am not going to tell you what an API is and how it works. Instead, I am going to tell you what you should keep in mind.
First, let’s start up Visual Studio and create a new project. I select the ASP.NET Core WebAPI. The name is important. I want to create an API for a movie database. So the name MovieDatabase should be a good one. But the name does not show what kind of project this is. If you give a clue in the name it can help you discover what the different projects in your solution are supposed to do.
In this case, I will call the API MovieDatabase.API. This way I can see it’s an API and a presentation layer.
The solution name is automatically set to the name of the project. Don’t do this. Change it to MovieDatabase. The solution will not only contain the API but also different projects that are all involved around the MovieDatabase. I change the solution name to MovieDatabase.
In the next screen I select the newest .NET version, keep the HTTPS selected, uncheck the Use controllers if needed, enable OpenAPI, and uncheck do not use top-level statements.

The application layer (business)
A presentation layer should be kept simple and even a bit stupid. Meaning it shouldn’t contain much logic. It’s only there to give a presentation of the data, which comes out of the application layer.
I don’t like the name application layer and I usually refer to it as we did years ago: the business. It still has the same function, namely handling the logic of your project.
Let’s add the business to our solution. Add a new class library project. Again, mind the name. This class library is for the movie database and it’s the business. Therefore I will call it the MovieDatabase.Business. You can also follow the rule that each project name starts with the solution name.
I’ll come back to this project later.
The data layer
I am a bit conflicted about this layer. If you would use Entity Framework, the lines and classes you need are minimal. Creating a whole new layer for this feels a bit overdone. But for this tutorial, I will keep it in.
To create it you do the same as the business; Add a new class library. I will call the data layer MovieDatabase.Data.
That’s it! All layers are ready.

Adding some code
Next up is some code. I will keep it simple but highlight the most common issues that you will encounter.
First, how do we get the layers to communicate? You can initialize each class, to be created, when you need it. But it would be better to use dependency injection. It will allow you to initialize all the needed classes once and use them throughout your application.
A simple service
To use and configure dependency injection, we need interfaces. These interfaces are connected to the classes. Let’s start with a simple interface and class in the business layer. I created a class called MovieService in the MovieDatabase.Business. This class has a single responsibility for movies. Nothing else.
I will also create an interface. It’s a good practice to put the interfaces in their own folder. I’ll add the folder Interfaces in the MoviesDatabase.Business. In this folder, I’ll add the interface IMovieService and I will add the following methods.
public interface IMovieService { Movie? Get(int id); IEnumerable<Movie> Get(); void Create(Movie movie); void Delete(int id); }
I need a Movie object. I will add this object (a class) in the folder Models in the MoviesDatabase.Business. The Movie class will have the following properties:
public class Movie { public int Id { get; set; } public string Title { get; set; } public DateTime ReleaseDate { get; set; } public int Rating { get; set; } }
Don’t forget to add the reference in the IMovieService if needed!
Now we can connect the interface to the MovieService class and implement the interface.
public class MovieService : IMovieService { public void Create(Movie movie) { throw new NotImplementedException(); } public void Delete(int id) { throw new NotImplementedException(); } public Movie Get(int id) { throw new NotImplementedException(); } public IEnumerable<Movie> Get() { throw new NotImplementedException(); } }
builder.Services.AddScoped<IMovieService, MovieService>();
Make sure you add the correct references and usings. Now we can inject the IMovieService wherever we want, such as a mapping.
Let’s add a mapping in the Program.cs:
app.MapGet("api/movies", (IMovieService movieService) => { return Results.Ok(movieService.Get()); }); app.MapGet("/api/movies/{id:int}", (int id, IMovieService movieService) => { return Results.Ok(movieService.Get(id)); }); app.MapDelete("api/movies/{id:int}", (int id, IMovieService movieService) => { movieService.Delete(id); return Results.NoContent(); }); app.MapPost("api/movies", (Movie movie, IMovieService movieService) => { movieService.Create(movie); return Results.NoContent(); });
Looks nice! We can now run the API and it will try to call the methods in the business layer, but you will get exceptions since none of the methods in the MovieService are implemented. For that to work, we need the data layer.
Implementing Entity Framework
Let’s do the same thing as we did for the business, but now for the data layer. Add a folder in the data layer with the name Interfaces. Also create a folder with the name Entities, not models. The reason why I use entities is that these are the objects that will be used by Entity Framework.
To make Entity Framework work in the data layer we need a DataContext class and some packages. And to gain some consistency I will be using the repository pattern. I will be showing why this is important later on.
First, add a Movie class to the folder Entities. This class will contain the same properties as the one in the business layer.
public class Movie { public int Id { get; set; } public string Title { get; set; } public DateTime ReleaseDate { get; set; } public int Rating { get; set; } }
Next, add the class DataContext to the root of the data layer. Add an inheritance to DbContext to that class and make sure you install the package Microsoft.EntityFrameworkCore.
Next, we use the DbSet<T> to tell Entity Framework which entities to connect to a database table. We want to use the Movie entity. We also add a constructor that sets the DbContextOptions, such as the connection string.
public class DataContext: DbContext { public DbSet<Movie> Movies { get; set; } public DataContext(DbContextOptions<DataContext> options) : base(options) { } }
Time to create the repository pattern. This will consist of an interface and the implementation. It will be this interface that we will be using in the business layer. The interface will be called IRepository, in the folder Interfaces, and it will be generic:
public interface IRepository<T> where T : class { IQueryable<T> GetAll(); void Delete(T entity); void Create(T entity); }
And the implementation:
public class Repository<T> : IRepository<T> where T : class { private readonly DataContext context; public Repository(DataContext context) { this.context=context; } public void Create(T entity) { context.Set<T>().Add(entity); context.SaveChanges(); } public void Delete(T entity) { context.Remove(entity); context.SaveChanges(); } public IQueryable<T> GetAll() { return context.Set<T>(); } }
Now we can use the IRepository in the business. The business has no idea what the implementation is since it is only using the interface. Keep this in mind.
Continue In The Business
Let’s turn back to the MovieService in the business. Now we have the IRepository we can use that to let the business communicate with the data layer. Let’s continue with the MovieService class.
public class MovieService : IMovieService { private readonly IRepository<Movie> repository; public MovieService(IRepository<Movie> repository) { this.repository=repository; } public void Create(Movie movie) { repository.Create(movie); } public void Delete(int id) { Movie? toDelete = Get(id) ?? throw new Exception("Movie not found."); repository.Delete(toDelete); } public Movie? Get(int id) { return repository.GetAll().SingleOrDefault(x => x.Id == id); } public IEnumerable<Movie> Get() { return repository.GetAll().ToList(); }
Looks nice, right? Except… Which Movie object is being used here? The business model or the data entity? I’ll give you a hint: It’s the wrong one.
The Movie model is being used here, which is fine, but the repository expects an entity. Both the model and the entity are the same. In this case, you can use the same model.
But then you should reference the business layer into the data layer and the data layer in the business layer. That causes the circular reference and gives you a lot of errors. And that is why I use another type of layer.
The Hidden Layer: The Domain
In some cases, you want to share models and interfaces across multiple projects in your solution. For this reason, we can make a domain layer. This layer does not get any form of logic. Only models, interfaces, and enums.
I add another class library to my solution and will call this one MovieDatabase.Domain. I create the folders Models and Interfaces in the domain project. Then I copy the Movie entity from the data layer into the folder Models of the domain and remove it from MovieDataBase.Data and MovieDatabase.Business. Don’t forget to set the references right!
I don’t move any interface to this project for now, because this is not needed.
Connect Them All
We now have all the layers complete and working together… But not really. If you start the API and try an endpoint you will get errors. We didn’t set up the dependency injection nor did we set up the DbContext. This is one of the last steps.
We also didn’t create the database with migrations, so that’s something we need to do as well.
Application setup
Let’s head to the Program.cs and add all the dependency injections we need. Find the line that says builder.Services.AddAuthorization(); and below that add the following lines:
builder.Services .AddDbContext<DataContext>(options => options.UseSqlServer(builder.Configuration["ConnectionStrings:Default"]), ServiceLifetime.Scoped); builder.Services.AddScoped<IMovieService, MovieService>(); builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
If you are working on a bigger project, this will grow over time. But the AddDbContext and the setup for the repository will always remain the same.
Updating the database
Next up is the database. Install the package Microsoft.EntityFrameworkCore.Tools in the API first. Then set the API project as a start project. Open the package console manager, set the default project to the data later (MovieDatabase.Data) and execute the following command in the package console manager:
add-migration Initial
You might want to install Microsoft.EntityFrameworkCore.Relational, since Visual Studio might “forget”.
When succeeds and everything works, execute the next command:
update-database
When that is done, you can run and test your API.
Conclusion On Use A 3-Tier Architecture With C#
And there you have it: A full, 3-tier, working solution. You can switch out the API for a console app, WinForms, or web application. Just know that the setup, which I showed you in the Program.cs, is different for other application types.
In the end, the solution should look like this. With different projects with their own responsibilities, connected with dependency injection.

So, now what? There are some practices I showed you that might be a bit weird or need some more information. Like, why I am using the repository pattern. Or maybe you want to see what a bigger application looks like with more services and methods.
And how about testing? Well, I already wrote some great tutorials about testing with C#.
For the other question, well my friends… That’s a story for another time!