Results 1 to 4 of 4

Thread: The repository pattern

  1. #1

    Thread Starter
    PowerPoster
    Join Date
    Jul 2002
    Location
    Dublin, Ireland
    Posts
    2,148

    The repository pattern

    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.

    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 :-

    vb.net Code:
    1. ''' <summary>
    2. ''' Interface defining any item we can store in a repository and can identify by
    3. ''' an unique key
    4. ''' </summary>
    5. ''' <remarks>
    6. ''' This interface is typed so we can make type-safe code for retrieving the entity
    7. ''' (don't pass in an integer if the entity is keyed by string etc.)
    8. ''' </remarks>
    9. Public Interface IKeyedEntity(Of TKeyType)
    10.  
    11.     ''' <summary>
    12.     ''' Get the key to find the entity by
    13.     ''' </summary>
    14.     Property Key As TKeyType
    15.  
    16. 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:-
    vb.net Code:
    1. ''' <summary>
    2. ''' Record for storing a client record in the common database
    3. ''' </summary>
    4. ''' <remarks></remarks>
    5. Public NotInheritable Class ClientRecord
    6.     Implements IKeyedEntity(Of Integer)
    7.  
    8.  
    9.     ''' <summary>
    10.     ''' The unique number by which we know this client
    11.     ''' </summary>
    12.     ''' <remarks>
    13.     ''' Every client has an unique id but this is not needed publically
    14.     ''' </remarks>
    15.     Public Property ClientUniqueKey As Integer Implements IKeyedEntity(Of Integer).Key
    16.  
    17.     ' Other non-key properties can go here
    18.     ''' <summary>
    19.     ''' The short code for the client
    20.     ''' </summary>
    21.     ''' <remarks>
    22.     ''' e.g. MCL for Merrion Computing Ltd etc.
    23.     ''' </remarks>
    24.     Public Property Code As String
    25. End Class

    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...

    vb.net Code:
    1. ''' <summary>
    2. ''' Interface to support reading entities from the backing store
    3. ''' </summary>
    4. ''' <typeparam name="TEntity">
    5. ''' The key-identified type of entity we are reading
    6. ''' </typeparam>
    7. ''' <typeparam name="TKey">
    8. ''' The type of the key
    9. ''' </typeparam>
    10. ''' <remarks>
    11. ''' In this architecture there is a seperate read and write interface but often this
    12. ''' pattern has just the one interface for both functions
    13. ''' </remarks>
    14. Public Interface IRepositoryRead(Of TKey, TEntity As IKeyedEntity(Of TKey))
    15.  
    16.     ''' <summary>
    17.     ''' Does a record exist in the repository identified by this key
    18.     ''' </summary>
    19.     ''' <param name="key">
    20.     ''' The unique identifier of the entity we are looking for
    21.     ''' </param>
    22.     Function Exists(ByVal key As TKey) As Boolean
    23.  
    24.     ''' <summary>
    25.     ''' Get the entity uniquely identified by the given key
    26.     ''' </summary>
    27.     ''' <param name="key">
    28.     ''' The unique identifier to use to get the entity
    29.     ''' </param>
    30.     Function GetByKey(ByVal key As TKey) As TEntity
    31.  
    32.     ''' <summary>
    33.     ''' Get a set of entities from the repository that match the where clause
    34.     ''' </summary>
    35.     ''' <param name="clause">
    36.     ''' A function to apply to filter the results from the repository
    37.     ''' </param>
    38.     Function GetWhere(ByVal clause As Func(Of TEntity, Boolean)) As IReadOnlyDictionary(Of TKey, TEntity)
    39.  
    40.     ''' <summary>
    41.     ''' Get all of this type of thing from the repository
    42.     ''' </summary>
    43.     Function GetAll() As IReadOnlyDictionary(Of Tkey, TEntity)
    44.  
    45. End Interface

    ..and on the write side....

    vb.net Code:
    1. ''' <summary>
    2. ''' Interface to support writing (and deletes) to a typed repository
    3. ''' </summary>
    4. ''' <typeparam name="TEntity">
    5. ''' The type of entity in the repository
    6. ''' </typeparam>
    7. ''' <typeparam name="TKey">
    8. ''' The type of the key to uniquely identify the entity
    9. ''' </typeparam>
    10. ''' <remarks>
    11. ''' In this architecture there is a seperate read and write interface but often this
    12. ''' pattern has just the one interface for both functions
    13. ''' </remarks>
    14. Public Interface IRepositoryWrite(Of TKey, TEntity As IKeyedEntity(Of TKey))
    15.  
    16.     ''' <summary>
    17.     ''' Delete the entity uniquely identified by this key
    18.     ''' </summary>
    19.     ''' <param name="key">
    20.     ''' The unique identifier of the record to delete
    21.     ''' </param>
    22.     Sub Delete(ByVal key As TKey)
    23.  
    24.     ''' <summary>
    25.     ''' Add or update the entity
    26.     ''' </summary>
    27.     ''' <param name="entity">
    28.     ''' The record to add or update on the repository
    29.     ''' </param>
    30.     ''' <param name="key" >
    31.     ''' The key that uniquely identifies the record to add or update
    32.     ''' </param>
    33.     Sub AddOrUpdate(ByVal entity As TEntity, ByVal key As TKey)
    34.  
    35.     ''' <summary>
    36.     ''' Adds an entity that we know to be new and returns its assigned key
    37.     ''' </summary>
    38.     ''' <param name="entity">
    39.     ''' The entity we are adding to the repository
    40.     ''' </param>
    41.     ''' <returns>
    42.     ''' The unique identifier for the entity
    43.     ''' </returns>
    44.     ''' <remarks>
    45.     ''' This is useful if the unique identifier is not an intrinsic property of
    46.     ''' the entity - for example if it is a memory address or a GUID
    47.     ''' </remarks>
    48.     Function AddNew(ByVal entity As TEntity) As TKey
    49.  
    50. End Interface

    Of course you most often want both read and write sides in one class so I have a combining interface for that:-
    vb.net Code:
    1. ''' <summary>
    2. ''' Read/write repository of typed entites
    3. ''' </summary>
    4. ''' <typeparam name="TKey">
    5. ''' The type by which the entity is uniquely identified
    6. ''' </typeparam>
    7. ''' <typeparam name="TEntity">
    8. ''' The type of entity in the repository
    9. ''' </typeparam>
    10. ''' <remarks></remarks>
    11. Public Interface IRepository(Of TKey, TEntity As IKeyedEntity(Of TKey))
    12.     Inherits IRepositoryRead(Of TKey, TEntity)
    13.     Inherits IRepositoryWrite(Of TKey, TEntity)
    14.  
    15. End Interface

    Worked example - a memory backed repository...

    To show this in action a very simple memory-backed repository could look like this:-
    vb.net Code:
    1. Namespace MemoryBacked
    2.  
    3.     Public Class ClientRepository
    4.         Implements IClientRepository
    5.  
    6.         ' Backing store for this data
    7.         Private m_data As New Dictionary(Of Integer, ClientRecord)
    8.  
    9.         Public Function Exists(key As Integer) As Boolean Implements IRepositoryRead(Of Integer, ClientRecord).Exists
    10.             Return m_data.ContainsKey(key)
    11.         End Function
    12.  
    13.         Public Function GetAll() As IReadOnlyDictionary(Of Integer, ClientRecord) Implements IRepositoryRead(Of Integer, ClientRecord).GetAll
    14.             Return m_data.Values()
    15.         End Function
    16.  
    17.         Public Function GetByKey(key As Integer) As ClientRecord Implements IRepositoryRead(Of Integer, ClientRecord).GetByKey
    18.             If (m_data.ContainsKey(key)) Then
    19.                 Return m_data(key)
    20.             Else
    21.                 Return Nothing
    22.             End If
    23.         End Function
    24.  
    25.         Public Function GetWhere(clause As Func(Of ClientRecord, Boolean)) As IReadOnlyDictionary(Of Integer, ClientRecord) Implements IRepositoryRead(Of Integer, ClientRecord).GetWhere
    26.             Return m_data.Values.Where(clause)
    27.         End Function
    28.  
    29.         Public Function AddNew(entity As ClientRecord) As Integer Implements IRepositoryWrite(Of Integer, ClientRecord).AddNew
    30.  
    31.             If (entity.ClientUniqueKey = 0) Then
    32.                 entity.ClientUniqueKey = m_data.Count
    33.             End If
    34.  
    35.             If Not (m_data.ContainsKey(entity.ClientUniqueKey)) Then
    36.                 m_data.Add(entity.ClientUniqueKey, entity)
    37.             End If
    38.  
    39.             Return entity.ClientUniqueKey
    40.         End Function
    41.  
    42.         Public Sub AddOrUpdate(entity As ClientRecord, key As Integer) Implements IRepositoryWrite(Of Integer, ClientRecord).AddOrUpdate
    43.             If Not (m_data.ContainsKey(entity.ClientUniqueKey)) Then
    44.                 m_data.Add(entity.ClientUniqueKey, entity)
    45.             Else
    46.                 m_data(entity.ClientUniqueKey) = entity
    47.             End If
    48.         End Sub
    49.  
    50.         Public Sub Delete(key As Integer) Implements IRepositoryWrite(Of Integer, ClientRecord).Delete
    51.             If (m_data.ContainsKey(key)) Then
    52.                 m_data.Remove(key)
    53.             End If
    54.         End Sub
    55.     End Class
    56.  
    57. 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.
    Last edited by Merrion; Feb 23rd, 2014 at 03:11 PM. Reason: Use IReadOnlyDictionary, makes more sense

  2. #2

    Thread Starter
    PowerPoster
    Join Date
    Jul 2002
    Location
    Dublin, Ireland
    Posts
    2,148

    Re: The repository pattern

    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:-

    vb.net Code:
    1. ''' <summary>
    2. ''' An exception that occured when reading from the repository backing store
    3. ''' </summary>
    4. ''' <remarks>
    5. ''' The inner exception is from whatever
    6. ''' </remarks>
    7. Public Class RepositoryReadException
    8.     Inherits Exception
    9.  
    10.     ReadOnly m_fatal As Boolean
    11.  
    12.     Public ReadOnly Property Fatal As Boolean
    13.         Get
    14.             Return m_fatal
    15.         End Get
    16.     End Property
    17.  
    18.     Public Sub New(ByVal message As String, ByVal innerExcption As Exception, ByVal fatalInit As Boolean)
    19.         MyBase.New(message, innerExcption)
    20.         m_fatal = fatalInit
    21.     End Sub
    22.  
    23.  
    24.     Public Sub New(ByVal message As String, ByVal fatalInit As Boolean)
    25.         MyBase.New(message)
    26.         m_fatal = fatalInit
    27.     End Sub
    28. End Class

  3. #3
    Angel of Code Niya's Avatar
    Join Date
    Nov 2011
    Posts
    8,598

    Re: The repository pattern

    Nice article. I love this kind of layered design. Been a while since I used anything this clean in my own projects rhough. Time constraints and sometimes laziness usually lead to poorly thought out designs which results in some clumsy coupling of layers.

    BTW, I'm speaking about layered designs in general, not just business/data layer designs like those found in POS apps.
    Treeview with NodeAdded/NodesRemoved events | BlinkLabel control | Calculate Permutations | Object Enums | ComboBox with centered items | .Net Internals article(not mine) | Wizard Control | Understanding Multi-Threading | Simple file compression | Demon Arena

    Copy/move files using Windows Shell | I'm not wanted

    C++ programmers will dismiss you as a cretinous simpleton for your inability to keep track of pointers chained 6 levels deep and Java programmers will pillory you for buying into the evils of Microsoft. Meanwhile C# programmers will get paid just a little bit more than you for writing exactly the same code and VB6 programmers will continue to whitter on about "footprints". - FunkyDexter

    There's just no reason to use garbage like InputBox. - jmcilhinney

    The threads I start are Niya and Olaf free zones. No arguing about the benefits of VB6 over .NET here please. Happiness must reign. - yereverluvinuncleber

  4. #4

    Thread Starter
    PowerPoster
    Join Date
    Jul 2002
    Location
    Dublin, Ireland
    Posts
    2,148

    Re: The repository pattern

    I feel that things are becoming easier for layered/architecture systems because things like NuGet are bringing more of them into "common" usage. Also MVC and MVVM.

    These code examples come from my own home code, not work - but when I have them working I intend turn them into a project template and that makes it easier to do the right thing than the wrong thing....

Tags for this Thread

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width