Bagaimana cara mencegat pemanggilan metode dalam C #?

154

Untuk kelas tertentu saya ingin memiliki fungsi penelusuran yaitu saya ingin mencatat setiap pemanggilan metode (metode tanda tangan dan nilai parameter aktual) dan setiap metode keluar (hanya metode tanda tangan).

Bagaimana saya melakukan ini dengan asumsi bahwa:

  • Saya tidak ingin menggunakan perpustakaan AOP pihak ketiga mana pun untuk C #,
  • Saya tidak ingin menambahkan kode duplikat ke semua metode yang ingin saya lacak,
  • Saya tidak ingin mengubah API publik dari kelas - pengguna kelas harus dapat memanggil semua metode dengan cara yang persis sama.

Untuk membuat pertanyaan lebih konkret mari kita asumsikan ada 3 kelas:

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

Bagaimana cara memanggil Logger.LogStart dan Logger.LogEnd untuk setiap panggilan ke Method1 dan Method2 tanpa memodifikasi metode Caller.Call dan tanpa menambahkan panggilan secara eksplisit ke Traced.Method1 dan Traced.Method2 ?

Sunting: Apa yang akan menjadi solusi jika saya diizinkan sedikit mengubah metode Panggilan?

Pekerja harian
sumber
1
Jika Anda ingin tahu cara kerja intersepsi di C # lihatlah Tiny Interceptor . Sampel ini berjalan tanpa ada ketergantungan. Perhatikan bahwa jika Anda ingin menggunakan AOP dalam proyek dunia nyata, jangan coba menerapkannya sendiri. Gunakan perpustakaan seperti PostSharp.
Jalal
Saya telah menerapkan pencatatan panggilan metode (sebelum dan sesudah) menggunakan perpustakaan MethodDecorator.Fody. Silakan lihat perpustakaan di github.com/Fody/MethodDecorator
Dilhan Jayathilake

Jawaban:

69

C # bukan bahasa yang berorientasi AOP. Ini memiliki beberapa fitur AOP dan Anda dapat meniru beberapa yang lain tetapi membuat AOP dengan C # itu menyakitkan.

Saya mencari cara untuk melakukan persis apa yang ingin Anda lakukan dan saya tidak menemukan cara mudah untuk melakukannya.

Seperti yang saya pahami, inilah yang ingin Anda lakukan:

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

dan untuk melakukan itu Anda memiliki dua opsi utama

  1. Mewarisi kelas Anda dari MarshalByRefObject atau ContextBoundObject dan tentukan atribut yang diwarisi dari IMessageSink. Artikel ini memiliki contoh yang bagus. Anda harus mempertimbangkan nontheless bahwa menggunakan MarshalByRefObject kinerja akan turun seperti neraka, dan maksud saya, saya sedang berbicara tentang kinerja 10x hilang jadi pikirkan baik-baik sebelum mencoba itu.

  2. Pilihan lainnya adalah menyuntikkan kode secara langsung. Dalam runtime, artinya Anda harus menggunakan refleksi untuk "membaca" setiap kelas, mendapatkan atributnya dan menyuntikkan panggilan yang sesuai (dan dalam hal ini saya pikir Anda tidak dapat menggunakan metode Reflection. Kirim seperti yang saya pikir Reflection.Emit wouldn memungkinkan Anda memasukkan kode baru ke dalam metode yang sudah ada). Pada waktu desain ini berarti membuat ekstensi ke kompiler CLR yang saya jujur ​​tidak tahu bagaimana melakukannya.

Opsi terakhir menggunakan kerangka kerja IoC . Mungkin itu bukan solusi yang sempurna karena sebagian besar kerangka kerja IoC bekerja dengan mendefinisikan titik masuk yang memungkinkan metode untuk dihubungkan tetapi, tergantung pada apa yang ingin Anda capai, itu mungkin merupakan perkiraan yang adil.

Jorge Córdoba
sumber
62
Dengan kata lain, 'aduh'
johnc
2
Saya harus menunjukkan bahwa jika Anda memiliki fungsi kelas satu, maka suatu fungsi dapat diperlakukan sama seperti variabel lainnya dan Anda dapat memiliki "metode kait" yang melakukan apa yang diinginkannya.
RCIX
3
Alternatif ketiga adalah untuk menghasilkan proxy berbasis warisan di saat menggunakan runtime Reflection.Emit. Ini adalah pendekatan yang dipilih oleh Spring.NET . Namun, ini akan memerlukan metode virtual Traceddan tidak benar-benar cocok untuk digunakan tanpa semacam wadah IOC, jadi saya mengerti mengapa opsi ini tidak ada dalam daftar Anda.
Marijn
2
Pilihan kedua Anda pada dasarnya adalah "Tulis bagian-bagian kerangka kerja AOP yang Anda butuhkan dengan tangan" yang kemudian akan menghasilkan inferrens dari "Oh, tunggu, mungkin saya harus menggunakan opsi pihak ke-3 yang dibuat khusus untuk memecahkan masalah yang saya miliki alih-alih turun tidak -inveted-here-road "
Rune FS
2
@jorge Bisakah Anda memberikan beberapa contoh / tautan untuk mencapai hal ini menggunakan Dependency Injection / IoC famework seperti nInject
Charanraj Golla
48

