Background
The repository pattern is a method to introduce a shearing layer between your business objects and the data access/persistence technology you are using and this especially useful in unit testing as the alternative (mocking an entire data access library) can be quite heart breaking.
There are a number of C# implementations on this site therefore this is just a VB.Net equivalent (as a reference article) for anyone wanting to achieve this pattern in VB.Net.
Motivation
The repository pattern introduces the following advantages over the traditional three-tier architecture over an ORM:
- The classes persisted by an ORM (Entity framework or the like) need to have a good deal of information about how they are stored. This is not ideal because when you make a change to the underlying storage you would need to change the business objects as well.
- Not all persistence is in the form of a relational database - the repository can be backed by a blended storage made of files, database tables and NoSQL records as well.
- Some fields exist only in order to allow navigation to a record or to identify child records - these fields should not be passed up into the business layer if they have no business meaning.
Method
The first thing we need to do in order to create a repository is to make sure our entities can be uniquely identified and that we know how so to do. For anyone from a database background this is like setting up the unique key field(s) on the database table.
I use a generic interface to do this in a highly flexible manner :-
Public Interface IKeyedEntity(Of TKeyType)
Property Key As TKeyType
End Interface
What this interface means is that for any class that implements it, an instance of that class can be meaningfully uniquely identified and the class will tell me how so to do.
For example if my Client class is uniquely identified by an integer we can declare it thus:-
Public NotInheritable Class ClientRecord
Implements IKeyedEntity(Of Integer)
Public Property ClientUniqueKey As Integer Implements IKeyedEntity(Of Integer).Key
Public Property Code As String
End Class
Where an entity has a key with multiple components (for example it might be a combination of two or more fields) then you can represent it with a Tuple or even better, you can create a lightweight class that only contains the key fields and use that in IKeyedEntity.
Now we'd need to define a set of standard operations to do with these objects and their backing storage. I split this into two parts - how I read from the data store and how I update the data store as this allows me to put together read-only data models quickly...this is particularily useful if you split your read and write models for example in CQRS.
Public Interface IRepositoryRead(Of TKey, TEntity As IKeyedEntity(Of TKey))
Function Exists(ByVal key As TKey) As Boolean
Function GetByKey(ByVal key As TKey) As TEntity
Function GetWhere(ByVal clause As Func(Of TEntity, Boolean)) As IReadOnlyDictionary(Of Tkey, TEntity)
Function GetAll() As IReadOnlyDictionary(Of Tkey, TEntity)
End Interface
..and on the write side....
Public Interface IRepositoryWrite(Of TKey, TEntity As IKeyedEntity(Of TKey))
Sub Delete(ByVal key As TKey)
Sub AddOrUpdate(ByVal entity As TEntity, ByVal key As TKey)
Function AddNew(ByVal entity As TEntity) As TKey
End Interface
Of course you most often want both read and write sides in one class so I have a combining interface for that:-
Public Interface IRepository(Of TKey, TEntity As IKeyedEntity(Of TKey))
Inherits IRepositoryRead(Of TKey, TEntity)
Inherits IRepositoryWrite(Of TKey, TEntity)
End Interface
Worked example - a memory backed repository...
To show this in action a very simple memory-backed repository could look like this:-
Namespace MemoryBacked
Public Class ClientRepository
Implements IClientRepository
Private m_data As New Dictionary(Of Integer, ClientRecord)
Public Function Exists(key As Integer) As Boolean Implements IRepositoryRead(Of Integer, ClientRecord).Exists
Return m_data.ContainsKey(key)
End Function
Public Function GetAll() As IReadOnlyDictionary(Of Integer, ClientRecord) Implements IRepositoryRead(Of Integer, ClientRecord).GetAll
Return m_data.Values.AsQueryable()
End Function
Public Function GetByKey(key As Integer) As ClientRecord Implements IRepositoryRead(Of Integer, ClientRecord).GetByKey
If (m_data.ContainsKey(key)) Then
Return m_data(key)
Else
Return Nothing
End If
End Function
Public Function GetWhere(clause As Func(Of ClientRecord, Boolean)) As IReadOnlyDictionary(Of Integer, ClientRecord) Implements IRepositoryRead(Of Integer, ClientRecord).GetWhere
Return m_data.Values.Where(clause)
End Function
Public Function AddNew(entity As ClientRecord) As Integer Implements IRepositoryWrite(Of Integer, ClientRecord).AddNew
If (entity.ClientUniqueKey = 0) Then
entity.ClientUniqueKey = m_data.Count
End If
If Not (m_data.ContainsKey(entity.ClientUniqueKey)) Then
m_data.Add(entity.ClientUniqueKey, entity)
End If
Return entity.ClientUniqueKey
End Function
Public Sub AddOrUpdate(entity As ClientRecord, key As Integer) Implements IRepositoryWrite(Of Integer, ClientRecord).AddOrUpdate
If Not (m_data.ContainsKey(entity.ClientUniqueKey)) Then
m_data.Add(entity.ClientUniqueKey, entity)
Else
m_data(entity.ClientUniqueKey) = entity
End If
End Sub
Public Sub Delete(key As Integer) Implements IRepositoryWrite(Of Integer, ClientRecord).Delete
If (m_data.ContainsKey(key)) Then
m_data.Remove(key)
End If
End Sub
End Class
End Namespace
(It's not thread safe or overly good but sufficient for unit test purposes)
Now you write your business layer against the repository classes and leave the data access completely alone to be independently implemented.
Business-meaning interfaces
I also use what I term "business meaningful" interfaces to seperate the what of the storage operation from the how - for example in the above case I have added IClientRepository which is an additional wrapper around IRepository thus:-
Public Interface IClientRepository
Inherits IRepository(Of Integer, ClientRecord)
End Interface
This is not neccessary in order to use the repository pattern but I find it makes for better unit testing and code understanding.
Exceptions
To further the shearing layer separation, I also recommend creating specific repository exceptions that your business layer can trap rather than it having to understand the underlying storage technology. These can wrap the inner exception so the developer can always get the required error information:-
Public Class RepositoryReadException
Inherits Exception
ReadOnly m_fatal As Boolean
Public ReadOnly Property Fatal As Boolean
Get
Return m_fatal
End Get
End Property
Public Sub New(ByVal message As String, ByVal innerExcption As Exception, ByVal fatalInit As Boolean)
MyBase.New(message, innerExcption)
m_fatal = fatalInit
End Sub
Public Sub New(ByVal message As String, ByVal fatalInit As Boolean)
MyBase.New(message)
m_fatal = fatalInit
End Sub
End Class