Archiv für die ‘WCF’ Kategorie

Die WCF mit einem eigenen Nachrichten Logger erweitern

Mittwoch, 7. Oktober 2009

Die WCF bietet einigen Diagnose Komfort an, um Problemen bei der Übertragung von Nachrichten auf die Spur zu kommen. Dies insbesondere wenn nicht nur WCF Dienste oder Clients beteiligt sind.
Das Message Logging kann benutzt werden, um den Inhalt von oder an Clients und Services aufzuzeichnen. Das Logging kann konfiguriert werden, um Nachrichten auf dem Service Level, dem Transport Level, oder fehlerhaft Nachrichten auszuzeichnen.
Das Logging ist per Default ausgeschaltet und kann so über die WCF Konfiguration eingeschaltet werden:

<system.serviceModel>
...
    <diagnostics>
        <messageLogging logEntireMessage="true"
                        logMalformedMessages="true"
                        logMessagesAtServiceLevel="true"
                        logMessagesAtTransportLevel="false"
                        maxMessagesToLog="1000">
            <filters>
                <clear/>
                <add>/*[local-name()='Envelope']/*[local-name()='Header']/*[local-name()='Action'][text()='http://tempuri.org/Operation']</add>
            </filters>
        </messageLogging>
    </diagnostics>
</system.serviceModel>

<system.diagnostics>
    <sources>
        <source name="System.ServiceModel.MessageLogging">
            <listeners>
                <add name="messages"
                     type="System.Diagnostics.XmlWriterTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
                     initializeData="messages.svclog"/>
            </listeners>
        </source>
    </sources>
    <trace autoflush="true"/>
</system.diagnostics>

Dieses Beispiel zeichnet alle Nachrichten auf, welche über die Service Methode “Operation” ein- oder ausgehen. Diese Einschränkung kann durch den Filter als XPath Ausdruck definiert werden.
Das Log wird in einer Datei gespeichert. Diese kann dann recht komfortabel mit dem Service Trace Viewer des SDKs ausgewertet werden.

Wenn nun aber die eigentliche Applikation selber ein Log führt, kann es bei der Fehlersuche recht mühsam sein, die beiden Logs zeitlich miteinander zu korrelieren. Viel praktischer wäre es doch, die WCF Nachrichten und das Applikationslog in einem einzigen Log zu haben.
Ich verwende meistens das Logging Framework log4net.
Die umfangreichen Erweiterungsmöglichkeiten der WCF machen es nun recht einfach, die WCF Nachrichten auch in das bereits vorhandene und konfigurierte log4net Log zu schreiben. Ob das Log in einer Datei oder einer Datenbank geführt wird, ist dabei völlig dem Logging Framework übrlassen.

Die sogenannten “Custom Behaviors” ermöglichen es, Code innerhalb der WCF Runtime und der Message Pipeline ausführen zu lassen. Diese Behaviors können via Code oder Konfiguration hinzugefügt werden. Ein solches Behavior ist also ideal für einen Logging Mechanismus.

Um ein Custom Behavior zu implementieren, sind drei Schritte notwendig:

  1. Erstelle ein Klasse, welche ein Inspector, Selector, Formatter oder Invoker Interface implementiert. Diese Klasse definiert dann meistens auch das eigentliche Verhalten des Behaviors. Um nun alle eingehenden Nachrichten an einen Service zu loggen, kann das Interface IDispatchBehaviour implementiert werden und in dessen Methode AfterReceiveRequest die Nachricht geloggt werden. Wenn auch die vom Service oder Client gesendeten Antworten mitgeschnitten werden sollen, kann zusätzlich das Interface IClientMessageInspector implementiert werden, und die ausgehende Nachricht in dessen AfterReceiveReply Methode geloggt werden.
    public class MessageInspector : IClientMessageInspector, IDispatchMessageInspector
    {
        public object BeforeSendRequest(ref Message request, IClientChannel channel)
        {
            Log.InfoFormat(GetType(), "MessageInspector::BeforeSendRequest(): Sending the following request{0}{1}", Environment.NewLine, request);
            return null;
        }
    
        public void AfterReceiveReply(ref Message reply, object correlationState)
        {
            Log.InfoFormat(GetType(), "MessageInspector::AfterReceiveReply(): Got the following reply{0}{1}", Environment.NewLine, reply);
        }
    
        public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
        {
            Log.InfoFormat(GetType(), "MessageInterceptor::AfterReceiveRequest(): Got the follwing request{0}{1}", Environment.NewLine, request);
            return null;
        }
    
        public void BeforeSendReply(ref Message reply, object correlationState)
        {
            Log.InfoFormat(GetType(), "MessageInterceptor::BeforeSendReply(): Sending the following reply{0}{1}", Environment.NewLine, reply);
        }
    }
    
  2. Erstelle ein weitere Klasse, welche eines der Behavior Interfaces IServiceBehavior, IEndpointBehavior, IOperationBehavior oder IContractBehavior implementiert. Die in Schritt 1 erstellt Klasse wird nun der Liste der Behaviors hinzugefügt.
    public class MessageInspectingBehavior : BehaviorExtensionElement, IEndpointBehavior
    {
        public override Type BehaviorType
        {
            get { return typeof(MessageInspectingBehavior); }
        }
    
        public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
        {
            // Ignore
        }
    
        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
            clientRuntime.MessageInspectors.Add(new MessageInspector());
        }
    
        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
            endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new MessageInspector());
        }
    
        public void Validate(ServiceEndpoint endpoint)
        {
            // Ignore
        }
    
        protected override object CreateBehavior()
        {
            return new MessageInspectingBehavior();
        }
    }
    
  3. Der Client oder Service kann nun konfiguriert werden, dieses neue Behavior zu verwenden:
    <system.serviceModel>
        <services>
            <service name="WcfDemoService.DemoService" behaviorConfiguration="WcfDemoService.DemoServiceBehavior">
            ...
                <endpoint address =""
                          binding="basicHttpBinding"
                          contract="WcfDemoService.IDemoService"
                          behaviorConfiguration="inspectorBehavior" />
            </service>
        </services>
        ...
        <extensions>
            <behaviorExtensions>
                <add name="messageInspector"
                     type="WcfLogging.MessageInspectingBehavior, WcfLogging, Version=1.0.0.0, Culture=neutral, PublicKeyToken=bb5b60893581ebcd"/>
            </behaviorExtensions>
        </extensions>
        <behaviors>
            <endpointBehaviors>
                <behavior name="interceptorBehavior">
                    <clientInterceptor />
                </behavior>
            </endpointBehaviors>
        </behaviors>
    </system.serviceModel>

Wichtig: Das Assembly, welches das Behavior enthält, muss unbedingt signiert werden!

Das vom Service erzeugte Log enthält nun die über den Service verarbeiteten Nachrichten, sowie Logeinträge der Applikation selber:

2009-10-07 11:51:05,100 [6  ] INFO  - MessageInterceptor::AfterReceiveRequest(): Got the follwing request
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
    <To s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://localhost:8731/Design_Time_Addresses/WcfDemoService/DemoService/</To>
    <Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://tempuri.org/IDemoService/GetData</Action>
  </s:Header>
  <s:Body>
    <GetData xmlns="http://tempuri.org/">
      <value>42</value>
    </GetData>
  </s:Body>
</s:Envelope>
2009-10-07 11:51:05,115 [6  ] DEBUG - DemoService::GetData(): Received 42
2009-10-07 11:51:05,116 [6  ] INFO  - MessageInterceptor::BeforeSendReply(): Sending the following reply
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
    <Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://tempuri.org/IDemoService/GetDataResponse</Action>
  </s:Header>
  <s:Body>
    <GetDataResponse xmlns="http://tempuri.org/">
      <GetDataResult>You entered: 42</GetDataResult>
    </GetDataResponse>
  </s:Body>
</s:Envelope>

Ein Beispielprojekt, welches so ein Behavior enthält, kann hier runtergeladen werden: WcfLogging.zip

Microsoft kündigt erste .NET 4.0 Features an

Mittwoch, 8. Oktober 2008

Während der Rest der (Finanz) Welt langsam zu Grunde geht, gibt Microsoft weiter Vollgas.
Normalerweise beschäftige ich mich erst im Detail mit Entwicklungstools und Technologien, wenn sie verfügbar sind. Betas, CTPs, Release Candidates und wie diese Versionen alle heissen, installiere ich schon lange nicht mehr. Die Zeit reicht einfach nicht mehr, in Projekten muss in der Regel das getan werden, was der Amerikaner lapidar “get the job done” nennt.
Ausserdem muss der interessierte .NET Entwickler erst mal die neuen Technologien des .NET 3.5 SP1 verdauen. Dies alles schliesst aber natürlich nicht aus, dass man sich über neue Technologien informiert.

So hat die am 1. Oktober von Microsoft veröffentlichte Meldung “Overview of WF 4.0, WCF 4.0, and Windows Server “Dublin” mein Interesse geweckt:

…Microsoft is enhancing both the .NET Framework and Windows Server. The company is adding significant functionality to the new version of Windows Communication Foundation (WCF) and Windows Workflow Foundation (WF) as part of the .NET Framework 4.0 release, including new messaging and REST capabilities in WCF, new workflow models, seamless integration between WF and WCF to support stateful and conversational services, and a new visual designer. The company is also introducing a set of enhanced Windows Server capabilities codenamed “Dublin” that will offer greater scalability and easier manageability, while extending Internet Information Services (IIS) to provide a standard host for applications that use workflow or communications.

Im Dokument, welches als Download zur Verfügung steht, fällt mir vor allem die Erwähnung der Dublin genannten Windows Server Technologie auf, welche den IIS erweitern wird. Ein Standard Host für WCF und WF Applikationen wäre eine feine Sache.
Natürlich kann man bereits jetzt mit überschaubarem Aufwand in IIS 6 + 7 die Workflow Services als zustandsorientierte, “durable” Dienste anbieten. Funktionen zur Versionierung, Überwachung und Monitoring und zum Deployment von Workflows oder WCF Diensten müssen jedoch noch selber implementiert werden.
Mit Dublin könnte sich der IIS langsam zum Ernst zu nehmenden Applikationsserver mausern. Dies werde ich mal im Auge behalten, mein persönliches Technologie Backlog wird also noch länger…

An der PDC2008, die Ende dieses Monats in Los Angeles stattfindet, wird eine CTP Version der drei Technologien abgegeben. Vielleicht mache ich ja da wieder einmal eine Ausnahme und installiere sie.

Ich bin ebenfalls gespannt, ob die für WF 4.0 angekündigten Erweiterungen die Workfow Foundation von einem reinen Toolset für Entwickler zu einer Technologie wandeln wird, die sich vermehrt in Produkten für Endanwender wieder findet.

Aktivierung von XOML Workflow Services

Dienstag, 7. Oktober 2008

Vor der Einführung der Workflow Services mit .NET 3.5 war das Leben eines .NET WF Entwicklers etwas einfacher. Man erzeugt einfach irgendwann eine (oder entgegen der weit verbreiteten Annahme auch mehrere) WorkflowRuntime, welche dann verschiedene Overloads der CreateWorkflow() Methode zur Verfügung stellten und so die Workflow Instanzen erzeugen können.

XOML Workflow Aktivierung mit der WorkflowRuntime

Je nachdem, ob die Workflow Definitionen in einem Assembly oder in XOML vorhanden sind, wählt man den entsprechenden Overload aus.

Für Workflow Definitionen, welche sich in einem Assembly befinden, stehen drei Methoden Overloads zur Verfügung, welche einen Typ als Parameter erwarten:

public WorkflowInstance CreateWorkflow(Type workflowType)

public WorkflowInstance CreateWorkflow(Type workflowType,
                                       Dictionary<string, Object> namedArgumentValues)

public WorkflowInstance CreateWorkflow(Type workflowType,
                                       Dictionary<string, Object> namedArgumentValues,
                                       Guid instanceId)

Wenn die Workflow Definitionen in XOML definiert sind, wählt man einen der anderen drei Overloads aus, welche einen XmlReader als Parameter erwarten. Der zweite XmlReader Parameter nimmt den Inhalt eines .rules files entgegen:

public WorkflowInstance CreateWorkflow(XmlReader workflowDefinitionReader)

public WorkflowInstance CreateWorkflow(XmlReader workflowDefinitionReader,
                                                 XmlReader rulesReader,
                                                 Dictionary<string, Object> namedArgumentValues)

public WorkflowInstance CreateWorkflow(XmlReader workflowDefinitionReader,
                                                 XmlReader rulesReader,
                                                 Dictionary<string, Object> namedArgumentValues,
                                                 Guid instanceId)

XOML basierte Workflows haben einige Vorteile, aber auch konzeptbedingt ein paar Einschränkungen:

  • Vorteil: Ein Workflow kann einfacher geändert werden. Dazu muss lediglich die XOML Datei oder wenn die Definition in einer Datenbank gehalten wird, der entsprechende Eintrag angepasst werden.
  • Vorteil: Die Regeln eines Workflows werden abenfalls in einer Datei (.rules) oder einem Record gehalten und können unabhängig vom Workflow aktualisiert werden. In Code basierten Workflows erfordert dies ein erneutes Kompilieren.
  • Vorteil: Die Workflow Definitionen müssen nicht kompiliert werden. Dies erleichtert das Management und die Verwaltung der Workflow Definitionen.
  • Nachteil: In den Workflow Definitionen können keine Properties definiert und keine Event Handler genutzt werden. Dies ist nur mit Code basierten Workflows möglich.

Nicht nur die XOML basierten Workflows enthalten meistens auch eigene Aktivitäten, welche in einem anderen Assembly bereitgestellt sind. Während der Entwicklung der Workflows wird lediglich ein Verweis auf diese Assemblies gesetzt, danach können die dort definierten Aktivitäten im Designer genutzt werden.
Wenn dieses Assembly nicht schon in der AppDomain der Workflow Runtime geladen ist, muss dies der WorkflowRuntime mitgeteilt werden. Dies erfolgt über einen TypeProvider, welcher der WorkflowRuntime als Service hinzugefügt wird:

WorkflowRuntime workflowRuntime = new WorkflowRuntime();

// Create a type provider for types referenced in the XOML
// which are not referenced by this project
TypeProvider typeProvider = new TypeProvider(workflowRuntime);

// Load referenced assemblies into the type provider
Assembly assembly = Assembly.Load("assemblyname");
typeProvider.AddAssembly(assembly);

// OR
// typeProvider.AddAssemblyReference(@"path to the assembly")

workflowRuntime.AddService(typeProvider);

Wenn dann eine Workflow Instanz mit einem der CreateWorkflow() Overloads erzeugt wird, wird die XOML basierte Workflow Definition validiert. Sollte dabei ein Problem auftreten, wird eine WorkflowValidationFailedException geworfen. Dies ist in der Dokumentation nur unter den Bemerkungen zu finden, die Exception wird nicht in der Liste der anderen Ausnahmen aufgeführt.
Die WorkflowValidationException besitzt ein Property Errors (Collection von ValidationError), welches genau Auskunft über die Probleme bei der Validierung der Workflow Definition gibt:

XmlReader workflowReader = XmlReader.Create("Workflow1.xoml");
WorkflowInstance workflowInstance = null;
try
{
    workflowInstance = workflowRuntime.CreateWorkflow(workflowReader);
    workflowInstance.Start();
}
catch (WorkflowValidationFailedException ex)
{
    foreach (ValidationError error in ex.Errors)
    {
        if (string.IsNullOrEmpty(error.PropertyName))
            Console.WriteLine("Validation error: {0}-{1}", error.ErrorNumber, error.ErrorText);
        else
            Console.WriteLine("Validation error for property \"{2}\": {0}-{1}", error.ErrorNumber, error.ErrorText, error.PropertyName);
    }
}

XOML Workflow Aktivierung mit Workflow Services

Wie funktioniert dies nun, wenn man Workflow Services einsetzt, und ebenfalls XOML basierte Workflows aktivieren möchte? Hier stellt sich auf den ersten Blick eine Art “Huhn-Ei Problem“. Der TypeProvider soll der WorkflowRuntime als Service hinzugefügt werden, diese existiert aber erst nach dem Erstellen des WorkflowServiceHosts. Man hat auch nicht direkten Zugriff auf die WorkflowRuntime, so dass nicht ohne weiteres ein Service hinzugefügt werden könnte.
Der Weg führt hier über die Verwendung einer eigenen WorkflowServiceHostFactory, welche einen eigenen WorkflowServiceHost erzeugt. Der Konstruktor dieses WorkflowServiceHosts ist sieben Mal überladen (plus einmal protected), dabei gibt es einen Konstruktor, welcher nebst dem Stream mit der Workflow Definition einen TypeProvider aufnimmt:

public WorkflowServiceHost(Stream workflowDefinition,
                           Stream ruleDefinition,
                           ITypeProvider typeProvider,
                           params Uri[] baseAddress)

Dieser TypeProvider kann nun wie folgt dem Konstruktor übergeben werden:

TypeProvider typeProvider = new TypeProvider(null);

// Load referenced assemblies into the type provider
Assembly assembly = Assembly.Load("assemblyname");
typeProvider.AddAssembly(assembly);

// OR
// typeProvider.AddAssemblyReference(@"path to the assembly")

Stream stream = new MemoryStream(ASCIIEncoding.Default.GetBytes("<MyXOML...>"));;
Uri uri = new Uri("myuri");

WorkflowServiceHost wfsh = new WorkflowServiceHost(stream, null, typeProvider, uri);

Der Konstruktor wirft nun bei Validierungsproblemen entgegen der Dokumentation ebenfalls eine WorkflowValidationFailedException, was so nicht in der Dokumentation steht, so dass man die Fehler entsprechend behandeln kann.