Cara paling sederhana untuk mencapai itu mungkin menggunakan PostSharp . Itu menyuntikkan kode di dalam metode Anda berdasarkan atribut yang Anda terapkan padanya. Ini memungkinkan Anda untuk melakukan apa yang Anda inginkan.

Pilihan lain adalah menggunakan API profil untuk menyuntikkan kode di dalam metode, tetapi itu benar-benar hardcore.

Antoine Aubry
sumber
3
Anda juga dapat menyuntikkan barang-barang dengan ICorDebug tetapi itu sangat jahat
Sam Saffron
9

Jika Anda menulis sebuah kelas - sebut saja Tracing - yang mengimplementasikan antarmuka IDisposable, Anda bisa membungkus semua badan metode dalam sebuah

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

Di kelas Tracing Anda bisa menangani logika jejak di konstruktor / Buang metode, masing-masing, di kelas Tracing untuk melacak masuk dan keluarnya metode. Seperti yang:

    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
sumber
sepertinya banyak usaha
LeRoi
3
Ini tidak ada hubungannya dengan menjawab pertanyaan.
Latency
9

Anda bisa mencapainya dengan fitur Intersepsi wadah DI seperti Castle Windsor . Memang, adalah mungkin untuk mengkonfigurasi wadah sedemikian rupa sehingga setiap kelas yang memiliki metode yang didekorasi oleh atribut tertentu akan dicegat.

Mengenai poin # 3, OP meminta solusi tanpa kerangka kerja AOP. Saya berasumsi dalam jawaban berikut bahwa apa yang harus dihindari adalah Aspect, JointPoint, PointCut, dll. Menurut dokumentasi Interception dari CastleWindsor , tidak ada yang diminta untuk memenuhi apa yang diminta.

Konfigurasikan pendaftaran generik Interceptor, berdasarkan keberadaan atribut:

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

Tambahkan IContributeComponentModelConstruction yang dibuat ke wadah

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

Dan Anda dapat melakukan apa pun yang Anda inginkan di interceptor itu sendiri

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

Tambahkan atribut logging ke metode Anda untuk login

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

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

Perhatikan bahwa beberapa penanganan atribut akan diperlukan jika hanya beberapa metode kelas yang perlu dicegat. Secara default, semua metode publik akan dicegat.

plog17
sumber
5

Jika Anda ingin melacak setelah metode Anda tanpa batasan (tidak ada adaptasi kode, tidak ada Kerangka AOP, tidak ada kode duplikat), izinkan saya memberi tahu Anda, Anda memerlukan beberapa keajaiban ...

Serius, saya memutuskan untuk mengimplementasikan Kerangka Kerja AOP yang bekerja saat runtime.

Anda dapat menemukan di sini: NConcern .NET AOP Framework

Saya memutuskan untuk membuat Kerangka AOP ini untuk merespons kebutuhan semacam ini. itu adalah perpustakaan sederhana yang sangat ringan. Anda dapat melihat contoh logger di beranda.

Jika Anda tidak ingin menggunakan rakitan pihak ke-3, Anda dapat menelusuri sumber kode (sumber terbuka) dan menyalin kedua file Aspect.Directory.cs dan Aspect.Directory.Entry.cs untuk diadaptasi sesuai keinginan Anda. Kelas-kelas tesis memungkinkan untuk mengganti metode Anda saat runtime. Saya hanya akan meminta Anda untuk menghormati lisensi.

Saya harap Anda akan menemukan apa yang Anda butuhkan atau meyakinkan Anda untuk akhirnya menggunakan Kerangka Kerja AOP.

Tony THONG
sumber
4

Saya telah menemukan cara berbeda yang mungkin lebih mudah ...

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

Saya kemudian mendefinisikan metode saya seperti itu

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

Sekarang saya dapat memiliki cek pada saat dijalankan tanpa injeksi ketergantungan ...

