¿Cómo intercepto una llamada al método en C #?

154

Para una clase dada, me gustaría tener una funcionalidad de rastreo, es decir, me gustaría registrar cada llamada a método (firma de método y valores de parámetros reales) y cada salida de método (solo la firma de método).

¿Cómo logro esto asumiendo que:

  • No quiero usar ninguna biblioteca AOP de terceros para C #,
  • No quiero agregar código duplicado a todos los métodos que quiero rastrear,
  • No quiero cambiar la API pública de la clase: los usuarios de la clase deberían poder llamar a todos los métodos exactamente de la misma manera.

Para hacer la pregunta más concreta, supongamos que hay 3 clases:

 public class Caller 
 {
     public static void Call() 
     {
         Traced traced = new Traced();
         traced.Method1();
         traced.Method2(); 
     }
 }

 public class Traced 
 {
     public void Method1(String name, Int32 value) { }

     public void Method2(Object object) { }
 }

 public class Logger
 {
     public static void LogStart(MethodInfo method, Object[] parameterValues);

     public static void LogEnd(MethodInfo method);
 }

¿Cómo invoco Logger.LogStart y Logger.LogEnd para cada llamada a Method1 y Method2 sin modificar el método Caller.Call y sin agregar las llamadas explícitamente a Traced.Method1 y Traced.Method2 ?

Editar: ¿Cuál sería la solución si se me permite cambiar ligeramente el método de llamada?

Oficial
fuente
1
Si desea saber cómo funciona la intercepción en C #, eche un vistazo a Tiny Interceptor . Esta muestra se ejecuta sin ninguna dependencia. Tenga en cuenta que si desea utilizar AOP en proyectos del mundo real, no intente implementarlo usted mismo. Use bibliotecas como PostSharp.
Jalal
Implementé el registro de una llamada al método (antes y después) usando la biblioteca MethodDecorator.Fody. Eche un vistazo a la biblioteca en github.com/Fody/MethodDecorator
Dilhan Jayathilake

Respuestas:

69

C # no es un lenguaje orientado a AOP. Tiene algunas características de AOP y puede emular algunas otras, pero hacer AOP con C # es doloroso.

Busqué formas de hacer exactamente lo que querías hacer y no encontré una manera fácil de hacerlo.

Según tengo entendido, esto es lo que quieres hacer:

[Log()]
public void Method1(String name, Int32 value);

y para hacer eso tienes dos opciones principales

  1. Herede su clase de MarshalByRefObject o ContextBoundObject y defina un atributo que herede de IMessageSink. Este artículo tiene un buen ejemplo. No obstante, debe tener en cuenta que al usar un MarshalByRefObject, el rendimiento disminuirá como el infierno, y lo digo en serio, estoy hablando de un rendimiento 10x perdido, así que piense cuidadosamente antes de intentarlo.

  2. La otra opción es inyectar código directamente. En tiempo de ejecución, lo que significa que tendrá que usar la reflexión para "leer" cada clase, obtener sus atributos e inyectar la llamada apropiada (y para el caso creo que no podría usar el método Reflection.Emit como creo que Reflection.Emit wouldn no le permite insertar nuevo código dentro de un método ya existente). En tiempo de diseño, esto significará crear una extensión para el compilador CLR que honestamente no tengo idea de cómo se hace.

La última opción es usar un marco de IoC . Tal vez no sea la solución perfecta, ya que la mayoría de los marcos de IoC funcionan definiendo puntos de entrada que permiten enganchar los métodos, pero, dependiendo de lo que desee lograr, puede ser una aproximación justa.

Jorge Córdoba
fuente
62
En otras palabras, 'ay'
johnc
2
Debo señalar que si tuviera funciones de primera clase, entonces una función podría tratarse como cualquier otra variable y podría tener un "método de enlace" que haga lo que quiere.
RCIX
3
Una tercera alternativa es generar proxies aop basados ​​en herencia en tiempo de ejecución utilizando Reflection.Emit. Este es el enfoque elegido por Spring.NET . Sin embargo, esto requeriría métodos virtuales Tracedy no es realmente adecuado para su uso sin algún tipo de contenedor IOC, por lo que entiendo por qué esta opción no está en su lista.
Marijn
2
su segunda opción es básicamente "Escribir a mano las partes de un marco de AOP que necesita", lo que debería dar como resultado "¡Oh, tal vez debería usar una opción de terceros creada específicamente para resolver el problema que tengo en lugar de bajar! -inveted-here-road "
Rune FS
2
@jorge ¿Puede proporcionar algún ejemplo / enlace para lograr esto usando Inyección de dependencia / fama de IoC como nInject
Charanraj Golla
48

La forma más sencilla de lograrlo es usar PostSharp . Inyecta código dentro de sus métodos en función de los atributos que le aplica. Te permite hacer exactamente lo que quieres.

Otra opción es usar la API de creación de perfiles para inyectar código dentro del método, pero eso es realmente duro.

Antoine Aubry
fuente
3
también puedes inyectar cosas con ICorDebug pero eso es súper malvado
Sam Saffron
9

Si escribe una clase, llamada Tracing, que implementa la interfaz IDisposable, podría ajustar todos los cuerpos de los métodos en un

Using( Tracing tracing = new Tracing() ){ ... method body ...}

En la clase Tracing puede manejar la lógica de las trazas en el método constructor / Dispose, respectivamente, en la clase Tracing para realizar un seguimiento de la entrada y salida de los métodos. Tal que:

    public class Traced 
    {
        public void Method1(String name, Int32 value) {
            using(Tracing tracer = new Tracing()) 
            {
                [... method body ...]
            }
        }

        public void Method2(Object object) { 
            using(Tracing tracer = new Tracing())
            {
                [... method body ...]
            }
        }
    }
Steen
fuente
parece mucho esfuerzo
LeRoi
3
Esto no tiene nada que ver con responder la pregunta.
Latencia
9

Puede lograrlo con la función de intercepción de un contenedor DI como Castle Windsor . De hecho, es posible configurar el contenedor de tal manera que todas las clases que tengan un método decorado por un atributo específico sean interceptadas.

Con respecto al punto 3, OP solicitó una solución sin el marco AOP. Asumí en la siguiente respuesta que lo que debería evitarse eran Aspect, JointPoint, PointCut, etc. De acuerdo con la documentación de Interceptación de CastleWindsor , ninguno de ellos debe cumplir lo que se le pide.

Configure el registro genérico de un interceptor, en función de la presencia de un atributo:

public class RequireInterception : IContributeComponentModelConstruction
{
    public void ProcessModel(IKernel kernel, ComponentModel model)
    {
        if (HasAMethodDecoratedByLoggingAttribute(model.Implementation))
        {
            model.Interceptors.Add(new InterceptorReference(typeof(ConsoleLoggingInterceptor)));
            model.Interceptors.Add(new InterceptorReference(typeof(NLogInterceptor)));
        }
    }

    private bool HasAMethodDecoratedByLoggingAttribute(Type implementation)
    {
        foreach (var memberInfo in implementation.GetMembers())
        {
            var attribute = memberInfo.GetCustomAttributes(typeof(LogAttribute)).FirstOrDefault() as LogAttribute;
            if (attribute != null)
            {
                return true;
            }
        }

        return false;
    }
}

Agregue el IContributeComponentModelConstruction creado al contenedor

container.Kernel.ComponentModelBuilder.AddContributor(new RequireInterception());

Y puedes hacer lo que quieras en el propio interceptor

public class ConsoleLoggingInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        Console.Writeline("Log before executing");
        invocation.Proceed();
        Console.Writeline("Log after executing");
    }
}

Agregue el atributo de registro a su método para iniciar sesión

 public class Traced 
 {
     [Log]
     public void Method1(String name, Int32 value) { }

     [Log]
     public void Method2(Object object) { }
 }

Tenga en cuenta que será necesario cierto manejo del atributo si solo necesita interceptarse algún método de una clase. Por defecto, todos los métodos públicos serán interceptados.

plog17
fuente
5

Si desea rastrear sus métodos sin limitación (sin adaptación de código, sin marco AOP, sin código duplicado), déjeme decirle que necesita algo de magia ...

En serio, lo resolví para implementar un AOP Framework que funcionara en tiempo de ejecución.

Puede encontrar aquí: NConcern .NET AOP Framework

Decidí crear este Marco de AOP para dar respuesta a este tipo de necesidades. Es una biblioteca simple muy ligera. Puede ver un ejemplo de registrador en la página de inicio.

Si no desea utilizar un ensamblado de terceros, puede explorar el código fuente (código abierto) y copiar ambos archivos Aspect.Directory.cs y Aspect.Directory.Entry.cs para adaptarlos según sus deseos. Estas clases permiten reemplazar sus métodos en tiempo de ejecución. Solo le pediría que respete la licencia.

