From Microsoft Web Site:
“The repository and unit of work patterns are intended to create an abstraction layer between the data access layer and the business logic layer of an application. Implementing these patterns can help insulate your application from changes in the data store and can facilitate automated unit testing or test-driven development (TDD)“.
In this post, we will see how to implement a Repository Pattern and Unit of Work in a Console Application using EF core Code First.
We start to open Visual Studio 2019, create a Blank Solution called “RepositoryPatternProject” and we add a Class Library project called “BE_RPProject”.
This project will be our Entity Layer, where we will define the two Entities used in the project:
[AUTHOR.CS]
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BE_RPProject
{
public class Author
{
// define the key as an Identity
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int AuthorId { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public ICollection<Book> Books { get; set; }
}
}
[BOOK.CS]
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BE_RPProject
{
public class Book
{
// define the key as an Identity
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int BookId { get; set; }
public string Title { get; set; }
public int AuthorId { get; set; }
public Author Author { get; set; }
}
}
Now, we create a Class Library project, called “DAL_RPProject”, that will be our Data Layer where we will define the DataContext, Repository Pattern and the Unit of Work.
First of all, we define the Data Context class called “RPContext”:
[RPCONTEXT.CS]
using BE_RPProject;
using Microsoft.EntityFrameworkCore;
namespace DAL_RPProject
{
public class RPContext : DbContext
{
public DbSet<Book> Books { get; set; }
public DbSet<Author> Authors { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// It would be better put the connection string in a Config file.
optionsBuilder.UseSqlServer(@"Server=.\;Database=RPDataBase;Trusted_Connection=True;MultipleActiveResultSets=true");
}
}
}
In order to use the method “UseSqlServer”, we have to install the package Microsoft.EntityFrameworkCore.SqlServer, with the command
Install-Package Microsoft.EntityFrameworkCore.SqlServer, in the Package Manager Console
Now, we write the Interfaces used to define the Generic Repository (for the common methods) and the Repository for Author and Book:
[IGENERICREPOSITORY.CS]
using System.Linq;
using System.Threading.Tasks;
namespace DAL_RPProject.RepositoryPattern.Interface
{
public interface IGenericRepository<TEntity> where TEntity : class
{
IQueryable<TEntity> GetAll();
Task<TEntity> GetById(int id);
Task Create(TEntity entity);
void Update(TEntity entity);
Task<bool> Delete(int id);
}
}
[IAUTHORREPOSITORY.CS]
using BE_RPProject;
using System.Threading.Tasks;
namespace DAL_RPProject.RepositoryPattern.Interface
{
public interface IAuthorRepository
{
Task<bool> DeleteAuthor(int id);
Task InsertAuthor(Author objAuthor);
}
}
[IBOOKREPOSITORY.CS]
using BE_RPProject;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace DAL_RPProject.RepositoryPattern.Interface
{
public interface IBookRepository
{
IEnumerable<Book> GetBooksWithPaging(int pageIndex, int pageSize);
IEnumerable<Book> GetAllBooksWithoutAuthor();
IEnumerable<Book> GetAllBooks();
Task InsertBook(Book objBook);
}
}
Now, we define the three Repositories:
[GENERICREPOSITORY.CS]
using DAL_RPProject.RepositoryPattern.Interface;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace DAL_RPProject.RepositoryPattern.Class
{
public abstract class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
{
private readonly RPContext context;
public GenericRepository(RPContext dbContext)
{
this.context = dbContext;
}
public async Task Create(TEntity entity)
{
await context.Set<TEntity>().AddAsync(entity);
}
public async Task<bool> Delete(int id)
{
var objEntity = await GetById(id);
context.Set<TEntity>().Remove(objEntity);
return true;
}
public IQueryable<TEntity> GetAll()
{
// With AsNoTracking extension it will work faster and prevent any updates to this specific IQueryable collection.
return context.Set<TEntity>().AsNoTracking();
}
public async Task<TEntity> GetById(int id)
{
return await context.Set<TEntity>().FindAsync(id);
}
public void Update(TEntity entity)
{
context.Set<TEntity>().Update(entity);
}
}
}
[AUTHORREPOSITORY.CS]
using BE_RPProject;
using DAL_RPProject.RepositoryPattern.Interface;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace DAL_RPProject.RepositoryPattern.Class
{
public class AuthorRepository : GenericRepository<Author>, IAuthorRepository
{
private RPContext _context;
public AuthorRepository(RPContext context) : base(context)
{
_context = context;
}
public async Task InsertAuthor(Author objAuthor)
{
await Create(objAuthor);
}
public async Task<bool> DeleteAuthor(int id)
{
var objAuthor = await GetById(id);
var countBook = _context.Set<Book>().Where(x => x.AuthorId == objAuthor.AuthorId).CountAsync();
if (countBook.Result == 0)
{
_context.Set<Author>().Remove(objAuthor);
return true;
}
else
{
return false;
}
}
}
}
[BOOKREPOSITORY.CS]
using BE_RPProject;
using DAL_RPProject.RepositoryPattern.Interface;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DAL_RPProject.RepositoryPattern.Class
{
public class BookRepository : GenericRepository<Book>, IBookRepository
{
private RPContext _context;
public BookRepository(RPContext context) : base(context)
{
_context = context;
}
public IEnumerable<Book> GetBooksWithPaging(int pageIndex, int pageSize)
{
return _context.Set<Book>().AsNoTracking().OrderBy(x => x.Title).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
}
public IEnumerable<Book> GetAllBooksWithoutAuthor()
{
return GetAll().ToList();
}
public IEnumerable<Book> GetAllBooks()
{
return _context.Set<Book>()
.Include(c => c.Author)
.OrderBy(c => c.Title)
.ToList();
}
public async Task InsertBook(Book objBook)
{
await Create(objBook);
}
}
}
UNIT OF WORK
The unit of work class serves one purpose: to make sure that when you use multiple repositories, they share a single database context.
That way, when a unit of work is complete you can call the SaveChanges method on that instance of the context and be assured that all related changes will be coordinated.
All that the class needs is a Save method and a property for each repository. Each repository property returns a repository instance that has been instantiated using the same database context instance as the other repository instances.
We define our Unit of Work:
[IUNITOFWORK.CS]
using DAL_RPProject.RepositoryPattern.Interface;
using System;
using System.Threading.Tasks;
namespace DAL_RPProject.UnitOfWork
{
interface IUnitOfWork : IDisposable
{
IBookRepository Books { get; }
IAuthorRepository Authors { get; }
Task<int> Save();
}
}
[UNITOFWORK.CS]
using DAL_RPProject.RepositoryPattern.Class;
using DAL_RPProject.RepositoryPattern.Interface;
using System.Threading.Tasks;
namespace DAL_RPProject.UnitOfWork
{
public class UnitOfWork : IUnitOfWork
{
private readonly RPContext _context;
public UnitOfWork(RPContext context)
{
_context = context;
Books = new BookRepository(_context);
Authors = new AuthorRepository(_context);
}
public IBookRepository Books { get; private set; }
public IAuthorRepository Authors { get; private set; }
public async Task<int> Save()
{
return await _context.SaveChangesAsync();
}
public void Dispose()
{
_context.Dispose();
}
}
}
Now, after the installation of Microsoft.EntityFrameworkCore.Tools and Microsoft.EntityFrameworkCore.Design, we can run
the first Migration in the Package Manger Console:
Install-Package Microsoft.EntityFrameworkCore.Tools
Install-Package Microsoft.EntityFrameworkCore.Design.
Add-Migration Initial
Update-Database
We can verify that everything went fine, opening SSMS and checking the DB creation:
Before to create our Business Layer, we will create another Class Library project, called “BEUI_RPProject“, that it will be the Entity UI Layer:
[AUTHORUI.CS]
namespace BEUI_RPProject
{
public class AuthorUI
{
public int IdAuthor { get; set; }
public string AuthorName { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
}
}
[BOOKUI.CS]
namespace BEUI_RPProject
{
public class BookUI
{
public int IdBook { get; set; }
public int IdAuthor { get; set; }
public string Author { get; set; }
public string Title { get; set; }
}
}
Now, we create a Class Library project called “BLL_RPProject”, where we will define our Business layer:
[IAUTHORCORE.CS]
using BEUI_RPProject;
using System.Threading.Tasks;
namespace BLL_RPProject.Interface
{
public interface IAuthorCore
{
Task InsertAuthor(AuthorUI objAuthorUI);
}
}
[IBOKCORE.CS]
using BEUI_RPProject;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace BLL_RPProject.Interface
{
public interface IBookCore
{
List<BookUI> GetListBook();
Task InsertBook(BookUI objBookUI);
}
}
[AUTHORCORE.CS]
using BE_RPProject;
using BEUI_RPProject;
using BLL_RPProject.Interface;
using DAL_RPProject;
using DAL_RPProject.UnitOfWork;
using System.Threading.Tasks;
namespace BLL_RPProject.Class
{
public class AuthorCore : IAuthorCore
{
public async Task InsertAuthor(AuthorUI objAuthorUI)
{
using (var unitOfWork = new UnitOfWork(new RPContext()))
{
Author objAuthor = new Author { Name = objAuthorUI.Name, Surname = objAuthorUI.Surname };
await unitOfWork.Authors.InsertAuthor(objAuthor);
await unitOfWork.Save();
objAuthorUI.IdAuthor = objAuthor.AuthorId;
}
}
}
}
[BOOKCORE.CS]
using BE_RPProject;
using BEUI_RPProject;
using BLL_RPProject.Interface;
using DAL_RPProject;
using DAL_RPProject.UnitOfWork;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace BLL_RPProject.Class
{
public class BookCore : IBookCore
{
public List<BookUI> GetListBook()
{
List<BookUI> lstBooksUI = new List<BookUI>();
using (var unitOfWork = new UnitOfWork(new RPContext()))
{
var lstBooks = unitOfWork.Books.GetAllBooks();
foreach (var item in lstBooks)
{
lstBooksUI.Add(new BookUI { IdBook = item.BookId, IdAuthor = item.AuthorId, Title = item.Title, Author = $"{item.Author.Name} {item.Author.Surname}" });
}
}
return lstBooksUI;
}
public async Task InsertBook(BookUI objBookUI)
{
using (var unitOfWork = new UnitOfWork(new RPContext()))
{
Book objBook = new Book { AuthorId = objBookUI.IdAuthor, Title = objBookUI.Title };
await unitOfWork.Books.InsertBook(objBook);
await unitOfWork.Save();
}
}
}
}
Finally, we create our Console Application called “UI_RPProject”:
using BEUI_RPProject;
using BLL_RPProject.Class;
using System;
using System.Threading.Tasks;
namespace UI_RPProject
{
class Program
{
static async Task Main(string[] args)
{
// Define variable
AuthorCore objAuthorCore = new AuthorCore();
AuthorUI objAuthorUI = new AuthorUI { Name = "Alessandro", Surname = "Manzoni" };
BookCore objBookCore = new BookCore();
// Insert Author
await InsertAuthor(objAuthorCore, objAuthorUI);
// Insert Books
BookUI objBook1 = new BookUI { IdAuthor = objAuthorUI.IdAuthor, Title = "I Promessi Sposi" };
BookUI objBook2 = new BookUI { IdAuthor = objAuthorUI.IdAuthor, Title = "Adelchi" };
await InsertBook(objBookCore, objBook1);
await InsertBook(objBookCore, objBook2);
// List of Books
var lstBooks = objBookCore.GetListBook();
foreach(var item in lstBooks)
{
Console.WriteLine($"Book title:{item.Title} Author:{item.Author}");
}
Console.ReadKey();
}
static async Task InsertAuthor(AuthorCore objAuthorCore, AuthorUI objAuthor)
{
await objAuthorCore.InsertAuthor(objAuthor);
}
static async Task InsertBook(BookCore objBookCore, BookUI objBook)
{
await objBookCore.InsertBook(objBook);
}
}
}
If we run the application, this will be the result:
and in the DB we will have these data: