Querschnittsfunktionen mit der Enterprise Library durch Interception automatisieren

24. August 2012

In jedem Projekt gibt es sogenannte Common Tasks, also Querschnittsfunktionalitäten, die häufig und oft auch von mehreren Schichten der Anwendung verwendet werden. Beispielhaft sei an dieser Stelle Logging genannt. Logging ist eine Funktionalität, die aber nichts mit dem eigentlichen Code beziehungsweise dessen Aufgabe zu tun hat und die Methoden erstens aufbläht und zweitens oftmals durch Copy und Paste auch fehlerhaft implementiert wird. Aber es gibt Abhilfe: Aspekt Orientierte Programmierung (AOP). Im folgenden werde ich auf die Möglichkeiten von Unity im Bezug auf AOP eingehen und unter anderem Logging und andere Ansätze zeigen.

Interception ist ein Bestandteil von Unity und damit auch der Enterprise Library. Die Enterprise Library ist kostenlos zum Download erhältlich. Der aspektorientierte Ansatz wird bei Interception im Gegensatz zu anderen Frameworks (bspw. Postsharp) über Proxy Objekte gelöst und nicht über den Compile Vorgang. Dies bringt einige Einschränkungen mit sich, doch dazu später mehr. Die folgende Grafik zeigt den Interception Mechnismus wie ich finde sehr anschaulich:

image

Quelle: http://msdn.microsoft.com/en-us/library/ff660861(v=pandp.20).aspx

Der Client (1) ist in diesem Fall ihre Applikation. Über die Unity Api (2) lässt man sich seine Klasseninstanz erzeugen. Hier wird auch sichtbar, dass man keine direkte Instanz der angeforderten Klasse erhält, sondern ein Proxy Objekt (3). Dieses Proxy Objekt gibt alle Aufrufe an die implementierten Behaviors (4) weiter (es können mehrere Behaviors auf eine Instanz angewendet werden deswegen auch Behavior Pipeline) die Behaviors wiederum erfüllen ihre Aufgabe und führen am Zielobjekt (5) dann die eigentliche Funktionalität (Methode/Property) aus.

Vorweg sei noch gesagt, dass es nicht notwendig ist den Dependency Injection Mechnismus von Unity zu verwenden, um Interception einzusetzen. Die Interception Funktionalität enthält eine eigene API. Nichts desto trotz ist es selbstverständlich möglich Interception in Verbindung mit dem Dependency Application Block zu nutzen. Ich werde in dieser Artikelserie beide Ansätze aufzeigen.

Generell gibt es bei Interception drei Möglichkeiten der Implementierung:

1. Transparent Proxy Interception

Hierbei müssen die Klassen, die um Funktionalität erweitert werden sollen, von der Basisklasse MarshalByRef abgeleitet werden. Ich persönlich bin kein Fan dieses Ansatzes, da dieser die Code Architektur und die verwendbaren Klassen von vornherein einschränkt. Ein weiterer Nachteil ist die schlechtere Performance im Vergleich mit den anderen Interception Ansätzen.

2. Interface Interception

Zur Verwendung der Interface Interception werden wie der Name schon sagt Interfaces verwendet. Außerdem muss jede Methode, auf die Interception angewendet werden soll, muss zwingend im Interface veröffentlicht sein. Dies ist mein bevorzugtes Verfahren.

3. Virtual Method Interception

Hierbei wird auf alle virtuellen Methoden der Klasse Interception angewendet.

Ich werde im Folgenden nur den Ansatz der Interface Interception verfolgen, da dieser in meinen Augen der praxisrelevanteste ist.

Die Klasse Dummy

Für die Code-Beispiele habe ich kurzerhand eine einfache Klasse mit einem Interface entworfen, diese wird im Verlauf des Artikels noch häufiger verwendet:

public class Dummy : IDummy
    {
        public int AddiereEtwas(int a, int b)
        {
            return a + b;
        }

        private string _Name;
        
        public string Name
        {
            get
            {
                if(string.IsNullOrEmpty(_Name))
                    return "Ich heisse Dummy";
                return _Name;
            }
            set { _Name = value; }
        }

        public void TueEtwasAnderes(string a, bool b)
        {
            VerheirateStringMitBool(a, b);
        }

        private string VerheirateStringMitBool(string a, bool b)
        {
            return a + b.ToString();
        }

        public static bool WurdeEineZahlKleinerZehnUebergeben(int zahl)
        {
            return zahl < 10;
        }
    }

Diese Klasse tut natürlich nichts sinnvolles und soll lediglich die Möglichkeiten und auch Grenzen von Interception aufzeigen.

Für diese Klasse benötigen wir jetzt noch das passende Interface:

    interface IDummy
    {
        int AddiereEtwas(int a, int b);

        void TueEtwasAnderes(string a, bool b);

        string Name { get; set; }
        
    }

Spätestens an dieser Stelle wird klar, dass durch den Interface Mechanismus an sich bereits die natürlichen Einschränkungen bezüglich Interfaces existieren (Public Methoden und Properties, nichts statisches …).

Nun haben wir bereits eine Klasse und ein Interface. Der Artikel hat ja in der Überschrift versprochen “common Tasks” zu automatisieren. Hierfür benötigen wir noch ein oder mehrere Behaviors. Das Behavior implementiert diese Common Aufgabe.

Behaviors

Mit Behaviors lassen sich Querschnittsfunktionen umsetzen die dann mit AOP-Mechanismen in bestehende Funktionen eingewebt werden. Der Aufruf einer Methode wird vom Behavior gekapselt. Damit ist es möglich Aktionen vor und nach dem Methodenaufruf durchzuführen. Hier ist (m)eine sehr simple Implementierung für Logging:

public class MyLoggingBehaviour : IInterceptionBehavior
    {
        public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
        {
            Console.WriteLine("MyLoggingBehaviour Start");
            Console.WriteLine("Method: " + input.MethodBase);

            for (int i = 0; i < input.Inputs.Count;i++ )
            {
               ParameterInfo param =  input.Inputs.GetParameterInfo(i);
               Console.WriteLine(string.Format("Type: {0} Name: {1} Wert {2} before calling", param.ParameterType, param.Name, input.Inputs[i]));
            }

            IMethodReturn result = getNext()(input,getNext);

            Console.WriteLine("Result was: " + result.ReturnValue);

            Console.WriteLine("MyLoggingBehaviour End");
            
            return result;
        }

        public IEnumerable<Type> GetRequiredInterfaces()
        {
            return Type.EmptyTypes;
        }

        public bool WillExecute
        {
            get
            {
                return true;
                
            }
        }
    }

Der eigentliche Methodenaufruf passiert bei

IMethodReturn result = getNext()(input,getNext); 

rund um diesen Aufruf sollte man die gewünschte Funktionalität implementieren. Wie im Code oben zu sehen ist, ist getNext ein Delegat, dieser gibt wiederum einen Delegat zurück. Damit wird die komplette Pipeline aller Behaviors (ja, es kann mehrere geben) durchlaufen und schlussendlich die eigentliche Methode aufgerufen. Besonders schön an Behaviors ist, das wie oben zu sehen ist Zugriff auf alle Parameter und deren Werte, sowie auf den Rückgabewert der eigentlichen Methode möglich ist.

So wir haben eine Klasse, ein Interface und das Behavior. Jetzt müssen wir nur noch alles zusammenstecken.

Implementieren von Interface Interception mit der Standalone API

Für die Standalone API ohne Unity benötigt man die Microsoft.Practices.Unity.Interception.dll. Die DLL kommt bei der Enterprise Library Installation mit und kann über Add Reference dem Projekt hinzugefügt werden.

Die statische Intercept Klasse bringt alles mit, was wir benötigen.

Hier die Definition der Methode ThroughProxy, mit der wir Instanzen von Klassen erzeugen können auf die wir Interception anwenden wollen:

 public static T ThroughProxy<T>(T target, IInstanceInterceptor interceptor, IEnumerable<IInterceptionBehavior> interceptionBehaviors) where T : class


Angewendet auf unsere Dummy Klasse sieht die Implementierung wie folgt aus:

var dummyInstance = Intercept.ThroughProxy<IDummy>(new Dummy(),new InterfaceInterceptor(), new IInterceptionBehavior[]
                                                                                          {
                                                                                              new MyLoggingBehaviour(),
                                                                                          });

Mit new Dummy() übergeben wir eine neue Instanz unserer IDummy Implementierung. Der Befehl new InterfaceInterceptor() übergibt den jeweiligen Interceptor und als letzten Parameter können wir beliebig viele Behaviors übergeben.

Zur weiteren Veranschaulichung habe ich ein kleines Programm geschrieben, das eine Dummy via Interception Instanz erzeugt und das Logging Behavior zuweist. Anschließend wird wir Methode AddiereEtwas aufgerufen.

 

    class Program
    {
        static void Main(string[] args)
        {
            var dummyInstance = Intercept.ThroughProxy<IDummy>(new Dummy(),new InterfaceInterceptor(), new IInterceptionBehavior[]
                                                                                          {
                                                                                              new MyLoggingBehaviour(),
                                                                                          });

            dummyInstance.AddiereEtwas(1, 2);
            Console.ReadKey();
        }
    }

Wenn man das Programm ausführt, so wird auf der Console folgendes ausgegeben:

MyLoggingBehaviour Start
Method: Int32 AddiereEtwas(Int32, Int32)
Type: System.Int32 Name: a Wert 1 before calling
Type: System.Int32 Name: b Wert 2 before calling
Result was: 3
MyLoggingBehaviour End

Voilá!

Fazit:

In diesem Artikel habe ich die grundlegenden Interception Mechanismen und eine einfache Implementierung ohne Dependency Injection aufgezeigt.

In den folgenden Artikeln dieser Serie werde ich verschiedene Interfaces verwenden, um unterschiedliche Methoden einer Klasse mit unterschiedlichen Behaviors zu behandeln. Dabei werde ich auch die Implementierung mit Unity Dependency Injection aufzeigen.

Quellenangaben und nützliche Informationen:

Hands-On Labs for Microsoft Enterprise Library 5.0

Unity Interception Techniques

MSDN-Magazin: Innovation – Abfangfunktionen in Unity 2.0