Espero que encuentre lo que necesita o para convencerlo de que finalmente use un Marco de AOP.

Tony tanga
fuente
4

He encontrado una forma diferente que puede ser más fácil ...

Declarar un método InvokeMethod

[WebMethod]
    public object InvokeMethod(string methodName, Dictionary<string, object> methodArguments)
    {
        try
        {
            string lowerMethodName = '_' + methodName.ToLowerInvariant();
            List<object> tempParams = new List<object>();
            foreach (MethodInfo methodInfo in serviceMethods.Where(methodInfo => methodInfo.Name.ToLowerInvariant() == lowerMethodName))
            {
                ParameterInfo[] parameters = methodInfo.GetParameters();
                if (parameters.Length != methodArguments.Count()) continue;
                else foreach (ParameterInfo parameter in parameters)
                    {
                        object argument = null;
                        if (methodArguments.TryGetValue(parameter.Name, out argument))
                        {
                            if (parameter.ParameterType.IsValueType)
                            {
                                System.ComponentModel.TypeConverter tc = System.ComponentModel.TypeDescriptor.GetConverter(parameter.ParameterType);
                                argument = tc.ConvertFrom(argument);

                            }
                            tempParams.Insert(parameter.Position, argument);

                        }
                        else goto ContinueLoop;
                    }

                foreach (object attribute in methodInfo.GetCustomAttributes(true))
                {
                    if (attribute is YourAttributeClass)
                    {
                        RequiresPermissionAttribute attrib = attribute as YourAttributeClass;
                        YourAttributeClass.YourMethod();//Mine throws an ex
                    }
                }

                return methodInfo.Invoke(this, tempParams.ToArray());
            ContinueLoop:
                continue;
            }
            return null;
        }
        catch
        {
            throw;
        }
    }

Luego defino mis métodos así

[WebMethod]
    public void BroadcastMessage(string Message)
    {
        //MessageBus.GetInstance().SendAll("<span class='system'>Web Service Broadcast: <b>" + Message + "</b></span>");
        //return;
        InvokeMethod("BroadcastMessage", new Dictionary<string, object>() { {"Message", Message} });
    }

    [RequiresPermission("editUser")]
    void _BroadcastMessage(string Message)
    {
        MessageBus.GetInstance().SendAll("<span class='system'>Web Service Broadcast: <b>" + Message + "</b></span>");
        return;
    }

Ahora puedo tener la verificación en tiempo de ejecución sin la inyección de dependencia ...

No hay problemas en el sitio :)

Esperemos que acepte que esto tiene menos peso que un AOP Framework o que se deriva de MarshalByRefObject o que utiliza clases remotas o proxy.

Arrendajo
fuente
4

Primero tiene que modificar su clase para implementar una interfaz (en lugar de implementar MarshalByRefObject).

interface ITraced {
    void Method1();
    void Method2()
}
class Traced: ITraced { .... }

A continuación, necesita un objeto contenedor genérico basado en RealProxy para decorar cualquier interfaz que permita interceptar cualquier llamada al objeto decorado.

class MethodLogInterceptor: RealProxy
{
     public MethodLogInterceptor(Type interfaceType, object decorated) 
         : base(interfaceType)
     {
          _decorated = decorated;
     }

    public override IMessage Invoke(IMessage msg)
    {
        var methodCall = msg as IMethodCallMessage;
        var methodInfo = methodCall.MethodBase;
        Console.WriteLine("Precall " + methodInfo.Name);
        var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
        Console.WriteLine("Postcall " + methodInfo.Name);

        return new ReturnMessage(result, null, 0,
            methodCall.LogicalCallContext, methodCall);
    }
}

Ahora estamos listos para interceptar llamadas al Método 1 y al Método 2 de ITraced

 public class Caller 
 {
     public static void Call() 
     {
         ITraced traced = (ITraced)new MethodLogInterceptor(typeof(ITraced), new Traced()).GetTransparentProxy();
         traced.Method1();
         traced.Method2(); 
     }
 }
Ibrahim ben Salah
fuente
2

Puede utilizar el marco de código abierto CInject en CodePlex. Puede escribir un código mínimo para crear un inyector y hacer que intercepte cualquier código rápidamente con CInject. Además, como se trata de código abierto, también puede ampliarlo.

O puede seguir los pasos mencionados en este artículo sobre Llamadas al método de interceptación usando IL y crear su propio interceptor usando las clases Reflection.Emit en C #.

