ฉันจะดักการเรียกเมธอดใน C # ได้อย่างไร


154

สำหรับคลาสที่กำหนดฉันต้องการมีฟังก์ชั่นการติดตามเช่นฉันต้องการบันทึกการเรียกใช้เมธอดทุกครั้ง (ลายเซ็นเมธอดและค่าพารามิเตอร์จริง) และทุกเมธอดออก (แค่ลายเซ็นเมธอด)

ฉันจะทำสิ่งนี้สำเร็จโดยสมมติว่า:

  • ฉันไม่ต้องการใช้ห้องสมุด AOP ของบุคคลที่สามสำหรับ C #
  • ฉันไม่ต้องการเพิ่มรหัสที่ซ้ำกันไปยังวิธีการทั้งหมดที่ฉันต้องการติดตาม
  • ฉันไม่ต้องการเปลี่ยน 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);
 }

ฉันจะวิงวอนขอLogger.LogStartและLogger.LogEndสำหรับการเรียกร้องให้ทุกMethod1และMethod2โดยไม่มีการแก้ไขCaller.Callวิธีการและโดยไม่ต้องเพิ่มสายอย่างชัดเจนTraced.Method1และTraced.Method2 ?

แก้ไข: จะมีวิธีแก้ไขอย่างไรหากฉันอนุญาตให้เปลี่ยนวิธีการโทรเล็กน้อย



1
หากคุณต้องการที่จะทราบวิธีการสกัดกั้นการทำงานใน C # จะดูที่เล็ก Interceptor ตัวอย่างนี้ทำงานโดยไม่มีการอ้างอิงใด ๆ โปรดสังเกตว่าหากคุณต้องการใช้ AOP ในโครงการในโลกแห่งความเป็นจริงอย่าพยายามใช้งานด้วยตนเอง ใช้ไลบรารีเช่น PostSharp
Jalal

ฉันใช้การบันทึกการเรียกใช้เมธอด (ก่อนและหลัง) โดยใช้ MethodDecorator.Fody library โปรดดูห้องสมุดที่github.com/Fody/MethodDecorator
Dilhan Jayathilake

คำตอบ:


69

C # ไม่ใช่ภาษาที่เน้น AOP มันมีคุณสมบัติของ AOP และคุณสามารถเลียนแบบคุณสมบัติอื่น ๆ ได้ แต่การทำให้ AOP ด้วย C # นั้นเจ็บปวด

ฉันค้นหาวิธีที่จะทำสิ่งที่คุณต้องการและฉันพบว่าไม่มีวิธีที่ง่ายที่จะทำ

ตามที่ฉันเข้าใจนี่คือสิ่งที่คุณต้องการทำ:

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

และเพื่อที่จะทำเช่นนั้นคุณมีสองตัวเลือกหลัก

  1. รับช่วงชั้นของคุณจาก MarshalByRefObject หรือ ContextBoundObject และกำหนดแอตทริบิวต์ที่สืบทอดจาก IMessageSink บทความนี้มีตัวอย่างที่ดี คุณต้องพิจารณาว่าการใช้ MarshalByRefObject การแสดงจะลงไปเหมือนนรกและฉันหมายความว่าฉันกำลังพูดถึงการสูญเสียประสิทธิภาพ 10 เท่าดังนั้นให้คิดอย่างรอบคอบก่อนลอง

  2. ตัวเลือกอื่นคือการฉีดรหัสโดยตรง ในรันไทม์ซึ่งหมายความว่าคุณจะต้องใช้การไตร่ตรองเพื่อ "อ่าน" ทุกคลาสรับคุณลักษณะและอัดฉีดการโทรที่เหมาะสม (และสำหรับเรื่องนั้นฉันคิดว่าคุณไม่สามารถใช้การสะท้อนกลับได้รับวิธีที่ฉันคิดว่า Reflection ไม่อนุญาตให้คุณแทรกรหัสใหม่ภายในวิธีที่มีอยู่แล้ว) ในขณะออกแบบนี้จะหมายถึงการสร้างส่วนขยายไปยังคอมไพเลอร์ CLR ซึ่งฉันไม่รู้ว่ามันเสร็จสิ้นแล้ว

ตัวเลือกสุดท้ายคือการใช้กรอบ IoC อาจไม่ใช่โซลูชันที่สมบูรณ์แบบเนื่องจากกรอบ IoC ส่วนใหญ่ทำงานโดยกำหนดจุดเข้าใช้งานซึ่งอนุญาตให้วิธีการติด แต่ขึ้นอยู่กับสิ่งที่คุณต้องการบรรลุซึ่งอาจเป็นการประมาณที่ยุติธรรม


62
กล่าวอีกนัยหนึ่ง 'ouch'
johnc

2
ฉันควรจะชี้ให้เห็นว่าถ้าคุณมีฟังก์ชั่นชั้นหนึ่งแล้วฟังก์ชั่นจะได้รับการปฏิบัติเช่นเดียวกับตัวแปรอื่น ๆ และคุณอาจมี "เมธอดเบ็ด" ที่ทำในสิ่งที่เขาต้องการ
RCIX

