RemovedEntities in EntityGraph.GetChanges()

Mar 2, 2011 at 1:14 PM

Hi! I am using Contrib with EntityGraph for a LOB-SL-Caliburn.Micro-Application for my company.

I have the following problem: When I remove an associated entity from an entity (one-to-many-rel. like one-company to many-employees), the removed 'employee' does not show up in CurrentCompanyEntity.EntityGraph().GetChanges().RemovedEntities. BUT: If I look into domainContext.EntityContainer.GetChanges(), the removed employee shows up there with EntityState.Deleted, as it should be.

Shouldn't the results be the same in this case? Did I do something wrong?

In case I forgot something:

  • I have the Attributes EntityGraph, Include and Association in the Metadata-Class of the CurrentCompanyEntity-Type.
  • I am using the same domainContext throughout.
  • Added and Modified entities show up in the EntityGraph.GetChanges() as it should be.

I am glad for every hint!

Thanks in advance, Roland

Developer
Mar 4, 2011 at 9:18 AM

Hi Roland,

Thanks for reporting this.

No, you didn't do anything wrong.

An entitygraph is not a domaincontext which is able to keep track of all
loaded entities. The GetChanges operation on an entity graph is only
able to return a changeset for the entities that are _in_ the entity
graph.

What happens in you case is that as soon as you remove the employee
using the Remove operation on the employee's entityset, the association
with this employee is removed from the corresponding
CurrentCompanyEntity (not from the employee though). (you can verify
yourself that after removing the employee, the CurrentCompanyEntity is
no longer associated with the employee, but the employee is still
associated with CurrentCompanyEntity).

The entitygraph keeps track of collection changed events to grow or
shrink the graph when entities are connected or disconnected from the
graph. In your situation, the employee is removed from the entity
collection of the CurrentCompanyEntity, which results in a collection
changed event. This means that the removed employee no longer belongs to
the entitygraph and therefore is not included in the changeset.

This makes sense, since the employee does not belong to the entitygraph
for CurrentCompanyEntity anymore. However, it also makes the usefulness
of GetChanged().RemovedEntities debatable. I will think of that.

 

I hope this answers your question

Mar 4, 2011 at 10:05 AM

Thanks for the answer, that helps a lot.

The question, that comes up now, is: How do I delete a single (associated) entity?

The reason for me to choose Ria Services Contrib is the ability to save/edit single entities. That's awesome, but I'm not able to delete single entities. Hopefully I just don't see the forest for the trees, and there is an obvious way to do it, but I just can't find.

After that, I discovered EntityGraph and that would fit my needs perfectly, regarding associations. But then I got to the problem described above.

It would be awesome to have a method like domainContext.SubmitChanges(EntityGraph graph) that saves the "core-entity" and all changes in the associated entities (including add and delete).

Is there already a way to achieve this?

Developer
Mar 4, 2011 at 10:25 AM

The ability to only submit the changes of an entity graph is a feature
that I've been thinking of myself as well. I've not implemented it
because I don't know an obvious way to do so.

Back to your question:
I'm not sure if I fully get your point. Deleting a single entity can be
done by calling the Remove method on the entity's entity set. So, given
a domain context 'ctx', an employee 'e' and a corresponding employee
entityset 'ctx.Employees' you can remove 'e' using the statement
'ctx.Employees.Remove(e);' When you call 'ctx.SubmitChanges()' the
pending changes in the domain context 'ctx', including the removed
entity' will be send to the server and executed there. But I'm sure you
already know that, right?

There is currently no simple way to control what changes are sent to the
server, other than creating a second domain context, detaching entities
from the first context, attaching them to the second, calling
SubmitChanges to the second context, and moving the entities back to the
first context. But this is probably more difficult in practice then I
just described. Entitygraph may be useful here, because it supports
detaching a complete Entitygraph from a context. So it may help migrating
entities from one context to another.

Mar 4, 2011 at 11:21 AM

Yes, I'm aware of ctx.SubmitChanges(). But assuming that I have three changed (deleted) entities in my context, and I just want to submit one, I can't call SubmitChanges(), cause that would submit all three.

Your last paragraph... It sounds quite interesting and gives me somewhat like an idea. Would it be that difficult? If i just populate the temporary domainContext with the entities (the entityset) I get via Entitygraph.GetChanges(), it should work as the Ria Services Contrib example with a single entity. If deleted entities would show up in RemovedEntities, you could modify, add and delete through that solution via Entitygraph.

I think I'm gonna investigate in that a bit more.

Developer
Mar 4, 2011 at 12:07 PM

I would be very much interested in your experiences doing that.

One of the difficulties is to get the entities that you attach to the
second domain context in the same state is they were in the first domain
context. You need the stuff from contrib for that. But I think it is
very interesting to give it a try.



