Per ottenere il massimo delle prestazioni quando si devono usare LINQ query è consigliato precompilarle e quando possibile usare anche l’opzione MergeOption.NoTracking. Quest’ultima evita di salvare le informazioni sulle modifiche degli oggetti nell’ObjectContext. E cosi’ ho fatto; ho compilate le query e le ho usate per leggere delle entità e modificarle. Ma ad un certo punto saltava fuori un errore sulla SaveChanges che non poteva essere eseguita poichè la query era stata compilata con l’opzione MergeOption.NoTracking.

Vediamo cosa era successo. Consideriamo il seguente codice che compila una query che ritorna un utente data la email. Poi ci sono due metodi che usano questa query; una che legge solamente il nome dell’utente e quindi viene preceduto dal NoTracking ed un secondo metodo che scrive il nome dell’utente e quindi implicitamente è preceduta da AppendOnly, il valore di default di MergeOption .

public class QueryCompiled 
{
  public static Func<MyEntities, string, IQueryable<Users>> GetUserByEmail = 
            CompiledQuery.Compile<MyEntities, string, IQueryable<Users>>( 
                                             (MyEntities ctx, string email) => 
                                             from a in ctx.Users 
                                             where a.Email == email 
                                             select a);
}
public class A
{
  public string GetUserName(string email)
  {
    using (MyEntities context = new MyEntities()) 
    {
      context.Users.MergeOption = MergeOption.NoTracking; 
      var users= (QueryCompiled.GetUserByEmail(context, email)); 
      return users.Name;
    }
  }
  public void SetUserName(string email, string name)
  {
    using (MyEntities context = new MyEntities()) 
    {
      var users= (QueryCompiled.GetUserByEmail(context, email)); 
      users.Name = name;
      context.SaveChanges();
    }
  }
}

Se l’ordine di esecuzione dei metodi della classe A è il seguente va tutto bene :

A classeA = new A();
classeA.SetUserName(“aaa@aaa.aa”, “pippo”);
classeA.GetUserName(“aaa@aaa.aa”);

Se l’ordine di esecuzione si inverte, come nel seguente caso, allora ottengo l’errore che dicevo prima :

A classeA = new A();
classeA.GetUserName(“aaa@aaa.aa”);
classeA.SetUserName(“aaa@aaa.aa”, “pippo”);

