Skip to main content

Unit of Work

The Unit of Work pattern maintains a list of objects affected by a business transaction and coordinates writing out changes and resolving concurrency problems. It ensures that multiple repository operations are treated as a single transaction.

Official Definition​

The Unit of Work pattern maintains a list of objects affected by a business transaction and coordinates writing out changes. It tracks changes to objects during a business transaction and coordinates writing out changes as a single operation.

Usage​

Implement by creating a class that manages multiple repositories and provides a single commit method. Often used with Entity Framework's DbContext, which already implements the Unit of Work pattern internally.

public interface IUnitOfWork : IDisposable
{
Task<int> SaveChangesAsync();
Task BeginTransactionAsync();
Task CommitTransactionAsync();
Task RollbackTransactionAsync();
}

Use Cases​

  • Coordinating multiple repository operations in a single transaction
  • Ensuring data consistency across multiple entities
  • Implementing complex business operations that span multiple aggregates
  • Managing database transactions at the application level
  • Batch processing multiple changes efficiently
  • Implementing saga patterns in microservices

When to Use vs When Not to Use​

Use Unit of Work when:​

  • Operations span multiple repositories or entities
  • Need explicit transaction control
  • Implementing complex business workflows
  • Coordinating changes across multiple bounded contexts
  • Building systems with strict consistency requirements
  • Need to optimize database round trips

