Search code examples
breeze

breeze - modify an entity, on the server, based upon it's navigational properties, before saving


Is there any way to get an Entity's navigational property's "current" value in BeforeSaveEntity (or anywhere else before save) in breeze on the server side? By current, I mean what exists in the database, with any incoming changes merged in. This isn't for validation, rather I am computing a value for a parent property (that I don't want on the client) based upon both the parent fields and children fields...

For example,

public class Parent {
  public ICollection<Child> Children{ get; set; }
}

. . .

protected override bool BeforeSaveEntity(EntityInfo entityInfo) {
  if (entityInfo.Entity.GetType() == typeof(Parent) &&
  (entityInfo.EntityState == EntityState.Added || entityInfo.EntityState == EntityState.Updated)) {

   // Lazy load Parent's Children collection out of breeze's context 
   // so items are "current' (existing merged with changes)

   Parent parent = (Parent)entityInfo.Entity;
   Context.Entry(parent).Collection(p => p.Children).Load();

   // this throws exception Member 'Load' cannot be called for property
   // 'Children' because the entity of type 'Parent' does not exist in the context.
  }
}  

I'm thinking they are not in the DBContext yet. All I can think to do is to retrieve the existing children from the database, and hand merge the changes in BeforeSaveEntities, which is a hassle.


Solution

  • Lazy loading is not enable in the DbContext that Breeze uses for saving. The reason is detailed in this SO answer.

    You should load any additional entities in a separate DbContext.


    Here's an example of how I did it in a project. Perhaps the MergeEntities and DetachEntities methods should be included with Breeze to make it simpler to do this.

    protected override Dictionary<Type, List<EntityInfo>> BeforeSaveEntities(Dictionary<Type, List<EntityInfo>> saveMap)
    {
        // create a separate context for computation, so we don't pollute the main saving context
        using (var newContext = new MyDbContext(EntityConnection, false))
        {
            var parentFromClient = (Parent)saveMap[typeof(Parent)][0].Entity;
    
            // Load the necessary data into the newContext
            var parentFromDb = newContext.Parents.Where(p => p.ParentId == parentFromClient.ParentId)
                .Include("Children").ToList();
    
            // ... load whatever else you need...
    
            // Attach the client entities to the ObjectContext, which merges them and reconnects the navigation properties
            var objectContext = ((IObjectContextAdapter)newContext).ObjectContext;
            var objectStateEntries = MergeEntities(objectContext, saveMap);
    
            // ... perform your business logic...
    
            // Remove the entities from the second context, so they can be saved in the original context
            DetachEntities(objectContext, saveMap);
        }
        return saveMap;
    }
    
    /// Attach the client entities to the ObjectContext, which merges them and reconnects the navigation properties
    Dictionary<ObjectStateEntry, EntityInfo> MergeEntities(ObjectContext oc, Dictionary<Type, List<EntityInfo>> saveMap)
    {
        var oseEntityInfo = new Dictionary<ObjectStateEntry, EntityInfo>();
        foreach (var type in saveMap.Keys)
        {
            var entitySet = this.GetEntitySetName(type);
            foreach(var entityInfo in saveMap[type])
            {
                var entityKey = oc.CreateEntityKey(entitySet, entityInfo.Entity);
                ObjectStateEntry ose;
                if (oc.ObjectStateManager.TryGetObjectStateEntry(entityKey, out ose))
                {
                    if (ose.State != System.Data.Entity.EntityState.Deleted)
                        ose.ApplyCurrentValues(entityInfo.Entity);
                }
                else
                {
                    oc.AttachTo(entitySet, entityInfo.Entity);
                    ose = oc.ObjectStateManager.GetObjectStateEntry(entityKey);
                }
    
                if (entityInfo.EntityState == Breeze.ContextProvider.EntityState.Deleted)
                {
                    ose.Delete();
                }
                oseEntityInfo.Add(ose, entityInfo);
            }
        }
        return oseEntityInfo;
    }
    
    /// Remove the entities in saveMap from the ObjectContext; this separates their navigation properties
    static void DetachEntities(ObjectContext oc, Dictionary<Type, List<EntityInfo>> saveMap)
    {
        foreach (var type in saveMap.Keys)
        {
            foreach (var entityInfo in saveMap[type])
            {
                try
                {
                    oc.Detach(entityInfo.Entity);
                }
                catch
                { // the object cannot be detached because it is not attached
                }
            }
        }
    }