Simple Unit of Work with Generic Repository C# and Entity Framework Core

Valentyn Osidach đź“š
3 min readAug 20, 2024

--

Implementing Unit of Work with a Generic Repository in C#

The Unit of Work and Generic Repository patterns are commonly used to create a clean and maintainable data access layer in C#. By using these patterns, developers can simplify database operations management and reduce the boilerplate code associated with Entity Framework or other ORM tools.

What is a Generic Repository?

A GenericRepository a design pattern is used to create a repository that can handle operations for any entity. It abstracts the common CRUD (Create, Read, Update, Delete) operations, providing a generic interface to interact with the data source. This pattern promotes the DRY (Don't Repeat Yourself) principle by reducing boilerplate code and centralizing data access logic.

What is the Unit of Work Pattern?

The Unit of Work pattern is a design pattern that maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems. In simpler terms, it ensures that all database operations are managed in a single transaction, preventing partial updates or inconsistent data.

Key Benefits:

  1. Transaction Management: It ensures that all operations within a transaction are committed or rolled back as a unit.
  2. Consistency: Helps maintain data integrity by ensuring that all changes within a unit of work are saved together.
  3. Separation of Concerns: Encapsulates database logic, making the codebase cleaner and more manageable.

Implementation with Entity Framework Core:

Repository Interface and Implementation:

interface IRepository<TEntity> where TEntity : class, IEntity
{
Task<IEnumerable<TEntity>> GetAllAsync();
Task<TEntity> GetByIdAsync(int id);
Task AddAsync(TEntity entity);
void Update(TEntity entity);
void Delete(TEntity entity);
}

class Repository<TEntity> : IRepository<TEntity>
where TEntity : class, IEntity
{
private readonly DbContext _context;
private readonly DbSet<TEntity> _dbSet;

public Repository(DbContext context)
{
_context = context;
_dbSet = context.Set<TEntity>();
}

public async Task<IEnumerable<TEntity>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}

public async Task<TEntity> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}

public async Task AddAsync(TEntity entity)
{
await _dbSet.AddAsync(entity);
}

public void Update(TEntity entity)
{
_dbSet.Update(entity);
}

public void Delete(TEntity entity)
{
_dbSet.Remove(entity);
}
}

Unit of Work Interface and Implementation:

interface IUnitOfWork : IDisposable
{
Task SaveAsync(CancellationToken cancellationToken = default);
}

class UnitOfWork(Context context) : IUnitOfWork, IDisposable
{
private bool _disposed;
private readonly Context _context = context;

public async Task SaveAsync(CancellationToken cancellationToken)
{
await _context.SaveChangesAsync(cancellationToken);
}

protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_context?.Dispose();
}

_disposed = true;
}
}

~UnitOfWork()
{
Dispose(false);
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}

Registering the Repository with Dependency Injection:

// As Scoped by default
builder.Services.AddDbContext<Context>(options =>
options.UseSqlServer(connectionString));

//it is mandatory to register services in the same LifeTimeScope
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));

How to use:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IUnitOfWork _unitOfWork;
private readonly IRepository<Product> _productRepository;

public ProductsController(IUnitOfWork unitOfWork, IRepository<Product> productRepository)
{
_unitOfWork = unitOfWork;
_productRepository = productRepository;
}

[HttpGet]
public async Task<IActionResult> GetAllProducts()
{
var products = await _productRepository.GetAllAsync();
return Ok(products);
}

[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(int id)
{
var product = await _productRepository.GetByIdAsync(id);
if (product == null)
{
return NotFound();
}
return Ok(product);
}

[HttpPost]
public async Task<IActionResult> CreateProduct(Product product)
{
await _productRepository.AddAsync(product);
await _unitOfWork.SaveAsync();
return Ok(product);
}

[HttpPut("{id}")]
public async Task<IActionResult> UpdateProduct(int id, Product product)
{
if (id != product.Id)
{
return BadRequest();
}

_productRepository.Update(product);
await _unitOfWork.SaveAsync();
return NoContent();
}

[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProduct(int id)
{
var product = await _productRepository.GetByIdAsync(id);
if (product == null)
{
return NotFound();
}

_productRepository.Delete(product);
await _unitOfWork.SaveAsync();
return NoContent();
}
}

Conclusion

In this example, we’ve created a simple ASP.NET Core application that uses the Unit of Work pattern with a Generic Repository. The Unit of Work and Repository classes are registered with a scoped lifetime, ensuring that each HTTP request gets its own instances. The controller demonstrates how to use these patterns to perform CRUD operations in a clean and maintainable way.

--

--

Valentyn Osidach đź“š

I am a software developer from Ukraine with more than 7 years of experience. I specialize in C# and .NET and love sharing my development knowledge. 👨🏻‍💻