Puneet Ghanshani
fuente
1

No conozco una solución, pero mi enfoque sería el siguiente.

Decora la clase (o sus métodos) con un atributo personalizado. En otro lugar del programa, deje que una función de inicialización refleje todos los tipos, lea los métodos decorados con los atributos e inyecte algún código IL en el método. En realidad, podría ser más práctico reemplazar el método por un trozo que llame LogStart, el método real y luego LogEnd. Además, no sé si puede cambiar los métodos usando la reflexión, por lo que podría ser más práctico reemplazar todo el tipo.

Konrad Rudolph
fuente
1

Potencialmente, podría utilizar el patrón decorador GOF y 'decorar' todas las clases que necesitan seguimiento.

Probablemente solo sea realmente práctico con un contenedor de IOC (pero como apunta antes, es posible que desee considerar la intercepción del método si va a seguir el camino del IOC).

Stacy A
fuente
1

AOP es imprescindible para la implementación de código limpio, sin embargo, si desea rodear un bloque en C #, los métodos genéricos tienen un uso relativamente más fácil. (con sentido de inteligencia y código fuertemente tipado) Ciertamente, NO puede ser una alternativa para AOP.

Aunque PostSHarp tiene pequeños problemas con errores (no me siento seguro para usar en producción), es algo bueno.

Clase genérica de envoltura,

public class Wrapper
{
    public static Exception TryCatch(Action actionToWrap, Action<Exception> exceptionHandler = null)
    {
        Exception retval = null;
        try
        {
            actionToWrap();
        }
        catch (Exception exception)
        {
            retval = exception;
            if (exceptionHandler != null)
            {
                exceptionHandler(retval);
            }
        }
        return retval;
    }

    public static Exception LogOnError(Action actionToWrap, string errorMessage = "", Action<Exception> afterExceptionHandled = null)
    {
        return Wrapper.TryCatch(actionToWrap, (e) =>
        {
            if (afterExceptionHandled != null)
            {
                afterExceptionHandled(e);
            }
        });
    }
}

el uso podría ser así (con sentido de inteligencia, por supuesto)

var exception = Wrapper.LogOnError(() =>
{
  MessageBox.Show("test");
  throw new Exception("test");
}, "Hata");
Nitro
fuente
Estoy de acuerdo en que Postsharp es una biblioteca AOP y manejará la intercepción, sin embargo, su ejemplo no ilustra nada de eso. No confunda IoC con intercepción. Ellos no son los mismos.
Latencia
-1
  1. Escribe tu propia biblioteca AOP.
  2. Use la reflexión para generar un proxy de registro sobre sus instancias (no estoy seguro si puede hacerlo sin cambiar alguna parte de su código existente).
  3. Vuelva a escribir el ensamblaje e inyecte su código de registro (básicamente el mismo que 1).
  4. Aloje el CLR y agregue el registro a este nivel (creo que esta es la solución más difícil de implementar, aunque no estoy seguro si tiene los ganchos necesarios en el CLR).
kokos
fuente
-3

Lo mejor que puede hacer antes de C # 6 con 'nameof' lanzado es usar StackTrace lento y Expresiones linq.

Por ejemplo, para tal método

    public void MyMethod(int age, string name)
    {
        log.DebugTrace(() => age, () => name);

        //do your stuff
    }

Tal línea puede ser producida en su archivo de registro

Method 'MyMethod' parameters age: 20 name: Mike

Aquí está la implementación:

    //TODO: replace with 'nameof' in C# 6
    public static void DebugTrace(this ILog log, params Expression<Func<object>>[] args)
    {
        #if DEBUG

        var method = (new StackTrace()).GetFrame(1).GetMethod();

        var parameters = new List<string>();

        foreach(var arg in args)
        {
            MemberExpression memberExpression = null;
            if (arg.Body is MemberExpression)
                memberExpression = (MemberExpression)arg.Body;

            if (arg.Body is UnaryExpression && ((UnaryExpression)arg.Body).Operand is MemberExpression)
                memberExpression = (MemberExpression)((UnaryExpression)arg.Body).Operand;

            parameters.Add(memberExpression == null ? "NA" : memberExpression.Member.Name + ": " + arg.Compile().DynamicInvoke().ToString());
        }

        log.Debug(string.Format("Method '{0}' parameters {1}", method.Name, string.Join(" ", parameters)));

        #endif
    }
irrisión
fuente
Esto falla el requisito definido en su segunda viñeta.
Ted Bigham