DEV Community

Gaurav
Gaurav

Posted on

.NET Core MVC Project Structure : Implementing a Generic Service and Repository Pattern

In this post, we'll dive into how the Unit of Work and Repository Pattern simplify database access in .NET Core MVC applications. These patterns promote clean architecture, reusability, and a clear separation of concerns. Let's explore their implementation through an example using BaseService, GenericRepository, and UnitOfWork with a UserMasterService.

Why Use the Repository and Unit of Work Pattern?
Repository Pattern: Encapsulates data access logic, keeping it clean and isolated. It serves as a mediator between the data access layer and the business logic layer.
Unit of Work Pattern: Ensures consistency across multiple repositories during transactions. By grouping operations under a single transaction, it simplifies error handling and rollback processes.

Repository Pattern: Encapsulates data access logic, making the codebase cleaner and easier to maintain.
Unit of Work Pattern: Ensures consistency across transactions by coordinating changes to multiple repositories.

Image description

1. BaseService

Purpose: A generic service layer that provides CRUD operations for any entity type inheriting from AuditableEntity. This promotes code reuse and consistency across the application.

Key Methods:

Create: Adds a new entity to the database. If the entity is AuditableEntity, it automatically sets audit properties like creation date.
Retrieve: Fetches all records for an entity type from the database with AsNoTracking for better performance on read-only queries.
RetrieveByID: Retrieves a single entity by its ID using the repository.
Update: Finds an existing entity by ID, updates its properties, and saves changes.
Delete: Performs a soft delete by setting the IsDeleted flag, ensuring data is not permanently removed.

