Làm cách nào để chặn cuộc gọi phương thức trong C #?


154

Đối với một lớp nhất định, tôi muốn có chức năng theo dõi tức là tôi muốn ghi nhật ký mọi cuộc gọi phương thức (chữ ký phương thức và giá trị tham số thực tế) và mỗi lần thoát phương thức (chỉ chữ ký phương thức).

Làm thế nào để tôi thực hiện điều này giả định rằng:

  • Tôi không muốn sử dụng bất kỳ thư viện AOP của bên thứ 3 nào cho C #,
  • Tôi không muốn thêm mã trùng lặp vào tất cả các phương thức mà tôi muốn theo dõi,
  • Tôi không muốn thay đổi API công khai của lớp - người dùng của lớp sẽ có thể gọi tất cả các phương thức theo cùng một cách chính xác.

Để làm cho câu hỏi cụ thể hơn, giả sử có 3 lớp:

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

Làm thế nào để invoke Logger.LogStartLogger.LogEnd cho mỗi cuộc gọi đến Method1Method2 mà không sửa đổi Caller.Call phương pháp và mà không cần thêm các cuộc gọi một cách rõ ràng để Traced.Method1Traced.Method2 ?

Chỉnh sửa: Điều gì sẽ là giải pháp nếu tôi được phép thay đổi một chút phương thức Gọi?



1
Nếu bạn muốn biết cách thức hoạt động của việc đánh chặn trong C #, hãy xem Tiny Interceptor . Mẫu này chạy mà không có bất kỳ phụ thuộc. Lưu ý rằng nếu bạn muốn sử dụng AOP trong các dự án trong thế giới thực, đừng cố gắng tự thực hiện. Sử dụng các thư viện như PostSharp.
Jalal

Tôi đã thực hiện ghi nhật ký một cuộc gọi phương thức (trước và sau) bằng thư viện MethodDecorator.Fody. Xin hãy xem thư viện tại github.com/Fody/MethodDecorator
Dilhan Jayathilake

Câu trả lời:


69

C # không phải là ngôn ngữ định hướng AOP. Nó có một số tính năng AOP và bạn có thể mô phỏng một số tính năng khác nhưng làm AOP bằng C # thì đau đớn.

Tôi tìm kiếm các cách để làm chính xác những gì bạn muốn làm và tôi thấy không có cách nào dễ dàng để làm điều đó.

Theo tôi hiểu, đây là những gì bạn muốn làm:

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

