Pobranie parametrów metody w C# wraz z wartościami

Author: Mateusz Kubiczek (madmatt) | październik 12th, 2009

Dosyć długo szukałem w jaki sposób pobrać nazwy parametrów metody wraz z ich wartościami w sposób uniwersalny.

Ułatwiłoby to logowanie wszystkiego, co zachodzi w aplikacji. Wywołując metodę:

public void CountPrice(int bar, string baz)
{

}

chciałbym otrzymać w logach wpis:

Method’s Foo arguments >> bar: 1, baz: zzz

Najpierw spróbowałem za pomocą metody GetParameters klasy MethodInfo. Zwrócone obiekty typu ParameterInfo zawierają jednak tylko informacje o typach parametrów – nie mają wartości tych parametrów w czasie wykonywania kodu.

Moim celem było stworzenie metody, którą mógłbym użyć do pobrania sformatowanej informacji o parametrach metody – na przykład w celu zalogowania ich. Załóżmy dla prostoty testowania, że nasza metoda Foo będzie zwracała sformatowany napis z jej parametrami i ich wartościami.

Pierwsza wersja:

public class TestClass
{
  public static string Foo(int bar, string baz)
  {
    return LogingHelpers.GetMethodArguments();
  }
}

public class LoggingHelpers
{
  public static string GetMethodArguments()
  {
    //TODO tutaj magia :-)
    return "";
  }
}

Niestety – z taką składnią nie udało mi się. Z kontekstu LoggingHelpers.GetMethodArguments nie da się (lub nadal nie potrafię :-) ) dostać się do parametrów metody wywołującej. Działająca składnia jest nieco inna i odrobinę mniej wygodna:

public class TestClass
{
  public static string Foo(int bar, string baz)
  {
    return GetMethodArguments(() => bar, () => baz);
  }

  public static string NoParameters()
  {
    return GetMethodArguments();
  }
}

public class LoggingHelpers
{
  public static string GetMethodArguments(params Func<object>[] expr)
  {
    //TODO tutaj magia, ujawnione już za chwilę!
    return "";
  }
}

Ale najpierw test z wykorzystaniem NUnit.

    using NUnit.Framework;
    using NUnit.Framework.SyntaxHelpers;

    [TestFixture]
    public class LoggingTests
    {

        [Test]
        public void TwoParametersCorrectlyFormatted
        {
            string parameterNamesAndValues = TestClass.Foo(1, "zzz");
            Assert.That(parameterNamesAndValues, Is.EqualTo("Method's Foo arguments >> bar: 1, baz: zzz");
        }

        [Test]
        public void NoParametersCorrectlyFormatted
        {
            string noParameters = TestClass.NoParameters();
            Assert.That(noParameters, Is.EqualTo("Method's Foo arguments >> No arguments.");
        }
    }

Odpalamy testy, który oczywiście nie przechodzą.

Żeby testy zadziałały, nasza klasa LoggingHelpers wraz z metodą GetMethodParameters może wyglądać tak:

using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;

public class LoggingHelpers
{
    public static string GetMethodParameters(params Func<object>[] expr)
    {
      // Pobieramy nazwę metody.
      string methodname = new StackFrame(1).GetMethod().Name;

      // wstępne formatowanie wynikowego napisu:
      string logmessage = "Method's " + methodname + " arguments >> ";

      // Tablica wyrażeń lambda jest pusta
      if (expr.Length == 0)
      {
        logmessage += "No arguments.";
      }
      else
      {
        try
        {
          // dla każdego Expression
          foreach (var par in expr)
          {
            // Zaawansowane sztuczki. Za pomocą MSIL znajdujemy informacje o Expression
            byte[] il = par.Method.GetMethodBody().GetILAsByteArray();

            // bajty 2-6 oznaczają uchwyt do naszych parametrów
            int fieldHandle = BitConverter.ToInt32(il, 2);

            // pobieramy FieldInfo tego parametru
            FieldInfo field = par.Target.GetType().Module.ResolveField(fieldHandle);

            // Formatujemy ostatecznie napis dla tego parametru.
            // Nazwę parametru uzyskujemy z FieldInfo, natomiast wartość wykonując przekazane Expression.
            logmessage += string.Format("{0}: {1}. ", field.Name, par() ?? "@null");
          }
        }
        catch
        {
        // Ignorujemy błędy - nic nie jest gorsze od doskonale działającej logiki aplikacji, która wywala się na narzędziach do logowania :-)
        // Tego typu try-catch to zła praktyka w aplikacji! W tym miejscu jest to stosowne, ale przestrzegam przed nadużywaniem "zjadania" wyjątków.

          logmessage += " FAILED reading arguments.";
        }
      }
    }
}

Klasa do wykorzystania w aplikacji.

W kolejnej notce pokażę, jak to spiąć z log4net.

Tags: , , ,

Leave a Reply