Introduction
With this tutorial I would like to explain how we can include an Object/Relational Mapping (ORM) framework into our project in such a way that generic situations
are handled at the lowest possible level. This also ensures that developers who use this solution do not get bothered with too much details.
I am using the Entity Framework as the ORM framework in this tutorial. However with a few adaptations it is certainly possible to replace this by another framework (e.g., NHibernate).
Abstracting the framework
To abstract the chosen ORM framework the combination of the Repository and the Unit of Work pattern is chosen.
Martin Fowler has the following definitions:
- A Repository mediates between the domain and data mapping layers. It allows us to work with the entities as a local collection on our end and carries out the necessary operations to persist/retrieve them behind the scenes.
- The Unit of Work maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems.
In other words a Unit of Work contains mutations to one or more entities, it combines the mutations done on multiple repositories. These mutations together act as one to ensure that the underlying data source maintains a consistent state. Either all mutations succeed or all fail. For this to work we (re)use a database transaction.
The following image contains an overview of the used classes and their dependencies.
UnitOfWork<T>
A generic implementation that ties together one or more sets of objects which have been added/removed/updated. It provides a function (‘Execute
’) which allows multiple repositories (done via the reference to the object sets) to be managed as one. Successful execution will ensure that the enclosed transaction is committed.
It is possible to use a UnitOfWork
recursively, the transaction within the
UnitOfWork
at the top will be responsible for either the commit or the rollback.
In the end the result of the mutations is that they either all succeed or all fail.
Repository<T>
A generic implementation for a repository with functionality to manage a specified type of entity in an underlying data source. This data source is represented by the object set.
Methods are available that allow CRUD style actions to be executed on the underlying entities. If possible they provide a possibility to pass a
LINQ expression for providing a way to customize these actions.
IObjectSetCreator<T>
This provides a possibility for the Repository
to create an instance of the set of objects via the
UnitOfWork
without needing to specify the ObjectContext
used.
ObjectContext
This provides us the connection with the data source which contains the entities which are used. The specific type DataModelObjectContext
contains information regarding the data source and the definition of the entities used.
Preparation for running the solution
Prerequisites
- SQL Express 2012, named instance 'SQLEXPRESS' with windows security enabled
- Visual Studio 2010
I will describe the first few steps so that people with no or not much experience with the Entity Framework know which steps need to be taken to try it out their selves in a new solution.
It is possible to start at Step 4 when one is interested in just running the provided solution.
Step 1
In my example I started with a new solution and directly add the DataAccess project. This project will contain the functionality used to work with the Entity Framework.
To add the Entity Framework to the solution we require NuGet. This can be installed via the Extension Manager within Visual Studio. Select the DataAccess project and right-click the References. Select ‘Manage NuGet Packages’ and install the Entity Framework.
Step 2
Continue by adding a new item to the DataAccess project, namely the ‘ADO.NET Entity Data Model’ and name it accordingly.
After pressing ‘Add’ we have to decide if we are going to use model-first, code-first or database-first. For this example we will use the model-first approach and select ‘Empty model’.
Step 3
In this empty model we will add the following 2 entities.
I added an extra scalar property to each entity of type string (Title and Name). I also added an association from Author to Book. This will directly configure the multiplicity as shown. The Navigation Properties are automatically added and these will allow us to easily reach any referenced entities.
Step 4
Right-click somewhere in the model and select 'Generate Database from model'.
Select 'New Connection …’
Select the data source that should be used, use ‘DataModel’ as the name of the database. Keep the rest of the properties unchanged and press ‘OK’.
Copy the selected entity connection string so that we can use it in our specific
DataModelObjectContext
class. This no longer requires saving the connection string in the
app.config file, so we can disable that option. The specific ObjectContext
class is used to maintain the correct connection string to make it easier to have more than one model in the solution. However in this tutorial we will have only one model.
Continue by clicking 'Next' and execute the generated sql script within Visual Studio to have the
two tables created.
Step 5
The constructor of the DataModelObjectContext
is looking like this:
public DataModelObjectContext()
: base(@"metadata=res://*/DataModel.csdl|res://*/DataModel.ssdl|" +
@"res://*/DataModel.msl;provider=System.Data.SqlClient;" +
@"provider connection string=""Data Source=localhost\SQLEXPRESS;" +
@"Initial Catalog=DataModel;Integrated Security=True""")
{
}
Beware that this connection string contains the name of the model and also the location & name of the data source used. In your case this can be different.
Using the Unit of Work / Repository as a developer
As a developer it is quite easy to work with the entities by using the provided
UnitOfWork
and Repository
classes. To give an idea how easy I added some snippets that indicate how to use them accordingly. The attached example solution has buttons which allow each of these snippets to be executed.
Storing
The following snippet shows the actions required to store a new author in the data source.
using (UnitOfWork<datamodelobjectcontext> unitOfWork =
new UnitOfWork<datamodelobjectcontext>())
{
bool result = unitOfWork.Execute(() =>
{
Repository<author> authorRepository =
new Repository<author>(unitOfWork);
Author author = new Author()
{
Name = "Stephen Hunt"
};
authorRepository.Add(author);
return true;
});
}
Working with two repositories
The following snippet shows how to create two books with Stephen Hunt as the author. It continues based on the result of the previous snippet, so a Find is done to get the added Author.
As one can see it is as easy as creating a book, setting the reference to the author and finally adding it to the repository. Finishing the execution flow normally will automatically trigger the saving of the changes and to trigger the commit of the enclosed transaction.
using (UnitOfWork<DataModelObjectContext> unitOfWork =
new UnitOfWork<DataModelObjectContext>())
{
bool result = unitOfWork.Execute(() =>
{
Repository<author> authorRepository =
new Repository<author>(unitOfWork);
Author author = authorRepository.Find(
item => item.Name.Equals("Stephen Hunt")).
SingleOrDefault<author>();
if (author != null)
{
Repository<book> bookRepository =
new Repository<book>(unitOfWork);
Book book1 = new Book()
{
Title = "Sliding Void",
Author = author
};
Book book2 = new Book()
{
Title = "In the Company of Ghosts",
Author = author
};
bookRepository.Add(book1);
bookRepository.Add(book2);
}
return true;
});
}
Retrieving
The next snippet shows the actions required to get the author and the referred books. This requires an extra action to ensure that the required navigation properties are retrieved from the data source. This way it is possible to have more control over the retrieval of the navigation properties and this directly impacts the query which is done on the underlying data source.
using (UnitOfWork<DataModelObjectContext> unitOfWork =
new UnitOfWork<DataModelObjectContext>())
{
bool result = unitOfWork.Execute(() =>
{
Repository<author> authorRepository =
new Repository<author>(unitOfWork);
Author author = authorRepository.Find(
item => item.Name.Equals("Stephen Hunt")).
SingleOrDefault<author>();
if (author != null)
{
unitOfWork.ObjectContext.LoadProperty(
author, item => item.Books);
}
return true;
});
}
Removing
The next snippet shows the actions required to remove an entity, in this case one of the books of the author.
using (UnitOfWork<DataModelObjectContext> unitOfWork =
new UnitOfWork<DataModelObjectContext>())
{
bool result = unitOfWork.Execute(() =>
{
Repository<author> authorRepository =
new Repository<author>(unitOfWork);
Author author = authorRepository.Find(
item => item.Name.Equals("Stephen Hunt")).
SingleOrDefault<author>();
if (author != null)
{
unitOfWork.ObjectContext.LoadProperty(
author, item => item.Books);
Book book = author.Books.FirstOrDefault<book>();
if (book != null)
{
new Repository<book>(unitOfWork).Remove(removedBook);
}
return true;
});
}
Data Consistency
Data consistency is very important when working with data sources. This is the reason that the Unit of Work internally uses a transaction to ensure that all mutations done within the Execute flow are atomic. To show how this works the following snippet is used.
Note that after removing the book from the author an exception is thrown. This will trigger a rollback of the underlying transaction and thus of all mutations done within the Unit of Work.
DisplayBooksForAuthor(("Stephen Hunt");
using (UnitOfWork<DataModelObjectContext> unitOfWork =
new UnitOfWork<datamodelobjectcontext>())
{
bool result = unitOfWork.Execute(() =>
{
Repository<author> authorRepository =
new Repository<author>(unitOfWork);
Author author = authorRepository.Find(
item => item.Name.Equals("Stephen Hunt")).
SingleOrDefault<author>();
if (author != null)
{
unitOfWork.ObjectContext.LoadProperty(
author, item => item.Books);
Book book = author.Books.FirstOrDefault<book>();
if (book != null)
{
new Repository<book>(unitOfWork).Remove(book);
throw new InvalidOperationException();
}
}
return true;
});
}
DisplayBooksForAuthor("Stephen Hunt");
Points of Interest
The execution of the mutations on the repositories within the context of the Unit of Work are handled via the Func delegate. This enables us to easily keep the generic functionality within the Unit of Work.
public bool Execute(Func<bool> function)
{
bool result = false;
try
{
using (TransactionScope transactionScope = new TransactionScope())
{
try
{
if (function())
{
result = SaveChanges();
if (result)
{
try
{
transactionScope.Complete();
}
catch (InvalidOperationException)
{
result = false;
}
}
}
else
{
}
}
catch (EntityCommandExecutionException)
{
}
catch (Exception)
{
}
}
}
catch (TransactionAbortedException e)
{
}
catch (TransactionInDoubtException e)
{
}
}
The Enclosed Solution
The enclosed solution contains two projects:
- AbstractingDataAccess: Contains the windows form
- DataAccess: Contains the specific implementations and the model for the Entity Framework
History
- 4 July 2013 - First version.