INTRO: I'm fairly new to programming still and in a class I took, we used quite a bit of Repository pattern and I got experience using ADO.NET with stored procedures. I want to build upon what I have already learned by implementing the Unit Of Work pattern along with Repository Pattern using ADO.NET stored procedures. I want to achieve the following goal below.
- I'm not using Generic repositories at this time. I'm not there yet.
- I am also aware that Entity Framework is preferred, so I've been told, but again just not there yet.
- I created a DvdLibrary where I can Add, Update, Edit, and Delete dvds (standard CRUD).
- I have 4 tables. Dvd, Rating, Director, and DvdDirector.
- Created stored procedures
I put some questions and comments in the code. I did not list all my classes just the main ones relating to UnitOfWork and Database Context. I just want to confirm I and Committing, Rollingback and applying the use of the single database context correctly. Any help you can provide is greatly appreciated! Thank you!
GOAL: While repositories are used to create an abstraction layer between the data layer and the business layer of an application, theunit of work pattern coordinates the work of multiple repositories by enforcing asingle database context class shared by all of them. Theunit of work tracks changes to the objects extracted from arepository and persist any of these changes when we tell theunit of work to commit the changes.
IUnitOfWork
public interface IUnitOfWork : IDisposable
{ //The purpose of IUnitOfWork is to define the contracts of the repositories we want to expose based on the entities we have in our ADOContext
IDirectorRepository Director { get; }
IDvdDirectorRepository DvdDirector { get; }
IDvdRepository Dvd { get; }
IRatingRepository Rating { get; }
void Complete(); //call this method to commit our changes to the database
}
//IDisposable - the primary use of this interface is to release unmanaged resources. The garbage collector automatically releases the memory allocated to a managed object when that object is no longer used. However, it is not possible to predict when garbage collection will occur.
//Use the Dispose method of this interface to explicitly release unmanaged resources in conjunction with the garbage collector. The consumer of an object can call this method when the object is no longer needed.UnitOfWorkADO - implements IUnitOfWork -Unsure if I did the Complete or Dispose method correctly
public class UnitOfWorkADO : IUnitOfWork
{
private readonly IContext _context;
private IDbTransaction _transaction;
private IDvdRepository _dvd;
private IDvdDirectorRepository _dvdDirector;
private IRatingRepository _rating;
private IDirectorRepository _director;
public UnitOfWorkADO(IContext context)
{
//ContextADO object is injected and stored in a field
_context = context;
//storing the transaction - is it better to just use the context here?
_transaction = context.Transaction;
}
//Each repository property checks whether the repository already exists. If not, it instantiates the repository, passing in the context instance. As a result, all repositories share the same context instance.
//I'm using a factory to return the type of repository based on the RepositoryType key/value in Web.config. Not sure this is actually needed because the UnitOfWork is specific to ADO
public IDirectorRepository Director
{
get
{
return _director ?? (_director = DirectorRepositoryFactory.GetRepository(_context));
}
}
public IDvdDirectorRepository DvdDirector
{
get
{
return _dvdDirector ?? (_dvdDirector = DvdDirectorRepositoryFactory.GetRepository(_context));
}
}
public IDvdRepository Dvd
{
get
{
return _dvd ?? (_dvd = DvdRepositoryFactory.GetRepository(_context));
}
}
public IRatingRepository Rating
{
get
{
return _rating ?? (_rating = RatingRepositoryFactory.GetRepository(_context));
}
}
//Attempt to commit the changes else Rollback
public void Complete()
{
try
{
if (_transaction == null)
{
throw new InvalidOperationException("The transaction has already been committed");
}
else
{
_transaction.Commit();
_transaction = null;
}
}
catch (Exception ex)
{
_transaction.Rollback();
throw ex;
}
}
//using "Using" statement when calling UnitOfWorkFactory.Create() method
//The purpose of Using statement is that when control will reach end of using it will dispose that object of using block and free up memory. its purpose is not only for auto connection close, basically it will dispose connection object and obviously connection also closed due to it.
public void Dispose()
{
if (_transaction != null)
{
_transaction.Dispose();
_transaction = null;
}
}
}public interface IContext
{
IDbTransaction Transaction { get; }
IDbCommand CreateCommand();
}ContextADO - Implement IContext - should IContext implement IDisposable or is how I have it okay?
public class ContextADO : IContext
{
//store our connection
private readonly IDbConnection _connection;
public ContextADO(IDbConnection connection)
{
//set connection and store in field
_connection = connection;
//Setting Transaction public property from IContext
//The _connection.BeginTransaction() - begins a database transaction and associates it with the connection object.
Transaction = _connection.BeginTransaction();
}
//Putting the Transaction into a public property so it can be passed to UnitOfWorkADO
public IDbTransaction Transaction { get; private set; }
public IDbCommand CreateCommand()
{
//Creates and returns a Command object associated with the connection.
var cmd = _connection.CreateCommand();
//cmd.Transaction = Gets or sets the transaction within which the Command object of a .NET Framework data provider executes.
cmd.Transaction = Transaction;
return cmd;
}
//Should IContext implement IDisposable like IUnitOfWork and then I can have a Dispose method???
//public void Dispose()
//{
// _connection.Dispose();
//}
}Example HomeController Action which calls on UnitOfWorkFactory
[HttpPost]
public ActionResult Add(DvdAddViewModel model)
{
if (!ModelState.IsValid)
{
using (var uow = UnitOfWorkFactory.Create())
{
model.Ratings = uow.Rating.GetAll();
}
return View(model);
}
else
{
try
{
using (var uow = UnitOfWorkFactory.Create())
{
uow.Dvd.Insert(model.Dvd);
uow.Director.Insert(model.Director);
var dvdDirector = new DvdDirector()
{
DvdId = model.Dvd.DvdId,
DirectorId = model.Director.DirectorId
};
uow.DvdDirector.Insert(dvdDirector);
uow.Complete();
return RedirectToAction("Index");
}
}
catch (Exception ex)
{
throw ex;
}
}
}UnitOfWork Factory
//UnitOfWorkFactory determines which UnitOfWork to return based on the RepositoryType key/value in Web.config
public static IUnitOfWork Create()
{
//Requests a Context which will be used with UnitOfWork
var context = ContextFactory.Create();
switch (Settings.GetRepositoryType())
{
case "ADO":
return new UnitOfWorkADO(context);
default:
throw new Exception("Could not find valid RepositoryType configuration value");
}
}ContextFactory
public static class ContextFactory
{
//ContextFactory determines which Context to return based on the RepositoryType key/value in Web.config
public static IContext Create()
{
switch (Settings.GetRepositoryType())
{
case "ADO":
return new ContextADO(OpenConnection()); //Open SqlConnection is passed into the Context
default:
throw new Exception("Could not find valid RepositoryType configuration value");
}
}
//Creates a new SqlConnection and opens that connection
private static IDbConnection OpenConnection()
{
var cn = new SqlConnection(Settings.GetConnectionString());
cn.Open();
return cn;
}
}One thing I did notice, that if the transaction fails (Mainly because I'm slowly stepping through the code in debug mode), nothing writes to the database, which is what is supposed to happen, however, the Id of the Dvd in the database is incremented. Why would this happen?
Thank you again for you help!
- Adam