DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Understanding and Implementing the Repository Pattern in .NET

In modern software development, clean and maintainable code is paramount. The Repository Pattern is one of the design patterns that help achieve this goal by abstracting the data access layer from the rest of the application, making your code more modular, testable, and easy to maintain.

What is the Repository Pattern?

The Repository Pattern is a design pattern that mediates between the domain and data mapping layers, such as Entity Framework (EF), by encapsulating the logic required to access data sources. It provides a centralized place to perform data operations, making it easier to manage and test the data access code.

Key Benefits:

  • Separation of Concerns: Keeps the business logic separate from the data access logic.
  • Testability: Makes it easier to mock and unit test the data access layer.
  • Centralized Data Access: Provides a consistent way to access data across the application.

Implementing the Repository Pattern in .NET

To demonstrate the Repository Pattern in action, we’ll create a simple .NET Console application that performs basic CRUD (Create, Read, Update, Delete) operations on a Product entity.

Step 1: Set Up the Console Application

  1. Create a new .NET Console application:
   dotnet new console -n RepositoryPatternDemo
   cd RepositoryPatternDemo
Enter fullscreen mode Exit fullscreen mode
  1. Add the necessary packages:
   dotnet add package Microsoft.EntityFrameworkCore
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the Entity and DbContext

First, define the Product entity and the AppDbContext:

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace RepositoryPatternDemo
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }

    public class AppDbContext : DbContext
    {
        public DbSet<Product> Products { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=ProductDb;Trusted_Connection=True;");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Create the Repository Interface and Implementation

Define the IRepository interface and implement it in the Repository class:

namespace RepositoryPatternDemo
{
    public interface IRepository<T> where T : class
    {
        Task<T> GetByIdAsync(int id);
        Task<IEnumerable<T>> GetAllAsync();
        Task AddAsync(T entity);
        void Update(T entity);
        void Delete(T entity);
    }

    public class Repository<T> : IRepository<T> where T : class
    {
        protected readonly AppDbContext _context;

        public Repository(AppDbContext context)
        {
            _context = context;
        }

        public async Task<T> GetByIdAsync(int id)
        {
            return await _context.Set<T>().FindAsync(id);
        }

        public async Task<IEnumerable<T>> GetAllAsync()
        {
            return await _context.Set<T>().ToListAsync();
        }

        public async Task AddAsync(T entity)
        {
            await _context.Set<T>().AddAsync(entity);
            await _context.SaveChangesAsync();
        }

        public void Update(T entity)
        {
            _context.Set<T>().Update(entity);
            _context.SaveChanges();
        }

        public void Delete(T entity)
        {
            _context.Set<T>().Remove(entity);
            _context.SaveChanges();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Create the Service Layer

Create a ProductService class that uses the repository to interact with the Product entity:

namespace RepositoryPatternDemo
{
    public class ProductService
    {
        private readonly IRepository<Product> _productRepository;

        public ProductService(IRepository<Product> productRepository)
        {
            _productRepository = productRepository;
        }

        public async Task<Product> GetProductById(int id)
        {
            return await _productRepository.GetByIdAsync(id);
        }

        public async Task<IEnumerable<Product>> GetAllProducts()
        {
            return await _productRepository.GetAllAsync();
        }

        public async Task AddProduct(Product product)
        {
            await _productRepository.AddAsync(product);
        }

        public void UpdateProduct(Product product)
        {
            _productRepository.Update(product);
        }

        public void DeleteProduct(Product product)
        {
            _productRepository.Delete(product);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Implement the Main Method

Finally, implement the main program logic to demonstrate how to use the ProductService to add, retrieve, update, and delete Product entities:

using System;
using System.Threading.Tasks;

namespace RepositoryPatternDemo
{
    class Program
    {
        static async Task Main(string[] args)
        {
            using var context = new AppDbContext();
            var productRepository = new Repository<Product>(context);
            var productService = new ProductService(productRepository);

            // Add a new product
            var newProduct = new Product { Name = "Laptop", Price = 1200.00m };
            await productService.AddProduct(newProduct);
            Console.WriteLine("Product added.");

            // Get all products
            var products = await productService.GetAllProducts();
            Console.WriteLine("Products in the database:");
            foreach (var product in products)
            {
                Console.WriteLine($"- {product.Name} (${product.Price})");
            }

            // Update a product
            newProduct.Price = 1000.00m;
            productService.UpdateProduct(newProduct);
            Console.WriteLine("Product updated.");

            // Get product by ID
            var fetchedProduct = await productService.GetProductById(newProduct.Id);
            Console.WriteLine($"Fetched product: {fetchedProduct.Name} (${fetchedProduct.Price})");

            // Delete a product
            productService.DeleteProduct(fetchedProduct);
            Console.WriteLine("Product deleted.");

            // Check the remaining products
            products = await productService.GetAllProducts();
            Console.WriteLine("Remaining products in the database:");
            foreach (var product in products)
            {
                Console.WriteLine($"- {product.Name} (${product.Price})");
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

The Repository Pattern is a powerful tool in a .NET developer's arsenal, providing a clear and maintainable way to handle data access. By abstracting the data operations, it promotes clean code architecture, making your application easier to manage, extend, and test. This example demonstrates how to implement the Repository Pattern in a simple .NET Console application, showcasing the benefits of a decoupled and testable data access layer.

Top comments (11)

Collapse
 
shauncurtis profile image
Shaun Curtis • Edited

I'm with Bradley. It's an anti-pattern because EF already implements the Repository and Unit of Work patterns. Basically you're adding the repository pattern over the repository pattern. See dev.to/anthonytr/why-you-shouldnt-... and search the net for "IRepository antipattern" to find many discussions on the topic.

Collapse
 
moh_moh701 profile image
mohamed Tayel

Thank you for your thoughtful feedback! I completely understand the point about the redundancy of adding a repository pattern over Entity Framework, especially since EF already implements both the Repository and Unit of Work patterns internally.

You mentioned that this could also impact testability. In that regard, if I were to skip the custom repository pattern and work directly with EF, how would you suggest mocking or unit testing the DbContext? I've found that mocking a custom repository interface is relatively straightforward, but I’d love to hear your thoughts or best practices on mocking DbContext itself in unit tests.

Thanks again for sharing your insights—I'm always looking to improve my approach!

Collapse
 
shauncurtis profile image
Shaun Curtis • Edited

Answering your query properly is beyond the scope of a comment. I'm putting together a working version of what I currently do here - github.com/ShaunCurtis/Blazr.Demo

Collapse
 
bradley_marks_bd38dd76c4e profile image
Bradley Marks

All fine to use the repository pattern in certain scenarios. After 30yrs of development, however, I've long taught it's an anti-pattern.

Better to nest the full logic to a command / event pattern and write a simple adapter or facade over the data layer. The full logic and query syntax (includes, filters, other) are almost always highly unique to that command. Rarely do commands need to be shared. There are ways to accomplish it via command chaining or optional pre/post processing checks but those are almost always avoided in favor of a better design / separate command.

Collapse
 
moh_moh701 profile image
mohamed Tayel

Thank you for sharing your insights!In my article, I focused on the Repository Pattern as it's often a good starting point for developers working on simpler applications or those who are newer to designing data access layer

Collapse
 
mahomudgamalfcis profile image
Mahmoud Elgazzar

thanks for sharing this insightful information about the Reporisotry pattern

Collapse
 
moh_moh701 profile image
mohamed Tayel

you are welcome

Collapse
 
lior__eb24ea49c profile image
Lior Goldemberg • Edited

What is the solution in your opinion to select required fields only but keep using this pattern .? Example- only product id..

Collapse
 
moh_moh701 profile image
mohamed Tayel

use dto like example
public class ProductIdDto
{
public int Id { get; set; }
}

Collapse
 
manuel_jimenez_5666007c39 profile image
Manuel Jimenez

Thanks for the clear explanation.

Collapse
 
moh_moh701 profile image
mohamed Tayel

you are welcome