Repository Pattern with working sample codes for data manipulation (CRUD)

What’s a Repository Pattern?

A Repository pattern is a design pattern that mediates data from and to the Domain and Data Access Layers ( like Entity Framework Core / Dapper). Repositories are classes that hide the logics required to store or retrieve data. Thus, our application will not care about what kind of ORM we are using, as everything related to the ORM is handled within a repository layer. This allows you to have a cleaner separation of concerns. Repository pattern is one of the heavily used Design Patterns to build cleaner solutions.

Benefits of Repository Pattern

Reduces Duplicate Queries

De-couples the application from the Data Access Layer

Repository with sample codes

What are we going to build?

We are going to build a sample bookstore which you can add books, view details, update and delete books.  Here is the main screen.

CRUD application with entity framework context

Here is the sample application to manage books with basic functions like Create Read Update Delete (CRUD). The first version we are going to use the entity framework context. You can easily create this type of application using scaffolded item function in Visual studio. I’m using visual studio 2022 with .NET 6.0 to create this application. 

We created three classed named Book.cs, Author.cs, Category.cs as our model

public class Book
    {
        public int Id { get; set; }
        [Required]
        [StringLength(100)]
        [Display(Name = "Title")]
        public string Title { get; set; }
        public DateTime Published { get; set; }
        public Author? Author { get; set; }
        public Category? Category { get; set; }
       
        [Display(Name = "Author")]
        public int AuthorId { get; set; }
        [Display(Name = "Category")]
        public int CategoryId { get; set; }
    }

public class Author
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Fullname => $"{FirstName} {LastName}";
    }

Next step, we are going to use scaffolded function in Visual Studio to create Controllers and Views. You can see we are using Linq queries in our controller

var book = _context.Books.Include(b => b.Author).Include(b => b.Category);
            return View(await book.ToListAsync());

While this style of coding is ok, but it can lead to couple of issues:

  • The query is repeated a couple of times in our controller.
  • Everytime we change our logic, we have to update our queries in controllers in a couple of places. It would be much nicer if we can update queries in one place and leave the controller alone. 
  • The database access layer is tight coupled with the controllers. This will cause issues when we would like to change to another database or make it difficult to do unit tests.

Re-factor CRUD application with Repositories

We are going to refactor our codes a bit to fix the issues we mentioned earlier and make the code a bit easier to maintain. We create a folder named Repository where it is going to store our repository classes.

Create IBookRepository.cs interface with basic functionality like GetAll, GetById, Insert, Delete, Save

public interface IBookRepository
	{
		Task<IEnumerable<Book>> GetAll();
		Task<Book> GetById(int id);
		Task<Book> Insert(Book entity);
		Task Delete(int id);
		Task Save();
	}

Create another class named BookRepository.cs to implement the functionalities

public class BookRepository : IBookRepository
	{
		private readonly BookContext context;
		public BookRepository(BookContext context)
		{
			this.context = context;
		}
		public async Task<IEnumerable<Book>> GetAll()
		{
			return await context.Books
								.Include(b => b.Author)
								.Include(b => b.Category)
								.ToListAsync();
		}
		public async Task<Book> GetById(int id)
		{
			return await context.Books.FindAsync(id);
		}
}

The BookRepository implements functions defined in the IBookRepository. All the query will be set up here so in the future, if we would like to change something, just need to update this class.

In the controller, this is how we call the data access from the repository.

From this 

 public async Task<IActionResult> Index()
        {
            var book = _context.Books.Include(b => b.Author).Include(b => b.Category);
            return View(await book.ToListAsync());
        }

Now we call this 

public async Task<IActionResult> Index()
        {
            var books = await bookRepository.GetAll();
            return View(books);
        }

Or from this

 var book = await _context.Books
                .Include(b => b.Author)
                .Include(b => b.Category)
                .FirstOrDefaultAsync(m => m.Id == id);

Now we call this

var book = await bookRepository.GetById((int)id);

Now the code is tidier and shorter. Continue doing the same with Author and Category we will end up with IAuthorRepository, AuthorRepository, ICategoryRepository, CategoryRepository.

Please don’t forget to add your interface and repository in Program.cs class in .NET 6.0. In .NET 3.1, you need to add it in Startup.cs with a little different code.

builder.Services.AddScoped<IBookRepository, BookRepository>();

builder.Services.AddScoped<IAuthorRepository, AuthorRepository>();

We can remove the context at this point as we don’t need it anymore. However, I keep it for now and will remove it in the next refactoring.

Now we already have our bookstore application with Repository Pattern. The logic is not tight-coupled in the controller anymore, the code is tidier and if we would like to change anything, we just need to change from the Repository classes in one place. However, we notice that our interface seems duplicated and doing the same thing. Example IBookRepository and IAuthorRepository are pretty much the same. Probably there is a way to make it generic to make the code even shorter. 

public interface IBookRepository
	{
		Task<IEnumerable<Book>> GetAll();
		Task<Book> GetById(int id);
		Task<Book> Insert(Book entity);
		Task Delete(int id);
		Task Save();
	}
public interface IAuthorRepository
	{
		Task<IEnumerable<Author>> GetAll();
		Task<Author> GetById(int id);
		Task<Author> Insert(Author entity);
		Task Delete(int id);
		Task Save();
	}

Re-factor CRUD application with generic interface 

For starting, we will need to create a generic interface named IRepository. Basically, I will copy the code from IBookRepository and change it to using generic type

public interface IBookRepository
	{
		Task<IEnumerable<Book>> GetAll();
		Task<Book> GetById(int id);
		Task<Book> Insert(Book entity);
		Task Delete(int id);
		Task Save();
	}


    public interface IRepository<T1, T2> where T1: class
	{
		Task<IEnumerable<T1>> GetAll();
		Task<T1> GetById(T2 id);
		Task<T1> Insert(T1 entity);
		Task Delete(T2 id);
		Task Save();
	}

Delete the IBookRepository cause we don’t need it anymore. Change the program.cs from 

builder.Services.AddScoped<IBookRepository, BookRepository>(); to use builder.Services.AddScoped<IRepository<Book, int>, BookRepository>();

Change BookRepository.cs and BooksController.cs to use the new generic interface.

Now rerun the program, you will the the program still function the same way

Do the same thing with IAuthorRepository, AuthorRepository, ICategoryRepository, CategoryRepository. We will have the program using a generic interface with a repository pattern.

Conclusion and source code

We have been walking through refactoring code to use the repository pattern and discuss its benefits. It is one of the most popular patterns you can see in any program. 

Here are source code which I develop and use in the sample:

To run the sample code, you will need to use NET 6.0. I’m using Visual Studio 2022. I hope you enjoy it!

Happy coding!

Ben Binh Q Nguyen


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *