Laden...

NotificationService als Erweiterung zu Rainbirds ApplikationServer Beispiel

Erstellt von Christoph Burgdorf vor 14 Jahren Letzter Beitrag vor 14 Jahren 3.100 Views
Christoph Burgdorf Themenstarter:in
365 Beiträge seit 2004
vor 14 Jahren
NotificationService als Erweiterung zu Rainbirds ApplikationServer Beispiel

Ich möchte euch hier einen Ansatz vorstellen, wie man die Komplexität von Remoting Events elegant verbergen und stattdessen auf eine sehr allgemeine und einfache Lösung zurückgreifen kann.

Bevor ich meine Lösung genauer vorstelle möchte ich kurz aufzeigen was „das Problem“ mit Remoting Events ist und warum es meiner Meinung nach Sinn macht den hier beschriebenen Weg zu gehen.

Die Interims Klasse

Wenn man auf klassische Art und Weise versucht ein Event serverseitig zu feuern und clientseitig zu empfangen, stößt man zu allerst auf eine System.IO.FileNotFoundException. Das hat den Hintergrund, dass der Server zugang zu den Metadaten der registrierten Methode haben muss. Da wir aber eine Methode registrieren möchten, die in der Client Assembly liegt und auf dem Server nicht bekannt ist, kommt es zu der genannten Exception. Die Lösung hierfür ist hinlänglich bekannt. Man benötigt eine Interimsklasse, die sowohl auf dem Server als auch auf dem Client bekannt ist. Die Interimsklasse stellt sowohl eine Methode bereit, die im Serverevent registriert wird als auch ein Event, dass wiederum vom Client abonniert werden kann.

Die verschwundene Event Subscription

Haben wir das erste Problem hinter uns gelassen, begegnen wir einer neuen Herausforderung. Ein Objekt, dass von der Remoting Infrastruktur als SingleCall Objekt publiziert wird, zeichnet sich dadurch aus, dass pro Methodenzugriff eine neue Instanz erstellt wird, die sofort nachdem die Methode abgearbeitet wurde, wieder verfällt. Was bedeutet das für unsere Events?

Nehmen wir an wir haben einen ProductService über den wir neue Produkte erstellen und bestehende bearbeiten können. Unsere Service stellt das Event „ProductCreated“ bereit, welches von einer Client Methode abonniert werden kann.

Unsere Clientklasse sieht dann in etwa so aus:


public class MyClient
{
    IProductService _productServiceProxy = null;
	
    public MyClient()
    {
        _productServiceProxy = ServiceFactory<IProductService>.CreateProxy();
        _productServiceProxy.ProductCreated += new EventHandler(ProductCreated);
    }

    public void CreateProduct()
    {
        _productServiceProxy.CreateProduct(new Product());
    }

    private void ProductCreated(object sender, EventArgs e)
    {
        //Do Something
    }
}

Da wie bereits erwähnt pro Methodenzugriff eine neue Instanz erstellt wird, ist unsere EventSubcription nicht nachhaltig. Die Instanz, die die EventSubscription hält lebt ja nur für einen kurzen Moment. Insofern wird die Methode ProductCreated im Beispielcode nie ausgeführt. Die Instanz, die den Methodenzugriff CreateProduct() bedient hat, hält nämlich gar keine EventSubscription mehr im Speicher.

Wie können wir diesem Problem begegnen? Wir könnten unseren ProductService als Singleton veröffentlichen. Das will man aber in der Praxis nicht. Damit unsere Anwendung gut skaliert sollte die Mittelschicht möglichst statuslos implementiert werden. Zudem müssten wir uns plötzlich mit weiteren möglichen Problemen auseinandersetzen. Ein als Singleton veröffentlichtes Objekt muss natürlich threadsicher implementiert werden.
Unser Problem ist also, dass wir die Mittelschicht einerseits statuslos implementieren möchten aber in einem statuslosen Objekt keine EventSubscriptions halten können.

Lösung über einen NotificationService

Helfen kann uns hierbei ein NotificationService dessen einzige Aufgabe darin besteht Events und dessen Subscriber zu verwalten und Events zu feuern. Die eigentlichen Dienste (z.B. ProductService) bleiben auf diese Art und Weise frei von jeglicher Eventlogik und können weiterhin als SingleCall publiziert werden. Am Ende erhalten wir auf diese Weise ein allgemeingültiges Konstrukt, dass die Verwendung von RemotingEvents genauso einfach (oder sogar einfacher) macht als lokale Events.

Ich habe mich in meinem Projekt stark an dem von Rainbird hier im Forum veröffentlichten Anwendungsserver orientiert, weshalb man die hier beschriebene Lösung ohne großen Aufwand als Erweiterung in seinen Anwendungsserver einarbeiten kann.

