martes, 25 de junio de 2019
 Pues hablábamos hace unos días del extensor
Pues hablábamos hace unos días del extensor TagWith() de Entity Framework Core que, como recordaréis, venía de perlas para incluir un comentario en las consultas SQL enviadas al servidor de datos para poder identificarlas a posteriori:LINQ
============================================
var query = context.Friends
    .OrderByDescending(friend => friend.Age)
    .Take(10)
    .Select(friend =>
        new { FriendName = friend.Name, 
              friend.Age, 
              CountryName = friend.Country.Name 
     })
    .TagWith("Get top 10 older friends with country");
Generated SQL
============================================
-- Get top 10 older friends with country
SELECT TOP(@__p_0) [friend].[Name] AS [FriendName], [friend].[Age], 
                   [friend.Country].[Name] AS [CountryName]
FROM [Friends] AS [friend]
LEFT JOIN [Countries] AS [friend.Country] 
     ON [friend].[CountryId] = [friend.Country].[Id]
ORDER BY [friend].[Age] DESC
TL;DR: Echad un vistazo a EF6.TagWith en NuGet o en el repo de GitHub.
1. Enfocando la solución
Manos a la obra. Conceptualmente, lo que pretendemos hacer es bastante sencillo:- 
Queremos que, en cualquier punto de una consulta LINQ, un desarrollador pueda invocar el extensor TagWith()y especificar una etiqueta personalizada para la misma.
 
- 
A la hora de generar el SQL de la consulta, tenemos que recuperar el valor de dicha etiqueta e insertarla a modo de comentario justo antes del SELECT, como en el ejemplo que veíamos anteriormente.
 
Por ello, la solución propuesta consiste en:
- 
Crear un método extensor TagWith()sobreIQueryable, que añadirá a la consulta una "marca" fácilmente identificable para detectar a posteriori que ésta incluye una etiqueta personalizada.
 
- 
A continuación, usando los interceptores de Entity Framework 6, descubrir las queries en las que haya sido introducida la "marca" anterior, extraer la etiqueta y reformular la sentencia SQL para incluir dicha etiqueta como comentario.
 
// 1: Consulta inicial (C#)
var query = _ctx.Friends
               .TagWith("Hello world!")
               .Where(f => f.Id < 10);
// 2: La llamada a TagWith() añade un predicado predefinido a la consulta,
//    de forma que es reescrita de la siguiente manera:
var query = _ctx.Friends
               .Where(f=> "!__tag!" == "Hello world")
               .Where(f => f.Id < 10);
// 3: Debido a lo anterior, el proveedor genera la siguiente consulta SQL:
SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Name] AS [Name], 
    [Extent1].[Country_Id] AS [Country_Id]
    FROM [dbo].[Friends] AS [Extent1]
    WHERE (N'!__tag!' = N'Hello world!') AND ([Extent1].[Id] < 10)
// 4: En un interceptor de EF detectamos la subcadena "!__tag!", por lo que sabemos
//    que se trata de una consulta etiquetada. Manipulando un poco la cadena,
//    transformamos la consulta SQL en la siguiente, donde se incluye la etiqueta como 
//    comentario y ha desaparecido el predicado falso introducido con TagWith():
-- Hello world!
SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Name] AS [Name], 
    [Extent1].[Country_Id] AS [Country_Id]
    FROM [dbo].[Friends] AS [Extent1]
    WHERE ([Extent1].[Id] < 10)
2. Implementación del extensor TagWith()
Esta parte es la más sencilla. El extensor TagWith() simplemente debe añadir al IQueryable<T> una nueva cláusula where fácilmente reconocible a posteriori:public static class TagWithExtensions
{
    public static string TagMarker = "!__tag!";
    public static IQueryable<T> TagWith<T>(this IQueryable<T> query, string tag)
    {
        var tagConstant = Expression.Constant(TagMarker);
        var tagValue = Expression.Constant(tag);
        var equalsExpression = Expression.Equal(tagConstant, tagValue);
        var predicate = Expression.Lambda<Func<T, bool>>(
                equalsExpression, Expression.Parameter(typeof(T))
        );
        return query.Where(predicate);
    }
}
El resultado de dicha comparación será siempre falso, pero en realidad esto no nos importa porque nunca irá al servidor de datos: eliminaremos este predicado de la sentencia SQL generada justo antes de ejecutarse. Veamos cómo.
3. Modificando la consulta
Entity Framework 6 incluye un interesante mecanismo que nos permite consultar y modificar el SQL generado por el proveedor antes de que sea enviado al servidor de datos: los interceptores de comandos.Estos componentes, que implementan la interfaz
IDbCommandInterceptor, permiten tomar el control antes de ejecutar una consulta para analizar o cambiar el SQL generado. Nosotros nos valdremos de ello para determinar si estamos ante una consulta etiquetada y, en su caso, modificarla para adaptarla a nuestros intereses.public class QueryTaggerInterceptor : DbCommandInterceptor
{
    private readonly ISqlTagger _sqlTagger;
    public QueryTaggerInterceptor(ISqlTagger sqlTagger)
    {
        _sqlTagger = sqlTagger;
    }
    public override void ReaderExecuting(DbCommand command, 
                DbCommandInterceptionContext<DbDataReader> context)
    {
            do
            {
                command.CommandText = _sqlTagger.GetTaggedSqlQuery(command.CommandText);
            } 
            while (command.CommandText.IndexOf(
                    TagWithExtensions.TagMarker, 
                    StringComparison.Ordinal) > -1);
    }
}
ISqlTagger que usamos en el interceptor simplemente contiene la operación GetTaggedSqlQuery(), que recibe la cadena de texto con la consulta SQL y la retorna modificada según la siguiente lógica:- 
Si la SQL no contiene la marca "!__tag!", es que estamos ante una consulta no etiquetada, por lo que la dejamos tal cual. En caso contrario, asumimos que la consulta está tageada, por lo que continuamos el procedimiento.
 
