Entity Framework SQL query tracer

Niejednokrotnie potrzebujemy podejrzeć zapytanie SQL wygenerowane przez EF. Często zdarza się, że takie zapytanie chcemy odpalić na bazie danych i zobaczyć wynik. Można wtedy podpiąć się profilerem do bazy i przechwycić je w całości. Niestety takie rozwiązanie jest bardzo czasochłonne. Można też wywołać metodę ToString() na obiekcie typu IQuarable, jednakże to rozwiązanie dostarczy nam zapytanie SQL bez parametrów filtrujących. W tym artykule opiszę jak bezpośrednio w kodzie uzyskać pełne zapytanie SQL wraz z parametrami filtrującymi.

Jak wcześniej wspominałem, wywołanie metody ToString() gwarantuje nam tylko połowiczny sukces, który może wyglądać jak poniżej.

SELECT 
    [Project1].[LanguageID] AS [LanguageID], 
    [Project1].[Name] AS [Name], 
    [Project1].[ShortCode] AS [ShortCode]
    FROM ( SELECT 
        [Extent1].[LanguageID] AS [LanguageID], 
        [Extent1].[Name] AS [Name], 
        [Extent1].[ShortCode] AS [ShortCode]
        FROM [dbo].[Languages] AS [Extent1]
        WHERE [Extent1].[LanguageID] = @p__linq__0
    )  AS [Project1]
    ORDER BY [Project1].[Name] ASC

Jak widać wygenerowany SQL nie posiada deklaracji zmiennej filtrującej. Aby dostać się do parametrów zapytania, a tym samym mieć możliwość deklaracji tej zmiennej, należy zrzutować obiekt IQuarable na typ ObjectQuery.

EF5

EF 5

W starym EF (tj. do wersji 5) typ ObjectQuery znajduje się w przestrzeni System.Data.Objects. Przykładowa implementacja metody zwracającej zapytanie SQL z parametrami mogła by wyglądać następująco:

public static string ToTraceStringWithParameters(IQueryable @this)
{
	const string LINE = "-----------------------------------------------";

	var objectQuery = @this as ObjectQuery;
	if (objectQuery == null)
	{
		return "QUERY TRACE ERROR";
	}

	var sb = new StringBuilder();
	sb.AppendLine(LINE);
	string sql = objectQuery.ToTraceString();

	if (objectQuery.Parameters.Count > 0)
	{
		// add parameters to query
	}
	sb.AppendLine();
	sb.AppendLine(sql);

	sb.AppendLine(LINE);
	return sb.ToString();
}

Powyższy kod jest stosunkowo łatwy toteż nie pokuszę się o wyjaśnienia.

EF 6

Inaczej natomiast będzie wyglądać implementacja tej metody w EF 6, ponieważ typ ObjectQuery znajduje się w przestrzeni System.Data.Entity.Core.Objects oraz zwykłe zrzutowanie obiektu IQuarable na ObjectQuery zwraca pusty wynik (null).

EF6

Przeszukałem pół internetów, aż w końcu natrafiłem na rozwiązanie (link). Otóż, należy skorzystać z refleksji, aby dostać się do właściwego pola, które z powodzeniem da się zrzutować na porządane ObjectQuery. Implementacja wygląda następująco.

public static string ToTraceStringWithParameters(IQueryable @this)
{
	const string LINE = "-----------------------------------------------";

	var internalQueryField = @this.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
		.FirstOrDefault(f => f.Name.Equals("_internalQuery"));
	var internalQuery = internalQueryField.GetValue(@this);
	var objectQueryField = internalQuery.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
		.FirstOrDefault(f => f.Name.Equals("_objectQuery"));
	var objectQuery = objectQueryField.GetValue(internalQuery) as ObjectQuery;

	if (objectQuery == null)
	{
		return "QUERY TRACE ERROR";
	}

	// rest code
}

Na tym etapie nie pozostaje już nic innego jak tylko zaimplementować brakującą część, czyli logikę odpowiedzialną za właściwą deklarację zmiennych wykorzystywanych w wygenerowanym zapytaniu SQL.

Finalna implementacja

Moja implementacja wygląda jak poniżej.

public static class QueryTraceExtensions
{
    private const string LINE = "-----------------------------------------------";

    public static void TraceQuery(this IQueryable @this)
    {
        if (!Debugger.IsAttached)
        {
            return;
        }

        try
        {
            Debug.WriteLine(ToTraceStringWithParameters(@this));
        }
        catch
        {

        }
    }

    private static string ToTraceStringWithParameters(IQueryable @this)
    {
        var internalQueryField = @this.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
            .FirstOrDefault(f => f.Name.Equals("_internalQuery"));
        var internalQuery = internalQueryField.GetValue(@this);
        var objectQueryField = internalQuery.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
            .FirstOrDefault(f => f.Name.Equals("_objectQuery"));

        var objectQuery = objectQueryField.GetValue(internalQuery) as ObjectQuery;

        if (objectQuery == null)
        {
            return "QUERY TRACE ERROR";
        }

        var sb = new StringBuilder();
        sb.AppendLine(LINE);
        string sql = objectQuery.ToTraceString();

        if (objectQuery.Parameters.Count > 0)
        {
            var sbParameters = new StringBuilder("DECLARE ");
            foreach (var parameter in objectQuery.Parameters)
            {
                sbParameters.AppendFormat(GetParameterString(parameter, true));
                sbParameters.AppendLine(",");
            }
            sb.AppendLine(sbParameters.ToString().TrimEnd('\r', '\n', ','));
        }
        sb.AppendLine();
        sb.AppendLine(sql);

        sb.AppendLine(LINE);
        return sb.ToString();
    }

    private static string GetParameterString(ObjectParameter parameter, bool inRunFormat)
    {
        if (!inRunFormat)
        {
            return string.Format("@{0} [{1}] = {2}", parameter.Name, parameter.ParameterType.FullName, parameter.Value);
        }

        string typeName = parameter.ParameterType.FullName;
        if (typeName.StartsWith("System.Nullable"))
        {
            typeName = typeName.Substring(typeName.IndexOf("[[") + 2, typeName.IndexOf(",") - typeName.IndexOf("[[") - 2);
        }

        string typeAndValue;
        switch (typeName)
        {
            case "System.Boolean":
                string value;
                if (parameter.Value == null)
                {
                    value = "NULL";
                }
                else
                {
                    value = (bool)parameter.Value ? "1" : "0";
                }
                typeAndValue = string.Format("{0} = {1}", "BIT", value);
                break;

            case "System.String":
                typeAndValue = string.Format("{0} = {1}", "VARCHAR(MAX)", GetStringValue(parameter.Value, true));
                break;

            case "System.DateTime":
                typeAndValue = string.Format("{0} = {1}", "DATETIME", GetStringValue(parameter.Value, true));
                break;

            case "System.Decimal":
            case "System.Double":
                typeAndValue = string.Format("{0} = {1}", "FLOAT", GetStringValue(parameter.Value, false));
                break;

            default:
                typeAndValue = string.Format("{0} = {1}", "INT", GetStringValue(parameter.Value, false));
                break;
        }

        return string.Format("@{0} {1}", parameter.Name, typeAndValue);
    }

    private static string GetStringValue(object value, bool withQuote)
    {
        if (value == null)
        {
            return "NULL";
        }

        return withQuote ? string.Format("'{0}'", value) : value.ToString();
    }
}

Rezultat natomiast wygląda następująco.

SqlTrace

Podbij ↑

6 thoughts on “Entity Framework SQL query tracer

    • Marcin,
      Nie o to chodzi. Twoja linika kodu dostarczy następujący wynik.

      SELECT 
          [Limit1].[LanguageID] AS [LanguageID], 
          [Limit1].[Name] AS [Name], 
          [Limit1].[ShortCode] AS [ShortCode]
          FROM ( SELECT TOP (1) 
              [Extent1].[LanguageID] AS [LanguageID], 
              [Extent1].[Name] AS [Name], 
              [Extent1].[ShortCode] AS [ShortCode]
              FROM [dbo].[Languages] AS [Extent1]
              WHERE [Extent1].[LanguageID] = @p__linq__0
          )  AS [Limit1]
      
      
      -- p__linq__0: '2' (Type = Int32, IsNullable = false)
      
      -- Executing at 1/19/2016 10:38:32 AM +01:00
      
      -- Completed in 54 ms with result: SqlDataReader
      

      Jak widać powyżej, zapytanie SQL jest w całości, a na dole jest tylko informacja o parametrze. Moja implementacja dostarcza zapytanie SQL wraz z pełną deklaracją parametrów. Całość można odpalić na bazie danych i otrzymać wynik. Daje to możliwość np. otrzymania planu zapytania.

      • Marcin,
        czy nie prostszym rozwiązaniem jest odpalenie profilera na bazie danych i podglądnięcie zapytania?
        MS SQL posiada takie narzędzie, zaś dla Bazy MS SQL Express możesz skorzystać z rozwiązań firm trzecich np. program ExpressProfiler.

        Pozdrawiam
        Krzysiek

        • Krzysiek,
          Dla mnie osobiście takie rozwiązanie jest bardziej czasochłonne – muszę uruchomić kolejny program, skonfigurować go by przechwytywał to co chcę (tak, wiem że są templaty), przejrzeć wyniki w poszukiwaniu mojego zapytania. Za dużo tego ;)

Leave a Reply

Your email address will not be published. Required fields are marked *