- Home
- LLBLGen Pro
- LLBLGen Pro Runtime Framework
Getting context to work without side effects
I am fetching an entity with a prefetch path that includes some lookup tables, and I am trying to keep unique instances of these lookup table entities rather than have dozens or hundreds of instances which are different but have the same data. So I am trying to use a context as a cache. I have seen many times on the forum Frans correct people and say that a context is not a cache, and talk about "semantic context", but it sure looks like it functions as a cache, so that is how I am trying to use it.
Here's my code where I set up the context and use it while fetching my top-level entity.
well = new WellEntity(wellID)
Context context = new Context();
context.Add(traceSetTypes);
context.Add(traceTypes);
context.Add(units);
context.Add(interpolationTypes);
adapter.FetchEntity(well, prefetchPath, context);
In this case, I'm adding 4 EntityCollections containing lookup table entities. This works as expected, the context appears to be used by FetchEntity and even though I might have lots of trace objects in "feet", they all have the same in-memory instance of a UnitEntity for "feet".
After I successfully fetch the well, I hook Well.TraceSets.EntityRemoved so that if a trace set is deleted by the user, I can put that in a UnitOfWork object for later deletion from the database. After I added the context to the FetchEntity() call, I began experiencing some odd behavior. If I closed the well without making any changes, then opened again the same well, my EntityRemoved handler would fire, and I could tell that trace sets that belong to the well were being removed from it. When I say "it" here, I mean from the earlier well instance, not the one I have just created again to reopen the well. This does not happen when I do not fetch with the context.
I reread the documentation on contexts, and the help says
When an entity object is added to a Context, its internally referenced related entity objects and entity collection objects are added to the same Context object as well.
...and so on. So I realized that when I was opening the well for the first time, my lookup lists were "clean", i.e., the entities had just been fetched from the database and were not attached to any other entities. When I closed the well, I was done with it as far as I was concerned, but the lookup entities were now attached to other entities which still live in memory, and which the context object knows about. Then, when I want to load the new well, and I create a new context and add these same entity collections to it, it appears the new context object wants to do a couple of things:
1) It wants to return objects other than what I asked for, like the trace set object it knows about. This was weird to me at first, because by adding the 4 lists of entities, I thought I was telling the context object, "When an entity is fetched that matches the type and primary key values of one of the entities in these lists, I want the existing instance to be used". But the context object says to me "sure, and also I'm going to do that for any other entities I can find in an object graph I will build based on the entities you explicitly asked to be cached". (I've been doing this for too many years, and often code talks to me).
2) Having decided that even though I don't want it to, it's going to cache trace sets, it also decides to unhook the trace sets from the previous well entity they belong to (which might be reasonable, or even required, but I can't think about it too much or my brain will hurt), and when it does that, my handler, which is still wired to the old well, fires and proceeds to put the trace sets in the uow to be deleted, which of course I don't want.
When I unload my well, I simply set my well variable to null. I do not unsubscribe to the events that I subscribed to. I suppose this is a good example of why I should; and maybe it would solve my problem, I'm not sure. However, I decided upon a different solution. Originally I was populating these collections only once, in my BLL constructor. Now in order to avoid confusing the context, I decided, at least for the moment, to simply reload the lookup tables before every fetch of a well, and thus always put "clean" (so to speak) lists of lookup entities into the context object, so it cannot know about any other entities.
And here's where this really gets weird. I did this, and was very surprised to find that it did not solve my problem at all. Really, this is a puzzler. Here's my code now
well = new WellEntity(wellID)
// notice I am refetching all lookup entities directly from the db!
LoadLookups();
// and the rest of the code is as before...
Context context = new Context();
context.Add(traceSetTypes);
context.Add(traceTypes);
context.Add(units);
context.Add(interpolationTypes);
adapter.FetchEntity(well, prefetchPath, context);
Once again, during the call to FetchEntity(), my handler for EntityRemoved fires and I am being told about disconnecting a trace set from a well. I cannot understand why.
For the sake of completeness here's LoadLookups(). As you can see, the lists are recreated and fetched from the db, and I verified in the debugger that the entities in them are not attached to anything when I fetch the well.
private void LoadLookups()
{
using (DataAccessAdapter adapter = new DataAccessAdapter())
{
traceSetTypes = new EntityCollection<TraceSetTypeEntity>();
ISortExpression sort = new SortExpression(TraceSetTypeFields.TraceSetType | SortOperator.Ascending);
adapter.FetchEntityCollection(traceSetTypes, null, 0, sort);
traceTypes = new EntityCollection<TraceTypeEntity>();
sort = new SortExpression(TraceTypeFields.TraceType | SortOperator.Ascending);
adapter.FetchEntityCollection(traceTypes, null, 0, sort);
units = new EntityCollection<UnitEntity>();
sort = new SortExpression(UnitFields.Unit | SortOperator.Ascending);
adapter.FetchEntityCollection(units, null, 0, sort);
interpolationTypes = new EntityCollection<InterpolationTypeEntity>();
adapter.FetchEntityCollection(interpolationTypes, null);
}
}
If I closed the well without making any changes, then opened again the same well, my EntityRemoved handler would fire
What do you mean by the Close and Open?
Then, when I want to load the new well, and I create a new context and add these same entity collections to it, it appears the new context object wants to do a couple of things:
Are you creating a new Context rather than using the old one (global one)?
By close I mean set my existing WellEntity variable to null. Here's the function that does that:
public void UnloadWell()
{
well = null;
uow.Reset();
}
So you can see I also reset the UnitOfWork object I maintain for deleted entities (in case the user was closing without saving changes, etc.).
I do create a new context every time, you can see that in the code in my first post.
Jim
Otis wrote:
Looking into it. (It's a complex problem, event handlers tend to keep objects in memory etc.).
Heh, I thought you might be, it certainly wouldn't be like you to just let something like this sit out there.
Otis wrote:
About the email: it might have ended in your spam filter?
It didn't arrive in my inbox (neither did one for your post). I get other emails from you guys, though. So that's strange.
After I initially posted (and checked the option to subscribe), I edited my post. I wonder if it got unhooked? Not that important, I'm just pointing it out, FYI.
Thanks Jim
JimFoye wrote:
(snip) When I unload my well, I simply set my well variable to null. I do not unsubscribe to the events that I subscribed to. I suppose this is a good example of why I should; and maybe it would solve my problem, I'm not sure.
Rule of thumb: every event you bind a handler to needs to be cleaned up by you, unless the HANDLER side is kept longer in memory than the EVENT side. Example: Entity e's EntityContentsChanged event is bound to Form F's event handler OnEEntityContentsChanged(..).
Now, if you don't clean up that binding, and F goes out of scope because the user closed the form, and e is kept around (in a cache or somewhere else), it will be that e keeps F in memory, because it holds a reference to F.OnEEntityContentsChanged.
In THIS particular case, the handler side is kept longer in memory, so the binding is cleaned up by the Garbage collector.
Now to your problem
When you add an entity with a graph to a Context, all entities in that graph which aren't already in a context are added to that context. This is done because the property 'ActiveContext' doesn't have a parameter, so recursive set is chosen. Not a big deal, really, unless the context is meant to live longer than some entities in it, as is the case here. Then you'll run into the problem that the Context holds a reference to the entity which is gone out of scope, so the entity (traceset for example) is kept in-memory and thus the event binding is also not cleaned up.
What you should do right before well=null; is: well.ActiveContext = null;
and then well = null;
Or, you can also do: context.Remove(well); and then well=null;
(edit) about the email, they're sent using a non-existing email address as from address. Perhaps that's the culprit.
Ah, all is clear now! Well, almost all.
First, I understand what you are saying about event handlers. In this case, as you point out, the objects that fire the events goes out of scope before the handlers do, so I don't have to explicitly clean them up. This may be a little sloppy on my part, and I'm a guy who was absolutely anal about deleting every little C++ object I ever created, but I guess I've gotten too comfortable in the managed code world, this was never a problem for me and I've just lapsed into not doing it. But like you say, I shouldn't have to.
Anyway, I now realize something very important that I didn't before. I assumed that the context object was used and thrown away. I allocate it locally just before my fetch, and thought it was out of scope afterwards.
Context context = new Context();
context.Add(traceSetTypes);
context.Add(traceTypes);
context.Add(units);
context.Add(interpolationTypes);
adapter.FetchEntity(well, prefetchPath, context);
I never knew this ActiveContext property existed on all entity objects! I'm sure there is a good reason why, though I don't know it. I just need the context when fetching in order to have unique entities instead of new ones created for every record in the lookup tables. I'm not sure why it needs to stick around. I may still be unclear on what the purpose of the context object is. I've reread the relevant section in the manual, but I guess I'm just not quite getting it.
Anyway, I can see when I unload the well that ActiveContext is set, and if I set that to null (as well as all the related trace sets and traces), then the problem goes away. Also, I've tried unhooking all the events on these objects, and that works, too.
Thanks!
JimFoye wrote:
Ah, all is clear now! Well, almost all.
First, I understand what you are saying about event handlers. In this case, as you point out, the objects that fire the events goes out of scope before the handlers do, so I don't have to explicitly clean them up. This may be a little sloppy on my part, and I'm a guy who was absolutely anal about deleting every little C++ object I ever created, but I guess I've gotten too comfortable in the managed code world, this was never a problem for me and I've just lapsed into not doing it. But like you say, I shouldn't have to.
Anyway, I now realize something very important that I didn't before. I assumed that the context object was used and thrown away. I allocate it locally just before my fetch, and thought it was out of scope afterwards.
Context context = new Context(); context.Add(traceSetTypes); context.Add(traceTypes); context.Add(units); context.Add(interpolationTypes); adapter.FetchEntity(well, prefetchPath, context);
I never knew this ActiveContext property existed on all entity objects! I'm sure there is a good reason why, though I don't know it. I just need the context when fetching in order to have unique entities instead of new ones created for every record in the lookup tables. I'm not sure why it needs to stick around. I may still be unclear on what the purpose of the context object is. I've reread the relevant section in the manual, but I guess I'm just not quite getting it.
An entity references the context it is in so when you pass the entity to a routine (not across a wire) and you add an entity there, you can add it to the same context there, by accessing the property. This reference keeps the context alive and thus the entities inside the context.