Como interceptar uma chamada de método em c #?

154

Para uma determinada classe, eu gostaria de ter a funcionalidade de rastreamento, ou seja, gostaria de registrar todas as chamadas de métodos (assinatura do método e valores reais dos parâmetros) e todas as saídas do método (apenas a assinatura do método).

Como faço para isso assumindo que:

  • Não quero usar nenhuma biblioteca AOP de terceiros para C #,
  • Não quero adicionar código duplicado a todos os métodos que desejo rastrear,
  • Não quero alterar a API pública da classe - os usuários da classe devem poder chamar todos os métodos exatamente da mesma maneira.

Para tornar a questão mais concreta, vamos assumir que existem 3 classes:

 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);
 }

Como invoco Logger.LogStart e Logger.LogEnd para todas as chamadas para Method1 e Method2 sem modificar o método Caller.Call e sem adicionar as chamadas explicitamente a Traced.Method1 e Traced.Method2 ?

Edit: Qual seria a solução se eu tivesse permissão para alterar um pouco o método de chamada?

Journeyman
fonte
1
Se você quiser saber como a interceptação funciona em C #, dê uma olhada no Tiny Interceptor . Este exemplo é executado sem nenhuma dependência. Observe que se você deseja usar o AOP em projetos do mundo real, não tente implementá-lo. Use bibliotecas como o PostSharp.
Jalal
Eu implementei o log de uma chamada de método (antes e depois) usando a biblioteca MethodDecorator.Fody. Por favor, dê uma olhada na biblioteca em github.com/Fody/MethodDecorator
Dilhan Jayathilake

Respostas:

69

C # não é uma linguagem orientada para AOP. Possui alguns recursos de AOP e você pode emular outros, mas fazer AOP com C # é doloroso.

Procurei maneiras de fazer exatamente o que você queria fazer e não achei uma maneira fácil de fazê-lo.

Pelo que entendi, é isso que você deseja fazer:

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

e para fazer isso você tem duas opções principais

  1. Herde sua classe de MarshalByRefObject ou ContextBoundObject e defina um atributo que herda de IMessageSink. Este artigo tem um bom exemplo. Você deve considerar, no entanto, que o uso de um MarshalByRefObject diminuirá como o inferno, e eu quero dizer, estou falando de um desempenho 10x perdido, portanto, pense com cuidado antes de tentar isso.

  2. A outra opção é injetar código diretamente. Em tempo de execução, o que significa que você terá que usar a reflexão para "ler" todas as classes, obter seus atributos e injetar a chamada apropriada (e, nesse caso, acho que você não poderia usar o método Reflection.Emit como penso que Reflection.Emit não permite inserir um novo código dentro de um método já existente). Em tempo de design, isso significará a criação de uma extensão para o compilador CLR, que sinceramente não faço ideia de como isso é feito.

A opção final é usar uma estrutura de IoC . Talvez não seja a solução perfeita, pois a maioria das estruturas de IoC funciona definindo pontos de entrada que permitem que os métodos sejam conectados, mas, dependendo do que você deseja obter, essa pode ser uma aproximação justa.

Jorge Córdoba
fonte
62
Em outras palavras, 'ai'
johnc
2
Devo salientar que, se você tivesse funções de primeira classe, uma função poderia ser tratada como qualquer outra variável e você poderia ter um "gancho de método" que faça o que ele deseja.
RCIX 26/08/09
3
Uma terceira alternativa é gerar uma herança baseada em AOP proxies em tempo de execução usando Reflection.Emit. Essa é a abordagem escolhida pelo Spring.NET . No entanto, isso exigiria métodos virtuais ativados Tracede não é realmente adequado para uso sem algum tipo de contêiner IOC, portanto, entendo por que essa opção não está na sua lista.
Marijn
2
sua segunda opção é basicamente "Escreva as partes de uma estrutura de AOP que você precisa manualmente", o que deve resultar nas inferências de "Ah, espere, talvez eu deva usar uma opção de terceiros criada especificamente para resolver o problema que tenho, em vez de diminuir -inveted-here-road "
Rune FS
2
@jorge Você pode fornecer alguns exemplos / link para conseguir isso usando Dependency Injection / IoC famework como Ninject
Charanraj Golla
48

A maneira mais simples de conseguir isso é provavelmente usando o PostSharp . Ele injeta código dentro de seus métodos com base nos atributos que você aplica a ele. Ele permite que você faça exatamente o que deseja.

Outra opção é usar a API de criação de perfil para injetar código dentro do método, mas isso é realmente sério.

Antoine Aubry
fonte
3
você também pode injetar coisas com ICorDebug mas isso é super mal
Sam Saffron
9

Se você escrever uma classe - chamada Tracing - que implementa a interface IDisposable, você pode agrupar todos os corpos de métodos em um

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

Na classe Tracing, você pode lidar com a lógica dos rastreamentos no método construtor / Dispose, respectivamente, na classe Tracing para acompanhar a entrada e a saída dos métodos. De tal modo 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
fonte
parece muito esforço
LeRoi 10/01
3
Isso não tem nada a ver com responder à pergunta.
Latência
9

Você poderia consegui-lo com o recurso de interceptação de um contêiner de DI, como o Castle Windsor . De fato, é possível configurar o contêiner de maneira que todas as classes que possuem um método decorado por um atributo específico sejam interceptadas.

