Como ya hemos dicho, tantas y tantas veces, desde este mismo blog, sin duda, una de las mejores cosas con la llegada de EF6 es la cantidad de puntos de extensibilidad de los que disponemos y que nos permiten jugar con ciertas cosas bastante interesantes. La nueva característica para subscribir código antes y después de cada ejecución, que Entity Framework hace contra la base de datos, es una de estas sobre la que siempre estamos pensando cosas que podemos hacer, en realidad hay muchas cosas que a la mayoría se nos ocurren como por ejemplo auditoria, cache, re-escritura de comandos etc etc etc. En el proyecto de EF 6 Contrib se hace uso de esta característica para darnos cierta información del rendimiento de nuestras aplicaciones haciendo uso de estos interceptores, gracias a los cuales se puede examinar la consulta y/o el plan de ejecución contra el motor de base de datos, por ahora solamente para Sql Server.
Para hacer uso de estos analizadores lo primero será incluir ef6.contrib desde nuestra consola de NuGet
1 2 3 4 5 6 |
PM> install-package ef6.contrib Attempting to resolve dependency <span class="str">'EntityFramework (≥ 6.0.2)'</span>. Attempting to resolve dependency <span class="str">'Common.Logging (≥ 2.1.2)'</span>. Installing <span class="str">'Common.Logging 2.1.2'</span>. Successfully installed <span class="str">'Common.Logging 2.1.2'</span>. Installing <span class="str">'EF6.Contrib 1.0.3'</span>. |
Una vez instalado, este pone a nuestra disposición el interceptor PerformanceInterceptor, que podemos registrar en nuestro archivo de configuración como sigue:
1 2 3 4 5 6 7 8 9 10 11 |
<span class="kwrd">public</span> <span class="kwrd">class</span> Configuration : DbConfiguration { <span class="kwrd">public</span> Configuration() { <span class="kwrd">this</span>.AddInterceptor(<span class="kwrd">new</span> PerformanceInterceptor(msg => { Console.WriteLine(<span class="str">"{0}-{1}"</span>, msg.Message, msg.Command); })); } } |
El delegado Action<PerformanceReport> nos permite decidir dónde queremos llevar la información generada, a un fichero, base de datos o como en nuestro caso, simplemente a la consola de salida. Amén de este parámetro en el constructor, esta clase nos permite incluir el conjunto de analizadores a usar. PerformanceInterceptor se basa en un conjunto de analizadores, IPerformanceAnalyzer, que podemos ampliar con los nuestros, para realizar el trabajo de revisión de las consultas. Si, como en este caso, no se hace uso de este parámetro el interceptor utilizará solamente el contenedor de dependencias para resolver los analizadores que se desean usar. Con el fin de ver un ejemplo nos crearemos un pequeño resolver que se encargará de registrar un analizador.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<span class="kwrd">class</span> DummyResolver : IDbDependencyResolver { <span class="kwrd">public</span> <span class="kwrd">object</span> GetService(Type type, <span class="kwrd">object</span> key) { <span class="kwrd">return</span> <span class="kwrd">null</span>; } <span class="kwrd">public</span> IEnumerable<<span class="kwrd">object</span>> GetServices(Type type, <span class="kwrd">object</span> key) { <span class="kwrd">if</span> (<span class="kwrd">typeof</span>(IPerformanceAnalyzer).IsAssignableFrom(type)) { <span class="kwrd">return</span> <span class="kwrd">new</span> List<IPerformanceAnalyzer>() { <span class="kwrd">new</span> ExecutionTimePerformanceAnalyzer(TimeSpan.FromSeconds(3)), }; } <span class="kwrd">return</span> Enumerable.Empty<<span class="kwrd">object</span>>(); } } |
Como podéis ver en en este código, se está registrando en este caso, un único analizador llamada ExecutionTimePerformanceAnalyzer, que como os imaginaréis, se encarga de analizar los tiempos de ejecución de cada consulta y para aquellas cuyo tiempo sea superior a 3 segundos enviará un “informe”. Crearse nuevos analizadores es sencillo, solamente tenemos que implementar la interfaz IPerformanceAnalyzer que es tan sencilla como sigue, por lo que os animo a crear y contribuir nuevos analizadores.
1 2 3 4 |
<span class="kwrd">public</span> <span class="kwrd">interface</span> IPerformanceAnalyzer { PerformanceCommandReport Analyze(DbCommand command, TimeSpan executionTime); } |
Si utilizas SqlServer, EF6 Contrib dispone de otro paquete, EF6.Contrib.SqlServerPerformanceAnalyzers, que contiene analizadores especiales para Sql Server, podríamos por ejemplo ampliar nuestro registro de analizadores como sigue, los nombres son representativos de sus funciones.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<span class="kwrd">class</span> DummyResolver : IDbDependencyResolver { <span class="kwrd">public</span> <span class="kwrd">object</span> GetService(Type type, <span class="kwrd">object</span> key) { <span class="kwrd">return</span> <span class="kwrd">null</span>; } <span class="kwrd">public</span> IEnumerable<<span class="kwrd">object</span>> GetServices(Type type, <span class="kwrd">object</span> key) { <span class="kwrd">if</span> (<span class="kwrd">typeof</span>(IPerformanceAnalyzer).IsAssignableFrom(type)) { <span class="kwrd">return</span> <span class="kwrd">new</span> List<IPerformanceAnalyzer>() { <span class="kwrd">new</span> ExecutionTimePerformanceAnalyzer(TimeSpan.FromSeconds(3)), <span class="kwrd">new</span> UnparametrizedWhereClausesPerformanceAnalyzer(), <span class="kwrd">new</span> TopSlowQueriesPerformanceAnalyzer(topQueries:10) }; } <span class="kwrd">return</span> Enumerable.Empty<<span class="kwrd">object</span>>(); } |
Estos son solo algunos ejemplos de analizadores, hay otros como la revisión de consultas de paginación etc etc etc. pero hay varios puntos de mejora como comentamos que se podrían utilizar, XEvents y el análisis de planes de ejecución, para ello EF6 Contrib está comenzando el trabajo con la clase SqlExecutionPlanPerformanceAnalyzer que servirá de base para todos los analizadores que deseen inspeccionar el plan de ejecución de un determinado comando:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
<span class="kwrd">public</span> <span class="kwrd">abstract</span> <span class="kwrd">class</span> SqlExecutionPlanPerformanceAnalyzer : IPerformanceAnalyzer { <span class="rem">///<inheritdoc/></span> <span class="kwrd">public</span> PerformanceCommandReport Analyze(DbCommand command, TimeSpan executionTime) { var plan = GetQueryPlan(command.Connection.ConnectionString, command.CommandText); <span class="kwrd">if</span> (plan != <span class="kwrd">null</span>) { <span class="kwrd">return</span> EvaluatePlan(plan); } <span class="kwrd">return</span> <span class="kwrd">null</span>; } <span class="rem">/// <summary></span> <span class="rem">/// Get the <see cref="PerformanceCommandReport"/> from Sql Plan</span> <span class="rem">/// </summary></span> <span class="rem">/// <param name="sqlPlan">The sql plan to evaluate</param></span> <span class="rem">/// <returns>The performance command report or null if no issues is present in plan</returns></span> <span class="kwrd">public</span> <span class="kwrd">abstract</span> PerformanceCommandReport EvaluatePlan(XDocument sqlPlan); <span class="rem">/// <summary></span> <span class="rem">/// Get query plan from specified command using database information</span> <span class="rem">/// </summary></span> <span class="rem">/// <param name="connectionstring">The connection string to use</param></span> <span class="rem">/// <param name="commandText">The command text</param></span> <span class="rem">/// <returns>The query plan in XML format or null</returns></span> <span class="kwrd">public</span> <span class="kwrd">virtual</span> XDocument GetQueryPlan(<span class="kwrd">string</span> connectionstring, <span class="kwrd">string</span> commandText) { <span class="kwrd">using</span> (var connection = <span class="kwrd">new</span> SqlConnection(connectionstring)) { var queryPlanCommand = connection.CreateCommand(); queryPlanCommand.CommandText = <span class="str">"SELECT qp"</span> + <span class="str">" FROM sys.dm_exec_cached_plans as cp CROSS APPLY sys.dm_exec_query_plan(plan_handle) as qp"</span> + <span class="str">" CROSS APPLY sys.dm_exec_sql_text(plan_handle) as qt WHERE qt=@qt"</span>; queryPlanCommand.Parameters.AddWithValue(<span class="str">"@qt"</span>, commandText); var plan = queryPlanCommand.ExecuteScalar(); <span class="kwrd">if</span> (plan != DBNull.Value) { <span class="kwrd">return</span> XDocument.Parse(plan.ToString()); } <span class="kwrd">return</span> <span class="kwrd">null</span>; } } } |
Si os ha gustado, me gustaría animaros a contribuir de las múltiples formas que se puede hacer…
Saludos
Unai