C #에서 메소드 호출을 인터셉트하려면 어떻게해야합니까?


154

주어진 클래스에 대해 추적 기능을 원합니다. 즉 모든 메소드 호출 (메소드 서명 및 실제 매개 변수 값) 및 모든 메소드 종료 (메소드 서명)를 기록하고 싶습니다.

다음을 가정하여 이것을 어떻게 수행합니까?

  • C #에 타사 AOP 라이브러리를 사용하고 싶지 않습니다.
  • 추적하려는 모든 메소드에 중복 코드를 추가하고 싶지 않습니다.
  • 클래스의 공개 API를 변경하고 싶지 않습니다. 클래스의 사용자는 모든 메소드를 정확히 같은 방식으로 호출 할 수 있어야합니다.

보다 구체적인 질문을하기 위해 3 가지 클래스가 있다고 가정 해 봅시다.

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

Caller.Call 메서드 를 수정 하지 않고 Traced.Method1Traced.Method2에 명시 적으로 호출을 추가 하지 않고 Method1Method2 를 호출 할 때마다 Logger.LogStartLogger.LogEnd 를 어떻게 호출 합니까?

편집 : Call 메서드를 약간 변경할 수 있다면 어떻게 될까요?



1
C #에서 인터 셉션이 어떻게 작동하는지 알고 싶다면 Tiny Interceptor를 살펴보십시오 . 이 샘플은 종속성없이 실행됩니다. 실제 프로젝트에서 AOP를 사용하려는 경우 직접 구현하지 마십시오. PostSharp와 같은 라이브러리를 사용하십시오.
Jalal

MethodDecorator.Fody 라이브러리를 사용하여 (전후에) 메소드 호출 로깅을 구현했습니다. github.com/Fody/MethodDecorator
Dilhan Jayathilake

답변:


69

C #은 AOP 지향 언어가 아닙니다. 일부 AOP 기능이 있으며 다른 일부를 에뮬레이트 할 수 있지만 C #으로 AOP를 만드는 것은 고통 스럽습니다.

나는 당신이하고 싶은 일을 정확하게 할 수있는 방법을 찾았고 쉬운 방법을 찾지 못했습니다.

내가 이해 한 것처럼, 이것은 당신이하고 싶은 일입니다.

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

이를 위해 두 가지 주요 옵션이 있습니다.

  1. MarshalByRefObject 또는 ContextBoundObject에서 클래스를 상속하고 IMessageSink에서 상속되는 속성을 정의하십시오. 이 기사 에는 좋은 예가 있습니다. 그럼에도 불구하고 MarshalByRefObject를 사용하면 성능이 지옥처럼 떨어질 것이라고 생각해야하며, 10 배의 성능 손실에 대해 이야기하고 있으므로 시도하기 전에 신중하게 생각하십시오.

  2. 다른 옵션은 코드를 직접 주입하는 것입니다. 런타임에 리플렉션을 사용하여 모든 클래스를 "읽고"속성을 가져 와서 적절한 호출을 주입해야합니다 (그 문제에 대해서는 Reflection.Emit 메소드를 사용할 수 없다고 생각합니다. 기존 방법에 새 코드를 삽입 할 수 없습니다). 디자인 타임에 이것은 CLR 컴파일러에 대한 확장을 만드는 것을 의미합니다.

마지막 옵션은 IoC 프레임 워크를 사용하는 것 입니다. 어쩌면 대부분의 IoC 프레임 워크가 메소드를 후크 할 수있는 진입 점을 정의하여 작동하기 때문에 완벽한 솔루션이 아닐 수도 있지만 달성하려는 목표에 따라 공정한 근접성이 될 수 있습니다.


62
즉, '아야'
johnc

2
만약 당신이 첫번째 클래스 함수를 가지고 있다면, 함수는 다른 변수처럼 취급 될 수 있고 그가 원하는 것을하는 "메소드 훅"을 가질 수 있다는 것을 지적해야합니다.
RCIX

3
세 번째 대안은을 사용하여 런타임에 상속 기반 aop 프록시를 생성하는 것 Reflection.Emit입니다. 이것은 Spring.NET에 의해 선택된 접근법 입니다. 그러나 여기에는 가상 메소드가 필요하며 TracedIOC 컨테이너가 없으면 실제로 사용하기에 적합하지 않으므로이 옵션이 왜 목록에 없는지 이해합니다.
Marijn

