Skip to content

Instantly share code, notes, and snippets.

@danielgreen
Last active May 27, 2016 06:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save danielgreen/5706441 to your computer and use it in GitHub Desktop.
Save danielgreen/5706441 to your computer and use it in GitHub Desktop.
Suggested Unit Of Work facade to wrap around the Entity Framework ObjectContext. May require adjustment for use with DbContext in latest versions on Entity Framework. Includes a mechanism to perform concurrency checks on entities based on timestamps.
// http://dillieodigital.wordpress.com/2011/10/12/automatic-user-and-time-stamping-in-entity-framework-4/
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Objects;
using System.Web;
using MyApp.BLL;
namespace MyApp.DAL
{
public partial class AppContainer
{
partial void OnContextCreated()
{
this.SavingChanges += new EventHandler(context_SavingChanges);
}
private static void context_SavingChanges(object sender, EventArgs e)
{
// Find any new or modified entities using our user/timestamping
// fields and update them accordingly.
foreach (ObjectStateEntry entry in
((ObjectContext)sender).ObjectStateManager
.GetObjectStateEntries (EntityState.Added | EntityState.Modified))
{
// Only work with entities that have our user/timestamp records in them.
if (!entry.IsRelationship)
{
CurrentValueRecord entryValues = entry.CurrentValues;
if (entryValues.GetOrdinal("updater_id") > 0)
{
HttpContext currContext = HttpContext.Current;
int userId = 0;
DateTime now = DateTime.UtcNow.TrimMilliseconds();
// store UTC time to ensure no ambiguity when clocks change
// trim milliseconds in case the client cannot retain the value to that degree of accuracy
if (currContext.User.Identity.IsAuthenticated)
{
if (currContext.Session["userId"] != null)
{
userId = (int)currContext.Session["userId"];
}
else
{
// Call custom moethod to retrieve User ID
userId = Security.GetUserId(currContext.User.Identity.Name);
}
}
entryValues.SetInt32(entryValues.GetOrdinal("updater_id"), userId);
entryValues.SetDateTime(entryValues.GetOrdinal("updated_at"), now);
if (entry.State == EntityState.Added)
{
entryValues.SetInt32(entryValues.GetOrdinal("creator_id"), userId);
entryValues.SetDateTime(entryValues.GetOrdinal("created_at"), now);
}
}
}
}
}
}
}
using System;
using App.Data.Plumbing.Service;
using App.Data.Services.Interfaces;
namespace App.Data.Services
{
public class AppUnitOfWork : UnitOfWork<AppContainer>, IAppUnitOfWork
{
public IFooService Foos { get; set; }
public IBarService Bars { get; set; }
public TORUnitOfWork(IFooService foos, IBarService bars)
{
this.Foos = foos;
this.Bars = bars;
}
}
}
using System.Data.Objects;
namespace App.Data.Plumbing.Service
{
public abstract class BaseEFService
{
public TORContainer Container { get; set; }
public BaseEFService(ObjectContext context)
{
Container = ((TORContainer)context);
}
}
}
using System;
using System.Linq;
using System.Linq.Expressions;
using LinqKit;
using App.Data.Plumbing.Repository;
namespace App.Data.Plumbing.Service
{
public abstract class CrudService<TEntity>: BaseEFService where TEntity : class
{
protected IRepository<TEntity> Repository { get; set; }
public void Delete(TEntity existingEntity)
{
Repository.Delete(existingEntity);
}
public void DeleteAll()
{
Repository.DeleteAll();
}
public virtual TEntity Insert(TEntity newEntity)
{
return Repository.Insert(newEntity);
}
public virtual IQueryable<TEntity> SelectAll()
{
return Repository.SelectAll();
}
public virtual TEntity SelectBy(Expression<Func<TEntity, bool>> predicate)
{
return SelectAll().AsExpandable().Where(predicate).SingleOrDefault();
}
public virtual void DeleteAll(Expression<Func<TEntity, bool>> predicate)
{
var toDelete = SelectAll().AsExpandable().Where(predicate).ToList();
foreach (var entity in toDelete)
{
Repository.Delete(entity);
}
}
public virtual IQueryable<TEntity> SelectFilteredList(Expression<Func<TEntity, bool>> predicate)
{
//build query starting with "all"
var all = SelectAll();
//now add optional filters
if (predicate != null)
{
all = all.AsExpandable().Where(predicate);
}
return all;
}
public TEntity Update(TEntity existingEntity)
{
return Repository.Update(existingEntity);
}
public TEntity Update(TEntity existingEntity, TEntity original)
{
return Repository.Update(existingEntity, original);
}
public int Count()
{
return Repository.Count();
}
protected CrudService(IRepository<TEntity> repository): base(repository.Context)
{
this.Repository = repository;
}
}
}

