Welcome to today’s post.
In today’s post I will be discussing the unit of work pattern and how it can be used within entity framework core.
What is the Unit of Work Pattern?
The Unit of Work data pattern is defined as a sequence of data operations under one transaction context, which would either have all the changes committed or rolled back in the same transaction. The pattern helps automate unit testing and test-driven development.
In the context of business processes, the unit of work is utilized in atomic transactions that are all or none: the transaction will succeed if all data changes succeed and fail if one of the commands fails. In the event of failure, the entire transaction block will rollback the data changes within the data entity context. In the event of all data changes succeeding, the entire transaction block is committed to the data entity context.
When we had the LINQ to SQL libraries in earlier versions of .NET and Visual Studio (as early as 2005), the unit of work pattern had to be implemented outside of the framework to provide transaction handling for multi-entity data contexts. We would have had to implement a partial class to extend the data context into custom repository classes, and then extend our custom repository business classes to use the unit of work pattern, which would use the data context from configured connection settings of the data context.
Following that, in the previous versions of Entity Framework versions 4-6, the Unit if Work pattern was supported and built into the Framework as a data context. Similarly, with Entity Framework Core, the unit of work does not need to be implemented, as the DbContext class already is a repository with a built-in unit of work pattern. The unit of work is essentially the DbContext. However, in applications that are of a more complex structure, to help refactor code into testable data access classes, it is more beneficial to add an additional layer of abstraction, such as a Repository (see previous section). Within the Repository, we can extend it with the Unit of Work to allow the context to be committed or rolled back as needed within a transaction.
The Unit of Work and Service Lifetime
The simplified Unit of Work pattern consists of an interface that consists of the function SaveChangesAsync().
public interface IUnitOfWork : IDisposable
{
Task<int> SaveChangesAsync(
CancellationToken cancellationToken = default(CancellationToken));
}
For the unit of work pattern to commit changes to multiple entities within the same request, the data context is preferred with a lifetime scope of Scoped, which is the default as shown here:
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString(connStr)));
With a Scoped lifetime, the injected data context within multiple repositories instantiated within the same request thread will not go out of scope until the request completes. If the lifetime is Transient, then each repository that injects the data context, will have a different instantiation of the data context, which will consume more memory resources and be less efficient in terms of performance. The scoped lifetime in this case will be more optimal in execution time and memory usage.
Warning: Attempting to use the Singleton lifetime will result in thread access conflicts with multiple dependencies using the same data context. Given the Entity Framework Core library is NOT multithread safe, you are recommended to use a scoped lifetime for the data context, which ensures that ONE thread per request can access library with its own instance of the data context. Where multiple threads from different requests call the Entity Framework Core library at the data at the same time, you are advised to use the await keyword to block other threads. The Entity Framework Core library does NOT support safe access to its library with a Singleton lifetime data context that is open to multi-threads. Conclusion: the data context should be disposed of on completion of each request!
The following can also configure the data context to Transient if none of your requests use more than one data context dependency:
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(config.GetConnectionString("AppDbContext")),
ServiceLifetime.Transient,
ServiceLifetime.Scoped
);
The Unit of Work in the Repository Pattern
Our book repository interface BookRepository includes a getter for the Unit of Work interface:
public interface IBookRepository
{
IUnitOfWork UnitOfWork { get; }
Task<List<BookViewModel>> GetAll();
Task<List<BookViewModel>> GetAllNoTracking();
Task<BookViewModel> GetByIdAsync(int id);
Task<BookViewModel> GetByTitle(string title);
Task<BookViewModel> AddAsync(BookViewModel entity);
Task<BookViewModel> UpdateAsync(BookViewModel entity);
Task<BookViewModel> DeleteAsync(BookViewModel entity);
}
The data context for our application will need to be extended with the unit of work interface as shown:
public class ApplicationDbContext : DbContext, IUnitOfWork
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
…
}
…
}
The Repository Unit of Work
The Repository class is replaced with a RepositoryUow class that does not auto-commit after each CRUD operation. Instead, it makes the changes to the entity (add, update, or delete) and returns the entity. The unit of work object within the BookRepository can be used to apply the commit instead after all the data context operations been applied to other entities within the transaction scope for the data context.
public class RepositoryUow<T> : IRepository<T> where T : class, new()
{
protected readonly ApplicationDbContext _db;
public RepositoryUow(ApplicationDbContext db)
{
_db = db;
}
public async Task<T> GetByIdAsync(int id)
{
return await _db.FindAsync<T>(id);
}
public async Task<List<T>> GetAll()
{
return await _db.Set<T>().ToListAsync();
}
public async Task<List<T>> GetAllNoTracking()
{
return await _db.Set<T>().AsNoTracking().ToListAsync();
}
public async Task<T> AddAsync(T entity)
{
if (entity == null)
throw new ArgumentNullException("AddAsync(): entity parameters is null.");
try
{
await _db.AddAsync(entity);
return entity;
}
catch (Exception ex)
{
throw new Exception($"AddAsync(): cannot insert data : {ex.Message}");
}
}
public async Task<T> UpdateAsync(T entity)
{
if (entity == null)
throw new ArgumentNullException("UpdateAsync(): entity parameters is null.");
try
{
_db.Update(entity);
return entity;
}
catch (Exception ex)
{
throw new Exception($"UpdateAsync(): cannot update data : {ex.Message}");
}
}
public async Task<T> DeleteAsync(T entity)
{
if (entity == null)
throw new ArgumentNullException("DeleteAsync(): entity parameters is null.");
try
{
_db.Remove(entity);
return entity;
}
catch (Exception ex)
{
throw new Exception($"DeleteAsync(): cannot delete data : {ex.Message}");
}
}
}
A Concrete Repository Unit of Work
Our concrete repository BookRepository inherits from the RepositoryUow and implements the UnitOfWork interface getter by returning the data context _db, which is a protected member within the Repository class.
public class BookRepository : RepositoryUow<BookViewModel>, IBookRepository
{
public IUnitOfWork UnitOfWork => _db;
public BookRepository(ApplicationDbContext db) : base(db) {}
public async Task<BookViewModel> GetByTitle(string title)
{
return await _db.Books
.Where(b => b.Title == title)
.SingleOrDefaultAsync();
}
}
In our service classes, we can apply the updated repository and unit of work pattern as shown:
public async Task SaveBook(BookViewModel vm)
{
var newRecord = await this._bookRepository.AddAsync(vm);
await this._bookRepository.UnitOfWork.SaveChangesAsync();
var book = await _bookRepository.GetByIdAsync(newRecord.ID);
if (book == null)
return;
int id = book.ID;
}
The difference is that we called the SaveChangesAync() method of the DBContext after applying all the changes to the DBContext entities.
When we execute the SaveBook() method, an object is added to the Books entity, then committed with the unit of work method SaveChangesAsync().
We can see the JSON data below that has been submitted into the SaveBook() method through an API POST request:
The resulting record insertion committed to the [aspnet-BookCatalog].[dbo].[Books] table within our database is shown:
Using the Repository Unit of Work in Multiple Transactions
I will show a more complex case where we update one table and add data to another table.
I have two entities:
Loan
Review
I have two operations that will simulate the following processes:
- I borrow a Book.
- I return the Book to the library and enter a review in the same submission.
The routine to loan a book by saving the loan dates, adding the loan record to the repository, then saving it under the unit of work is below:
public async Task SaveLoan(LoanViewModel vm)
{
// Logic to check if current user has outstanding overdue loans.
…
vm.DateCreated = DateTime.Now;
vm.DateUpdated = DateTime.Now;
await this._loanRepository.AddAsync(vm);
await this._loanRepository.UnitOfWork.SaveChangesAsync();
}
After running the above method though an API with the following payload:
{
"id": 0,
"loanedBy": "andy@adb.com.au",
"dateLoaned": "2021-07-10T00:01:57.486Z",
"dateDue": "2021-07-24T00:01:57.486Z",
"dateReturn": "0001-01-01T00:00:00",
"onShelf": false,
"dateCreated": "2021-07-10T00:01:57.486Z",
"dateUpdated": "2021-07-10T00:01:57.486Z",
"bookID": 10,
"returnMethod": "",
}
We get the following data insertion committed to the Loans table:
Next, for the loan return and review we run an API submission that specifies both the loan return details and review parameters as shown:
{
"id": 17,
"loanedBy": "andy@adb.com.au",
"dateLoaned": "2021-07-10T00:01:57.486Z",
"dateDue": "2021-07-24T00:01:57.486Z",
"dateReturn": "0001-01-01T00:00:00",
"onShelf": true,
"bookID": 10,
"returnMethod": "",
"reviewHeading": "Could be better",
"reviewComments": "Good beginning, the middle is boring, and the end is predictable.",
"rating": 2
}
The method that returns the loan and adds a new review is shown below:
public async Task ReturnLoanAndReview(LoanReviewViewModel vm)
{
LoanViewModel loanViewModel = new LoanViewModel()
{
ID = vm.ID,
LoanedBy = vm.LoanedBy,
DateCreated = vm.DateCreated,
DateLoaned = vm.DateLoaned,
DateDue = vm.DateDue,
OnShelf = true,
BookID = vm.BookID,
ReturnMethod = _bookReturn.GetReturnMethod(),
DateUpdated = DateTime.Now,
DateReturn = DateTime.Now
};
ReviewViewModel reviewViewModel = new ReviewViewModel()
{
BookID = vm.BookID,
Approver = vm.LoanedBy,
IsVisible = true,
Heading = vm.ReviewHeading,
Comment = vm.ReviewComments,
Rating = vm.Rating,
DateReviewed = DateTime.Now,
DateCreated = DateTime.Now,
DateUpdated = DateTime.Now
};
await this._loanRepository.UpdateAsync(loanViewModel);
await this._reviewRepository.AddAsync(reviewViewModel);
await this._loanRepository.UnitOfWork.SaveChangesAsync();
}
In our debugger the loan details are extracted into the Loan POCO model as shown:
And the review details are extracted into the Review POCO model as shown:
After execution of the method, we can see that both the tables have their record sets modified, first the [aspnet-BookCatalog].[dbo].[Loans] table has the [DateReturn] and [DateUpdated] fields updated to the current date and time:
And the [aspnet-BookCatalog].[dbo].[Reviews] table has it’s [DateCreated] and [DateUpdated] fields updated with the same date and time stamp:
What this shows us is the Unit of Work pattern has been successfully applied to synchronize the application of data operations across multiple entities in the data context.
Once we apply the unit of work pattern to one repository, it is a routine exercise to apply it to other entities with repositories, like we did for the Book repository.
That is all for today’s post.
I hope you found this post useful and informative.
Andrew Halil is a blogger, author and software developer with expertise of many areas in the information technology industry including full-stack web and native cloud based development, test driven development and Devops.