Am Ende kann unser oben erwähnter ProductService das Event „ProductCreated“ mit diesem simplen Einzeiler feuern:


ApplicationServer.RaiseEvent(„ProductCreated“, EventArgs.Empty);

Unser Client von oben sähe entsprechend so aus:


public class MyClient
{
    IProductService _productServiceProxy = null;
	
    public MyClient()
    {
        _productServiceProxy = ServiceFactory<IProductService>.CreateProxy();
        ApplicationServer.SubscribeEvent(“ProductCreated”, ProductCreated);
    }

    public void CreateProduct()
    {
        _productServiceProxy.CreateProduct(new Product());
    }

    private void ProductCreated(object sender, EventArgs e)
    {
	//Do Something
    }
}

Man sieht: Die Verwendung ist denkbar einfach! Der Service ist konfigurationslos. Das bedeutet, dass ein Event automatisch im Service erstellt wird, sobald ein Client es abonniert. Die Kehrseite der Medaille ist natürlich, dass die Eventnamen untypsiert vorliegen. Der Service stellt allerdings Methoden zur Überprüfung der vorliegenden Events und Subscriptions bereit um einen bei einer etwaigen Fehleranalyse zu unterstützen. Die fehlende Typisierung wiegt für mich jedoch weniger schwer als der Aufwand RemotingEvents ohne einheitliche Komponente zu verwenden. Dies wird noch deutlicher, wenn wir uns im Detail ansehen, wie der Service im Hintergrund arbeitet.

Es folgt nun eine Schritt für Schritt Anleitung um den Anwendungsserver von Rainbird um diesen NotificationService zu erweitern.

1. Erweiterung der Server API um das INotificationService Interface

Dieses Interface muss in Rainbird.Appserver.API zu den anderen Interfaces der allgemeinen Serverdienste (ILockingService, ISecurityService)


public interface INotificationService
{
	void Subscribe(string eventName, EventHandler handler);
		
	void RaiseEvent(string eventName, EventArgs e);
		
	Dictionary<string, int> GetInfo();
}

2. Erweiterung der Server API um die EventWrapper Klasse

Diese Klasse muss ebenfalls in die API. Die eigentliche Callback Methode (vom Client) wird in dieser Klasse verpackt. Diese Klasse ist bedingt, dadurch das sie in der API Assembly liegt sowohl für Server als auch Client zugänglich.


public class EventWrapper : MarshalByRefObject
{     
    public event EventHandler WrapperServerEvent;
            
    public EventWrapper(EventHandler handler)
    {
        WrapperServerEvent += new EventHandler(handler);
    }

    public void WrapperServerEventHandler(object sender, EventArgs e)
    {
        EventHandler wrapperServerEvent = WrapperServerEvent;
                  
        if (wrapperServerEvent != null)
            wrapperServerEvent(this, e);                   
    }
            
    public override object InitializeLifetimeService()
    {
        return null;
    }
} 

3. Erweiterung der statischen ApplicationServer Klasse

Folgendes muss in die statische ApplicationServer Klasse. Das macht die Verwendung am Ende so einfach wie die Verwendung der Datensatzsperrungen.


public static void RaiseEvent(string eventName, EventArgs e)
{
    INotificationService notificationServiceProxy = ServiceFactory<INotificationService>.CreateProxy();
    notificationServiceProxy.RaiseEvent(eventName, e);
}
		
public static EventHandler SubscribeEvent(string eventName, EventHandler handler)
{
    EventWrapper wrapper = new EventWrapper(handler);
    INotificationService notificationServiceProxy = ServiceFactory<INotificationService>.CreateProxy();
    notificationServiceProxy.Subscribe(eventName, wrapper.WrapperServerEventHandler);
			
    return wrapper.WrapperServerEventHandler;
}

4. Implementation des NotificationService

Diese Klasse muss in die Serverassembly (wo auch LockingService etc. liegt). Es handelt sich um den eigentlichen NotificationService den ich auch manuell über RemotingServices.Marshal direkt veröffentliche. Hier passiert schon ein bisschen mehr, weshalb ich das mal ein wenig genauer erklären werde.

Zunächst einmal benutze ich ein Dictionary<string, EventHandler> um die Abos zu speichern. Wenn also beispielsweise zum aller ersten Mal das Event „ProductCreated“ über Subscribe() abonniert wird, erstelle ich einen Eintrag im Dictionary mit der Bezeichnung als Key („productchanged“) und dem übergebenen EventHandler als Value. Wird dieses Event nun noch weitere male aboniert füge ich alle weiteren EventHandler diesem ersten EventHandler im Dictionary hinzu.