3
ทางเลือกที่สามคือการสร้างการรับมรดกตามผู้รับมอบฉันทะ AOP Reflection.Emitที่รันไทม์ใช้ นี่คือวิธีการที่ถูกเลือกโดย Spring.NET อย่างไรก็ตามวิธีนี้ต้องใช้วิธีเสมือนจริงTracedและไม่เหมาะสำหรับการใช้งานหากไม่มีคอนเทนเนอร์ IOC บางประเภทดังนั้นฉันจึงเข้าใจว่าทำไมตัวเลือกนี้ไม่อยู่ในรายการของคุณ
Marijn

2
ตัวเลือกที่สองของคุณนั้นโดยทั่วไป "เขียนส่วนของกรอบ AOP ที่คุณต้องการด้วยมือ" ซึ่งจะทำให้ผลลัพธ์ของ "โอ้รอบางทีฉันควรใช้ตัวเลือกของบุคคลที่สามที่สร้างขึ้นโดยเฉพาะเพื่อแก้ปัญหาที่ฉันมีแทนที่จะลงไปไม่ใช่ -inveted-here-road "
Rune FS

2
@jorge คุณช่วยให้ตัวอย่าง / ลิงค์เพื่อให้บรรลุนี้โดยใช้การฉีดพึ่งพา / famework IoC เช่น nInject
Charanraj Golla

48

วิธีที่ง่ายที่สุดเพื่อให้บรรลุว่าน่าจะเป็นที่จะใช้PostSharp มันฉีดรหัสในวิธีการของคุณขึ้นอยู่กับคุณลักษณะที่คุณใช้กับมัน ช่วยให้คุณทำสิ่งที่คุณต้องการ

อีกทางเลือกหนึ่งคือการใช้API การทำโปรไฟล์เพื่อฉีดโค้ดภายในเมธอด


3
คุณยังสามารถฉีดสิ่งที่มี ICorDebug แต่ที่เป็นความชั่วร้ายสุด
แซม Saffron

9

ถ้าคุณเขียนคลาส - เรียกว่า Tracing - ซึ่งใช้อินเทอร์เฟซ IDisposable คุณสามารถรวมเนื้อหาวิธีการทั้งหมดใน

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

ในคลาส Tracing คุณสามารถจัดการตรรกะของการติดตามในเมธอด Constructor / 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
สิ่งนี้ไม่เกี่ยวกับการตอบคำถาม
Latency

9

คุณสามารถบรรลุด้วยการสกัดกั้นคุณสมบัติของภาชนะ DI เช่นปราสาทวินด์เซอร์ แท้จริงแล้วมันเป็นไปได้ที่จะกำหนดค่าคอนเทนเนอร์ในลักษณะที่ว่าทุกคลาสที่มีวิธีการตกแต่งโดยคุณลักษณะเฉพาะจะถูกดัก

เกี่ยวกับจุด # 3 OP ขอวิธีแก้ปัญหาโดยไม่มีกรอบ AOP ฉันสันนิษฐานในคำตอบต่อไปนี้ว่าสิ่งที่ควรหลีกเลี่ยงคือ Aspect, JointPoint, PointCut ฯลฯ ตามเอกสารการสกัดกั้นจาก CastleWindsorไม่จำเป็นต้องทำสิ่งเหล่านี้เพื่อให้บรรลุตามที่ขอ

กำหนดค่าการลงทะเบียนทั่วไปของ Interceptor ตามการมีอยู่ของแอตทริบิวต์:

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

เพิ่มแอ็ตทริบิวต์การบันทึกลงในเมธอดของคุณเพื่อบันทึก

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

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

โปรดทราบว่าจำเป็นต้องมีการจัดการแอตทริบิวต์บางอย่างหากวิธีการบางอย่างของคลาสจำเป็นต้องถูกดักจับ โดยค่าเริ่มต้นวิธีการสาธารณะทั้งหมดจะถูกดัก


5

หากคุณต้องการติดตามหลังจากวิธีการของคุณโดยไม่มีข้อ จำกัด (ไม่มีการดัดแปลงรหัสไม่มี AOP Framework ไม่มีรหัสซ้ำกัน) ให้ฉันบอกคุณคุณต้องใช้เวทมนตร์ ...

อย่างจริงจังฉันแก้ไขมันเพื่อใช้ AOP Framework ทำงานที่รันไทม์

คุณสามารถค้นหาได้ที่นี่: NConcern .NET AOP Framework

ฉันตัดสินใจที่จะสร้าง AOP Framework นี้เพื่อตอบสนองความต้องการประเภทนี้ มันเป็นห้องสมุดง่าย ๆ ที่มีน้ำหนักเบามาก คุณสามารถดูตัวอย่างของคนตัดไม้ในหน้าแรก

หากคุณไม่ต้องการใช้แอสเซมบลีของบุคคลที่สามคุณสามารถเรียกดูซอร์สโค้ด (โอเพ่นซอร์ส) และคัดลอกทั้งไฟล์Aspect.Directory.csและAspect.Directory.Entry.csเพื่อดัดแปลงตามความต้องการของคุณ คลาสเหล่านี้อนุญาตให้แทนที่เมธอดของคุณที่รันไทม์ ฉันแค่ขอให้คุณเคารพใบอนุญาต

