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

  • Anonymous
    June 14, 2010
    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.

  • Anonymous
    August 14, 2010
    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

  • Anonymous
    August 16, 2010
    The comment has been removed

  • Anonymous
    February 08, 2011
    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

  • Anonymous
    February 09, 2011
    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?

  • Anonymous
    August 06, 2011
    The comment has been removed