Applicable to Silverlight primarily
Because all data retrieval and save operations in Silverlight are required to be asynchronous, navigation properties must be able to execute asynchronously as well. This means that they must be able to return their results within lambdas or callback methods that will execute at an indeterminate time in the future. What is unusual about this process is that property navigation by its nature must also return an immediate result. If the query needs to execute asynchronously, then this immediate result must have some way of indicating that it is not (yet) a real result and that the 'real' result is coming. This introduces the concepts of a PendingEntity and a PendingEntityList. Consider the following code:
| C# | public void NavigationBasicAsynchronous() { _em1.UseAsyncNavigation = true; // not needed in SILVERLIGHT var query = _em1.Orders.Where(o => o.OrderID == 10248); _query.ExecuteAsync(GotOrder); } private void GotOrder(EntityQueryOperation<Order> args) { if (args.Error != null) { Console.WriteLine(args.Error.Message); } else { // Retrieve a single related entity using a scalar navigation property Order targetOrder = (Order)args.Results.ToList()[0]; Console.WriteLine("Order: {0}", targetOrder.OrderID.ToString()); targetOrder.Customer.EntityAspect.PendingEntityResolved += Customer_PendingEntityResolved; Customer aCustomer = targetOrder.Customer; Console.WriteLine("Customer (from GotOrders): {0}", aCustomer.CompanyName); // Retrieve a collection of related entities using a //collection navigation properties targetOrder.OrderDetails.PendingEntityListResolved += OrderDetails_PendingEntityListResolved; } } void Customer_PendingEntityResolved(object sender, PendingEntityResolvedEventArgs e) { Customer customer = (Customer)e.ResolvedEntity; Console.WriteLine("Customer (from Customer_PendingEntityResolved): {0}", customer.CompanyName); } void OrderDetails_PendingEntityListResolved(object sender, PendingEntityListResolvedEventArgs<OrderDetail> e) { Console.WriteLine("OrderDetails retrieved: {0}", e.ResolvedEntities.Count); } private void PromptToContinue() { Console.WriteLine(); Console.WriteLine("Press ENTER to continue..."); Console.ReadLine(); } |
| VB | Public Sub NavigationBasicAsynchronous() ResetEntityManager(_em1) _em1.UseAsyncNavigation = True Dim query As IEntityQuery(Of Order) = _ _em1.Orders.Where(Function(o) o.OrderID = 10248) _em1.ExecuteQueryAsync(Of Order)(query, AddressOf GotOrder, Nothing) PromptToContinue() End Sub Private Sub GotOrder(ByVal args As EntityQueryOperation(Of Order)) If args.Error IsNot Nothing Then Console.WriteLine(args.Error.Message) Else ' Retrieve a single related entity using a scalar navigation property Dim targetOrder As Order = CType(args.Results.ToList()(0), Order) Console.WriteLine("Order: {0}", targetOrder.OrderID.ToString()) AddHandler targetOrder.Customer.EntityAspect.PendingEntityResolved, _ AddressOf Customer_PendingEntityResolved Dim aCustomer As Customer = targetOrder.Customer Console.WriteLine("Customer (from GotOrders): {0}", aCustomer.CompanyName) ' Retrieve a collection of related entities ' using a collection navigation property AddHandler targetOrder.OrderDetails.PendingEntityListResolved, _ AddressOf OrderDetails_PendingEntityListResolved Console.WriteLine("OrderDetails retrieved: {0}", _ targetOrder.OrderDetails.ToList().Count) End If End Sub Private Sub Customer_PendingEntityResolved(ByVal sender As Object, _ ByVal e As PendingEntityResolvedEventArgs) Dim customer As Customer = CType(e.ResolvedEntity, Customer) Console.WriteLine( _ "Customer (from Customer_PendingEntityResolved): {0}", customer.CompanyName) End Sub Private Sub OrderDetails_PendingEntityListResolved(ByVal sender _ As Object, ByVal e As PendingEntityListResolvedEventArgs(Of OrderDetail)) Console.WriteLine("OrderDetails retrieved: {0}", e.ResolvedEntities.Count) End Sub Private Sub ResetEntityManager(ByVal em As EntityManager) em.Clear() em.UseAsyncNavigation = False End Sub |
In the method’s first statement we set the UseAsyncNavigation property of the EntityManager to true. This step would be unnecessary in a Silverlight application, as true is the default, (and only) setting for that property in that environment. But the above code could run in both Silverlight and non-Silverlight environments.
Now consider the statements that retrieve the Order. For a couple of reasons, we can’t simply say this …
| C# | Order anOrder = _em1.Orders.FirstOrNullEntity(); |
| VB | Dim anOrder As Order = _em1.Orders.FirstOrNullEntity() |
…, because the attempt to execute the above statement would fail in a Silverlight app with a message to the effect that Queries in Silverlight must be executed asynchronously. So to get our single Order, we need to submit a query with a condition that retrieves the desired Order, as you saw in the main snippet. That query must, of course, also be submitted asynchronously, and a callback method provided to process the results. To retrieve a scalar result, such as First(), Single(), Count(), Sum() and others, you may also use the AsScalarAsync() extension on IEntityQuery<T> to convert your query into a scalar query which can be executed asynchronously.
| C# | ... var query = _em1.Orders.Where(o => o.OrderID == 10248); query.ExecuteAsync(GotOrder); ... } private void GotOrder(EntityQueryOperation<Order> args) { if (args.Error != null) { Console.WriteLine(args.Error.Message); } else { // Retrieve a single related entity using a scalar navigation property Order targetOrder = (Order)args.Results.ToList()[0]; Console.WriteLine("Order: {0}", targetOrder.OrderID.ToString()); } } |
| VB | ... Dim query As IEntityQuery(Of Order) = _ _em1.Orders.Where(Function(o) o.OrderID = 10248) _em1.ExecuteQueryAsync(Of Order)(query, AddressOf GotOrder, Nothing) ... End Sub Private Sub GotOrder(ByVal args As EntityQueryOperation(Of Order)) If args.Error IsNot Nothing Then Console.WriteLine(args.Error.Message) Else ' Retrieve a single related entity using a scalar navigation property Dim targetOrder As Order = CType(args.Results.ToList()(0), Order) Console.WriteLine("Order: {0}", targetOrder.OrderID.ToString()) End If End Sub |
In this case, since we’re using the primary key to fetch our Order, we know that args.Result will contain at most one entity; so we simply cast it into an Order and proceed.
To get the Customer related to that Order (refer back to the full snippet), we set up a handler for the PendingEntityResolved event of the Customer navigation property, targetOrder.Customer. Then to initiate the asynchronous retrieval of that customer, we reference it in a code statement:
| C# | Customer aCustomer = targetOrder.Customer; |
| VB | Dim aCustomer As Customer = targetOrder.Customer |
We included a call to Console.WriteLine() immediately following the above statement just to show that the desired Customer simply isn’t going to be available at that point. The statement will write out a blank for the Customer’s CompanyName. Where we will get results is in the Customer_PendingEntityResolved handler:
| C# | void Customer_PendingEntityResolved(object sender, PendingEntityResolvedEventArgs e) { Customer customer = (Customer)e.ResolvedEntity; Console.WriteLine("Customer (from Customer_PendingEntityResolved): {0}", customer.CompanyName); } |
| VB | Private Sub Customer_PendingEntityResolved(ByVal sender As Object, _ ByVal e As PendingEntityResolvedEventArgs) Dim customer As Customer = CType(e.ResolvedEntity, Customer) Console.WriteLine("Customer (from Customer_PendingEntityResolved): {0}", _ customer.CompanyName) End Sub |
For navigation properties that return a collection, DevForce provides a PendingEntityListResolved event, similar to the PendingEntityResolved event we’ve just discussed:
| C# | private void GotOrder(EntityQueryOperation<Order> args) { // Retrieve a collection of related entities usin // a collection navigation property Order targetOrder = (Order)args.Results.ToList()[0]; targetOrder.OrderDetails.PendingEntityListResolved += new EventHandler<PendingEntityListResolvedEventArgs<OrderDetail>>( OrderDetails_PendingEntityListResolved); } void OrderDetails_PendingEntityListResolved(object sender, PendingEntityListResolvedEventArgs<OrderDetail> e) { Console.WriteLine("OrderDetails retrieved: {0}", e.ResolvedEntities.Count); } |
| VB | Private Sub GotOrder(ByVal args As EntityQueryOperation(Of Order)) ' Retrieve a collection of related entities usin ' a collection navigation property Dim targetOrder As Order = CType(args.Results.ToList()(0), Order) AddHandler targetOrder.OrderDetails.PendingEntityListResolved, _ AddressOf OrderDetails_PendingEntityListResolved End Sub Private Sub OrderDetails_PendingEntityListResolved(ByVal sender As Object, _ ByVal e As PendingEntityListResolvedEventArgs(Of OrderDetail)) Console.WriteLine("OrderDetails retrieved: {0}", e.ResolvedEntities.Count) End Sub |
When we run the full snippet, the code displays the following results in the Console window:

The output line “Press ENTER to continue..” comes from the utility method PromptToContinue(), which executes synchronously and immedately. Then we see reflected back the OrderID of the retrieved Order; the non-existent CompanyName of the not-yet-retrieved, related Customer; the CompanyName of the Customer written after its retrieval by the Customer_PendingEntityResolved callback method; and the display of OrderDetails retrieved, written by the OrderDetails_PendingEntityListResolved method.
If you’re working in C#, you can also use inline, anonymous methods for your ExecuteQueryAsync() callbacks:
| C# | public void NavigationBasicAsynchronousAnonymousCallback() { _em1.UseAsyncNavigation = true; IEntityQuery<Order> query = _em1.Orders.Where(o => o.OrderID == 10248); _em1.ExecuteQueryAsync<Order>( query, // IEntityQuery<Order> (args) => { // AsyncCompletedCallbac Console.WriteLine("Order: {0}", // " ((Order)args.Results.ToList()[0]).OrderID); // " }, // " null // UserState object ); PromptToContinue(); } |
| VB | Public Sub NavigationBasicAsynchronousAnonymousCallback() _em1.UseAsyncNavigation = True Dim query As IEntityQuery(Of Order) = _ _em1.Orders.Where(Function(o) o.OrderID = 10248) _em1.ExecuteQueryAsync(Of Order)(query, Sub(args) _ Console.WriteLine("Order: {0}", (CType(args.Results.ToList()(0), _ Order)).OrderID), Nothing) ' UserState object - " - " - " PromptToContinue() End Sub |
These are handy when the logic to be included in the callback isn’t too involved.
When does the entitymanager fetch myOrder’s line items from the data source?
We might have written DevForce to fetch them automatically when it fetched myOrder. But if DevForce were to get the line items automatically, why stop there? It could get the customer for the order, the sales rep for the order, and the products for each line item.
Those are just the immediate neighbors. It could get the customer’s headquarter address, the sales rep’s address and manager, and each product’s manufacturer. If it continued like this, it might fetch most of the database.
Retrieving the entire graph is obviously wasteful and infeasible. How often do we want to know the manager of the sales rep who booked the order? Clearly we have to prune the object graph. But where do we prune? How can we know in advance which entities we will need and which we can safely exclude?
We cannot know. Fortunately, we don’t have to know. We don’t have to know if we can be certain of continuous connection to the data source. If we expect the application to run offline, we’ll have to anticipate the related entities we’ll need and pre-fetch them. We’ll get to this issue later. We keep it simple. We use an entity query to get the root entities (such as myOrder). Then we use entity navigation to retrieve neighboring related entities as we need them.
This just-in-time approach is called deferred retrieval (also known as "lazy instantiation", "lazy loading", "Just-In-Time [JIT] data retrieval", and so on).