Il motivo è il seguente; quando la query compilata viene invocata la prima volta allora viene memorizzata in una chache una forma ottimizzata della query. Durante questa prima invocazione vengono memorizzate altre informazioni tra cui anche il tipo di MergeOption attivo nel momento dell’invocazione. Quest’ultimo è un particolare importante il cui dettaglio non avevo trovato nella documentazione ufficiale. Nel secondo caso indicato sopra succede che la prima volta viene invocata la query compilata con MergeOption.NoTracking e questo valore viene associato alla query compilata per cui quando dopo vado a richiamare la stessa query compilata per modificare un valore nel momento in cui si esegue SaveChanges la query fallisce perche’ risulta che è attivo il NoTracking. Notare che non cambia nulla se vado ad aggiungere un MergeOption.AppendOnly nel metodo SetUserName. Non cambia nulla anche se richiamo la query con un diverso ObjectContext; insomma, il MergeOption è associato indissolubilmente con la query compilata.
La soluzione è quella di duplicare la compilazione della query o meglio fare una compilazione separata per ogni valore della MergeOption che si intende usare. Purtroppo si ripeterebbe piu’ volte il codice della query LINQ. La soluzione ottimale è quella di usare un CompiledQueryReplicator che è una classe che permette di specificare la query LINQ una volta sola e automaticamente di compilare piu’ istanze della stessa. Le istanze vengono inserite in un Dictionary la cui Key è il tipo di MergeOption con cui si vuole compilare la query. L’idea è stata già esplicitata in questo progetto su CodePlex : http://dcutilities.codeplex.com e li si puo’ andare a recuperare le due classi CompiledQueryReplicator e EfCompiledQueryReplicatorFactory. Consiglio anche di leggere il tutorial per la spiegazione di come usarle. Di seguito riporto per semplicità il codice delle due classi.

    /// <summary>
    /// The CompiledQueryReplicator is a class that allows you to specify a LINQ query once, then
    /// automatically compile different instances of it. This is useful when you need multiple instances
    /// of the same query compiled with different <see cref="System.Data.Objects.MergeOption"/>s, 
    /// for example. Each instance is identified with a particular key of type <typeparamref name="TKey"/>.
    /// </summary>
    /// <remarks>
    /// <para>
    /// If you're using the Entity Framework (LINQ to Entities), it is suggested that you use the 
    /// <see cref="EfCompiledQueryReplicatorFactory{TKey}"/> class to instantiate instances of this
    /// class, as it makes the syntax shorter and automatically provides you with the Entity Framework
    /// compiled query compiler.
    /// </para>
    /// <para>
    /// This class is thread-safe.
    /// </para>
    /// </remarks>
    /// <typeparam name="TQuery">The type of the query</typeparam>
    /// <typeparam name="TKey">The type to use as the key</typeparam>
    public class CompiledQueryReplicator<TQuery, TKey>
    {
        private readonly Expression<TQuery> _QueryExpressionTree;
        private readonly Func<Expression<TQuery>, TQuery> _Compiler;
        private readonly IDictionary<TKey, TQuery> _CompiledQueries;
        /// <summary>
        /// Creates a CompiledQueryReplicator using the specified query that will be compiled with
        /// the specified compiler.
        /// </summary>
        /// <param name="query">The query's expression tree</param>
        /// <param name="compiler">
        /// A delegate that can compile the query expression tree into an actual query
        /// </param>
        public CompiledQueryReplicator(Expression<TQuery> query, Func<Expression<TQuery>, TQuery> compiler)
        {
            _QueryExpressionTree = query;
            _Compiler = compiler;
            _CompiledQueries = new Dictionary<TKey, TQuery>();
        }
        /// <summary>
        /// Returns the compiled query instance associated with the specified key. If
        /// no query instance has been created for the specified key, one will be
        /// and it will be returned.
        /// </summary>
        /// <param name="key">The key</param>
        /// <returns>The compiled query.</returns>
        public TQuery this[TKey key]
        {
            get
            {
                TQuery query;
                lock (_CompiledQueries)
                {
                    if (_CompiledQueries.ContainsKey(key) == false)
                    {
                        query = _Compiler(_QueryExpressionTree);
                        _CompiledQueries.Add(key, query);
                    }
                    else
                        query = _CompiledQueries[key];
                }
                return query;
            }
        }
    }
    /// <summary>
    /// Factory class that makes it easier to create <see cref="CompiledQueryReplicator{TQuery,TKey}"/>
    /// instances that hold LINQ to Entities compiled queries.
    /// </summary>
    /// <typeparam name="TKey">T
    /// he type to use as the key for the CompiledQueryReplicators created
    /// </typeparam>
    public static class EfCompiledQueryReplicatorFactory<TKey>
    {
        /// <summary>
        /// Creates a <see cref="CompiledQueryReplicator{TQuery,TKey}"/> that compiles LINQ to Entities
        /// queries.
        /// </summary>
        /// <typeparam name="TArg0">
        /// The <see cref="ObjectContext"/> that encapsulates the model connection and metadata.
        /// </typeparam>
        /// <typeparam name="TResult">
        /// The type of the query results returned by executing the delegate returned by the Compile method.
        /// </typeparam>
        /// <param name="query">The LINQ to Entities query expression to compile</param>
        /// <returns>The <see cref="CompiledQueryReplicator{TQuery,TKey}"/> created</returns>
        public static CompiledQueryReplicator<Func<TArg0, TResult>, TKey> Create<TArg0, TResult>(Expression<Func<TArg0, TResult>> query)
            where TArg0 : ObjectContext
        {
            return new CompiledQueryReplicator<Func<TArg0, TResult>, TKey>(query, CompiledQuery.Compile);
        }
        /// <summary>
        /// Creates a <see cref="CompiledQueryReplicator{TQuery,TKey}"/> that compiles LINQ to Entities
        /// queries.
        /// </summary>
        /// <typeparam name="TArg0">
        /// The <see cref="ObjectContext"/> that encapsulates the model connection and metadata.
        /// </typeparam>
        /// <typeparam name="TArg1">
        /// Represents the type of the parameter that has to be passed in when executing the delegate returned
        /// by the Compile method.
        /// </typeparam>
        /// <typeparam name="TResult">
        /// The type of the query results returned by executing the delegate returned by the Compile method.
        /// </typeparam>
        /// <param name="query">The LINQ to Entities query expression to compile</param>
        /// <returns>The <see cref="CompiledQueryReplicator{TQuery,TKey}"/> created</returns>
        public static CompiledQueryReplicator<Func<TArg0, TArg1, TResult>, TKey> Create<TArg0, TArg1, TResult>(Expression<Func<TArg0, TArg1, TResult>> query)
            where TArg0 : ObjectContext
        {
            return new CompiledQueryReplicator<Func<TArg0, TArg1, TResult>, TKey>(query, CompiledQuery.Compile);
        }
        /// <summary>
        /// Creates a <see cref="CompiledQueryReplicator{TQuery,TKey}"/> that compiles LINQ to Entities
        /// queries.
        /// </summary>
        /// <typeparam name="TArg0">
        /// The <see cref="ObjectContext"/> that encapsulates the model connection and metadata.
        /// </typeparam>
        /// <typeparam name="TArg1">
        /// Represents the type of the parameter that has to be passed in when executing the delegate returned
        /// by the Compile method.
        /// </typeparam>
        /// <typeparam name="TArg2">
        /// Represents the type of the parameter that has to be passed in when executing the delegate returned
        /// by the Compile method.
        /// </typeparam>
        /// <typeparam name="TResult">
        /// The type of the query results returned by executing the delegate returned by the Compile method.
        /// </typeparam>
        /// <param name="query">The LINQ to Entities query expression to compile</param>
        /// <returns>The <see cref="CompiledQueryReplicator{TQuery,TKey}"/> created</returns>
        public static CompiledQueryReplicator<Func<TArg0, TArg1, TArg2, TResult>, TKey> Create<TArg0, TArg1, TArg2, TResult>(Expression<Func<TArg0, TArg1, TArg2, TResult>> query)
            where TArg0 : ObjectContext
        {
            return new CompiledQueryReplicator<Func<TArg0, TArg1, TArg2, TResult>, TKey>(query, CompiledQuery.Compile);
        }
        /// <summary>
        /// Creates a <see cref="CompiledQueryReplicator{TQuery,TKey}"/> that compiles LINQ to Entities
        /// queries.
        /// </summary>
        /// <typeparam name="TArg0">
        /// The <see cref="ObjectContext"/> that encapsulates the model connection and metadata.
        /// </typeparam>
        /// <typeparam name="TArg1">
        /// Represents the type of the parameter that has to be passed in when executing the delegate returned
        /// by the Compile method.
        /// </typeparam>
        /// <typeparam name="TArg2">
        /// Represents the type of the parameter that has to be passed in when executing the delegate returned
        /// by the Compile method.
        /// </typeparam>
        /// <typeparam name="TArg3">
        /// Represents the type of the parameter that has to be passed in when executing the delegate returned
        /// by the Compile method.
        /// </typeparam>
        /// <typeparam name="TResult">
        /// The type of the query results returned by executing the delegate returned by the Compile method.
        /// </typeparam>
        /// <param name="query">The LINQ to Entities query expression to compile</param>
        /// <returns>The <see cref="CompiledQueryReplicator{TQuery,TKey}"/> created</returns>
        public static CompiledQueryReplicator<Func<TArg0, TArg1, TArg2, TArg3, TResult>, TKey> Create<TArg0, TArg1, TArg2, TArg3, TResult>(Expression<Func<TArg0, TArg1, TArg2, TArg3, TResult>> query)
            where TArg0 : ObjectContext
        {
            return new CompiledQueryReplicator<Func<TArg0, TArg1, TArg2, TArg3, TResult>, TKey>(query, CompiledQuery.Compile);
        }
    }

 

Il codice di esempio riscritto con queste classi diventa :

 

public class QueryCompiled 
{
  public static readonly CompiledQueryReplicator<Func<MyEntities, string, Users>, MergeOption> 
             GetUserByEmail = EfCompiledQueryReplicatorFactory<MergeOption>.Create( 
                                     (MyEntities ctx, string email) => 
                                     (from a in ctx.Users 
                                     where a.Email == email 
                                     select a).FirstOrDefault());
}
public class A
{
  public string GetUserName(string email)
  {
    using (MyEntities context = new MyEntities()) 
    {
      context.Users.MergeOption = MergeOption.NoTracking; 
      var users= (QueryCompiled.GetUserByEmail[MergeOption.NoTracking](context, email));
      return users.Name;
    }
  }
  public void SetUserName(string email, string name)
  {
    using (MyEntities context = new MyEntities()) 
    {
      var users= (QueryCompiled.GetUserByEmail[MergeOption.AppendOnly](context, email)); 
      users.Name = name;
      context.SaveChanges();
    }
  }
}

Da notare che nell’invocazione della query si specifica il tipo di MergeOption che si intende usare. Se esiste una query compilata con quell’opzione nel dizionario viene ritornata altrimenti ne viene creata una.