2
두 번째 옵션은 기본적으로 "수동으로 필요한 AOP 프레임 워크의 일부를 작성"입니다. 그러면 "아무 말고, 내가하지 않는 문제를 해결하기 위해 특별히 만든 타사 옵션을 사용해야합니다." -inveted-여기에 도로 "
룬 FS

2
@jorge nInject와 같은 Dependency Injection / IoC 명성을 사용하여 예제 / 링크를 제공 할 수 있습니까?
Charanraj Golla

48

이를 달성하는 가장 간단한 방법은 아마도 PostSharp 를 사용하는 것입니다 . 적용하는 속성에 따라 메소드 내부에 코드를 삽입합니다. 원하는대로 정확하게 할 수 있습니다.

또 다른 옵션은 프로파일 링 API 를 사용 하여 메소드 내부에 코드를 삽입하는 것이지만 실제로는 하드 코어입니다.


3
당신은 또한 ICorDebug로 물건을 주입 할 수 있지만 그것은 매우 사악합니다
Sam Saffron

9

IDisposable 인터페이스를 구현하는 클래스 (Tracing이라고 함)를 작성하면 모든 메소드 본문을

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

Tracing 클래스에서는 생성자 / Dispose 메서드의 추적 논리를 각각 Tracing 클래스에서 처리하여 메서드의 시작 및 종료를 추적 할 수 있습니다. 그런 :

    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 ...]
            }
        }
    }

많은 노력처럼 보인다
LeRoi

3
이것은 질문에 대답하는 것과 아무 관련이 없습니다.
대기 시간

9

Castle Windsor 와 같은 DI 컨테이너의 가로 채기 기능으로 이를 달성 할 수 있습니다 . 실제로 특정 속성으로 장식 된 메서드가있는 모든 클래스를 가로 챌 수 있도록 컨테이너를 구성 할 수 있습니다.

지점 # 3과 관련하여 OP는 AOP 프레임 워크가없는 솔루션을 요청했습니다. 다음 답변에서 피해야 할 것은 Aspect, JointPoint, PointCut 등이라고 가정했습니다. CastleWindsor의 인터 셉션 문서 에 따르면 요청 된 사항을 달성하는 데 필요한 것은 없습니다.

속성의 존재 여부에 따라 인터셉터의 일반 등록을 구성하십시오.

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

작성된 IContributeComponentModelConstruction을 컨테이너에 추가

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

인터셉터 자체에서 원하는 모든 것을 할 수 있습니다

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

로깅 할 메소드에 logging 속성을 추가하십시오

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

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

클래스의 일부 메소드 만 인터셉트해야하는 경우 일부 속성 처리가 필요합니다. 기본적으로 모든 공용 메소드는 인터셉트됩니다.


5

제한없이 (코드 적응, AOP 프레임 워크, 중복 코드 없음) 메소드를 추적하려면 마술이 필요합니다.

심각하게 런타임에 작동하는 AOP 프레임 워크를 구현하도록 해결했습니다.

당신은 여기에서 찾을 수 있습니다 NConcern .NET AOP 프레임 워크

이런 종류의 요구에 부응하기 위해이 AOP 프레임 워크를 만들기로 결정했습니다. 매우 가벼운 간단한 라이브러리입니다. 홈페이지에서 로거의 예를 볼 수 있습니다.

타사 어셈블리를 사용하지 않으려는 경우 코드 소스 (오픈 소스)를 탐색하고 Aspect.Directory.csAspect.Directory.Entry.cs 파일을 모두 원하는대로 복사 할 수 있습니다 . 이러한 클래스를 사용하면 런타임에 메소드를 대체 할 수 있습니다. 라이센스를 존중하라고 부탁합니다.

필요한 것을 찾거나 AOP 프레임 워크를 최종적으로 사용하도록 설득하기를 바랍니다.



4

더 쉬운 다른 방법을 찾았습니다 ...

메소드 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;
        }
    }

그런 다음 내 방법을 다음과 같이 정의하십시오.

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

이제 의존성 주입없이 런타임에 확인할 수 있습니다 ...

사이트에 문제가 없습니다 :)