Don't use Unit of Work when:​

  • Using Entity Framework DbContext directly (it's already a UoW)
  • Operations are simple and single-entity focused
  • Over-engineering simple CRUD operations
  • Team lacks understanding of transaction concepts
  • Performance overhead is not justified

Market Alternatives and Market Adoption​

Entity Framework Core's DbContext implements the Unit of Work pattern internally. Alternatives include manual transaction management, distributed transaction coordinators, and message-based eventual consistency patterns. Many modern applications use DbContext directly rather than implementing additional UoW abstractions.

Pros and Cons​

Pros:​

  • Ensures transactional consistency
  • Optimizes database round trips
  • Clear transaction boundaries
  • Supports complex business operations
  • Enables rollback of multiple changes
  • Coordinates multiple repositories

Cons:​

  • Adds complexity to simple operations
  • Can create tight coupling between entities
  • Memory overhead for tracking changes
  • Potential for long-running transactions
  • May duplicate EF Core functionality
  • Requires careful lifecycle management

Sample Usage​

// Package reference: Microsoft.EntityFrameworkCore
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;

// Domain models
public class Guest
{
public int Id { get; set; }
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}

public class Booking
{
public int Id { get; set; }
public int GuestId { get; set; }
public int RoomId { get; set; }
public DateTime CheckInDate { get; set; }
public DateTime CheckOutDate { get; set; }
public decimal TotalAmount { get; set; }
public Guest Guest { get; set; } = null!;
}

public class Payment
{
public int Id { get; set; }
public int BookingId { get; set; }
public decimal Amount { get; set; }
public DateTime PaymentDate { get; set; }
public string PaymentMethod { get; set; } = string.Empty;
public Booking Booking { get; set; } = null!;
}

// Repository interfaces
public interface IGuestRepository
{
Task<Guest> AddAsync(Guest guest);
Task<Guest?> GetByEmailAsync(string email);
}

public interface IBookingRepository
{
Task<Booking> AddAsync(Booking booking);
Task<Booking?> GetByIdAsync(int id);
}

public interface IPaymentRepository
{
Task<Payment> AddAsync(Payment payment);
Task<IEnumerable<Payment>> GetByBookingIdAsync(int bookingId);
}

// Unit of Work interface
public interface IUnitOfWork : IDisposable
{
IGuestRepository Guests { get; }
IBookingRepository Bookings { get; }
IPaymentRepository Payments { get; }

Task<int> SaveChangesAsync();
Task BeginTransactionAsync();
Task CommitTransactionAsync();
Task RollbackTransactionAsync();
}

// DbContext
public class HotelDbContext : DbContext
{
public HotelDbContext(DbContextOptions<HotelDbContext> options) : base(options) { }

public DbSet<Guest> Guests { get; set; }
public DbSet<Booking> Bookings { get; set; }
public DbSet<Payment> Payments { get; set; }
}

// Repository implementations
public class GuestRepository : IGuestRepository
{
private readonly HotelDbContext _context;

public GuestRepository(HotelDbContext context)
{
_context = context;
}

public async Task<Guest> AddAsync(Guest guest)
{
_context.Guests.Add(guest);
return guest;
}

public async Task<Guest?> GetByEmailAsync(string email)
{
return await _context.Guests.FirstOrDefaultAsync(g => g.Email == email);
}
}

public class BookingRepository : IBookingRepository
{
private readonly HotelDbContext _context;

public BookingRepository(HotelDbContext context)
{
_context = context;
}

public async Task<Booking> AddAsync(Booking booking)
{
_context.Bookings.Add(booking);
return booking;
}

public async Task<Booking?> GetByIdAsync(int id)
{
return await _context.Bookings
.Include(b => b.Guest)
.FirstOrDefaultAsync(b => b.Id == id);
}
}

public class PaymentRepository : IPaymentRepository
{
private readonly HotelDbContext _context;

public PaymentRepository(HotelDbContext context)
{
_context = context;
}

public async Task<Payment> AddAsync(Payment payment)
{
_context.Payments.Add(payment);
return payment;
}

public async Task<IEnumerable<Payment>> GetByBookingIdAsync(int bookingId)
{
return await _context.Payments
.Where(p => p.BookingId == bookingId)
.ToListAsync();
}
}

// Unit of Work implementation
public class UnitOfWork : IUnitOfWork
{
private readonly HotelDbContext _context;
private IDbContextTransaction? _transaction;

public UnitOfWork(HotelDbContext context)
{
_context = context;
Guests = new GuestRepository(_context);
Bookings = new BookingRepository(_context);
Payments = new PaymentRepository(_context);
}

public IGuestRepository Guests { get; }
public IBookingRepository Bookings { get; }
public IPaymentRepository Payments { get; }

public async Task<int> SaveChangesAsync()
{
return await _context.SaveChangesAsync();
}

public async Task BeginTransactionAsync()
{
_transaction = await _context.Database.BeginTransactionAsync();
}

public async Task CommitTransactionAsync()
{
if (_transaction != null)
{
await _transaction.CommitAsync();
await _transaction.DisposeAsync();
_transaction = null;
}
}

public async Task RollbackTransactionAsync()
{
if (_transaction != null)
{
await _transaction.RollbackAsync();
await _transaction.DisposeAsync();
_transaction = null;
}
}

public void Dispose()
{
_transaction?.Dispose();
_context.Dispose();
}
}

// Business service using Unit of Work
public class BookingService
{
private readonly IUnitOfWork _unitOfWork;

public BookingService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}

public async Task<Booking> CreateBookingWithPaymentAsync(
string guestEmail,
string firstName,
string lastName,
int roomId,
DateTime checkIn,
DateTime checkOut,
decimal totalAmount,
decimal paymentAmount,
string paymentMethod)
{
await _unitOfWork.BeginTransactionAsync();

try
{
// Find or create guest
var guest = await _unitOfWork.Guests.GetByEmailAsync(guestEmail);
if (guest == null)
{
guest = new Guest
{
FirstName = firstName,
LastName = lastName,
Email = guestEmail
};
await _unitOfWork.Guests.AddAsync(guest);
await _unitOfWork.SaveChangesAsync(); // Save to get ID
}

// Create booking
var booking = new Booking
{
GuestId = guest.Id,
RoomId = roomId,
CheckInDate = checkIn,
CheckOutDate = checkOut,
TotalAmount = totalAmount
};

await _unitOfWork.Bookings.AddAsync(booking);
await _unitOfWork.SaveChangesAsync(); // Save to get booking ID

// Create payment
var payment = new Payment
{
BookingId = booking.Id,
Amount = paymentAmount,
PaymentDate = DateTime.UtcNow,
PaymentMethod = paymentMethod
};

await _unitOfWork.Payments.AddAsync(payment);
await _unitOfWork.SaveChangesAsync();

await _unitOfWork.CommitTransactionAsync();
return booking;
}
catch
{
await _unitOfWork.RollbackTransactionAsync();
throw;
}
}
}

// Dependency injection setup (Program.cs)
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<HotelDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<BookingService>();

var app = builder.Build();

// Usage example
public class BookingsController : ControllerBase
{
private readonly BookingService _bookingService;

public BookingsController(BookingService bookingService)
{
_bookingService = bookingService;
}

[HttpPost("with-payment")]
public async Task<ActionResult<Booking>> CreateBookingWithPayment([FromBody] CreateBookingWithPaymentRequest request)
{
try
{
var booking = await _bookingService.CreateBookingWithPaymentAsync(
request.GuestEmail,
request.FirstName,
request.LastName,
request.RoomId,
request.CheckIn,
request.CheckOut,
request.TotalAmount,
request.PaymentAmount,
request.PaymentMethod);

return CreatedAtAction(nameof(GetBooking), new { id = booking.Id }, booking);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}

public class CreateBookingWithPaymentRequest
{
public string GuestEmail { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public int RoomId { get; set; }
public DateTime CheckIn { get; set; }
public DateTime CheckOut { get; set; }
public decimal TotalAmount { get; set; }
public decimal PaymentAmount { get; set; }
public string PaymentMethod { get; set; } = string.Empty;
}