With EF Self Tracking Entities, it appears that using a single context for a "read" and then a subsequent "save" does not work. I guess I could let this one go as "By Design", but that doesn't mean I have to like it…
The Scenario: EF Self Tracking Entities for an MVC 3 site
I have an MVC3 site wherein I'm using Self Tracking Entities. These are exposed by repositories that I'm injecting into the controllers via Ninject. Since we're letting our IoC container create these items, the lifetime of the instances are "per request".
Inside the repository, I create an instance of the EF context in the constructor and use it for the lifetime of the instance. I know that it's bad form to hold on to an EF context for a long time, but a single request isn't all that long. Should be ok.
The Problem: Deletes don't work
My team started writing pages and stumbled on a problem. Deletes don't work!
I quickly assumed that this was the classic misunderstanding that Remove() != MarkAsDeleted(). Once we got past that stumbling block, we found that deletes still didn't work.
So I dug into the code, setup more unit tests to exercise the repositories. Definitely not working, in spite of the fact that I have nearly identical code in another application.
… Hours pass
… And I finally located the issue after much digging and searching.
If I re-use the same context for the "read" and the subsequent "save", then things don't work quite right. The changes to a given entity are detected, but deletes are not.
However, if I use a separate, newly created context for the save, then things do work. How come?
Digging into the STE template a bit further, we find that its ApplyChanges() method only analyzes the supplied object hierarchy for instances that don't already exist in the current EF context. If the EF context already knows about the instance, then ApplyChanges() doesn't analyze it's ObjectChangeTracker for changes. However if the object is not already tracked by the context, then the STE code will inspect the ObjectChangeTracker for changes and update the context accordingly.
That's a problem. I fetch an instance and it's children. Then I delete a child instance out of the collection. This sets the deleted instance's state to "deleted" and registers it in the parent's ObjectChangeTracker as an "item removed from a collection". Now, I call ApplyChanges(), but it says… "yeah, I already know about that entity. I don't need to check for changes."
Solution #1 - Always create a new EF Context
Set up the repositories to create a new context for each method call. I'm still considering this option, and may update the repository to do so, but I feel like this is a work around, not a fix.
Solution #2 - Alter the Self Tracking Entities T4 Template
While solution #1 would work, I kinda feel that STE's context should be able to support read -> change -> save on the same context. Why should I create another context? The objects are already in memory, tracked by EF.
So I looked into altering the EF STE T4 template to analyze "added" as well as "existing" entities when calling ApplyChanges(). Turns out it wasn't too bad:
Essentially, the above code gets the union of the entities already in the context, plus entities supplied to ApplyChanges(). Then it analyzes this complete set of entities for changes.
My unit tests against the modified ApplyChanges() implementation showed that it handled the "Read -> Change -> Save" sequence against a single instance of the EF context. I haven't tested this down every possible path, but so far it seems solid. And if you don't like this approach, then just go back to solution #1.
Side Note - Why am I using STE in the first place?
You may ask, "If you want to "read -> change -> save" all on the same context, why use STE in the first place? Good question!
Personally, I like the capabilities of entities generated by the STE template. The entities themselves are basically POCOs, but have the necessary logic in place to keep the hierarchy in tact and consistent. Since they're POCOs, I don't need any EF stuff anywhere outside my repository.
And while some of the logic in the application does "read -> change -> save" on the same context, there are other cases where I do work with them detached from the context.