ฉันหวังว่าคุณจะพบสิ่งที่คุณต้องการหรือเพื่อโน้มน้าวให้คุณใช้ AOP Framework ในที่สุด


4

ลองดูที่ - สิ่งที่หนักมาก .. http://msdn.microsoft.com/en-us/magazine/cc164165.aspx

Essential. net - don box มีบทหนึ่งเกี่ยวกับสิ่งที่คุณต้องการเรียกว่าการสกัดกั้น ฉันคัดลอกบางส่วนมาที่นี่ (ขออภัยเกี่ยวกับสีแบบอักษร - ฉันมีชุดรูปแบบที่มืดแล้ว ... ) http://madcoderspeak.blogspot.com/2005/09/essential-interception-using-contexts.html


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

ตอนนี้ฉันสามารถตรวจสอบ ณ รันไทม์โดยไม่ต้องพึ่งพาการฉีด ...

ไม่มี gotchas ในไซต์ :)

หวังว่าคุณจะเห็นด้วยว่านี่คือน้ำหนักที่น้อยกว่าจากนั้น AOP Framework หรือมาจาก MarshalByRefObject หรือใช้ remoting หรือคลาสพร็อกซี


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

ตอนนี้เราพร้อมที่จะสกัดกั้นการเรียกใช้วิธีที่ 1 และวิธีที่ 2 ของ ITraced

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

2

คุณสามารถใช้กรอบโอเพนซอร์สCInjectบน CodePlex คุณสามารถเขียนโค้ดขั้นต่ำเพื่อสร้าง Injector และรับรหัสเพื่อดักจับรหัสใด ๆ ได้อย่างรวดเร็วด้วย CInject นอกจากนี้เนื่องจากนี่คือโอเพ่นซอร์สคุณสามารถขยายได้เช่นกัน

หรือคุณสามารถทำตามขั้นตอนที่กล่าวถึงในบทความนี้เกี่ยวกับวิธีการดักจับการโทรโดยใช้ ILและสร้างเครื่องดักฟังของคุณเองโดยใช้ Reflection.Emit class ใน C #


1

ฉันไม่รู้วิธีแก้ปัญหา แต่แนวทางของฉันจะเป็นดังนี้

ตกแต่งคลาส (หรือวิธีการ) ด้วยแอตทริบิวต์ที่กำหนดเอง ที่อื่นในโปรแกรมให้ฟังก์ชั่นการเริ่มต้นสะท้อนทุกประเภทอ่านวิธีการตกแต่งด้วยคุณสมบัติและฉีดรหัส IL ลงในวิธีการ มันจริงอาจจะมีการปฏิบัติมากขึ้นที่จะเปลี่ยนวิธีที่ต้นขั้วที่โทรวิธีการที่เกิดขึ้นจริงแล้วLogStart LogEndนอกจากนี้ฉันไม่ทราบว่าคุณสามารถเปลี่ยนวิธีการใช้การไตร่ตรองได้หรือไม่ดังนั้นจึงเป็นการดีกว่าถ้าจะเปลี่ยนทั้งประเภท


1

คุณสามารถใช้รูปแบบ GOF Decorator และ 'ตกแต่ง' คลาสทั้งหมดที่ต้องมีการติดตาม

อาจเป็นประโยชน์จริง ๆ กับคอนเทนเนอร์ IOC เท่านั้น (แต่เนื่องจากตัวชี้ออกก่อนหน้านี้คุณอาจต้องการพิจารณาการสกัดกั้นเมธอดหากคุณกำลังจะลงเส้นทาง IOC)



1

AOP เป็นสิ่งจำเป็นสำหรับการนำโค้ดที่สะอาดไปใช้อย่างไรก็ตามหากคุณต้องการล้อมรอบบล็อกใน C # วิธีทั่วไปมีการใช้งานที่ค่อนข้างง่ายกว่า (ด้วยความรู้สึกภายในและพิมพ์รหัสอย่างยิ่ง) แน่นอนว่ามันอาจไม่ใช่ทางเลือกสำหรับ AOP

แม้ว่าPostSHarpมีปัญหาบั๊กกี้เล็กน้อย (ฉันไม่มั่นใจในการใช้งานที่การผลิต) แต่ก็เป็นสิ่งที่ดี

คลาส wrapper ทั่วไป

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 และเพิ่มการบันทึกในระดับนี้ (ฉันคิดว่านี่เป็นทางออกที่ยากที่สุดในการติดตั้ง แต่ไม่แน่ใจว่าคุณมี hooks ที่จำเป็นใน CLR หรือไม่)

-3

สิ่งที่ดีที่สุดที่คุณสามารถทำได้ก่อน C # 6 ด้วย 'nameof' ที่ปล่อยออกมาคือการใช้ StackTrace และ linq Expression ที่ช้า

เช่นสำหรับวิธีการดังกล่าว

    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.