// 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; } } } } }