using manage_my_assets.App;
using manage_my_assets.Models;
using manage_my_assets.Service.Interface;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace manage_my_assets.Service.Implementation
{
    public class BaseService<T> : IBaseService<T> where T : AuditableEntity
    {
        private readonly IUnitOfWork _unitOfWork;
        private readonly AppDbContext _dbContext;

        public BaseService(AppDbContext dbContext, IUnitOfWork unitOfWork)
        {
            _dbContext = dbContext;
            _unitOfWork = unitOfWork;
        }

        public async Task<T> Create(T entity)
        {
            if (entity is AuditableEntity auditableEntity)
            {
                auditableEntity.SetAuditableProperties();
            }

            await _unitOfWork.Repository<T>().Insert(entity);
            await _unitOfWork.SaveChangesAsync();
            return entity;
        }

        public async Task<bool> Delete(int id)
        {
            var entity = await _unitOfWork.Repository<T>().GetById(id);
            if (entity == null) return false;

            _unitOfWork.Repository<T>().Delete(entity);
            await _unitOfWork.SaveChangesAsync();
            return true;
        }

        public async Task<List<T>> Retrieve()
        {
            return await _dbContext.Set<T>().AsNoTracking().ToListAsync();
        }

        public async Task<T> RetrieveByID(int id)
        {
            return await _unitOfWork.Repository<T>().GetById(id);
        }

        public async Task<T> Update(int id, T entity)
        {
            var existingEntity = await _unitOfWork.Repository<T>().GetById(id);
            if (existingEntity == null) return null;

            if (existingEntity is AuditableEntity auditableEntity)
            {
                auditableEntity.UpdateAuditableProperties();
            }

            // Assuming a method to update entity properties from the provided entity
            _unitOfWork.Repository<T>().Update(existingEntity);
            await _unitOfWork.SaveChangesAsync();

            return existingEntity;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

2. GenericRepository

Purpose: A reusable repository that handles data access operations such as Insert, Update, Delete, and GetAll for any entity.

CRUD Operations:

GetAll: Fetches all non-deleted records.
Insert: Adds a new entity to the database.
Update: Updates an entity’s state in the database.
GetById: Retrieves an entity by ID, ensuring it hasn’t been soft deleted.

using manage_my_assets.App;
using manage_my_assets.Models;
using manage_my_assets.Service.Interface;
using Microsoft.EntityFrameworkCore;

namespace manage_my_assets.Service.Implementation
{
    public class GenericRepository<T> : IGenericRepository<T> where T : AuditableEntity
    {
        protected readonly AppDbContext _dbContext;

        public GenericRepository(AppDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public T Delete(T entity)
        {
            //_dbContext.Set<T>().Remove(entity); //hard delete
            entity.IsDeleted = true; //soft delete
            _dbContext.Set<T>().Update(entity);
            return entity;
        }

        public async Task<IEnumerable<T>> GetAll()
        {
            return await _dbContext.Set<T>().Where(x => x.IsDeleted == false).ToListAsync();
        }

        public async Task<T> GetById(int id)
        {
            var response = await _dbContext.Set<T>().FindAsync(id);
            if (response.IsDeleted) //check for soft delete
                return null;
            return response;
        }

        public async Task<T> Insert(T entity)
        {
            var res = typeof(T);

            await _dbContext.Set<T>().AddAsync(entity);
            return entity;
        }

        public T Update(T entity)
        {
            _dbContext.Set<T>().Update(entity);
            return entity;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

3. UnitOfWork

Purpose: Orchestrates transactions across multiple repositories, ensuring a consistent and reliable database state.

using manage_my_assets.App;
using manage_my_assets.Models;
using manage_my_assets.Service.Interface;

namespace manage_my_assets.Service.Implementation
{
    public class UnitOfWork : IUnitOfWork
    {
        protected readonly AppDbContext _dbContext;
        private readonly IDictionary<Type, dynamic> _repositories;

        public UnitOfWork(AppDbContext dbContext)
        {
            _repositories = new Dictionary<Type, dynamic>();
            _dbContext = dbContext;
        }

        public IGenericRepository<T> Repository<T>() where T : AuditableEntity
        {
            var entityType = typeof(T);

            if (_repositories.ContainsKey(entityType))
            {
                return _repositories[entityType];
            }

            var repositoryType = typeof(GenericRepository<>);

            var repository = Activator.CreateInstance(repositoryType.MakeGenericType(typeof(T)), _dbContext);

            if (repository == null)
            {
                throw new NullReferenceException("Repository should not be null");
            }

            _repositories.Add(entityType, repository);

            return (IGenericRepository<T>)repository;
        }

        public async Task<int> SaveChangesAsync()
        {
            return await _dbContext.SaveChangesAsync();
        }

        public async Task RollBackChangesAsync()
        {
            await _dbContext.Database.RollbackTransactionAsync();
        }


    }
}

Enter fullscreen mode Exit fullscreen mode

Dependency Injection in Program.cs

using manage_my_assets.App;
using manage_my_assets.Service.Implementation;
using manage_my_assets.Service.Interface;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

builder.Services.AddDbContext<AppDbContext>(x=>x.UseSqlServer(builder.Configuration.GetConnectionString("ManagerConnectionString")));

builder.Services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped(typeof(IBaseService<>), typeof(BaseService<>));

builder.Services.AddScoped<IUserMasterService, UserMasterService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/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.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Enter fullscreen mode Exit fullscreen mode

Integration in Application

public class ProductController : Controller
{
    private readonly IUnitOfWork _unitOfWork;

    public ProductController(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public async Task<IActionResult> Index()
    {
        var products = await _unitOfWork.Repository<Product>().GetAll();
        return View(products);
    }

    [HttpPost]
    public async Task<IActionResult> AddProduct(Product product)
    {
        await _unitOfWork.Repository<Product>().Insert(product);
        await _unitOfWork.SaveChangesAsync();
        return RedirectToAction("Index");
    }
}

Enter fullscreen mode Exit fullscreen mode

Summary of Flow
Services like UnitOfWork, BaseService, and repositories are registered in the DI container.
Controllers or services request these dependencies through constructor injection.
The UnitOfWork is used to coordinate repository operations and manage transactions.
The application pipeline ensures a structured flow for requests, middleware, and responses.

Conclusion

Implementing the Unit of Work and Repository Pattern in an ASP.NET Core MVC application provides a modular, maintainable, and testable architecture. Here's why this approach is effective:
Centralized Data Access: The UnitOfWork ensures a single entry point for managing multiple repositories, making it easier to coordinate operations across multiple data entities.
Separation of Concerns: By abstracting data access logic into repositories and services, this design keeps your controllers focused on application flow and user interactions.
Consistency and Reusability:
The generic repository pattern eliminates redundant code by providing reusable CRUD operations.
Soft delete mechanisms (as seen in GenericRepository) ensure data consistency while preserving historical records.
Transaction Management: The UnitOfWork ensures that database changes across repositories are saved or rolled back as a single unit, enhancing data integrity.
Scalability: As the application grows, new entities and business logic can be easily added without disrupting the existing structure.
Ease of Testing: Dependency injection of repositories and UnitOfWork allows for mocking during unit tests, ensuring testable and reliable code.
By adopting this structure, your application gains a robust foundation that streamlines development, reduces redundancy, and improves code readability. This design pattern is ideal for scalable applications with complex business logic and multiple data entities.

Connect with me:@ LinkedIn

Top comments (0)