Part 3: Building a Multi-Layered Architecture in C# .NET Core for a Film/Series Review Application

Kaan Celiker
4 min readOct 30, 2024

Overview

In this part, we’ll set up a multi-layered architecture to organize the application efficiently and ensure scalability. Each layer has a specific responsibility, promoting modularity, reusability, and maintainability in our codebase. The layers we’ll implement are:

  1. Core Layer: Defines the fundamental entities and shared structures.
  2. Data Access Layer (DAL): Manages database access and CRUD operations using the Repository Pattern. It also includes WithSp methods to call Stored Procedures (SPs).
  3. Business Layer: Manages complex business logic and processes data from the Data Access Layer before sending it to the Service Layer.
  4. Service Layer: Acts as a bridge between Controllers and the DAL, handling both SP and non-SP calls asynchronously.
  5. Common Layer: Provides utility functions and helpers for logging, validation, and error handling.
  6. API Layer (Controllers): Exposes endpoints for external access, handling HTTP requests, and responses.

Step 1: Core Layer

The Core Layer is the foundation of the application, containing entity classes that map to our database tables, along with any interfaces and data structures shared across layers. This layer is independent of any business logic or database operations, serving as the backbone of our project.

namespace MovieSeries.CoreLayer.Entities
{
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }
public string Genre { get; set; }
public DateTime ReleaseDate { get; set; }
public string Description { get; set; }
public ICollection<MovieSeriesTag> MovieSeriesTags { get; set; }
}
}
namespace MovieSeries.CoreLayer.Entities
{
public class Review
{
public int Id { get; set; }
public int MovieId { get; set; }
public int UserId { get; set; }
public string ReviewText { get; set; }
public DateTime ReviewDate { get; set; }
}
}

Step 2: Data Access Layer (DAL)

The Data Access Layer (DAL) handles all direct interactions with the database. This layer uses the Repository Pattern to abstract database operations for each entity. We also include WithSp methods here to call stored procedures separately from the standard async CRUD methods.

AppDbContext

AppDbContext manages the connection to the database and defines the entity mappings. We configure relationships and foreign keys in this class, ensuring our database schema aligns with our entity model.

using Microsoft.EntityFrameworkCore;
using MovieSeries.CoreLayer.Entities;

namespace MovieSeries.DataAccessLayer
{
public class AppDbContext : DbContext
{
public DbSet<Movie> Movies { get; set; }
public DbSet<Review> Reviews { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<MovieSeriesTag> MovieSeriesTags { get; set; }

public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MovieSeriesTag>()
.HasKey(mst => new { mst.MovieSeriesId, mst.TagId });

modelBuilder.Entity<MovieSeriesTag>()
.HasOne(mst => mst.Movie)
.WithMany(m => m.MovieSeriesTags)
.HasForeignKey(mst => mst.MovieSeriesId);

modelBuilder.Entity<MovieSeriesTag>()
.HasOne(mst => mst.Tag)
.WithMany(t => t.MovieSeriesTags)
.HasForeignKey(mst => mst.TagId);
}
}
}

Repositories and WithSp Methods

Each repository has methods for both CRUD operations and stored procedure calls, labeled with the WithSp suffix to distinguish them. For example, the MovieRepository might include GetAllMoviesAsync for async CRUD and GetTopRatedMoviesWithSpAsync for an SP call.

public async Task<IEnumerable<Movie>> GetAllMoviesAsync()
{
return await _context.Movies.ToListAsync();
}

public async Task<IEnumerable<Movie>> GetTopRatedMoviesWithSpAsync(int topCount)
{
return await _context.Movies
.FromSqlRaw("EXEC GetTopRatedMovies @top_count = {0}", topCount)
.ToListAsync();
}

Step 3: Business Layer

The Business Layer applies more complex business rules and combines operations from different repositories if needed. This layer centralizes and organizes data transformations and calculations before they are passed to the Service Layer.

Example: Calculating Average Rating

The RatingCalculator class in this layer can calculate the average rating of a movie, encapsulating this calculation as a reusable function.

using MovieSeries.CoreLayer.Entities;
using System.Collections.Generic;
using System.Linq;

namespace MovieSeries.BusinessLayer
{
public static class RatingCalculator
{
public static decimal CalculateAverageRating(IEnumerable<Rating> ratings)
{
if (ratings == null || !ratings.Any())
return 0;

return ratings.Average(r => r.Value);
}
}
}

Step 4: Service Layer

The Service Layer acts as a bridge between Controllers and the DAL. It manages both CRUD and SP calls by calling the appropriate methods in the repository. This layer organizes data received from the DAL before sending it to the Controller layer.

MovieService Example

The MovieService class provides methods for CRUD operations and calls the WithSp methods for stored procedures when needed.

public async Task<IEnumerable<Movie>> GetAllMoviesAsync()
{
return await _movieRepository.GetAllMoviesAsync();
}

public async Task<IEnumerable<Movie>> GetTopRatedMoviesWithSpAsync(int topCount)
{
return await _movieRepository.GetTopRatedMoviesWithSpAsync(topCount);
}

Step 5: Common Layer

The Common Layer contains utility classes for logging, error handling, and validation that can be reused throughout the application. For instance, we might create Logger and ErrorHandler classes here to manage application-wide error handling and logging.

ErrorHandler Example

namespace MovieSeries.CommonLayer.Utilities
{
public static class ErrorHandler
{
public static string GetErrorMessage(Exception ex)
{
return ex.Message;
}
}
}

Step 6: API Layer (Controllers)

The API Layer exposes endpoints to external clients, handling HTTP requests and responses. Each controller is mapped to a specific entity and uses the Service Layer to retrieve or manipulate data.

MovieController Example

The MovieController provides endpoints for CRUD operations as well as SP-based data retrieval.

[HttpGet("{id}")]
public async Task<IActionResult> GetMovie(int id)
{
var movie = await _movieService.GetMovieByIdAsync(id);
if (movie == null) return NotFound();
return Ok(movie);
}

[HttpGet("top-rated/{count}")]
public async Task<IActionResult> GetTopRatedMovies(int count)
{
var movies = await _movieService.GetTopRatedMoviesWithSpAsync(count);
return Ok(movies);
}

Conclusion

In this part, we’ve structured our application with multiple layers, each serving a distinct purpose:

  • Core Layer: Defines entities.
  • Data Access Layer: Manages database operations and stored procedure calls.
  • Business Layer: Encapsulates complex calculations and rules.
  • Service Layer: Connects controllers and the DAL.
  • Common Layer: Provides reusable utility classes.
  • API Layer: Exposes application endpoints.

This layered architecture enables a clean separation of concerns, enhancing maintainability and scalability for our application. In the next part, we will focus on implementing stored procedures and testing their integration in detail.

Next Part

--

--

Kaan Celiker
Kaan Celiker

Written by Kaan Celiker

Assistant Software Test Specialist

No responses yet