Meeting the needs of your business from a distance

Repository Pattern with Entity Framework 4

by Mark Shiffer 28. June 2010 13:41

An original post for once! :-)

I am in the process of bringing up a new architecture for a project that I am working on that uses MVVM with WPF, WCF, Unity and Entity Framework 4 with Self Tracking Entities. One of the foundational items that is necessary to create as part of this architecture is a Repository to abstract the data store. As a related item, a proper Unit of Work pattern needs to be instantiated in order to provide transactional boundaries for some of the data processes. I will talk more about Unit of Work in my next post, but the following is what I came up with to implement the Repository pattern.

Before I started designing my approach I read several different attempts by others at implementing these patterns. Some were better than others, but in the end no one approach seemed to be a best fit for my application. However, the heaviest influence on my design came from NCommon, a library that contains implementations of commonly used design patterns when developing applications. If you haven’t looked at NCommon before, I would suggest taking a gander as it has some interesting stuff. However, for my purposes, it introduced too much complexity by trying to be a generic framework that applied to several different technologies, whereas, directing my approach to my specific set of technologies (while still abstracting mind you), made the code much more straight forward.

The Repository interface contains the standard set of Find methods that you would expect a repository to have using predicate Expressions in order to pass through to LINQ. This makes it extremely flexible and easy to use while still remaining type safe to call. In addition, two ApplyChanges methods are provided to allow for add, modify, deletes via Self-Tracking Entities, and a Refresh method to force an entity to re-load its values from the data store.