Em relação ao ponto 3, o OP solicitou uma solução sem estrutura de AOP. Presumi na resposta a seguir que o que deveria ser evitado eram Aspect, JointPoint, PointCut etc. De acordo com a documentação da Interception da CastleWindsor , nenhum deles é necessário para realizar o que é solicitado.

Configure o registro genérico de um Interceptor, com base na presença de um 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;
    }
}

Adicione o IContributeComponentModelConstruction criado ao contêiner

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

E você pode fazer o que quiser no próprio interceptador

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

Adicione o atributo de log ao seu método para registrar

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

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

Observe que alguma manipulação do atributo será necessária se apenas algum método de uma classe precisar ser interceptado. Por padrão, todos os métodos públicos serão interceptados.

plog17
fonte
5

Se você deseja rastrear seus métodos sem limitação (sem adaptação de código, sem AOP Framework, sem código duplicado), deixe-me dizer, você precisa de alguma mágica ...

Sério, resolvi implementar um AOP Framework trabalhando em tempo de execução.

Você pode encontrar aqui: NConcern .NET AOP Framework

Decidi criar esse quadro de AOP para responder a esse tipo de necessidades. é uma biblioteca simples muito leve. Você pode ver um exemplo de logger na página inicial.

Se você não quiser usar um assembly de terceiros, poderá procurar a fonte de código (código aberto) e copiar os arquivos Aspect.Directory.cs e Aspect.Directory.Entry.cs para - conforme desejado. Essas classes permitem substituir seus métodos em tempo de execução. Eu apenas pediria que respeitasse a licença.

Espero que você encontre o que precisa ou o convença a finalmente usar um quadro de AOP.

Tony THONG
fonte
4

Eu encontrei uma maneira diferente que pode ser mais fácil ...

Declarar um 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;
        }
    }

Eu então defino meus métodos assim

[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;
    }

Agora posso fazer a verificação em tempo de execução sem a injeção de dependência ...

Não há truques no site :)

Espero que você concorde que isso é menos pesado que um Framework AOP ou derivado de MarshalByRefObject ou usando classes de comunicação remota ou proxy.

Jay
fonte
4

Primeiro você precisa modificar sua classe para implementar uma interface (em vez de implementar o MarshalByRefObject).

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

Em seguida, você precisa de um objeto wrapper genérico baseado no RealProxy para decorar qualquer interface e permitir a interceptação de qualquer chamada para o 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);
    }
}

Agora estamos prontos para interceptar chamadas para o Método1 e o Método2 do 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
fonte
2

Você pode usar a estrutura de código aberto CInject no CodePlex. Você pode escrever um código mínimo para criar um Injetor e interceptar qualquer código rapidamente com o CInject. Além disso, como esse é um código aberto, você também pode estender isso.

Ou você pode seguir as etapas mencionadas neste artigo em Interceptando chamadas de método usando IL e criar seu próprio interceptador usando as classes Reflection.Emit em C #.

Puneet Ghanshani
fonte
1

Não conheço uma solução, mas minha abordagem seria a seguinte.

Decore a classe (ou seus métodos) com um atributo personalizado. Em outro lugar do programa, deixe uma função de inicialização refletir todos os tipos, leia os métodos decorados com os atributos e injete algum código IL no método. Na verdade, pode ser mais prático substituir o método por um esboço que chama LogStart, o método real e depois LogEnd. Além disso, não sei se você pode alterar os métodos usando a reflexão, para que seja mais prático substituir o tipo inteiro.

Konrad Rudolph
fonte
1

Você pode usar o GOF Decorator Pattern e 'decorar' todas as classes que precisam de rastreamento.

Provavelmente é apenas realmente prático com um contêiner IOC (mas, como apontador anterior, você pode considerar a interceptação de método se quiser seguir o caminho do IOC).

Stacy A
fonte
1

AOP é uma obrigação para a implementação de código limpo; no entanto, se você deseja colocar um bloco em C #, métodos genéricos têm um uso relativamente mais fácil. (com senso intelectual e código fortemente tipado) Certamente, NÃO pode ser uma alternativa para AOP.

Embora o PostSHarp tenha poucos problemas com bugs (não me sinto confiante em usar na produção), é uma coisa boa.

Classe de wrapper genérico,

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);
            }
        });
    }
}

o uso pode ser assim (com senso de inteligência, é claro)

var exception = Wrapper.LogOnError(() =>
{
  MessageBox.Show("test");
  throw new Exception("test");
}, "Hata");
Nitro
fonte
Concordo que o Postsharp é uma biblioteca AOP e manipulará a interceptação, no entanto, seu exemplo não ilustra nada disso. Não confunda IoC com interceptação. Eles não são os mesmos.
Latência
-1
  1. Escreva sua própria biblioteca de AOP.
  2. Use a reflexão para gerar um proxy de log em suas instâncias (não tenho certeza se você pode fazê-lo sem alterar alguma parte do código existente).
  3. Reescreva a montagem e injete seu código de log (basicamente o mesmo que 1).
  4. Hospede o CLR e adicione log nesse nível (acho que essa é a solução mais difícil de implementar, não tenho certeza se você possui os ganchos necessários no CLR).
kokos
fonte
-3

O melhor que você pode fazer antes do C # 6 com o 'nameof' lançado é usar as expressões StackTrace e linq lentas.

Por exemplo, para esse método

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

        //do your stuff
    }

Essa linha pode ser produzida no seu arquivo de log

Method 'MyMethod' parameters age: 20 name: Mike

Aqui está a implementação:

    //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
    }
irritar
fonte
Isso falha no requisito definido em seu segundo ponto.
Ted Bigham