Direct EntityQuery Execution – “Non-Accumulating execution”


For a while internally on the team we’ve discussed the need for an easy way to invoke a query operation without having those results accumulated in the DomainContext. The normal way queries are executed is of course to call DomainContext.Load for the query:



EntityQuery<Product> query = productCtxt.SearchProductsQuery(search);
productCtxt.Load(query);

Behind the scenes what happens is DomainContext uses its DomainClient to execute the query, gets the results and loads them into its EntityContainer. Therefore results are constantly accumulated into the context as queries are loaded over the lifetime of the context. However, in some cases, you might want to execute a query without causing that result accumulation or affecting the DomainContext entity cache in any way. For example, you might have a search form taking a search string and showing a set of search results. In that case you want a simple way to execute the query and get the raw results directly:



EntityQuery<Product> query = productCtxt.SearchProductsQuery(search);
QueryOperation<Product> queryOp = query.Execute();
this.productGrid.ItemsSource = queryOp.Entities;

In the above example an EntitiyQuery.Execute method is called to execute the query. A QueryOperation instance representing the asynchronous query operation is returned which can then be data-bound immediately (QueryOperation.Entities is an observable collection). While this currently isn’t a first class feature of the framework, we have ensured that the scenario is enabled. In the code listing below a simple set of extension methods to EntityQuery are show which support the above example:



   1: public static class EntityQueryExtensions
   2: {
   3:     public static QueryOperation<T> Execute<T>(this EntityQuery<T> entityQuery, 
   4:         Action<QueryOperation<T>> callback, object userState) where T : Entity
   5:     {
   6:         if (entityQuery == null)
   7:         {
   8:             throw new ArgumentNullException("entityQuery");
   9:         }
  10:  
  11:         QueryOperation<T> queryOperation = 
  12:             new QueryOperation<T>(entityQuery, callback, userState);
  13:         object[] state = new object[] { queryOperation, SynchronizationContext.Current };
  14:  
  15:         entityQuery.DomainClient.BeginQuery(entityQuery, QueryCompleted<T>, state);
  16:  
  17:         return queryOperation;
  18:     }
  19:  
  20:     public static QueryOperation<T> Execute<T>(this EntityQuery<T> entityQuery) 
  21:                                                 where T : Entity
  22:     {
  23:         return Execute(entityQuery, null, null);
  24:     }
  25:  
  26:     public static QueryOperation<T> Execute<T>(this EntityQuery<T> entityQuery, 
  27:                                                 bool throwOnError) where T : Entity
  28:     {
  29:         if (!throwOnError)
  30:         {
  31:             Action<QueryOperation<T>> callback = (op) =>
  32:             {
  33:                 if (op.HasError)
  34:                 {
  35:                     op.MarkErrorAsHandled();
  36:                 }
  37:             };
  38:             return Execute(entityQuery, callback, null);
  39:         }
  40:         return Execute(entityQuery);
  41:     }
  42:  
  43:     private static void QueryCompleted<T>(IAsyncResult asyncResult) where T : Entity
  44:     {
  45:         object[] state = (object[])asyncResult.AsyncState;
  46:         QueryOperation<T> queryOperation = (QueryOperation<T>)state[0];
  47:         SynchronizationContext syncContext = (SynchronizationContext)state[1];
  48:  
  49:         syncContext.Post(result =>
  50:         {
  51:             try
  52:             {
  53:                 QueryCompletedResult queryResult = 
  54:                     queryOperation.EntityQuery.DomainClient.EndQuery((IAsyncResult)result);
  55:  
  56:                 if (queryResult.ValidationErrors.Any())
  57:                 {
  58:                     queryOperation.Complete(queryResult.ValidationErrors);
  59:                 }
  60:                 else
  61:                 {
  62:                     queryOperation.Complete(queryResult);
  63:                 }
  64:             }
  65:             catch (DomainOperationException ex)
  66:             {
  67:                 queryOperation.Complete(ex);
  68:             }
  69:  
  70:         }, asyncResult);
  71:     }
  72: }

 