và để làm được điều đó, bạn có hai lựa chọn chính

  1. Kế thừa lớp của bạn từ MarshalByRefObject hoặc ContextBoundObject và xác định một thuộc tính kế thừa từ IMessageSink. Bài viết này có một ví dụ tốt. Bạn phải cân nhắc hàng tháng rằng việc sử dụng MarshalByRefObject hiệu suất sẽ đi xuống như địa ngục, và ý tôi là, tôi đang nói về hiệu suất 10 lần bị mất nên hãy suy nghĩ cẩn thận trước khi thử.

  2. Tùy chọn khác là tiêm mã trực tiếp. Trong thời gian chạy, có nghĩa là bạn sẽ phải sử dụng sự phản chiếu để "đọc" mọi lớp, nhận các thuộc tính của nó và thực hiện cuộc gọi thích hợp (và đối với vấn đề đó tôi nghĩ rằng bạn không thể sử dụng phương thức Reflection.Emit như tôi nghĩ Reflection.Emit sẽ 't cho phép bạn chèn mã mới bên trong một phương thức đã có sẵn). Tại thời điểm thiết kế, điều này có nghĩa là tạo ra một phần mở rộng cho trình biên dịch CLR mà tôi thực sự không biết gì về cách nó được thực hiện.

Tùy chọn cuối cùng là sử dụng khung IoC . Có thể đó không phải là giải pháp hoàn hảo vì hầu hết các khung IoC hoạt động bằng cách xác định các điểm vào cho phép các phương thức được nối nhưng tùy thuộc vào những gì bạn muốn đạt được, đó có thể là một sự ước lượng công bằng.


62
Nói cách khác, 'ouch'
johnc

2
Tôi nên chỉ ra rằng nếu bạn có các hàm hạng nhất, thì một hàm có thể được xử lý giống như bất kỳ biến nào khác và bạn có thể có một "phương thức hook" thực hiện những gì nó muốn.
RCIX

3
Một cách khác thứ ba là tạo ra một sự thừa kế dựa trên các proxy trên thời gian chạy bằng cách sử dụng Reflection.Emit. Đây là cách tiếp cận được Spring.NET chọn . Tuy nhiên, điều này sẽ yêu cầu các phương thức ảo trên Tracedvà không thực sự phù hợp để sử dụng mà không có một số loại IOC container, vì vậy tôi hiểu tại sao tùy chọn này không có trong danh sách của bạn.
Marijn

2
tùy chọn thứ hai của bạn về cơ bản là "Viết các phần của khung AOP mà bạn cần bằng tay", điều này sẽ dẫn đến kết quả là "Đợi đã, có lẽ tôi nên sử dụng tùy chọn bên thứ 3 được tạo riêng để giải quyết vấn đề tôi gặp thay vì không đi xuống -inveted-here-road "
Rune FS

2
@jorge Bạn có thể cung cấp một số ví dụ / liên kết để đạt được điều này bằng cách sử dụng phụ thuộc vào nội dung tiêm / phụ thuộc IoC như nInject
Charanraj Golla

48

Cách đơn giản nhất để đạt được điều đó có lẽ là sử dụng PostSharp . Nó tiêm mã bên trong các phương thức của bạn dựa trên các thuộc tính mà bạn áp dụng cho nó. Nó cho phép bạn làm chính xác những gì bạn muốn.

Một tùy chọn khác là sử dụng API lược tả để tiêm mã bên trong phương thức, nhưng điều đó thực sự khó.


3
bạn cũng có thể tiêm thứ với ICorDebug nhưng đó là siêu ác
Sam Saffron

9

Nếu bạn viết một lớp - gọi nó là Truy tìm - thực hiện giao diện IDis Dùng một lần, bạn có thể bao bọc tất cả các thân phương thức trong một

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

Trong lớp Truy tìm, bạn có thể xử lý logic của các dấu vết trong phương thức xây dựng / Loại bỏ, tương ứng, trong lớp Truy tìm để theo dõi việc nhập và thoát các phương thức. Như vậy mà:

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

có vẻ như rất nhiều nỗ lực
LeRoi

3
Điều này không có gì để làm với việc trả lời câu hỏi.
Độ trễ

9

Bạn có thể đạt được nó với tính năng Đánh chặn của một container DI như Castle Windsor . Thật vậy, có thể cấu hình container theo cách mà mọi lớp có phương thức được trang trí bởi một thuộc tính cụ thể sẽ bị chặn.

Về điểm số 3, OP đã yêu cầu một giải pháp không có khung AOP. Tôi đã giả sử trong câu trả lời sau đây rằng những gì nên tránh là Aspect, JointPoint, PointCut, v.v. Theo tài liệu Đánh chặn từ CastleWindsor , không ai trong số họ được yêu cầu phải hoàn thành những gì được yêu cầu.

Định cấu hình đăng ký chung của một Thiết bị chặn, dựa trên sự hiện diện của một thuộc tính:

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

Thêm IContributionComponentModelCon dựng vào container

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

Và bạn có thể làm bất cứ điều gì bạn muốn trong chính thiết bị đánh chặn

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

Thêm thuộc tính đăng nhập vào phương thức của bạn để đăng nhập

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

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

Lưu ý rằng một số xử lý thuộc tính sẽ được yêu cầu nếu chỉ một số phương thức của một lớp cần phải bị chặn. Theo mặc định, tất cả các phương thức công khai sẽ bị chặn.


5

Nếu bạn muốn theo dõi các phương thức của mình mà không giới hạn (không thích ứng mã, không có AOP Framework, không có mã trùng lặp), hãy để tôi nói với bạn, bạn cần một số phép thuật ...

Nghiêm túc mà nói, tôi đã giải quyết nó để triển khai Khung AOP làm việc trong thời gian chạy.

Bạn có thể tìm thấy ở đây: NConcern .NET AOP Framework

Tôi quyết định tạo Khung AOP này để đáp ứng nhu cầu này. nó là một thư viện đơn giản rất nhẹ Bạn có thể xem một ví dụ về logger trong trang chủ.

Nếu bạn không muốn sử dụng hội đồng bên thứ 3, bạn có thể duyệt nguồn mã (nguồn mở) và sao chép cả hai tệp Aspect.Directory.csAspect.Directory.Entry.cs để điều chỉnh theo ý muốn của bạn. Các lớp luận án cho phép thay thế các phương thức của bạn trong thời gian chạy. Tôi sẽ chỉ yêu cầu bạn tôn trọng giấy phép.

Tôi hy vọng bạn sẽ tìm thấy những gì bạn cần hoặc thuyết phục bạn cuối cùng sử dụng Khung AOP.



4

Tôi đã tìm thấy một cách khác có thể dễ dàng hơn ...

Khai báo một phương thức 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;
        }
    }

Sau đó tôi xác định phương thức của mình như vậy

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

