Well, since you seem to be learning about repositories, I’ll save you some time and headache.
The most important thing about repositories and units of work is that they are two very distint types of constructs, with very different goals in mind. A lot of the online tutorials I’ve seen suggest that you should either pass a unit of work into your repositories, or let the units of work act as repository factories. Both of these methods are fundamentally wrong in their implementation. A car and a train have two different roles to fulfill. You wouldn’t expect a car to produce the functionality of a train, or have a train produce a car upon request would you?
Let’s look at the industry standard unit of work interface as defined in the PoEAA book:
public interface IUnitOfWork : IDisposable
{
void RegisterNew(object entity);
void RegisterDirty(object entity);
void RegisterDeleted(object entity);
void Commit();
void Rollback();
}
The role of this object should be obvious. As it’s name implies, it perform the work, or heavy lifting, for the application. It’s only job is to batch-transactionalize insert, update and delete commands. This leaves relatively few things for the repository to be responsible for. So let’s take a look at what’s left:
public interface IRepository<TEntity>
where TEntity : class
{
IEnumerable<TEntity> GetAll();
TEntity GetById(object id);
}
The repository and unit of work act as separate contracts. Neither should be injected into the other, nor should either act as a factory for the other. In this arrangement, the name ‘repository’ is a bit of a misnomer. The name suggests ‘the place you put things’, but in reality it is ‘the place you get things’. I’d call for a change in name, but it’s too widely used under this name.
So how would you go about implementing these? First, let’s do the repository, as it is the simplest:
public class Repository<TEntity> : IRepository<TEntity>
where TEntity : class
{
private readonly DbContext _context;
public Repository(DbContext context)
{
_context = context;
}
public IEnumerable<TEntity> GetAll()
{
return _context.Set<TEntity>();
}
public TEntity GetById(object id)
{
return _context.Set<TEntity>().Find(id);
}
}
Let’s address some questions now:
- Why did you declare TEntity as class when it should be a common entity base class?
You can, and should. At first, you may simply use a common “Entity” base class, but as you progress, you will start using distinct types for aggregate root and dependent entities, at which point, you will want to force your repository to act only upon aggregate roots. For now, an exact type isn’t required.
- Why did you declare the id parameter of GetById as object?
This is a good idea because, as you progress, you will find yourself using things other than a db-filled int. The Find method supports any type usable as an id, including int, string, Guid, and others. Having this method be open to any id type means less work on your part.
Ok, now let’s take a look at the unit of work implementation. This one is a little more involved, so take your time and review the sample code:
public class UnitOfWork : IUnitOfWork
{
private readonly DbContext _context;
private readonly IList<object> _registeredNew;
private readonly IList<object> _registeredDirty;
private readonly IList<object> _registeredDeleted;
public UnitOfWork(DbContext context)
{
_context = context;
}
public void RegisterNew(object entity)
{
_registeredNew.Add(entity);
}
public void RegisterDirty(object entity)
{
_registeredDirty.Add(entity);
}
public void RegisterDeleted(object entity)
{
_registeredDeleted.Add(entity);
}
public void Commit()
{
foreach (var entity in _registeredNew)
_context.Entry(entity).State = EntityState.Added;
foreach (var entity in _registeredDirty)
_context.Entry(entity).State = EntityState.Modified;
foreach (var entity in _registeredDeleted)
_context.Entry(entity).State = EntityState.Deleted;
try
{// if this fails, data is rollback due to internal transaction
_context.SaveChanges();
}
catch (Exception exception)
{// report the failure
throw new Exception("Transaction failed!", exception);
}
finally
{// good idea to clear our own cache after success or failure
Rollback();
}
}
public void Rollback()
{
_registeredNew.Clear();
_registeredDirty.Clear();
_registeredDeleted.Clear();
}
public void Dispose()
{
Rollback();
}
}
And lastly, a simple factory class for the UoWs:
public static class UnitOfWorkFactory
{
public static IUnitOfWOrk Create()
{
DbContext context = DependencyResolver.Current.GetService<DbContext>();
return new UnitOfWork(context);
}
}
Usage would look like this:
[HttpPost, ValidateAntiForgeryToken]
public ActionResult Edit(int id, FooModel model)
{
if (ModelState.IsValid)
{
using (var uow = UnitOfWorkFactory.Create())
{
var foo = _fooRepository.GetById(id);
// update foo values (or call a business method if using DDD)
uow.RegisterDirty(foo);
uow.Commit();
}
return RedirectToRoute("some_route_name");
}
return View(model);
}
The repository is injected into the controller, as it will be used in almost every single action. The unit of work will only be used when work is needed, and is therefore instantiated through the factory class.
I hope that heads you in the right direction, without having to read through an entire volume.