4

I am loading data from an external source as xml, it gets deserialized and then I loop over the object to funnel them into my domain entities.

In order to create the relationships between data and cut down on database calls I wrote an extension method to attempt to retrieve items from DbSet.Local first if it doesnt find the item then it uses DbSet.SingleOrDefault() to query the database as shown below.

public static TEntity SingleOrDefaultLocalFirst<TEntity>(this IDbSet<TEntity> set,
        Func<TEntity, bool> predicate) where TEntity : class
    {
        if (set == null)
            throw new ArgumentNullException("set");
        if (predicate == null)
            throw new ArgumentNullException("predicate");

        TEntity results = null;

        try
        {
            results = set.Local.SingleOrDefault(predicate);
        }
        catch (Exception e)
        {
            Debug.WriteLine(e.Message, "Error");
        }

        if (results != null)
        {
            return results;
        }
        return set.SingleOrDefault(predicate);
    }

The try catch block is there to suppress the problem I am trying to fix.

For some reason the navigation properties when querying the local store are not populated. So if I use something like

(x=>x.Participant.Event.ExternalId==newItem.Id)

as my lambda, the Participant nav property is null.

I feel like there should be some way to get this code to not consistently generate null reference errors.

I have tried explicitly loading the Participant and Event data from the database before my loop starts using

context.Participant.Load()

but this makes no difference.

Can someone tell me why the navigation properties are null and how to populate them the most efficient way?

And, if anyone is wondering why I am not using Find(), it is because the external data is keyed on a number of properties as well as an external id field which is not the primary key in my system.

Update:

Im not going to take the time to include my real code because there is just too much to whittle down to a usable example so I will try and use the Customer/Order/OrderItem typical example.

The real issue is that when you have a nested entity and you try and check for existence using something like:

var orderLine = context.OrderLineItems.Local.SingleOrDefault(x=>x.Order.Number == 1234)

it will throw a nullreference error for Order even if the Orders and Customers are loaded explicitly into the context prior to this using

context.Orders.Load()

But, if you do this:

var orderLine = context.OrderLineItems.SingleOrDefault(x=>x.Order.Number == 1234)

it will work.

I want to understand why it doesnt work when calling Local. Why should we have to make a trip to the database to get the related nav properties when they already have been loaded into the context? or am I missing something?

jmichas
  • 1,139
  • 1
  • 10
  • 21
  • By default navigation properties are not loaded. If you want to load them on the fly then use **lazy-loading**; you should only have to mark the properties as `virtual`. – Pragmateek Jul 09 '14 at 14:41
  • Yeah the properties are virtual and the data is being loaded explicitly using Load() so they exist in the context. My latest solution is to just check for null on the navigation property in the lambda first. Which is working, but I sill want to know why the navigation properties are null when they exist in the context. Maybe there is no solution besides what Im doing. – jmichas Jul 09 '14 at 16:54
  • Could you post the code of your entities classes on which you testes that piece of code? – mr100 Jul 09 '14 at 21:02
  • If you attach the deserialized objects to the context the associations should be restored automatically. – Gert Arnold Jul 09 '14 at 21:30
  • @Gert the deserialized objects are not domain entities. To ease the deserialization process the objects mimic the structure of the Xml. Even if they were mapped to the domain via automapper or something they could not just be attached because they would just be added since the external data does not include the internal key for the entities. – jmichas Jul 10 '14 at 16:37
  • @GertArnold "If you attach the deserialized objects to the context the associations should be restored automatically" no they should not. Unless you deserialized them into proxy objects. The reason that the properties need to be virtual is so that EF can override them with proxy classes. By doing the object instanciation outside of EF, EF has not had a chance to AOP its Lazy loading magic. – Aron Jul 10 '14 at 18:19
  • @Aron Well, I do this all the time. EF executes relationship fixup whether the objects are proxies or not. But see the OP's comment. This is not the problem. – Gert Arnold Jul 10 '14 at 18:28

2 Answers2

2

Well, I'm not sure why the behavior of DbSet.Local.xx is different than DbSet.xx than but what I eventually used as a solution was just to simply check for null in my lambda:

SingleOrDefaultLocalFirst(x=>
...
x.Participant!=null &&
x.Participant.Event.ExternalId==newItem.Id &&
...);

This seems to prevent my extension method from throwing an error when checking the Local items first and so gracefully moves onto calling the data store.

jmichas
  • 1,139
  • 1
  • 10
  • 21
0

The problem here is that the object you "deserialized" aren't Proxied.

When you get items out of a DbSet<T>, Entity Framework does not give you objects of type T. Instead it creates a new class TProxy, which overrides the association properties. That way, when you get the property, Entity Framework will know, and can intercept the call.

By deserializing your objects outside of Entity Framework, it does not have a chance to add the "hooks" to the getter, that it needs to Lazy load. So when you call get on the object's properties, EF does not lazy load.

The solution to this is to use Entity Framework to instanciate ALL your T objects, then deserialize into these objects, then attach them.

DbSet<T> set = ...;
T item = set.Create();
T deserialized = Deserialize(..);
AutoMapper.Map(deserialized, item);
set.Attach(item);
Aron
  • 15,464
  • 3
  • 31
  • 64