Bây giờ tôi có thể kiểm tra trong thời gian chạy mà không cần tiêm phụ thuộc ...

Không có vấn đề nào trong trang web :)

Hy vọng rằng bạn sẽ đồng ý rằng đây là trọng số ít hơn sau đó là Khung AOP hoặc xuất phát từ MarshalByRefObject hoặc sử dụng các lớp từ xa hoặc proxy.


4

Trước tiên, bạn phải sửa đổi lớp của mình để thực hiện một giao diện (thay vì triển khai MarshalByRefObject).

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

Tiếp theo, bạn cần một đối tượng trình bao bọc chung dựa trên RealProxy để trang trí bất kỳ giao diện nào để cho phép chặn bất kỳ cuộc gọi nào đến đối tượng được trang trí.

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

Bây giờ chúng tôi đã sẵn sàng chặn các cuộc gọi đến Phương thức 1 và Phương thức 2 của ITraced

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

2

Bạn có thể sử dụng CInject khung nguồn mở trên CodePlex. Bạn có thể viết mã tối thiểu để tạo một Vòi phun và khiến nó nhanh chóng chặn bất kỳ mã nào bằng CInject. Thêm vào đó, vì đây là Nguồn mở, bạn cũng có thể mở rộng điều này.

Hoặc bạn có thể làm theo các bước được đề cập trong bài viết này về Cuộc gọi phương thức chặn bằng cách sử dụng IL và tạo công cụ chặn của riêng bạn bằng các lớp Reflection.Emit trong C #.


1

Tôi không biết một giải pháp nhưng cách tiếp cận của tôi sẽ như sau.

Trang trí lớp (hoặc phương thức của nó) với một thuộc tính tùy chỉnh. Ở một nơi khác trong chương trình, hãy để một hàm khởi tạo phản ánh tất cả các loại, đọc các phương thức được trang trí với các thuộc tính và đưa một số mã IL vào phương thức. Nó thực sự có thể thực tế hơn để thay thế phương thức bằng một sơ khai gọi LogStart, phương thức thực tế và sau đó LogEnd. Ngoài ra, tôi không biết liệu bạn có thể thay đổi phương pháp bằng cách sử dụng sự phản chiếu để có thể thực tế hơn để thay thế toàn bộ loại.


1

Bạn có khả năng có thể sử dụng Mẫu trang trí GOF và 'trang trí' tất cả các lớp cần theo dõi.

Có lẽ nó chỉ thực sự thiết thực với bộ chứa IOC (nhưng như con trỏ ra trước đó, bạn có thể muốn xem xét việc chặn phương thức nếu bạn sẽ đi xuống đường dẫn IOC).



1

AOP là điều bắt buộc để triển khai mã sạch, tuy nhiên nếu bạn muốn bao quanh một khối trong C #, các phương thức chung có cách sử dụng tương đối dễ dàng hơn. (với ý nghĩa intelli và mã được gõ mạnh) Chắc chắn, nó KHÔNG thể thay thế cho AOP.

Mặc dù PostSHarp có một số vấn đề nhỏ (tôi không cảm thấy tự tin khi sử dụng khi sản xuất), nhưng đây là một thứ tốt.

Lớp bao bọc chung,

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

cách sử dụng có thể như thế này (tất nhiên với ý nghĩa intelli)

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

Tôi đồng ý rằng Postsharp là một thư viện AOP và sẽ xử lý việc chặn, tuy nhiên ví dụ của bạn không minh họa gì cả. Đừng nhầm lẫn IoC với việc đánh chặn. Chúng không giống nhau.
Độ trễ

-1
  1. Viết thư viện AOP của riêng bạn.
  2. Sử dụng sự phản chiếu để tạo proxy đăng nhập qua các phiên bản của bạn (không chắc bạn có thể làm điều đó mà không thay đổi một phần mã hiện tại của bạn không).
  3. Viết lại cụm và nhập mã đăng nhập của bạn (về cơ bản giống như 1).
  4. Lưu trữ CLR và thêm ghi nhật ký ở cấp độ này (tôi nghĩ rằng đây là giải pháp khó thực hiện nhất, không chắc bạn có móc nối cần thiết trong CLR hay không).

-3

Điều tốt nhất bạn có thể làm trước khi C # 6 với 'nameof' được phát hành là sử dụng Biểu thức StackTrace và linq chậm.

Ví dụ cho phương pháp như vậy

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

        //do your stuff
    }

Dòng này có thể được tạo ra trong tệp nhật ký của bạn

Method 'MyMethod' parameters age: 20 name: Mike

Đây là cách thực hiệ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
    }

Điều này không thành công yêu cầu được xác định trong điểm đạn thứ hai của mình.
Ted Bigham
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.