- 
Extraemos la etiqueta indicada por el usuario en la llamada a TagWith().
 
- 
Eliminamos de la SQL el predicado completo.
 
- 
Y, por último, fijaos que el proceso se repite siempre qe sigamos encontrando marcas en la consulta, puesto que un desarrollador podría haber incluido más de una llamada a Tagwith()sobre la misma.
 
ISqlTagger es la siguiente:public interface ISqlTagger
{
    string GetTaggedSqlQuery(string sql);
}
public class SqlServerTagger: ISqlTagger
{
    public string GetTaggedSqlQuery(string sql)
    {
        public string GetTaggedSqlQuery(string sql)
        {
            var indexOfTagConstant = sql.IndexOf(
                TagWithExtensions.TagMarker, StringComparison.Ordinal
            );
            if (indexOfTagConstant == -1)
                return sql;
            var predicateStartIndex = indexOfTagConstant - 2;
            var startOfTagIndex = predicateStartIndex + TagWithExtensions.TagMarker.Length + 8;
            var predicateEndIndex = sql.IndexOf("'", startOfTagIndex, StringComparison.Ordinal);
            var endOfTagIndex = predicateEndIndex - 1; // Remove the final single quote
            var tag = sql.Substring(startOfTagIndex, endOfTagIndex - startOfTagIndex + 1);
            var startsWithAnd = CmpStr(sql, predicateStartIndex - 5, "AND (");
            var endsWithAnd = CmpStr(sql, endOfTagIndex + 2, ") AND");
            string finalSql;
            if (startsWithAnd)
            {
                // Predicate pattern: ... AND (N'__tag' = N'mytag')
                finalSql = sql.Substring(0, indexOfTagConstant - 8) 
                           + sql.Substring(endOfTagIndex + 3);
            }
            else if (endsWithAnd)
            {
                // Predicate pattern: (N'__tag' = N'mytag') AND ...
                finalSql = sql.Substring(0, indexOfTagConstant - 3) 
                           + sql.Substring(endOfTagIndex + 8);
            }
            else
            {
                // It is the only predicate, so remove the whole "Where" section
                finalSql = sql.Substring(0, indexOfTagConstant - 8)
                           + sql.Substring(predicateEndIndex + 1);
            }
            finalSql = AddTagToQuery(finalSql, tag);
            return finalSql;
        }
        private string AddTagToQuery(string sql, string tag)
        {
            [...] // Inserts the tag in the SQL query
        }
        private static bool CmpStr(string str, int startIndex, string compare)
        {
            [...] // Detects if a substring is contained in another string
        }
}
global.asax.cs si se trata de una aplicación web):DbInterception.Add(new QueryTaggerInterceptor(new SqlServerTagger()));
4. ¡Quiero probarlo!
He colgado el proyecto completo en Github por si queréis echar un vistazo al componente o ver algunos ejemplos de uso (en los tests, básicamente). Y para facilitar aún más las cosas, he creado el paquete NuGetEF6.TagWith, que podéis usar sin tener que copiar y pegar nada ;)Para utilizarlo sólo tenéis que instalarlo en un proyecto donde uséis Entity Framework 6:
PM> install-package EF6.TagWith
DbInterception.Add(new QueryTaggerInterceptor(new SqlServerTagger()));
var query = ctx.Friends
    .OrderByDescending(friend => friend.Age)
    .Take(10)
    .Select(friend => new { FriendName = friend.Name, CountryName = friend.Country.Name })
    .TagWith("Get top 10 older friends with country");

Espero que os sea de utilidad :)
Publicado en: www.variablenotfound.com.


 
 
 



 
 
 
 
 
 
 
 
 
 
2 Comentarios:
Conoces alguna forma de taggear de forma que se pueda ver en el Query Store?
Hola!
Ostras, pues no sabría decirte, ni siquiera he probado por ese camino... a ver si un día de estos tengo un ratillo y le echo un vistazo.
Saludos!
Enviar un nuevo comentario