Up Query using property navigation

Asynchronous navigation properties

Last modified on October 21, 2011 12:47

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:  

(Note that this example has been written to run in a desktop environment, instead of Silverlight, in order to more easily make use of the Console.WriteLine to illustrate the timing of events)
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

Asynchronous collection navigation properties

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:

ConsoleWindow-retrieved.png

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.

Using an anonymous method for navigation property callback

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.

Deferred retrieval

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).

Tags: Navigation
Created by DevForce on December 06, 2010 17:38

This wiki is licensed under a Creative Commons 2.0 license. XWiki Enterprise 3.2 - Documentation. Copyright © 2012 IdeaBlade