// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.Objects;
using System.Linq;
using System.Web.Http.Controllers;
using Microsoft.Web.Http.Data.EntityFramework.Metadata;
namespace Microsoft.Web.Http.Data.EntityFramework
{
///
/// Base class for DataControllers operating on LINQ To Entities data models
///
/// The Type of the LINQ To Entities ObjectContext
[LinqToEntitiesMetadataProvider]
public abstract class LinqToEntitiesDataController : DataController where TContext : ObjectContext, new()
{
private TContext _objectContext;
private TContext _refreshContext;
///
/// Protected constructor because this is an abstract class
///
protected LinqToEntitiesDataController()
{
}
///
/// Gets the
///
protected internal TContext ObjectContext
{
get
{
if (_objectContext == null)
{
_objectContext = CreateObjectContext();
}
return _objectContext;
}
}
///
/// Gets the used by retrieving store values
///
private ObjectContext RefreshContext
{
get
{
if (_refreshContext == null)
{
_refreshContext = CreateObjectContext();
}
return _refreshContext;
}
}
///
/// Initializes this .
///
/// The for this
/// instance. Overrides must call the base method.
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
// TODO: should we be turning this off categorically? Can we do this only
// for queries?
ObjectContext.ContextOptions.LazyLoadingEnabled = false;
// We turn this off, since our deserializer isn't going to create
// the EF proxy types anyways. Proxies only really work if the entities
// are queried on the server.
ObjectContext.ContextOptions.ProxyCreationEnabled = false;
}
///
/// Creates and returns the instance that will
/// be used by this provider.
///
/// The ObjectContext
protected virtual TContext CreateObjectContext()
{
return new TContext();
}
///
/// See .
///
/// A indicating whether or not the instance is currently disposing.
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_objectContext != null)
{
_objectContext.Dispose();
}
if (_refreshContext != null)
{
_refreshContext.Dispose();
}
}
base.Dispose(disposing);
}
///
/// This method is called to finalize changes after all the operations in the specified changeset
/// have been invoked. All changes are committed to the ObjectContext, and any resulting optimistic
/// concurrency errors are processed.
///
/// True if the was persisted successfully, false otherwise.
protected override bool PersistChangeSet()
{
return InvokeSaveChanges(true);
}
private bool InvokeSaveChanges(bool retryOnConflict)
{
try
{
ObjectContext.SaveChanges();
}
catch (OptimisticConcurrencyException ex)
{
// Map the operations that could have caused a conflict to an entity.
Dictionary operationConflictMap = new Dictionary();
foreach (ObjectStateEntry conflict in ex.StateEntries)
{
ChangeSetEntry entry = ChangeSet.ChangeSetEntries.SingleOrDefault(p => Object.ReferenceEquals(p.Entity, conflict.Entity));
if (entry == null)
{
// If we're unable to find the object in our changeset, propagate
// the original exception
throw;
}
operationConflictMap.Add(conflict, entry);
}
SetChangeSetConflicts(operationConflictMap);
// Call out to any user resolve code and resubmit if all conflicts
// were resolved
if (retryOnConflict && ResolveConflicts(ex.StateEntries))
{
// clear the conflics from the entries
foreach (ChangeSetEntry entry in ChangeSet.ChangeSetEntries)
{
entry.StoreEntity = null;
entry.ConflictMembers = null;
entry.IsDeleteConflict = false;
}
// If all conflicts were resolved attempt a resubmit
return InvokeSaveChanges(retryOnConflict: false);
}
// if there was a conflict but no conflict information was
// extracted to the individual entries, we need to ensure the
// error makes it back to the client
if (!ChangeSet.HasError)
{
throw;
}
return false;
}
return true;
}
///
/// This method is called to finalize changes after all the operations in the specified changeset
/// have been invoked. All changes are committed to the ObjectContext.
/// If the submit fails due to concurrency conflicts will be called.
/// If returns true a single resubmit will be attempted.
///
///
/// The list of concurrency conflicts that occurred
/// Returns true if the was persisted successfully, false otherwise.
protected virtual bool ResolveConflicts(IEnumerable conflicts)
{
return false;
}
///
/// Insert an entity into the , ensuring its is
///
/// The entity type
/// The entity to be inserted
protected virtual void InsertEntity(TEntity entity) where TEntity : class
{
ObjectStateEntry stateEntry;
if (ObjectContext.ObjectStateManager.TryGetObjectStateEntry(entity, out stateEntry) &&
stateEntry.State != EntityState.Added)
{
ObjectContext.ObjectStateManager.ChangeObjectState(entity, EntityState.Added);
}
else
{
ObjectContext.CreateObjectSet().AddObject(entity);
}
}
///
/// Update an entity in the , ensuring it is treated as a modified entity
///
/// The entity type
/// The entity to be updated
protected virtual void UpdateEntity(TEntity entity) where TEntity : class
{
TEntity original = ChangeSet.GetOriginal(entity);
ObjectSet objectSet = ObjectContext.CreateObjectSet();
if (original == null)
{
objectSet.AttachAsModified(entity);
}
else
{
objectSet.AttachAsModified(entity, original);
}
}
///
/// Delete an entity from the , ensuring that its is
///
/// The entity type
/// The entity to be deleted
protected virtual void DeleteEntity(TEntity entity) where TEntity : class
{
ObjectStateEntry stateEntry;
if (ObjectContext.ObjectStateManager.TryGetObjectStateEntry(entity, out stateEntry) &&
stateEntry.State != EntityState.Deleted)
{
ObjectContext.ObjectStateManager.ChangeObjectState(entity, EntityState.Deleted);
}
else
{
ObjectSet objectSet = ObjectContext.CreateObjectSet();
objectSet.Attach(entity);
objectSet.DeleteObject(entity);
}
}
///
/// Updates each entry in the ChangeSet with its corresponding conflict info.
///
/// Map of conflicts to their corresponding operations entries.
private void SetChangeSetConflicts(Dictionary operationConflictMap)
{
object storeValue;
EntityKey refreshEntityKey;
foreach (var conflictEntry in operationConflictMap)
{
ObjectStateEntry stateEntry = conflictEntry.Key;
if (stateEntry.State == EntityState.Unchanged)
{
continue;
}
// Note: we cannot call Refresh StoreWins since this will overwrite Current entity and remove the optimistic concurrency ex.
ChangeSetEntry operationInConflict = conflictEntry.Value;
refreshEntityKey = RefreshContext.CreateEntityKey(stateEntry.EntitySet.Name, stateEntry.Entity);
RefreshContext.TryGetObjectByKey(refreshEntityKey, out storeValue);
operationInConflict.StoreEntity = storeValue;
// StoreEntity will be null if the entity has been deleted in the store (i.e. Delete/Delete conflict)
bool isDeleted = (operationInConflict.StoreEntity == null);
if (isDeleted)
{
operationInConflict.IsDeleteConflict = true;
}
else
{
// Determine which members are in conflict by comparing original values to the current DB values
PropertyDescriptorCollection propDescriptors = TypeDescriptor.GetProperties(operationInConflict.Entity.GetType());
List membersInConflict = new List();
object originalValue;
PropertyDescriptor pd;
for (int i = 0; i < stateEntry.OriginalValues.FieldCount; i++)
{
originalValue = stateEntry.OriginalValues.GetValue(i);
if (originalValue is DBNull)
{
originalValue = null;
}
string propertyName = stateEntry.OriginalValues.GetName(i);
pd = propDescriptors[propertyName];
if (pd == null)
{
// This might happen in the case of a private model
// member that isn't mapped
continue;
}
if (!Object.Equals(originalValue, pd.GetValue(operationInConflict.StoreEntity)))
{
membersInConflict.Add(pd.Name);
}
}
operationInConflict.ConflictMembers = membersInConflict;
}
}
}
}
}