Kilka dni temu w poście Statystyka… zapowiedziałem, iż postaram się zaproponować kawałki kodu, które umożliwią włączenie statystyk dla zapytań LINQ to SQL. Zadanie okazało się trochę trudniejsze niż myślałem, ale udało się coś osiągnąć. Zobaczmy jak.

Rozwiązanie Naïve

Pierwsze co przychodzi na myśl to proste rozszerzenie obiektu DataContext o nasze metody. Mniej więcej tak:

public static class DataContextExt
{
    public static T WithStatistics(this T context) where T: DataContext
    {
        EnableStatistics(context);
        return context;
    }
    [Conditional(“DEBUG”)]
    private static void EnableStatistics(T context) where T: DataContext
    {
        if (context != null && context.Connection as SqlConnection != null)
            (context.Connection as SqlConnection).StatisticsEnabled = true;
    }
    [Conditional(“DEBUG”)]
    public static void PrintStatistics(this T context, TextWriter writer, bool reset) where T: DataContext
    {
        if (context != null && context.Connection as SqlConnection != null)
        {
            var connection = context.Connection as SqlConnection;
            var stats = connection.RetrieveStatistics();
            Array.ForEach(stats.Cast<DictionaryEntry>().ToArray(),
                    entry => writer.WriteLine(string.Format(“Klucz: {0}, Wartość: {1}”,
                                                              entry.Key, entry.Value)));
            writer.WriteLine();
            if (reset)
                connection.ResetStatistics();
        }
    }
}

Co robimy powyżej? Rozszerzamy obiekt, który jest typu DataContext o dwie metody jak poprzednio. Pierwsza wyciąga SqlConnection i ustawia generowanie statystyk, druga drukuje raport. Nic tu nadzwyczajnego. A uruchomienie?

var dc = new DataClasses1DataContext().WithStatistics();
var query = (from t in dc.tests
            where t.t2.Contains(“12”)
            select t);
foreach (var test in query)
{
    Console.WriteLine(test.t1);
}
dc.PrintStatistics(Console.Out, false); 

Statystyki włączamy wywołując metodę .WithStatistics. Aby wydrukować statystyki posługujemy się tym samym DataContext. Jakie minusy? Brakuje nam informacji o tym jakie zapytanie wywołaliśmy – ono znajduje się w obiekcie query. Czemu zatem nie dodamy metody PrintStatistics? Niestety nie możemy tędy dostać się do obiektu SqlConnection. Przynajmniej nie bezpośrednio.

Rozwiązanie zaawansowane

Tutaj na początku wielkie podziękowania dla Marcina Najdera za podanie rozwiązania. W zasadzie 99,9% kodu poniżej to dzieło Marcina. Chcielibyśmy, aby nasz kod można było wywołać w następujący sposób:

var query = (from t in dc.tests.WithStatistics()
            where t.t2.Contains(“12”)
            select t);
foreach (var test in query)
{
    Console.WriteLine(test.t1);
}
query.PrintStatistics(Console.Out, true);
var q2 = query.Where(t => t.t2.Contains(“123”));
foreach (var test in q2)
{
    Console.WriteLine(test.t1);
}
q2.PrintStatistics(Console.Out, true);

Czyli pominięcie operowania na DataContex. Niestety, aby to osiągnąć musimy napisać własny provider LINQu. Fajnie 🙂

public static IQueryable WithStatistics(this IQueryable query, SqlConnection connection)
{
    return new StatsQueryable(query, connection.WithStatistics());
}
public static IQueryable WithStatistics(this Table table)
    where T : class
{
    return table.WithStatistics((SqlConnection)table.Context.Connection);
}

To nasze główne metody. Reszta to niestety “plumbing code”, który musimy wykonać. A zatem co robimy? Rozszerzamy obiekt Table, i zwracamy nasz nowy obiekt StatsQueryable. Zapamiętujemy sobie w nim także nasze połączenie, tak abyśmy mogli później wydrukować informacje dotyczące zapytania. Reszta kodu czyli nasz provider LINQ, który tak na prawdę robi niewiele (nic) a całą pracę deleguje do elementów, które otrzymał w konstruktorze czyli Query oraz SqlConnection.

[Conditional(“DEBUG”)]
public static void PrintStatistics(this IQueryable query, TextWriter writer, bool reset)
{
    var myQueryable = query as StatsQueryable;
    if (myQueryable != null)
        myQueryable.SqlConnection.PrintStatistics(writer, reset, query.ToString());
}
private class StatsQueryable : IQueryable
{
    protected readonly IQueryable Query;
    internal readonly SqlConnection SqlConnection;
    public StatsQueryable(IQueryable query, SqlConnection sqlConnection)
    {
        Query = query;
        SqlConnection = sqlConnection;
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return Query.GetEnumerator();
    }
    public Expression Expression
    {
        get { return Query.Expression; }
    }
    public Type ElementType
    {
        get { return Query.ElementType; }
    }
    public IQueryProvider Provider
    {
        get { return new StatsProvider(Query.Provider, SqlConnection); }
    }
    public override string ToString()
    {
        return Query.ToString();
    }
}
private class StatsQueryable : StatsQueryable, IQueryable
{
    public StatsQueryable(IQueryable query, SqlConnection connection)
        : base(query, connection)
    {
    }
    public IEnumerator GetEnumerator()
    {
        return ((IQueryable)Query).GetEnumerator();
    }
}
private class StatsProvider : IQueryProvider
{
    private readonly IQueryProvider Provider;
    private readonly SqlConnection SqlConnection;
    public StatsProvider(IQueryProvider provider, SqlConnection sqlConnection)
    {
        Provider = provider;
        SqlConnection = sqlConnection;
    }
    public IQueryable CreateQuery(Expression expression)
    {
        return new StatsQueryable(Provider.CreateQuery(expression), SqlConnection);
    }
    public IQueryable CreateQuery(Expression expression)
    {
        return new StatsQueryable(Provider.CreateQuery(expression), SqlConnection);
    }
    public object Execute(Expression expression)
    {
        return Provider.Execute(expression);
    }
    public TResult Execute(Expression expression)
    {
        return Provider.Execute(expression);
    }
}

I wszystko działa pięknie. Wynik działania:
resultLinq
Jakby ktoś potrzebował – pełny plik do pobrania: SqlStatisticsExtensions.cs
Gdyby ktoś szukał fajnych pomocy do LINQu to Marcin napisał własny provider, który umożliwia wyświetlanie naszego zapytania w rozbiciu na poszczególne części, które pozwalają nam prześledzić co dzieje się w środku – Debugging Reactive Framework (RxDebugger) and Linq to objects (LinqDebugger).
Miłego kodowania!

Founder of Octal Solutions a .NET software house.
Passionate dev, blogger, occasionally speaker, one of the leaders of Wroc.NET user group. Microsoft MVP. Podcaster – Ostrapila.pl