Developer
Apr 20, 2011 at 8:35 PM
Edited Apr 20, 2011 at 8:37 PM
RAuer wrote:

Yes, I'm aware of ctx.SubmitChanges(). But assuming that I have three changed (deleted) entities in my context, and I just want to submit one, I can't call SubmitChanges(), cause that would submit all three.


I just wanted to share with you what I've done to satisfy this demand.
(30 min ago this post was going to be: I cant figure out how to do this either, since not only do I care about deleting more than a single entity, I also care about new/updated entities as well)

in my VM

                var toDelete = this.SelectedAnTn;

                if (toDelete.EntityState == EntityState.New)
                { /* it's new and unsaved, so simply remove & detach */
                    this.Customer.ANsTNs.Remove(toDelete);
                    this.ServiceAgent.ReferenceContext
                        .CustomerAnTns.Detach(toDelete);
                    
                    toDelete = null;
                }
                else
                { /* it's in the db. remove from EntitySet then submit */
                    this.ServiceAgent.ReferenceContext.CustomerAnTns.Remove(toDelete);
                    toDelete.SubmitSingleChanges<CustomerAnTn, ReferenceContext>(
                        (ent) =>
                        {
                            toDelete = null;
                        },
                        (ex) => { });
                }
Ria Services Contrib Extension methods .. (sorry this is so verbose)
    public static class RiaContribEntityExtensions
    {

        public static void SubmitSingleChanges<EntityType, DomainContextType>(this EntityType originalEntity, Action<EntityType> onSuccess, Action<Exception> onError)
            where EntityType : Entity
            where DomainContextType : DomainContext
        {
            // create temporary DomainContext & Entity that will be saved.
            var tempContext = (DomainContextType)Activator.CreateInstance(typeof(DomainContextType));
            var tempEntity = (EntityType)Activator.CreateInstance(typeof(EntityType));
            // get the corresponding EntitySet in the DomainContext for the EntityType
            var entitySet = tempContext.EntityContainer.GetEntitySet<EntityType>();

            CloneEntityIntoEntitySet<EntityType>(originalEntity, tempEntity, entitySet);

            tempContext.SubmitChanges
                (w =>
                    originalEntity.SyncToOriginalEntity(w, onSuccess, onError),
                    null);
        }

        private static void CloneEntityIntoEntitySet<EntityType>(EntityType originalEntity, EntityType tempEntity, EntitySet<EntityType> entitySet) where EntityType : Entity
        {

            if (originalEntity.EntityState == EntityState.Modified)
            {
                entitySet.Attach(tempEntity);
                tempEntity.ApplyState(
                    originalEntity.ExtractState(ExtractType.OriginalState),
                    originalEntity.ExtractState(ExtractType.ModifiedState));
            }
            else if (originalEntity.EntityState == EntityState.Deleted)
            {
                entitySet.Attach(tempEntity);
                tempEntity.ApplyState(
                    originalEntity.ExtractState(ExtractType.OriginalState),
                    originalEntity.ExtractState(ExtractType.ModifiedState));
                entitySet.Remove(tempEntity);
            }
            else
            {
                tempEntity.ApplyState(
                    originalEntity.ExtractState(ExtractType.ModifiedState),
                    null);
                entitySet.Add(tempEntity);
            }
        }

        public static void SyncToOriginalEntity<T>(this T originalEntity, SubmitOperation submitOp, Action<T> onSuccess, Action<Exception> onError) 
            where T : Entity
        {
            try
            {
                if (submitOp.Error != null)
                {
                    submitOp.MarkErrorAsHandled();
                    throw new Exception(
                        "The Submit operation encountered an error during Save/Update.", 
                        submitOp.Error);
                }

                T saved = null;
                if (submitOp.ChangeSet.AddedEntities.Count == 1)
                {
                    saved = submitOp.ChangeSet.AddedEntities.FirstOrDefault() as T;
                }
                else if (submitOp.ChangeSet.ModifiedEntities.Count == 1)
                {
                    saved = submitOp.ChangeSet.ModifiedEntities.FirstOrDefault() as T;
                }
                else if (submitOp.ChangeSet.RemovedEntities.Count == 1)
                {
                    saved = submitOp.ChangeSet.RemovedEntities.FirstOrDefault() as T;
                }
                else
                {
                    throw new Exception(
                        "unexpected: no entities added, modified, or deleted?",
                        new NotImplementedException());
                }

                if (saved != null)
                {
                    originalEntity.ApplyState(
                        saved.ExtractState(ExtractType.OriginalState),
                        saved.ExtractState(ExtractType.ModifiedState));

                    onSuccess(originalEntity);
                }
                else
                {
                    throw new Exception(
                        "unexpected: added or modified entity returned, but was null?",
                        new NotImplementedException());
                }
            }
            catch (Exception ex)
            {
                onError(ex);
            }
        }
    }