Intro
In this article we will be exploring Unit of work and Repository pattern with EF Core and .Net 5.
You can watch the full video on Youtube:
Please find the source code on GitHub:
https://github.com/mohamadlawand087/v33-repo-uow
So what we will cover today:
- What is Repository Pattern
- Why do we want to use Repository Pattern
- What is Unit of Work (UoW)
- Benefits of UoW
- Ingredients & Accounts
- Code and Implementations
As always you will find the source code in the description down below. Please like, share and subscribe if you like the video. It will really help the channel
What is a Repository Pattern
The repository pattern is talked about a lot, especially in the API-and-microservice-heavy world that .net core shines in.
The repository pattern is a strategy for abstracting data access layer. So what is a data layer? it is made up of the code in the application that is responsible of storing and retrieving the data.
Adding, removing, updating, and selecting items from this collection is done through a series of straightforward methods, without the need to deal with database concerns like connections, commands, cursors, or readers. Using this pattern can help achieve loose coupling and can keep domain objects persistence ignorant.
Why use Repository Pattern
There are many reasons why we want to use code absatractions
- Reduce code duplication: it will allow us to use the DRY design principle, where we write the code once and we can utilise it anywhere we want in our code
- loose coupling to underlying persistance technology: in case we need to switch our database from MSSQL to PostgreSQL. Only on the data layer implementation changes will need to be made, not where we are consuming the data access layer. This will facilitate the changes, reduce the chance of errors.
- Testability is much more easier, Repository pattern will allow us to mock our database so we can perform our tests
- Separation of Concerns: seperate application functionalities based on function, which facilitates evolving and maintaining the code.
What is Unit of Work (UoW)
If the Repository pattern is our abstraction over the idea of persistent storage, the Unit of Work (UoW) pattern is our abstraction over the idea of atomic operations. It will allow us to finally and fully decouple our service layer from the data layer.
The unit of work pattern now manages the database states. Once all updates of the entities in a scope are completed, the tracked changes are played onto the database in a transaction so that the database reflects the desired changes.
Thus, the unit of work pattern tracks a business transaction and translates it into a database transaction, wherein steps are collectively run as a single unit. To ensure that data integrity is not compromised, the transaction commits or is rolled back discretely, thus preventing indeterminate state.
Benefits of Unit of Work (UoW)
- Abstract Data Access Layer and Business Access Layer from the Application.
- Manage in-memory database operations and later saves in-memory updates as one transaction into database.
- Facilitates to make the layers loosely-coupled using dependency injection.
- Facilitates to follow unit testing or test-driven development (TDD).
Ingredients
- VS Code (https://code.visualstudio.com/download)
- Dotnet 5 SDK (https://dotnet.microsoft.com/download)
Code time
We will start by checking our dotnet SDK
dotnet --version
Now we need to install the entity framework tool
dotnet tool install --global dotnet-ef
Now we need to create our application
dotnet new webapi -n "PocketBook"
Once the application is created we navigate to our source code in Vs Code, the first thing we do is check that the application build successfully.
We open the terminal if you don't see it open go to View ⇒ Terminal
dotnet build
dotnet run
Now we need to add the required packages to utilise SQLLite and Entity Framework Core
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Once we add the packages we need to update the appsettings.json to include the connection string to our database
"ConnectionStrings": {
"DefaultConnection": "DataSource=app.db;Cache=Shared"
}
We will start by cleaning up our application from some of the boiler plate code that has been created. We need to delete the following files
- WeatherForecast.cs
- Controllers/WeatherForecastController.cs
After the clean up we will start by creating our ApplicationDbContext. We need to create a Data folder in he root directory, and then will create the ApplicationDbContext class
using Microsoft.EntityFrameworkCore;
namespace PocketBook.Data
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
}
Once we add our ApplicationDbContext we need to update the startup class to utilise the DbContext
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
We will continue by creating our Models, inside our root folder directory. Inside the Models folder we will create a new class called User
public class User
{
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
Now we need to add our Model to the application DbContext by adding the code below
public class ApplicationDbContext : DbContext
{
// The DbSet property will tell EF Core tha we have a table that needs to be created
public virtual DbSet<User> Users { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
// On model creating function will provide us with the ability to manage the tables properties
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
Once we update the ApplicationDbContext we need to create a new migration script to prepare the EF Core to update our database
dotnet ef migrations add "Initial migration and Adding the User table"
dotnet ef database update
After the database update is completed, we can check our sqlite db with the SQLite browser, we can see that the table has been created for us.
Now we need to start by creating our repositories. Inside the root directory of our application let us create a new folder called Core, inside the core folder will create another folder called IRepositories. Inside the IRepositories folder will create a new interface called IGenericRepository and we populate the interface as following
public interface IGenericRepository<T> where T : class
{
Task<IEnumerable<T>> All();
Task<T> GetById(Guid id);
Task<bool> Add(T entity);
Task<bool> Delete(Guid id);
Task<bool> Upsert(T entity);
Task<IEnumerable<T>> Find(Expression<Func<T, bool>> predicate);
}
Now we need a new interface called IUserRepository
public interface IUserRepository : IGenericRepository<User>
{
}
Now inside the Core folder we need to create a new folder called IConfiguration where the UoW configs will be. In side the IConfiguration we need to create an interface called IUnitOfWork
public interface IUnitOfWork
{
IUserRepository Users { get; }
Task CompleteAsync();
}
Then we need to create a Repository folder inside the Core folder, inside the Repository folder we need to create GenericRepository class and utilise it as follow
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
protected ApplicationDbContext context;
internal DbSet<T> dbSet;
public readonly ILogger _logger;
public GenericRepository(
ApplicationDbContext context,
ILogger logger)
{
this.context = context;
this.dbSet = context.Set<T>();
_logger = logger;
}
public virtual async Task<T> GetById(Guid id)
{
return await dbSet.FindAsync(id);
}
public virtual async Task<bool> Add(T entity)
{
await dbSet.AddAsync(entity);
return true;
}
public virtual Task<bool> Delete(Guid id)
{
throw new NotImplementedException();
}
public virtual Task<IEnumerable<T>> All()
{
throw new NotImplementedException();
}
public async Task<IEnumerable<T>> Find(Expression<Func<T, bool>> predicate)
{
return await dbSet.Where(predicate).ToListAsync();
}
public virtual Task<bool> Upsert(T entity)
{
throw new NotImplementedException();
}
}
Then we need to create our UserRepository in the Repository folder as well
public class UserRepository : GenericRepository<User>, IUserRepository
{
public UserRepository(ApplicationDbContext context, ILogger logger) : base(context, logger) { }
public override async Task<IEnumerable<User>> All()
{
try
{
return await dbSet.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "{Repo} All function error", typeof(UserRepository));
return new List<User>();
}
}
public override async Task<bool> Upsert(User entity)
{
try
{
var existingUser = await dbSet.Where(x => x.Id == entity.Id)
.FirstOrDefaultAsync();
if (existingUser == null)
return await Add(entity);
existingUser.FirstName = entity.FirstName;
existingUser.LastName = entity.LastName;
existingUser.Email = entity.Email;
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "{Repo} Upsert function error", typeof(UserRepository));
return false;
}
}
public override async Task<bool> Delete(Guid id)
{
try
{
var exist = await dbSet.Where(x => x.Id == id)
.FirstOrDefaultAsync();
if (exist == null) return false;
dbSet.Remove(exist);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "{Repo} Delete function error", typeof(UserRepository));
return false;
}
}
}
Once the Repository has been created now we need to create our UnitofWork class inside the Data folder.
public class UnitOfWork : IUnitOfWork, IDisposable
{
private readonly ApplicationDbContext _context;
private readonly ILogger _logger;
public IUserRepository Users { get; private set; }
public UnitOfWork(ApplicationDbContext context, ILoggerFactory loggerFactory)
{
_context = context;
_logger = loggerFactory.CreateLogger("logs");
Users = new UserRepository(context, _logger);
}
public async Task CompleteAsync()
{
await _context.SaveChangesAsync();
}
public void Dispose()
{
_context.Dispose();
}
}
Now that the Unit of work is created we need to update the startup class, so it will be injected in our dependency injection framework. To do this we need to go to the startup class in the root folder and add the following in the ConfigureServices method.
services.AddScoped<IUnitOfWork, UnitOfWork>();
Now let us create our controller inside our controller folder, create a new class called UsersController.cs
[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
private readonly ILogger<UsersController> _logger;
private readonly IUnitOfWork _unitOfWork;
public UsersController(
ILogger<UsersController> logger,
IUnitOfWork unitOfWork)
{
_logger = logger;
_unitOfWork = unitOfWork;
}
[HttpGet]
public async Task<IActionResult> Get()
{
var users = await _unitOfWork.Users.All();
return Ok(users);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetItem(Guid id)
{
var item = await _unitOfWork.Users.GetById(id);
if(item == null)
return NotFound();
return Ok(item);
}
[HttpPost]
public async Task<IActionResult> CreateUser(User user)
{
if(ModelState.IsValid)
{
user.Id = Guid.NewGuid();
await _unitOfWork.Users.Add(user);
await _unitOfWork.CompleteAsync();
return CreatedAtAction("GetItem", new {user.Id}, user);
}
return new JsonResult("Somethign Went wrong") {StatusCode = 500};
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateItem(Guid id, User user)
{
if(id != user.Id)
return BadRequest();
await _unitOfWork.Users.Upsert(user);
await _unitOfWork.CompleteAsync();
// Following up the REST standart on update we need to return NoContent
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteItem(Guid id)
{
var item = await _unitOfWork.Users.GetById(id);
if(item == null)
return BadRequest();
await _unitOfWork.Users.Delete(id);
await _unitOfWork.CompleteAsync();
return Ok(item);
}
}
Top comments (2)
Hey @moe23
Thanks for putting up this nice Article. Just one suggestion though. I believe we may need to avoid using the new operator inside the constructor of the UnitOfWork to make if more unit testable.
What are reasons why you should NOT use Unit of Work? Everything has tradeoffs, right?
Would have been nice to mention that in this blog post