As you can see, the fact that EntityQuery has a reference to its DomainClient (line 15) means the query can be directly executed. The asynchronous callback QueryCompleted above completes the query execution. The query results are not loaded into any DomainContext/EntityContainer which means the entities remain in the Detached state. If you wanted to, you could subsequently attach them if you wished. One subtle side effect of not loading the results into an EntityContainer is that inter-entity associations are disabled, since they rely on EntityContainer. However it is a simple matter to modify the QueryCompleted callback above to load the results into a container if that is the desired default behavior.


Not shown in this listing is the QueryOperation<T> type – that file and the above code can be found in the sample application attached to this post. QueryOperation is derived from the same base class that the other core operation types LoadOperation/SubmitOperation/InvokeOperation are which means that it gives the same programming model.


In the future we might consider adding something like this to the framework, so we’d be very interested in hearing if this addresses any scenarios you have, or if you have any other requirements in this area.

EntityQuerySample.zip

Comments (6)
  1. Yes, there are times when one needs to get "fresh" results from server.

    Maybe it would solve a problem of deleted entities that can still be seen in DomainContext.

    Maybe it would solve the problems with multiple DomainContext.Load<> operations (we are getting errors about "exposed context" when trying to call multiple Load<> operations in short period of time).

    Nevertheless it would be a strong workaround for such problems.

  2. Greg Hollywood says:

    I came accross a need for a non-accumulating query today and dropped this in and it worked great.  I do, however, need the associations.  How would I go about loading the results into an EntityContainer?

    Thanks very much,

    Greg

  3. Greg,

    On the success path in the QueryCompleted callback, you need to create an EntityContainer instance and call ec.Load for both the Entities and IncludedEntities result collections on the QueryCompletedResult. Note that you'll need to define your own derived EntityContainer type for this since the ctor and CreateEntitySet members you need are protected (take a look at how our default codegen does this). Once you have that type, you can just create an instance (ensuring that CreateEntitySet has been called to add sets for all the types) and load the entities.

  4. Jan says:

    Hi Mathew,

    thanks for the code, it's appreciated. However, I'm running into an issue where I am using MVVM approach to databinding and therefore cannot use your code:

       this.productGrid.ItemsSource = queryOp.Entities;

    What i'm using is:

       CustomerList = new ObservableCollection<Customer>(queryOp.Entities);

    however that remains empty. I guess it's because its value gets assigned before the queryOp.Entities gets filled and so it remains empty. How would I go about doing this? How could I wire it up so that when the queryOp completes it would populate my List of customers which in turns populates the datagrid? Thank you.

    Jan

  5. Jan,

    QueryOperation.Entities is already an ObservableCollection. It is typed as IEnumerable<T>, but the backing collection is a ReadOnlyObservableCollection<T>. So can't you assign that to your CustomerList?

  6. Joe says:

    I have a situation where I might want to use this. However, like Greg, I also need the associations.

    In my situation, I'm returning (at most) 3000 entities to the client as a the results of a search. These entities are purely for this search. They will not be re-used if the user wants to edit one – instead we load the entire entity from the service/db. (This is because we created this SearchResult entity which is a slimmed-down version of our real entity, in hopes of improving performance).

    The problem in our case is our queries still take WAAY too long (or at least appear to).

    Tracing a query, we see the database time is usually between 10-1000ms .. hydrating the entities anywhere from 500-2500ms .. and we have a counter on the client which starts when we execute the query, and finishes as soon as we get into the loadop's callback. This elapsed time is regularly 3-8 seconds longer than the time it takes the query + hydration. In addition, the entire SL UI hangs /as soon as/ the entities are hydrated (seemingly, anyway. very little network latency  – quicker than the eye at least). The results are bound to a grid which is bound to a PagedCV that is refreshed in the callback too (after the timer stops, though, so I don't suspect any of the hang/wait is the grid loading. wall-clock timing seems to confirm this).

    Now .. I suspect part of this hang + wait is client deserialization, and some is the associations.

    I have yet to profile this client behavior with ANTS or similar, yet (next steps as soon as I'm done here), so I can't narrow that assumption down any further.

    I guess my question is .. what benefit does the non-accumulating query give me if I need to go ahead and accumulate them after-the-fact anyway, for associations to work?

Comments are closed.

Skip to main content