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