Wird eine Subscription mit einem anderen EventNamen eingetragen, wird ein weitere Eintrag im Dictionary erstellt usw. So ergibt sich also die Situation, dass wir pro EventTyp einen Dictionary Eintrag mit n Callback Methoden haben.

Beim Feuern des Events ist außerdem darauf zu achten, dass man „tote Clients“ wieder aus der Invocationlist entfernt. Andernfalls wird der Server auf Dauer immer langsamer. Nun ist es aber so, dass ich die Events asynchron feuere und damit über den ThreadPool abwickeln lasse (BeginInvoke). Das hat sich für mich in der Praxis besser bewährt. Dadurch bekommt man aber von der Exception nichts mit und tote Clients können nicht entfernt werden. Das habe ich umgangen indem ich das eigentliche Delegat nochmals in ein InnerDelegate gewrapped habe. Ich feuere nun das InnerDelegate asynchron und das eigentliche Delegate synchron, sodass ich auf die Exception reagieren und die EventSubscription wieder entfernen kann


public class NotificationService : MarshalByRefObject, INotificationService
{
    private delegate void InnerDelegate(string eventName, EventArgs e, EventHandler eventDelegate);
    private static NotificationService _singleton = null;
    private Dictionary<string, EventHandler> _eventList;
    private object _thisLock = new Object();
         
    private NotificationService()
    {
        _eventList = new Dictionary<string, EventHandler>();
    }
		
    public void Subscribe(string eventName, EventHandler handler)
    {
        lock(_thisLock)
	{							
            eventName = eventName.ToLower();
				
            if (_eventList.ContainsKey(eventName))
            {
                _eventList[eventName] += handler;
            }
            else
            {
                _eventList.Add(eventName, handler);
            }
        }
    }
			
    public static NotificationService Instance
    {
        get
        {
            if (_singleton == null)
                _singleton = new NotificationService();
				
            return _singleton;
        }
    }
		
    public override object InitializeLifetimeService()
    {
        return null;
    }
		
    public void RaiseEvent(string eventName, EventArgs e)
    {			
        eventName = eventName.ToLower();
        OnServerEvent(eventName, e);
    }
		
    public Dictionary<string, int> GetInfo()
    {
        Dictionary<string, int> info = new Dictionary<string, int>();
			
        foreach (KeyValuePair<string, EventHandler> kvp in _eventList)
        {
            info.Add(kvp.Key, kvp.Value.GetInvocationList().Length);
        }
			
        return info;
    }
				
    private void OnServerEvent(string eventName, EventArgs e)
    {
        lock(_thisLock)
        {
            if (_eventList.ContainsKey(eventName) && _eventList[eventName] != null)
            {
                EventHandler eventDelegate = null;
                Delegate[] invocationList = null;
					
                try
                {
                    invocationList = _eventList[eventName].GetInvocationList();
                }
                catch(MemberAccessException ex)
                {
                    throw ex;
                }
					
                if (invocationList != null)
                {
                    foreach (Delegate del in invocationList)
                    {
                        eventDelegate = (EventHandler)del;
                        InnerDelegate innerDelegate = new InnerDelegate(BeginSend);
                        innerDelegate.BeginInvoke(eventName, e, eventDelegate, null, null);
                    }
                }
            }
        }
    }
		
    private void BeginSend(string eventName, EventArgs e, EventHandler eventDelegate)
    {
        try
        {
            eventDelegate(this, e);
        }
        catch (Exception ex)
        {
            _eventList[eventName] -= eventDelegate;
        }
    }
}

Das ist im Prinzip schon der ganze Zauber. Momentan fehlt dem Service leider noch eine Unsubscribe Methode. Für den Client sollte das Stornieren des Abos so einfach sein wie das Abonnieren. Demnach sollte man das Abo mit diesem simplen Einzeiler wieder auflösen können:


ApplicationServer.UnsubscribeEvent(“ProductCreated”, ProductCreated);

Das Kernproblem hierbei ist, das der NotificationService mit der Client Methode ProductCreated nur leider herzlich wenig anfangen kann (deshalb benötigt man ja den Wrapper). Mir schweben zwar bereits zwei Lösungen hierfür vor, jedoch habe ich bisher nicht die Zeit gefunden die Umsetzung weiter zu verfolgen.

Besonderen Dank möchte ich an dieser Stelle noch einmal an die Adresse von Rainbird richten, der mir bei der Umsetzung meiner Idee mit Rat und Tat zur Seite stand.

Über Feedback, Fragen und Anregungen zu der hier gezeigen Lösung würde ich mich sehr freuen.

Gruß

Christoph