AOP 프레임 워크 나 MarshalByRefObject에서 파생되거나 원격 또는 프록시 클래스를 사용하는 것보다 가중치가 적다는 데 동의 할 것입니다.


4

먼저 MarshalByRefObject를 구현하는 대신 인터페이스를 구현하도록 클래스를 수정해야합니다.

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

다음으로 RealProxy 기반의 일반 래퍼 객체가 필요합니다. 데코 레이팅 된 객체에 대한 호출을 가로 챌 수 있도록 인터페이스를 장식하십시오.

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

이제 ITraced의 Method1 및 Method2에 대한 호출을 가로 챌 준비가되었습니다.

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

2

CodePlex에서 오픈 소스 프레임 워크 CInject 를 사용할 수 있습니다 . 최소한의 코드를 작성하여 인젝터를 작성하고 CInject로 코드를 신속하게 가로 챌 수 있습니다. 또한 이것은 오픈 소스이므로이를 확장 할 수 있습니다.

또는 IL을 사용하여 메소드 호출 인터셉트 에 대한이 기사에서 언급 한 단계를 수행 하고 C #에서 Reflection.Emit 클래스를 사용하여 자체 인터셉터를 작성할 수 있습니다.


1

나는 해결책을 모른다. 그러나 나의 접근 방식은 다음과 같다.

사용자 정의 속성으로 클래스 (또는 그 메소드)를 장식하십시오. 프로그램의 다른 곳에서는 초기화 함수가 모든 유형을 반영하고 속성으로 장식 된 메소드를 읽고 일부 IL 코드를 메소드에 삽입하십시오. 실제로 더 실용적 일 수도 교체 그루터기 그 호출에 의해 방법을 LogStart한 후, 실제 방법 LogEnd. 또한 리플렉션을 사용하여 메소드를 변경할 수 있는지 모르겠으므로 전체 유형을 바꾸는 것이 더 실용적입니다.


1

GOF Decorator Pattern을 사용하고 추적이 필요한 모든 클래스를 '장식'할 수 있습니다.

아마도 IOC 컨테이너에서만 실용적 일 것입니다 (그러나 이전에 포인터로 IOC 경로를 내려 가면 메소드 차단을 고려할 수 있습니다).



1

AOP는 깨끗한 코드 구현을위한 필수 요소이지만 C #에서 블록을 둘러싸고 싶다면 일반적인 메서드의 사용이 비교적 쉽습니다. (intelli 감각과 강력한 형식의 코드 사용) 확실히 AOP의 대안이 될 수는 없습니다.

PostSHarp 에는 버그가 거의 없지만 (제작에 자신감이 없다고 생각하지만) 좋은 것입니다.

일반 래퍼 클래스

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

사용법은 다음과 같습니다 (물론 인텔리 센스)

var exception = Wrapper.LogOnError(() =>
{
  MessageBox.Show("test");
  throw new Exception("test");
}, "Hata");

Postsharp는 AOP 라이브러리이며 차단을 처리하지만 동의하지만 예제는 아무것도 설명하지 않습니다. IoC를 차단과 혼동하지 마십시오. 그들은 동일하지 않습니다.
대기 시간

-1
  1. 자신의 AOP 라이브러리를 작성하십시오.
  2. 리플렉션을 사용하여 인스턴스를 통해 로깅 프록시를 생성하십시오 (기존 코드의 일부를 변경하지 않고 수행 할 수 있는지 확실하지 않음).
  3. 어셈블리를 다시 작성하고 로깅 코드를 주입하십시오 (기본적으로 1과 동일).
  4. CLR을 호스팅 하고이 수준에서 로깅을 추가하십시오 (CLR에 필요한 후크가 있는지 확실하지 않은 경우 구현하기 가장 어려운 솔루션이라고 생각합니다).

-3

'nameof'가 릴리스 된 C # 6 이전에 수행 할 수있는 최선의 방법은 느린 StackTrace 및 linq 표현식을 사용하는 것입니다.

예를 들어 그러한 방법

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

        //do your stuff
    }

이러한 줄은 로그 파일에 생성 될 수 있습니다

Method 'MyMethod' parameters age: 20 name: Mike

구현은 다음과 같습니다.

    //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
    }

이것은 두 번째 글 머리 기호에 정의 된 요구 사항에 실패합니다.
Ted Bigham
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.