Today I've been playing around with my business object implementation which acts as a wrapper around the data context. It holds a reference to the data context and all operations that occur in the business object generally are tied to this data context.
LINQ to SQL by default enables object tracking on each of the objects it returns from a query, which is not necessary though if you are truly just returning query results. So I figured it might be a good idea to add another ReadOnlyContext to the business object and explicitly allow turning off object tracking to improve performance and overhead.
At first this worked well, but I started running into odd problems where the full context works and the ReadOnlyContext would not. A number of these were my fault - I was modifying the query results and applying the non-read-only context to the queries. But a few other queries mysteriously failed without much of a clue of what was going on.
It turns out I've run into another oddity in LINQ to SQL in that ReadOnly context's cannot deal with deferred loading of entities.Before I found out I ended up simplifying the problem to a single query operation to demonstrate/
In the following query I retrieve a set of Entry objects for a time tracking app. The entries have related project entities (1-1 relation). If I run the following query with object tracking enabled:
TimeTrakkerContext context = new TimeTrakkerContext();
IQueryable<EntryEntity> q =
from ent in context.EntryEntities
where ent.UserPk == 1
orderby ent.TimeIn descending
select ent;
Response.Write(q.ToList()[0].ProjectEntity.ProjectName);
I get the right result. The project entity is filled and the project name returned.
But simply adding the ObjectTrackingEnabled flag and setting it to false like this:
TimeTrakkerContext context = new TimeTrakkerContext();
context.ObjectTrackingEnabled = false;
IQueryable<EntryEntity> q =
from ent in context.EntryEntities
where ent.UserPk == 1
orderby ent.TimeIn descending
select ent;
// fails here - Object Reference not set to an instance of an object
Response.Write(q.ToList()[0].ProjectEntity.ProjectName);
makes this very same query fail to retrieve the project entity properly.
It turns out that this is a known issue that has been reported on Connect before. Specifically here's a quote from Microsoft's Alex Turner from this Connect Bug report:
Without Object Tracking, each time you request a Customer from an Order, we can't tell whether we've already made an object for that Customer when navigating some other Order, and so we'll have to make a new Customer each time. Therefore, when Object Tracking is disabled, we do not allow navigation over relationships that would cause further queries to be executed, and thus further objects to be materialized, perhaps for the second time.
he goes on to suggest to either run with object tracking on or turn on eager loading.
The solution is either to leave Object Tracking enabled, or to use Eager Loading by setting LoadWith clauses on a DataLoadOptions object you give to your DataContext. With Eager Loading, you can tell LINQ to SQL that when you retrieve a set of Orders with Object Tracking off that you want to also get each Order's Customer object at the same time. This way, you can maintain Object Identity and a healthy object graph, even without Object Tracking.
Hmmph... that sure puts a damper on using a ReadOnly DataContext.
So at this point I'm reevaluating my use of a ReadOnlyContext in my application. I suppose it makes sense in true one level queries but for complex queries where relationships are required we're screwed.
Add that as another oddity of LINQ to SQL to the slew of inconsistent behaviors that LINQ to SQL exhibits...
Incidentally I decided to a little performance checking on the difference between using ObjectTrackingEnabled and not after I got into this a little further and while there's a small bit of overhead in terms of processing time, it's minute. As long as entities aren't updated there's not a lot of overhead. The penalty is mainly in additional startup code for the query, which is relatively small.
Given that having both a regular and read-only context can be confusing as there are other instances where the business object helper classes internally use the context to perform additional tasks such as data conversion. With two types of context there need to be overloads or separate objects to handle that scenario and it's not always clear to the consumer of a business method whether he's received a ReadOnly context or the Full context.
It sure would be nice that if an IQueryable (or maybe it should be IDataQueryable) could get a reference to the actual active DataContext that created the query - that would facilitate this scenario drastically. But alas this deferred loading behavior really puts the nail in the coffin of this whole idea anyway.
Single full context it is.
Other Posts you might also like