Repository Interface:

   1: /// <summary>Base interface for all repositories 
   2: /// </summary>
   3: /// <typeparam name="T">Aggregate type the repository is serving</typeparam>
   4: public interface IRepository<T> : IDisposable where T : class, IObjectWithChangeTracker
   5: {
   6:     /// <summary>
   7:     /// Return the entire set of persisted items
   8:     /// </summary>
   9:     /// <param name="includes">Sub-entities to include.</param>
  10:     /// <returns>Collection of all persisted items</returns>
  11:     IList<T> FindAll(params string[] includes);
  12:  
  13:     /// <summary>
  14:     /// The first element in source that passes the test in predicate
  15:     /// </summary>
  16:     /// <param name="predicate">The predicate.</param>
  17:     /// <param name="includes">The includes.</param>
  18:     /// <returns>The first element in source that passes the test in predicate.</returns>
  19:     [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Intended")]
  20:     T First(Expression<Func<T, bool>> predicate, params string[] includes);
  21:  
  22:     /// <summary>
  23:     /// Gets the first element of a sequence, or a default value if the sequence
  24:     /// contains no elements.
  25:     /// </summary>
  26:     /// <param name="predicate">The predicate.</param>
  27:     /// <param name="includes">The includes.</param>
  28:     /// <returns>Returns the first element of a sequence, or a default value if the sequence
  29:     /// contains no elements.</returns>
  30:     [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Intended")]
  31:     T FirstOrDefault(Expression<Func<T, bool>> predicate, params string[] includes);
  32:  
  33:     /// <summary>
  34:     /// Gets the single element of the input sequence.
  35:     /// </summary>
  36:     /// <param name="predicate">The predicate.</param>
  37:     /// <param name="includes">The includes.</param>
  38:     /// <returns>The single element of the input sequence.</returns>
  39:     [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Intended"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Single", Justification = "Intended")]
  40:     T Single(Expression<Func<T, bool>> predicate, params string[] includes);
  41:  
  42:     /// <summary>
  43:     /// Gest a collection of all persisted items that pass the test in the predicate
  44:     /// </summary>
  45:     /// <param name="predicate">The predicate.</param>
  46:     /// <param name="includes">The includes.</param>
  47:     /// <returns>Collection of all persisted items that pass the test in the predicate</returns>
  48:     [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Intended")]
  49:     IList<T> Find(Expression<Func<T, bool>> predicate, params string[] includes);
  50:  
  51:     /// <summary>
  52:     /// Persist changes a single entity to storage
  53:     /// </summary>
  54:     /// <param name="toSave">Entity to save</param>
  55:     void ApplyChanges(T toSave);
  56:  
  57:     /// <summary> Persist changes a set of entities to storage
  58:     /// </summary>
  59:     /// <param name="toSave">Collection of entities to save</param>
  60:     void ApplyChanges(IEnumerable<T> toSave);
  61:  
  62:     /// <summary>
  63:     /// Refreshes the specified entities from the data store.
  64:     /// </summary>
  65:     /// <param name="entities">The entities.</param>
  66:     void Refresh(params T[] entities);
  67: }

Two generic implementations are provided. In order to deal with how Entity Framework handles the hierarchy of an aggregate, we need to know about both the base type of the aggregate and the final concrete type. For example, if you have an Employee entity that descends from a person Entity, the ObjectSet in Entity Framework is retrieved via Person and the OfType<T>() method is used with the concrete type to get only Employee entities. So, Repository<BaseType> is provided as a quick way to access the repository when there is no hierarchy (BaseType=ConcreteType). Repository<BaseType, ConcreteType> is where the meat of the implementation is.

The majority of the class is self-explanatory from the code, just a couple of things to mention:

1. Object Sets – I customized the T4 template for the Entity Framework ObjectContext to include a GetObjectSet method. This method allows me to tap into the helper object sets that the context already contains in a generic manner and reuse them across the life of a context.

2. ApplyToContext – This method is a bit messy, frankly, because Entity Framework itself is a bit messy at times. The ApplyChanges method of the ObjectContext in Entity Framework only works when the entity you are trying to save was loaded from outside of the context, otherwise the changes are not recognized properly. So I make a call to TryGetObjectStateEntry to see if my entity was previously loaded and then call ChangeObjectState for a delete or ApplyCurrentValues for a modify based upon the Self Tracking Entities state.

Repository Implementation:

   1: /// <summary>
   2: /// Simple entity repository
   3: /// </summary>
   4: /// <typeparam name="BaseType">Base class type of entity being managed by the repository</typeparam>
   5: [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleClass", Justification = "Makes sense in this circumstance.")]
   6: public class Repository<BaseType> : Repository<BaseType, BaseType>
   7:     where BaseType : class, IObjectWithChangeTracker
   8: {
   9:     /// <summary>
  10:     /// Initializes a new instance of the <see cref="Repository&lt;BaseType&gt;"/> class.
  11:     /// </summary>
  12:     /// <param name="contextFactory">The context factory.</param>
  13:     /// <param name="transactionManager">The transaction manager.</param>
  14:     public Repository(IContextFactory contextFactory, ITransactionManager transactionManager)
  15:         : base(contextFactory, transactionManager)
  16:     {
  17:     }
  18: }
  19:  
  20: /// <summary>Base class for entity repositories 
  21: /// </summary>
  22: /// <typeparam name="BaseType">Base class type of entity being managed by the repository</typeparam>
  23: /// <typeparam name="ConcreteType">Concrete (derived) class type of entity being managed by the repository</typeparam>
  24: public class Repository<BaseType, ConcreteType> : IRepository<ConcreteType>, IDisposable
  25:     where BaseType : class, IObjectWithChangeTracker
  26:     where ConcreteType : class, BaseType
  27: {
  28:     private ITransactionManager transactionManager;
  29:     private ObjectContext localContext;
  30:     private bool disposed;
  31:  
  32:     // hide the context from descendants, they should not be able to get to a context directly if we can help it. They can get to it through the ObjectSet which
  33:     // we can't hide, but make it difficult to keep the repository single focused.
  34:     private IContextFactory contextFactory;
  35:  
  36:     /// <summary>
  37:     /// Initializes a new instance of the <see cref="Repository{BaseType,ConcreteType}"/> class.
dding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; font-size: 8pt; border-left-style: none; overflow: visible; padding-top: 0px">  38:     /// </summary>
  39:     /// <param name="contextFactory">Factory that generates ObjectContext objects.</param>
  40:     /// <param name="transactionManager">The transaction manager.</param>
  41:     public Repository(IContextFactory contextFactory, ITransactionManager transactionManager)
  42:     {
  43:         this.transactionManager = transactionManager;
  44:         this.contextFactory = contextFactory;
  45:     }
  46:  
  47:     /// <summary>
  48:     /// Gets the object set to act on given the current transaction state
  49:     /// </summary>
  50:     /// <value>The current object set.</value>
  51:     /// <returns>The ObjectSet to use for this repository</returns>
  52:     protected ObjectSet<BaseType> CurrentObjectSet
  53:     {
  54:         get
  55:         {
  56:             // if currently in a transaction, then assume it's context, otherwise use a locally created one.
  57:             ObjectContext context = TransactionManager != null ? TransactionManager.CurrentContext : null;
  58:  
  59:             // create a context and manage its life if we are not already under a transaction scope
  60:             if (context == null)
  61:             {
  62:                 if (localContext == null)
  63:                 {
  64:                     localContext = contextFactory.CreateContext();
  65:                 }
  66:  
  67:                 context = localContext;
  68:             }
  69:  
  70:             return ((SrsModelContainer)context).GetObjectSet<BaseType>();
  71:         }
  72:     }
  73:  
  74:     /// <summary>
  75:     /// Gets the transaction manager.
  76:     /// </summary>
  77:     /// <value>The transaction manager.</value>
  78:     private ITransactionManager TransactionManager
  79:     {
  80:         get
  81:         {
  82:             return transactionManager;
  83:         }
  84:     }
  85:  
  86:     /// <summary>
  87:     /// Gets the first element in source that passes the test in predicate.
  88:     /// </summary>
  89:     /// <param name="predicate">The predicate.</param>
  90:     /// <param name="includes">The includes.</param>
  91:     /// <returns>
  92:     /// The first element in source that passes the test in predicate.
  93:     /// </returns>
  94:     public ConcreteType First(Expression<Func<ConcreteType, bool>> predicate, params string[] includes)
  95:     {
  96:         return GetObjectSet(includes).First(predicate);
  97:     }
  98:  
  99:     /// <summary>
 100:     /// Returns the first element of a sequence, or a default value if the sequence
 101:     /// contains no elements
 102:     /// </summary>
 103:     /// <param name="predicate">The predicate.</param>
 104:     /// <param name="includes">The includes.</param>
 105:     /// <returns>Returns the first element of a sequence, or a default value if the sequence
 106:     /// contains no elements.</returns>
 107:     public ConcreteType FirstOrDefault(Expression<Func<ConcreteType, bool>> predicate, params string[] includes)
 108:     {
 109:         return GetObjectSet(includes).FirstOrDefault(predicate);
 110:     }
 111:  
 112:     /// <summary>
 113:     /// The single element of the input sequence
 114:     /// </summary>
 115:     /// <param name="predicate">The predicate.</param>
 116:     /// <param name="includes">The includes.</param>
 117:     /// <returns>The single element of the input sequence.</returns>
 118:     public ConcreteType Single(Expression<Func<ConcreteType, bool>> predicate, params string[] includes)
 119:     {
 120:         return GetObjectSet(includes).Single(predicate);
 121:     }
 122:  
 123:     /// <summary>
 124:     /// Gets a collection of all persisted items that pass the test in the predicate
 125:     /// </summary>
 126:     /// <param name="predicate">The predicate.</param>
 127:     /// <param name="includes">The includes.</param>
 128:     /// <returns>Collection of all persisted items that pass the test in the predicate</returns>
 129:     public IList<ConcreteType> Find(Expression<Func<ConcreteType, bool>> predicate, params string[] includes)
 130:     {
 131:         return GetObjectSet(includes).Where(predicate).ToList();
 132:     }
 133:  
 134:     /// <summary>
 135:     /// Gets a collection of all persisted items
 136:     /// </summary>
 137:     /// <param name="includes">Sub-entities to include.</param>
 138:     /// <returns>Collection of all persisted items</returns>
 139:     public virtual IList<ConcreteType> FindAll(params string[] includes)
 140:     {
 141:         return GetObjectSet(includes).ToList();
 142:     }
 143:  
 144:     /// <summary>
 145:     /// Applies the changes for a single entity to the context
 146:     /// </summary>
 147:     /// <param name="toSave">Modified entity to apply changes for.</param>
 148:     public virtual void ApplyChanges(ConcreteType toSave)
 149:     {
 150:         ValidateAggregate(toSave);
 151:  
 152:         ApplyToContext(toSave);
 153:  
 154:         // if no current transaction, then save, otherwise it belongs to the unit of work scope
 155:         if (TransactionManager == null || TransactionManager.CurrentContext == null)
 156:         {
 157:             GetObjectSet().Context.SaveChanges();
 158:         }
 159:     }
 160:  
 161:     /// <summary> Applies the changes for a collection of entities to the context
 162:     /// </summary>
 163:     /// <param name="toSave">Collection of modified entities to apply changes for.</param>
 164:     public virtual void ApplyChanges(IEnumerable<ConcreteType> toSave)
 165:     {
 166:         foreach (var item in toSave)
 167:         {
 168:             ValidateAggregate(item);
 169:  
 170:             ApplyToContext(item);
 171:         }
 172:  
 173:         // if no current transaction, then save, otherwise it belongs to the unit of work scope
 174:         if (TransactionManager == null || TransactionManager.CurrentContext == null)
 175:         {
 176:             GetObjectSet().Context.SaveChanges();
 177:         }
 178:     }
 179:  
 180:     /// <summary>
 181:     /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
 182:     /// </summary>
 183:     public void Dispose()
 184:     {
 185:         this.Dispose(true);
 186:         GC.SuppressFinalize(this);
 187:     }
 188:  
 189:     /// <summary>
 190:     /// Refreshes the specified entities from the data store.
 191:     /// </summary>
 192:     /// <param name="entities">The entities.</param>
 193:     public void Refresh(params ConcreteType[] entities)
 194:     {
 195:         CurrentObjectSet.Context.Refresh(RefreshMode.StoreWins, entities);
 196:     }
 197:  
 198:     /// <summary>
 199:     /// Validates the specified record to save and throws ValidationException if condition is not met
 200:     /// </summary>
 201:     /// <param name="toSave">The record to save.</param>
 202:     protected virtual void ValidateAggregate(ConcreteType toSave)
 203:     {
 204:         // no default implementation
 205:     }
 206:  
 207:     /// <summary>
 208:     /// Gets the object set.
 209:     /// </summary>
 210:     /// <param name="includes">The includes.</param>
 211:     /// <returns>
 212:     /// Object set with the specified dependent objects included.
 213:     /// </returns>
 214:     protected ObjectQuery<ConcreteType> GetObjectSet(params string[] includes)
 215:     {
 216:         var objectSet = CurrentObjectSet.OfType<ConcreteType>();
 217:         foreach (var include in includes)
 218:         {
 219:             objectSet = objectSet.Include(include);
 220:         }
 221:  
 222:         return objectSet;
 223:     }
 224:  
 225:     /// <summary>
 226:     /// Applies changes for the given entity to context.
 227:     /// </summary>
 228:     /// <param name="entityToSave">The entity to save.</param>
 229:     protected void ApplyToContext(ConcreteType entityToSave)
 230:     {
 231:         // If the object toSave was an existing record loaded from the same context that we are attempting to save it back to 
 232:         // then some special considerations are needed.
 233:         ObjectStateEntry entityStateEntry;
 234:         if (CurrentObjectSet.Context.ObjectStateManager.TryGetObjectStateEntry(entityToSave, out entityStateEntry))
 235:         {
 236:             object contextEntity = entityStateEntry.Entity;
 237:  
 238:             if (entityToSave.ChangeTracker.State == ObjectState.Deleted)
 239:             {
 240:                 // it's possible to have a different reference to the same entity (entity keys are the same). This will cause an error
 241:                 // when trying to delete, so we need to make sure that we are deleting the entity that is already in the store.
 242:                 CurrentObjectSet.Context.ObjectStateManager.ChangeObjectState(contextEntity, EntityState.Deleted);
 243:             }
 244:             else if (entityToSave.ChangeTracker.State == ObjectState.Modified)
 245:             {
 246:                 // if the entity already in the store is not the same reference as the entity we are trying to save (even though they represent the same record in the store)
 247:                 // we need to move the changes over to the record already in the store. If they are the same, the changes are already there.                        
 248:                 if (!object.ReferenceEquals(contextEntity, entityToSave))
 249:                 {
 250:                     CurrentObjectSet.Context.ApplyCurrentValues(CurrentObjectSet.EntitySet.Name, entityToSave);
 251:                 }
 252:             }
 253:         }
 254:         else
 255:         {
 256:             // Apply changes only works when the object toSave does not already have a reference in the context's ObjectStateManager
 257:             CurrentObjectSet.ApplyChanges(entityToSave);
 258:         }
 259:     }
 260:  
 261:     /// <summary>
 262:     /// Releases unmanaged and - optionally - managed resources
 263:     /// </summary>
 264:     /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
 265:     private void Dispose(bool disposing)
 266:     {
 267:         if (disposed)
 268:         {
 269:             return;
 270:         }
 271:  
 272:         if (disposing)
 273:         {
 274:             if (localContext != null)
 275:             {
 276:                 localContext.Dispose();
 277:             }
 278:         }
 279:  
 280:         disposed = true;
 281:     }
 282: }

That’s it. Next time we’ll look at the Unit of Work Pattern implementation that I came up with which is a bit more involved than the Repository.

Tags: ,

Programming

Comments

Add comment


(Will show your Gravatar icon)

  Country flag


  • Comment
  • Preview
Loading



Copyright © 2001-2012 MS Consulting, Inc. All Rights Reserved.