Tidak ada gotcha di situs :)

Semoga Anda akan setuju bahwa ini lebih ringan daripada Kerangka Kerja AOP atau berasal dari MarshalByRefObject atau menggunakan kelas remoting atau proxy.

Jay
sumber
4

Pertama, Anda harus memodifikasi kelas Anda untuk mengimplementasikan antarmuka (daripada mengimplementasikan MarshalByRefObject).

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

Selanjutnya Anda memerlukan objek pembungkus generik berdasarkan RealProxy untuk menghias antarmuka apa pun untuk memungkinkan mencegat panggilan apa pun ke objek yang didekorasi.

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

Sekarang kita siap untuk mencegat panggilan ke Method1 dan Method2 dari 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
sumber
2

Anda dapat menggunakan framework open source CInject pada CodePlex. Anda dapat menulis kode minimal untuk membuat Injector dan mendapatkannya untuk mencegat kode apa pun dengan cepat dengan CInject. Plus, karena ini adalah Open Source, Anda dapat memperpanjang ini juga.

Atau Anda dapat mengikuti langkah-langkah yang disebutkan pada artikel ini tentang Mencegat Metode Panggilan menggunakan IL dan membuat pencegat Anda sendiri menggunakan kelas Reflection.Emit di C #.

Puneet Ghanshani
sumber
1

Saya tidak tahu solusinya tetapi pendekatan saya adalah sebagai berikut.

Hiasi kelas (atau metodenya) dengan atribut khusus. Di tempat lain dalam program, biarkan fungsi inisialisasi mencerminkan semua jenis, baca metode yang didekorasi dengan atribut dan masukkan beberapa kode IL ke dalam metode. Mungkin sebenarnya lebih praktis untuk mengganti metode dengan sebuah rintisan yang memanggil LogStart, metode aktual dan kemudian LogEnd. Selain itu, saya tidak tahu apakah Anda dapat mengubah metode menggunakan refleksi sehingga mungkin lebih praktis untuk mengganti seluruh tipe.

Konrad Rudolph
sumber
1

Anda dapat berpotensi menggunakan Pola Dekorator GOF, dan 'mendekorasi' semua kelas yang perlu ditelusuri.

Mungkin hanya benar-benar praktis dengan wadah IOC (tetapi sebagai penunjuk keluar sebelumnya Anda mungkin ingin mempertimbangkan metode intersepsi jika Anda akan pergi ke jalur IOC).

Stacy A
sumber
1

AOP adalah suatu keharusan untuk menerapkan kode bersih, namun jika Anda ingin mengelilingi blok dalam C #, metode umum memiliki penggunaan yang relatif lebih mudah. (dengan akal dan kode yang sangat diketik) Tentu saja, itu TIDAK bisa menjadi alternatif untuk AOP.

Meskipun PostSHarp memiliki sedikit masalah kereta (saya tidak merasa percaya diri untuk digunakan di produksi), itu adalah hal yang baik.

Kelas pembungkus umum,

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

penggunaan bisa seperti ini (tentu saja dengan rasa intelli)

var exception = Wrapper.LogOnError(() =>
{
  MessageBox.Show("test");
  throw new Exception("test");
}, "Hata");
Nitro
sumber
Saya setuju bahwa Postsharp adalah pustaka AOP dan akan menangani intersepsi, namun contoh Anda tidak menggambarkan hal semacam itu. Jangan bingung antara IoC dengan intersepsi. Mereka tidak sama.
Latency
-1
  1. Tulis pustaka AOP Anda sendiri.
  2. Gunakan refleksi untuk menghasilkan proksi logging atas instance Anda (tidak yakin apakah Anda bisa melakukannya tanpa mengubah beberapa bagian dari kode Anda yang ada).
  3. Menulis ulang perakitan dan menyuntikkan kode logging Anda (pada dasarnya sama dengan 1).
  4. Host CLR dan tambahkan logging pada level ini (saya pikir ini adalah solusi tersulit untuk diterapkan, tidak yakin apakah Anda memiliki kait yang diperlukan dalam CLR).
kokos
sumber
-3

Yang terbaik yang dapat Anda lakukan sebelum C # 6 dengan 'nameof' dirilis adalah menggunakan StackTrace dan Linq Expressions lambat.

Misalnya untuk metode semacam itu

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

        //do your stuff
    }

Baris seperti itu dapat diproduksi di file log Anda

Method 'MyMethod' parameters age: 20 name: Mike

Berikut implementasinya:

    //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
    }
irriss
sumber
Ini gagal persyaratan yang ditentukan dalam poin kedua.
Ted Bigham