AppContainer.cs helps to automatically timestamp entities as they are added and updated. The CrudService.Update method wraps a call to ApplyOriginalValues; this allows you to supply an object with the entity's original values, allowing Entity Framework to carry out a concurrency check. For the check to take place, the entity should have a field (e.g. updated_at) marked with ConcurrencyMode = Fixed in the entity model. The original value is the value as it was when the entity was retrieved and send to the client. When the client posts an update, the original value should be included with the posted values. When the updated_at value is sent to the client, it should be explicitly defined as a UTC time (i.e. have Kind = UTC). Similarly, after it is received from the client, convert it to UTC if it is not already in that state. These steps ensure that the concurrency check works as expected. Use DateTime extension methods such as ToUniversalSpecifyKind to help with this conversion. Of course, the client could modify the original value in the postback to fool the concurrency check. Maybe the client should only receive a hash of the value. Or could HTTP headers such as ETag or Last-Modified help us?

The ObjectContext does not act as a cache; by default, queries fetch from the database and don't include any new uncommitted objects previously added to the context. DbContext.Find does allow you to query the in-memory context.

See http://msdn.microsoft.com/en-us/data/hh949853.aspx for EF performance considerations.

using System.Data;
using System.Data.Objects;
using System.Data.Objects.DataClasses;
using System.Linq;
using System;
namespace App.Data.Plumbing.Repository
{
public class EntityRepository<TEntity> : IRepository<TEntity> where TEntity : class
{
private EntityKey CreateEntityKey(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
return Context.CreateEntityKey(Context.GetEntitySetName(entity.GetType()), entity);
}
public void Delete(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
Entities.DeleteObject(GetAttachedEntity(entity));
}
public void DeleteAll()
{
foreach (TEntity entity in Entities.ToList())
{
Entities.DeleteObject(entity);
}
}
private TEntity GetAttachedEntity(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
if (!Context.EntityIsDetached(entity))
{
return entity;
}
var ek = CreateEntityKey(entity);
return (TEntity)Context.GetObjectByKey(ek);
}
public TEntity Insert(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
Entities.AddObject(entity);
return entity;
}
public IQueryable<TEntity> SelectAll()
{
return Entities;
}
public TEntity Update(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
if (Context.EntityIsDetached(entity))
{
object dbEntity = GetAttachedEntity(entity);
Entities.ApplyCurrentValues(entity);
return (TEntity)dbEntity;
}
// otherwise, it's already attached, and modifications are already tracked
return entity;
}
public TEntity Update(TEntity entity, TEntity original)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
if (original == null)
{
throw new ArgumentNullException("original");
}
TEntity entityToReturn = entity;
if (Context.EntityIsDetached(entity))
{
object dbEntity = GetAttachedEntity(entity);
Entities.ApplyCurrentValues(entity);
entityToReturn = (TEntity)dbEntity;
}
// otherwise, it's already attached, and modifications are already tracked
// Use the entity with the original values to be applied to the context
Entities.ApplyOriginalValues(original);
return entityToReturn;
}
public int Count()
{
return Entities.Count();
}
public IObjectContext Context { get; set; }
private IDataSet<TEntity> Entities { get; set; }
public EntityRepository(IObjectContext context, IDataSet<TEntity> entities)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
Context = context;
//Entities = context.CreateObjectSet<TEntity>();
}
}
}
using App.Data.Plumbing.Repository;
using App.Data.Plumbing.Service;
using App.Data.Services.Interfaces;
namespace App.Data.Services
{
/// <summary>
/// Service wrapper for Foo data access
/// </summary>
public class FooService : CrudService<EventPriority>, IFooService
{
public FooService(IRepository<Foo> repository) : base(repository)
{
}
}
}
using App.Data.Plumbing.Service;
namespace App.Data.Services.Interfaces
{
public interface IAppUnitOfWork : IUnitOfWork
{
IFooService Foos { get; }
IBarService Bars { get; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace App.Data.Plumbing.Interfaces
{
public interface IDataSet<T> : IQueryable<T>, IEnumerable<T> where T : class
{
T Create();
void Add(T entity);
void Remove(T entity);
void Attach(T entity);
void Detach(T entity);
void SetOriginalValues(T original);
}
}
using System;
using System.Linq;
using System.Linq.Expressions;
namespace App.Data.Plumbing.Service
{
public interface IEntityService<TEntity>
{
/// <summary>
/// Deletes an entity from the context/database.
/// </summary>
/// <param name="entity">An entity used to supply key information. Only the primary key values are important here. Does not have to be attached to a context.</param>
void Delete(TEntity entity);
/// <summary>
/// Deletes all entities from this repository.
/// </summary>
void DeleteAll();
/// <summary>
/// Deletes all entities from this repository matching the query
/// </summary>
void DeleteAll(Expression<Func<TEntity, bool>> predicate);
/// <summary>
/// Insert a new entity into the context/database.
/// </summary>
/// <param name="entity">Values for the new entity.</param>
/// <returns>The newly inserted entity. PK values from the DB won't be populated until you call UnitOfWork.Commit();</returns>
TEntity Insert(TEntity entity);
// Select() is reserved by the compiler for LINQ. So we need a different name.
/// <summary>
/// Returns a typed query which you can enumerate to get all instances of this type, refine with .Where, project onto a new type, etc.
/// To return a single object, you can do something like: repository.SelectAll().Where(o => o.Id == 123).Single();
/// For a list of POCOs, try var list = repository.SelectAll().Select(o => new PocoObject { Id = o.Id, Data = o.Data });
/// </summary>
/// <returns>Strongly-typed query.</returns>
IQueryable<TEntity> SelectAll();
/// <summary>
/// Selects a single entity using a predicate
/// </summary>
/// <param name="predicate"></param>
/// <returns></returns>
TEntity SelectBy(Expression<Func<TEntity, bool>> predicate);
/// <summary>
///
/// </summary>
/// <param name="page"></param>
/// <param name="size"></param>
/// <param name="count"></param>
/// <param name="?"></param>
/// <param name="predicate"> </param>
/// <returns></returns>
IQueryable<TEntity> SelectFilteredList(Expression<Func<TEntity, bool>> predicate);
/// <summary>
/// Update an entity in the context/database. If the entity is attached (i.e., was returned from Select(),
/// then only modified properties will be saved. If it's detached, all properties will be assumed to be modified.
/// </summary>
/// <param name="entity">Entity instance used for both key information and new property values.</param>
/// <returns>The updated, attached entity.</returns>
TEntity Update(TEntity entity);
/// <summary>
/// Update an entity in the context/database. If the entity is attached (i.e., was returned from Select(),
/// then only modified properties will be saved. If it's detached, all properties will be assumed to be modified.
/// Also sets the entity's original values in the context. This can be used to ensure an accurate concurrency check.
/// </summary>
/// <param name="entity">Entity instance used for both key information and new property values.</param>
/// <param name="original">An entity used for key information and original property values.</param>
/// <returns>The updated, attached entity.</returns>
TEntity Update(TEntity entity, TEntity original);
/// <summary>
/// Returns a count of all entities
/// </summary>
/// <returns></returns>
int Count();
}
}
using App.Data.Plumbing.Service;
namespace App.Data.Services.Interfaces
{
/// <summary>
/// Public interface for the FooService
/// </summary>
public interface IFooService : IEntityService<Foo>
{
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace App.Data.Plumbing.Interfaces
{
public interface IObjectContext : IDisposable
{
void Save();
void RefreshFromApplication(IEnumerable entities);
void RefreshFromDatabase(IEnumerable entities);
}
}
using System;
using System.Data.Objects;
using System.Data.Objects.DataClasses;
using System.Linq;
using System.Linq.Expressions;
namespace App.Data.Plumbing.Repository
{
public interface IRepository<TEntity> where TEntity : class
{
/// <summary>
/// Deletes an entity from the context/database.
/// </summary>
/// <param name="entity">An entity used to supply key information. Only the primary key values are important here. Does not have to be attached to a context.</param>
void Delete(TEntity entity);
/// <summary>
/// Deletes all entities from this repository.
/// </summary>
void DeleteAll();
/// <summary>
/// Insert a new entity into the context/database.
/// </summary>
/// <param name="entity">Values for the new entity.</param>
/// <returns>The newly inserted entity. PK values from the DB won't be populated until you call UnitOfWork.Commit();</returns>
TEntity Insert(TEntity entity);
// Select() is reserved by the compiler for LINQ. So we need a different name.
/// <summary>
/// Returns a typed query which you can enumerate to get all instances of this type, refine with .Where, project onto a new type, etc.
/// To return a single object, you can do something like: repository.SelectAll().Where(o => o.Id == 123).Single();
/// For a list of POCOs, try var list = repository.SelectAll().Select(o => new PocoObject { Id = o.Id, Data = o.Data });
/// </summary>
/// <returns>Strongly-typed query.</returns>
IQueryable<TEntity> SelectAll();
/// <summary>
/// Update an entity in the context/database.
/// </summary>
/// <param name="entity">Entity instance used for both key information and new property values.
/// If the entity is attached (i.e., was returned from Select(), then only modified properties will be saved.
/// If it's detached, all properties will be assumed to be modified.</param>
/// <returns>The updated, attached entity.</returns>
TEntity Update(TEntity entity);
/// <summary>
/// Update an entity in the context/database. If the entity is attached (i.e., was returned from Select(),
/// then only modified properties will be saved. If it's detached, all properties will be assumed to be modified.
/// Also sets the entity's original values in the context. This can be used to ensure an accurate concurrency check.
/// </summary>
/// <param name="entity">Entity instance used for both key information and new property values.</param>
/// <param name="getOriginalValues">An entity used for key information and original property values.</param>
/// <returns>The updated, attached entity.</returns>
TEntity Update(TEntity entity, TEntity original);
/// <summary>
/// Returns a count of all entities
/// </summary>
/// <returns></returns>
int Count();
IObjectContext Context { get; set; }
}
}
using System;
namespace App.Data.Plumbing.Service
{
public interface IUnitOfWork : IDisposable
{
/// <summary>
/// Save changes to the database
/// </summary>
/// <returns></returns>
int Commit();
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using App.Data.Plumbing.Interfaces;
using System.Data;
using System.Data.Objects;
using App.Utility;
namespace App.Data.Plumbing.Adapters
{
public class ObjectContextAdapter : IObjectContext
{
virtual protected ObjectContext Context { get; set; }
public ObjectContextAdapter(ObjectContext context)
{
this.Context = context;
}
#region IObjectContext Members
virtual public void Save()
{
try
{
Context.SaveChanges();
}
catch (OptimisticConcurrencyException ex)
{
throw new AppConcurrencyException("The information has been updated by another user. You will need to reload before you can save any changes.", ex);
}
}
virtual public void RefreshFromApplication(IEnumerable entities)
{
Context.Refresh(RefreshMode.ClientWins, entities);
}
virtual public void RefreshFromDatabase(IEnumerable entities)
{
Context.Refresh(RefreshMode.StoreWins, entities);
}
#endregion
#region IDisposable Members
public void Dispose()
{
Context.Dispose();
}
#endregion
}
}
using System;
using App.Data.Plumbing.Repository;
using System.Data.Objects;
using System.Data;
namespace App.Data.Plumbing.Service
{
public abstract class UnitOfWork<TContext> : IUnitOfWork where TContext : IObjectContext
{
public TContext Context { get; set; }
protected UnitOfWork(TContext context)
{
this.Context = context;
}
public int Commit()
{
return Context.SaveChanges();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (Context != null)
{
Context.Dispose();
Context = null;
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment