C#: Eine Message Queuing-Dienstanwendung

 

Carl Nolan
Microsoft Corporation

Aktualisiert im März 2002

Zusammenfassung: In diesem Artikel wird eine Windows-Dienstlösung beschrieben, die für die Verarbeitung mehrerer Nachrichtenwarteschlangen konzipiert ist und sich auf die Anwendung der Microsoft .NET Framework und C# konzentriert. (39 gedruckte Seiten)

Laden Sie CSharpMessageService.exeherunter.

Inhalte

Einführung
Die .NET Framework-Anwendung
     Anwendungsstruktur
     Dienstklassen
     Instrumentierung
     Sprachneutralität
     Installation
Schlussbemerkung
Referenzen

Einführung

Kürzlich hat Microsoft eine neue Plattform zum Erstellen integrierter Anwendungen eingeführt, die Microsoft® .NET Framework. Mit diesem .NET Framework kann man schnell Webdienste und Anwendungen in jeder Programmiersprache erstellen und bereitstellen. Dieses sprachneutrale Framework wird durch den Microsoft Intermediate Language- und JIT-Compiler ermöglicht.

Zusammen mit dem .NET Framework eine neue Programmiersprache, C# (ausgesprochen C Sharp). C# ist eine einfache, moderne, objektorientierte und typsichere Programmiersprache. Mithilfe der .NET Framework und C# (zusätzlich zu Microsoft Visual Basic® und Managed C++) kann man hochfunktionelle Microsoft Windows®- und Webanwendungen und -dienste schreiben. In diesem Artikel wird eine Lösung vorgestellt, die sich auf die Anwendung der .NET Framework und C# und nicht auf die Programmiersprache konzentriert. Eine Einführung in die C#-Sprache finden Sie unter https://msdn.microsoft.com/vstudio/techinfo/articles/upgrade/Csharpintro.asp\!href(https://msdn.microsoft.com/vstudio/techinfo/articles/upgrade/Csharpintro.asp).

In einem aktuellen MSDN-Artikel wurde eine Lösung für eine hochverfügbare , skalierbare MSMQ-Lastenausgleichslösungsarchitektur vorgestellt. Diese Lösung beinhaltete die Entwicklung eines Windows-Diensts, der als Smart Message-Router fungierte. Früher war eine solche Lösung der Bereich des C++-Programmierers. Mit dem Aufkommen der .NET Framework ist dies nicht mehr der Fall.

Die .NET Framework-Anwendung

Die zu beschreibende Lösung ist ein Windows-Dienst, der für die Verarbeitung mehrerer Nachrichtenwarteschlangen konzipiert ist. jeder wird von mehreren Threads verarbeitet, die Nachrichten empfangen und verarbeiten. Beispielprozesse werden zum Weiterleiten von Nachrichten mithilfe einer Roundrobin-Technik oder eines anwendungsspezifischen Werts (der AppSpecific-Eigenschaft der Nachricht) als Index für eine Liste von Zielwarteschlangen, zum Verteilen der Nachricht in mehrere Warteschlangen und zum Aufrufen einer Komponentenmethode mit den Nachrichteneigenschaften angegeben. Im letzteren Fall ist die Anforderung der Komponente, dass sie eine bestimmte Schnittstelle namens IProcessMessage implementiert. Um Fehler zu behandeln, sendet die Anwendung Nachrichten, die nicht in eine Fehlerwarteschlange verarbeitet werden können.

Die Messaginganwendung ist ähnlich wie die vorherige ATL-Anwendung strukturiert. Die Standard Unterschiede sind die Kapselung des Codes zum Verwalten des Diensts und die Verwendung der .NET Framework-Komponenten. Da die .NET Framework objektorientiert ist, sollte es keine Überraschung sein, dass zum Erstellen eines Windows-Diensts nur eine Klasse erstellt werden muss, die die ServiceBase-Klasse von der System.ServiceControl-Assembly erbt.

Anwendungsstruktur

Die Standard-Klasse in der Anwendung ist ServiceControl, die Klasse, die die ServiceBase-Klasse erbt. Beim Erben von ServiceBase muss zusätzlich zu den optionalen Methoden OnPause und OnContinue die Methoden OnStart und OnStop implementiert werden. Die -Klasse wird tatsächlich innerhalb der statischen Methode Main erstellt:

using System;
using System.ServiceProcess;

public class ServiceControl: ServiceBase
{
   // main entry point that creates the service object
   public static void Main()
   {
      ServiceBase.Run(new ServiceControl());
   }

   // constructor object that defines the service parameters
   public ServiceControl()
   {
      CanPauseAndContinue = true;
      ServiceName = "MSDNMessageService";
      AutoLog = false;
   }
   
   protected override void OnStart(string[] args) {...}
   protected override void OnStop() {...}
   protected override void OnPause() {...}
   protected override void OnContinue() {...}
}

Die ServiceControl-Klasse erstellt eine Reihe von WorkerInstance-Objekten, eine instance einer WorkerInstance-Klasse, die für jede Nachrichtenwarteschlange erstellt wird, die eine Verarbeitung erfordert. Die WorkerInstance-Klasse erstellt wiederum eine Reihe von WorkerThread-Objekten basierend auf der erforderlichen Anzahl von Threads, die zum Verarbeiten der Warteschlange definiert sind. Die WorkerThread-Klasse erstellt tatsächlich einen Verarbeitungsthread, der die eigentliche Dienstarbeit ausführt.

Der Standard Zweck der WorkerInstance- und WorkerThread-Klassen ist die Bestätigung der Befehle Start, Stop, Pause und Continue des Dienststeuerelements. Da diese Prozesse nicht blockieren müssen, werden die Befehlsaktionen letztendlich eine Aktion für einen Hintergrundverarbeitungsthread ausführen.

WorkerThread ist eine abstrakte Klasse, die von WorkerThreadAppSpecific, WorkerThreadRoundRobin, WorkerThreadDisperse und WorkerThreadAssembly geerbt wird. Jede dieser Klassen verarbeitet Nachrichten auf unterschiedliche Weise. Die ersten drei verarbeiten eine Nachricht, indem sie sie an eine andere Warteschlange senden (der Unterschied besteht darin, wie der empfangende Warteschlangenpfad bestimmt wird), während letztere die Nachrichteneigenschaften verwendet, um eine Komponentenmethode aufzurufen.

Die Behandlung von Anwendungsfehlern innerhalb des .NET Framework basiert auf einer ApplicationException-Klasse, die von der Exception-Basisklasse abgeleitet wird. Wenn fehler ausgelöst oder abfangen werden, müssen die Fehler von einer klasse sein, die von ApplicationException abgeleitet ist. Die WorkerThreadException-Klasse stellt eine solche Implementierung dar, die die Basisklasse um das Hinzufügen einer Eigenschaft erweitert, die definiert, ob der Dienst weiterhin ausgeführt werden soll.

Schließlich enthält die Anwendung zwei Strukturen. Diese Werttypen definieren die Laufzeitparameter eines Workerprozesses oder Threads, um die Konstruktion der WorkerInstance - und WorkerThread-Objekte zu vereinfachen. Die Verwendung einer Werttypstruktur anstelle einer Verweistypklasse stellt sicher, dass Werte anstelle von Verweisen auf diese Laufzeitparameter beibehalten werden.

IProcessMessage-Schnittstelle

Eine der bereitgestellten WorkerThread-Implementierungen ist eine Klasse, die eine Komponentenmethode aufruft. Diese Klasse namens WorkerThreadAssembly verwendet eine Schnittstelle namens IProcessMessage, um den Vertrag zwischen dem Dienst und der Komponente zu definieren.

Im Gegensatz zur aktuellen Version von Visual Studio® können C#-Schnittstellen explizit in jeder Beliebigen Sprache definiert werden, wodurch das Erstellen und Kompilieren von IDL-Dateien entfällt. Daher wird die IProcessMessage mit C# wie folgt definiert:

[ComVisible(true)]
public interface IProcessMessage
{
   ProcessMessageReturn Process
      (string messageLabel, string messageBody, int messageAppSpecific);
   void Release();
}

Die Process-Methode ist wie im ATL-Code für die Verarbeitung von Nachrichten bestimmt. Der Rückgabecode der Process-Methode wird durch den Enumerationstyp ProcessMessageReturn definiert. Die Enumerationsdefinitionen lauten wie folgt: Gut setzt die Verarbeitung fort, ungültig schreibt die Nachricht in die Fehlerwarteschlange, und Abort beendet die Verarbeitung.

public enum ProcessMessageReturn 
{
   ReturnGood,
   ReturnBad,
   ReturnAbort
}

Die Release-Methode bietet einen Mechanismus, mit dem der Dienst die Klasse instance. Da der Destruktor der instance der Klasse nur während einer Garbage Collection aufgerufen wird, empfiehlt es sich, sicherzustellen, dass alle Klassen mit teuren Ressourcen (z. B. Datenbankverbindungen) über eine Methode verfügen, die vor der Zerstörung aufgerufen werden kann, um diese Ressourcen freizugeben.

Namespaces

An diesem Punkt ist eine kurze Erwähnung von Namespaces gerechtfertigt. Namespaces ermöglichen die Organisation von Anwendungen in logischen Elementen, sowohl für die interne als auch für die externe Darstellung. Der gesamte Code in diesem Dienst ist im NAMESPACE MSDNMessageService.Service enthalten. Obwohl der Dienstcode in mehreren Dateien enthalten ist, muss nicht auf die anderen Dateien verwiesen werden, da sie im selben Namespace enthalten sind.

Da die IProcessMessage-Schnittstelle im MSDNMessageService.Interface-Namespace enthalten ist, verfügt die Threadklasse, die diese Schnittstelle verwendet, über einen Schnittstellennamespace-Import.

Dienstklassen

Der Zweck der Anwendung besteht darin, Nachrichtenwarteschlangen zu überwachen und zu verarbeiten. Jede Warteschlange führt einen anderen Prozess für empfangene Nachrichten aus, und die Anwendung wird als Windows-Dienst implementiert.

Die ServiceBase-Klasse

Wie bereits erwähnt, ist die grundlegende Struktur eines Diensts eine Klasse, die von ServiceBase erbt. Die wichtigen Methoden sind OnStart, OnStop, OnPause und OnContinue, wobei jede überschriebene Methode direkt einer Dienststeuerungsaktion entspricht. Der Zweck der OnStart-Methode besteht darin , WorkerInstance-Objekte zu erstellen Die WorkerInstance-Klasse erstellt wiederum WorkerThread-Objekte , aus denen die Threads erstellt werden, die die Dienstarbeit ausführen.

Die Laufzeitkonfiguration des Diensts und damit die Eigenschaften der WorkerInstance- und WorkerThread-Objekte werden in einer XML-Konfigurationsdatei mit einer zugeordneten XSD (XML-Schemadefinition) verwaltet. Eine BEISPIEL-XML-Konfigurationsdatei wäre:

<?xml version="1.0" encoding="utf-8" ?>
<ProcessList
      xmlns="urn:messageservice-schema"
      xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
      xsi:schemaLocation="urn:messageservice-schema MessageService.xsd">
   <ProcessDefinition ProcessName="Worker1"
         ProcessType="RoundRobin" NumberThreads="8" Transactions="false">
      <Description>RoundRobin 8 Threads No Transactions</Description>
      <InputQueue>.\private$\test_load1</InputQueue>
      <ErrorQueue>.\private$\test_error</ErrorQueue>
      <OutputList>
         <OutputName>.\private$\test_out11</OutputName>
         <OutputName>.\private$\test_out12</OutputName>
         <OutputName>.\private$\test_out13</OutputName>
      </OutputList>
   </ProcessDefinition>
   <ProcessDefinition ProcessName="Worker2"
         ProcessType="AppSpecific" NumberThreads="4" Transactions="true">
      <Description>AppSpecific 4 Threads and Transactions</Description>
      <InputQueue>.\private$\test_load2_t</InputQueue>
      <ErrorQueue>.\private$\test_error_t</ErrorQueue>
      <OutputList>
         <OutputName>.\private$\test_out21_t</OutputName>
         <OutputName>.\private$\test_out22_t</OutputName>
      </OutputList>
   </ProcessDefinition>
   <ProcessDefinition ProcessName="Worker3"
         ProcessType="Disperse" NumberThreads="2" Transactions="true">
      <Description>Disperse 2 Threads and Transactions</Description>
      <InputQueue>.\private$\test_load3_t</InputQueue>
      <ErrorQueue>.\private$\test_error_t</ErrorQueue>
      <OutputList>
         <OutputName>.\private$\test_out31_t</OutputName>
         <OutputName>.\private$\test_out32_t</OutputName>
      </OutputList>
   </ProcessDefinition>
   <ProcessDefinition ProcessName="Worker4"
         ProcessType="Assembly" NumberThreads="4" Transactions="false">
      <Description>Assembly 4 Threads No Transactions</Description>
      <InputQueue>.\private$\test_load4</InputQueue>
      <ErrorQueue>.\private$\test_error</ErrorQueue>
      <AssemblyDefinition>
         <FullPath>MessageExample.dll</FullPath>
         <ClassName>MSDNMessageService.MessageSample.ExampleClass</ClassName>
      </AssemblyDefinition>
   </ProcessDefinition>
</ProcessList>

Obwohl nicht unbedingt erforderlich, stellt das XSD-Dokument, auf das im schemaLocation-Attribut verwiesen wird, einen gültigen Satz von Eigenschaften für jedes WorkerInstance- und WorkerThread-Objekt sicher. Eine solche Schemadefinition wäre:

<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema targetNamespace="urn:messageservice-schema" xmlns="urn:messageservice-schema" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
   <xsd:annotation>
      <xsd:documentation xml:lang="en">
         Message Queueing Service Schema
         Created September 2001
      </xsd:documentation>
   </xsd:annotation>
   <xsd:element name="ProcessList" type="PROCESSLIST" />
   <xsd:complexType name="PROCESSLIST">
      <xsd:sequence>
         <xsd:element name="ProcessDefinition"
            minOccurs="1" maxOccurs="unbounded">
            <xsd:complexType>
               <xsd:sequence>
                  <xsd:element name="Description" type="xsd:string" />
                  <xsd:element name="InputQueue" type="xsd:string" />
                  <xsd:element name="ErrorQueue" type="xsd:string" />
                  <xsd:choice>
                     <xsd:element name="OutputList" type="OUTPUTLIST" />
                     <xsd:element name="AssemblyDefinition"
                        type="ASSEMBLYDEFINITION" />
                  </xsd:choice>
               </xsd:sequence>
               <xsd:attribute name="ProcessName" type="xsd:string" />
               <xsd:attribute name="ProcessType">
                  <xsd:simpleType>
                     <xsd:restriction base="xsd:string">
                        <xsd:enumeration value="RoundRobin" />
                        <xsd:enumeration value="AppSpecific" />
                        <xsd:enumeration value="Disperse" />
                        <xsd:enumeration value="Assembly" />
                     </xsd:restriction>
                  </xsd:simpleType>
               </xsd:attribute>
               <xsd:attribute name="NumberThreads" type="xsd:integer" />
               <xsd:attribute name="Transactions" type="xsd:boolean" />
            </xsd:complexType>
         </xsd:element>
      </xsd:sequence>
   </xsd:complexType>
   <xsd:complexType name="OUTPUTLIST">
      <xsd:sequence>
         <xsd:element name="OutputName" type="xsd:string"
            minOccurs="1" maxOccurs="unbounded" />
      </xsd:sequence>
   </xsd:complexType>
   <xsd:complexType name="ASSEMBLYDEFINITION">
      <xsd:sequence>
         <xsd:element name="FullPath" type="xsd:string" />
         <xsd:element name="ClassName" type="xsd:string" />
      </xsd:sequence>
   </xsd:complexType>
</xsd:schema>

Der Name der XML-Konfigurationsdatei ist in einer Anwendungskonfigurationsdatei enthalten. Anwendungskonfigurationsdateien enthalten Einstellungen, die Eine Assemblybindungsrichtlinie, Remotingobjekte, benutzerdefinierte Kanäle und Einstellungen enthalten, die die Anwendung lesen kann. Der Name der Anwendungskonfigurationsdatei ist der Name der Anwendung mit einer .config-Erweiterung, die im ausführbaren Ordner der Anwendung abgelegt wird. Beispielsweise wird die Nachrichtenserveranwendung MessageService.EXE genannt, wobei die Konfigurationsdatei MessageService.EXE.config heißt.

In der Anwendungskonfigurationsdatei ermöglicht ein spezieller Abschnitt mit dem Namen appSettings Einstellungen, die eine Anwendung lesen kann. Bei diesen Einstellungen handelt es sich um eine Reihe von Schlüsselwertpaaren. In diesem Fall stellt ein Schlüssel namens ConfigurationFile den vollständigen Pfad zur XML-Konfigurationsdatei bereit.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <appSettings>
      <add key="ConfigurationFile" value="C:\Path\MessageService.xml" />
      <add key="SchemaFile" value="C:\Path\MessageService.xsd" />
   </appSettings>
</configuration>

Der Zugriff auf diese Informationen wird über die ConfigurationSettings-Klasse von der System.Configuration-Assembly verwaltet. Die AppSettings-Methode gibt den Wert des angeforderten Schlüssels zurück, der in diesem Fall der vollqualifizierte Pfad der XML-Konfigurationsdatei ist.

string configFile = ConfigurationSettings.AppSettings["ConfigurationFile"];

Sobald der Speicherort der XML-Konfigurationsdatei ermittelt wurde, kann sie problemlos als XML-Dokument verarbeitet werden. Beim Laden des XML-Dokuments wird eine Überprüfung anhand der Schemadefinition durchgeführt:

XmlDocument configXmlDoc = new XmlDocument();
XmlValidatingReader configReader = new XmlValidatingReader(new XmlTextReader(configFile));
configReader.ValidationType = ValidationType.Schema;
configXmlDoc.PreserveWhitespace = false;
configXmlDoc.Load(configReader);

In der Konfigurationsdatei wird über das schemaLocation-Attribut auf die entsprechende Schemadefinition verwiesen. Dabei ist es nicht erforderlich, die Schemadatei der Überprüfenden ReaderSchemas-Auflistung hinzuzufügen:

string schemaFile = ConfigurationSettings.AppSettings["SchemaFile"];
configReader.Schemas.Add(null, schemaFile);

Bevor Xpath-Abfragen ausgeführt werden können, muss ein entsprechender XML-Namespaceverweis definiert werden. Die XML-Konfigurationsdatei verwendet keinen NULL-Namespace, sondern einen, der für die Schemadefinition definiert ist. Beim Ausführen von Xpath-Abfragen muss im Namespace-Manager auf diesen Namespace verwiesen werden:

XmlNamespaceManager configNamespace = new XmlNamespaceManager(configXmlDoc.NameTable);
configNamespace.AddNamespace("default", "urn:messageservice-schema");

Zum Verarbeiten der XML-Konfigurationsdatei muss nur die Auflistung von Knoten für jeden erforderlichen Workerprozess und dann für jede Prozessabfrage das entsprechende Attribut und die Elementeigenschaften verarbeitet werden. Für die Attribute wird dies mithilfe der GetNamedItem-Methode der Attributeauflistung ausgeführt. Bei Elementen wird dies mithilfe der Funktionen SelectNodes und SelectSingleNode des XML-Dokuments ausgeführt. die Eingaben für diese sind der erforderliche Xpath-Abfrage- und Namespace-Manager.

Da die Service-Klasse eine Liste der erstellten Workerobjekte verwalten muss, wird die Hashtable-Auflistung verwendet, die eine Liste von Namenswertpaaren vom Typ -Objekt enthält. Zusätzlich zur Unterstützung von Enumerationen ermöglicht die Hashtable das Abfragen von Werten nach Schlüssel. In der Anwendung wird der XML-Prozessname als eindeutiger Schlüssel verwendet:

foreach (XmlNode processXmlDefinition in configXmlDoc.SelectNodes("descendant::default:ProcessDefinition", configNamespace)) 
{
   // found a process so create a new WorkerFormatter struct
   workerDefintion = new WorkerFormatter();
   string processName =
      processXmlDefinition.Attributes.GetNamedItem("ProcessName").Value;
   string processType =
      processXmlDefinition.Attributes.GetNamedItem("ProcessType").Value;

   // define the process name and type placing them in the worker struct
   workerDefintion.ProcessName = processName;
   switch (processType) 
   {
      case "RoundRobin":
         workerDefintion.ProcessType =
            WorkerFormatter.SFProcessType.ProcessRoundRobin;
         break;
      case "AppSpecific":
         workerDefintion.ProcessType =
            WorkerFormatter.SFProcessType.ProcessAppSpecific;
         break;
      case "Disperse":
         workerDefintion.ProcessType =
            WorkerFormatter.SFProcessType.ProcessDisperse;
         break;
      case "Assembly":
         workerDefintion.ProcessType =
            WorkerFormatter.SFProcessType.ProcessAssembly;
         break;
      default:
         throw new ApplicationException("Unknown Processing Type");
   }
   workerDefintion.NumberThreads = Convert.ToInt32(
      processXmlDefinition.Attributes.GetNamedItem("NumberThreads").Value
   );

   // determine the transaction status of the processing
   switch (Convert.ToBoolean(
      processXmlDefinition.Attributes.GetNamedItem("Transactions").Value
      )) 
   {
      case false:
         workerDefintion.Transactions =
            WorkerFormatter.SFTransactions.NotRequired;
         break;
      case true:
         workerDefintion.Transactions =
            WorkerFormatter.SFTransactions.Required;
         break;
      default:
         throw new ApplicationException("Unknown Required Transaction State");
   }

   // place all remaining elements of the process defintition in the worker formatter
   workerDefintion.ProcessDesc = processXmlDefinition.SelectSingleNode
      ("descendant::default:Description", configNamespace).InnerText;
   workerDefintion.InputQueue = processXmlDefinition.SelectSingleNode
      ("descendant::default:InputQueue", configNamespace).InnerText;
   workerDefintion.ErrorQueue = processXmlDefinition.SelectSingleNode
      ("descendant::default:ErrorQueue", configNamespace).InnerText;

   // based on the process type either
   // locate the assembly defintion or the output queue names
   // in the advent that the process is neither throw an exception
   switch (processType) 
   {
      case "Assembly":
         workerDefintion.OutputName = new string[2];
         workerDefintion.OutputName[0] =
            processXmlDefinition.SelectSingleNode(
            "descendant::default:AssemblyDefinition/default:FullPath",
            configNamespace).InnerText;
         workerDefintion.OutputName[1] =
            processXmlDefinition.SelectSingleNode(
         "descendant::default:AssemblyDefinition/default:ClassName",
         configNamespace).InnerText;
         break;
      case "AppSpecific":
      case "Disperse":
      case "RoundRobin":
         XmlNodeList processXmlOutputs;
         processXmlOutputs = processXmlDefinition.SelectNodes
            ("descendant::default:OutputList/default:OutputName",
            configNamespace);
         if (processXmlOutputs.Count == 0) 
         {
            throw new ApplicationException
               ("No Output Parameters " + processName);
         }
         // allocate the output array based on the number of output names
         workerDefintion.OutputName =
            new string[processXmlOutputs.Count];
         // iterate through output list for the queue/assembly names
         int idx = 0;
         foreach (XmlNode outputName in processXmlOutputs) 
         {
            workerDefintion.OutputName[idx] = outputName.InnerText;
            idx++;
         }
         break;
      default:
         throw new ApplicationException("Unknown Processing Type");
   }
   // add the information into the collection of Worker Formatters
   if (workerReferences != null) {
      // validate the name is unique
      string processName = workerDefintion.ProcessName;
      if (workerReferences.ContainsKey(processName)) 
      {
         throw new ArgumentException
            ("Process Name Must be Unique: " + processName);
      }
      // create a worker object in the worker array
      workerReferences.Add(processName,
         new WorkerInstance(workerDefintion));
   }
}

Sowohl die WorkerInstance - als auch die WorkerThread-Klasse verfügen über entsprechende Dienststeuerungsmethoden, die basierend auf der Dienststeuerungsaktion aufgerufen werden. Da in der Hashtable auf jedes WorkerInstance-Objekt verwiesen wird, werden die Inhalte der Hashtable aufgelistet, um die entsprechende Dienststeuerungsmethode aufzurufen:

foreach (WorkerInstance workerReference in workerReferences.Values)
{
   workerReference.StartService();
}

In ähnlicher Weise werden die implementierten Methoden OnPause, OnContinue und OnStop ausgeführt, indem die entsprechenden Methoden für die WorkerInstance-Objekte aufgerufen werden.

Die WorkerInstance-Klasse

Die primäre Funktion der WorkerInstance-Klasse besteht darin, WorkerThread-Objekte zu erstellen und zu verwalten. Die Methoden StartService, StopService, PauseService und ContinueService rufen die entsprechenden WorkerThread-Methoden auf. Die eigentlichen WorkerThread-Objekte werden in der StartService-Methode erstellt. Wie die Service-Klasse , die eine Hashtable verwendet, um die Verweise auf die Workerobjekte zu verwalten, verwendet WorkerInstance ein ArrayList, ein array, das einfach dynamisch dimensioniert ist, um eine Liste von Threadobjekten zu verwalten.

Innerhalb dieses Arrays erstellt die WorkerInstance-Klasse eine der implementierten Versionen der WorkerThread-Klasse . Die WorkerThread-Klasse ist eine abstrakte Klasse, die geerbt werden muss. Die abgeleiteten Klassen definieren, wie eine Nachricht verarbeitet wird.

threadReferences = new ArrayList();
for (int idx=0; idx<workerDefintion.NumberThreads; idx++)
{
   WorkerThreadFormatter threadDefinition = new WorkerThreadFormatter();
   threadDefinition.ProcessName = workerDefintion.ProcessName;
   threadDefinition.ProcessDesc = workerDefintion.ProcessDesc;
   threadDefinition.ThreadNumber = idx;
   threadDefinition.InputQueue = workerDefintion.InputQueue;
   threadDefinition.ErrorQueue = workerDefintion.ErrorQueue;
   threadDefinition.OutputName = workerDefintion.OutputName;

   // define the worker type and insert into the work thread struct
   WorkerThread workerThread;
   switch (workerDefintion.ProcessType)
   {
      case WorkerFormatter.SFProcessType.ProcessRoundRobin:
         workerThread =
            new WorkerThreadRoundRobin(this, threadDefinition);
         break;
      case WorkerFormatter.SFProcessType.ProcessAppSpecific:
         workerThread =
            new WorkerThreadAppSpecific(this, threadDefinition);
         break;
      case WorkerFormatter.SFProcessType.ProcessDisperse:
         workerThread =
            new WorkerThreadDisperse(this, threadDefinition);
         break;
      case WorkerFormatter.SFProcessType.ProcessAssembly:
         workerThread =
            new WorkerThreadAssembly(this, threadDefinition);
         break;
      default:
         throw new ApplicationException("Unknown Processing Type");
   }
   threadReferences.Insert(idx, workerThread);
}

Nachdem alle Objekte erstellt wurden, können sie durch Aufrufen der StartService-Methode jedes Threadobjekts gestartet werden:

foreach(WorkerThread threadReference in threadReferences)
{
   threadReference.StartService();
}

Die Methoden StopService, PauseService und ContinueService führen alle ähnliche Vorgänge innerhalb einer foreach-Schleife aus. Die StopService-Methode verfügt über den folgenden Garbage Collection-Vorgang (GC):

GC.SuppressFinalize(this);

Innerhalb des Klassen-Destruktors wird die StopService-Methode aufgerufen. Dadurch können die Objekte ordnungsgemäß beendet werden, wenn die StopService-Methode nicht explizit aufgerufen wird. Wenn die StopService-Methode aufgerufen wird, wird der Destruktor nicht benötigt. Die SuppressFinalize-Methode verhindert, dass die Finalize-Methode ( die tatsächliche Implementierung des Destruktors ) aufgerufen wird.

Die abstrakte WorkerThread-Klasse

WorkerThread ist eine abstrakte Klasse, die von WorkerThreadAppSpecific, WorkerThreadRoundRobin, WorkerThreadDisperse und WorkerThreadAssembly geerbt wird. Da die Verarbeitung einer Warteschlange größtenteils identisch ist, stellt die WorkerThread-Klasse diese Funktionalität bereit, unabhängig davon, wie die Nachricht verarbeitet wird. Die -Klasse stellt abstrakte Methoden bereit, die überschrieben werden müssen, um Ressourcen zu verwalten und Nachrichten zu verarbeiten.

Die Arbeit der -Klasse wird erneut in den Methoden StartService, StopService, PauseService und ContinueService implementiert. In der StartService-Methode wird auf die Eingabe- und Fehlerwarteschlange verwiesen. Innerhalb des .NET Framework wird messaging vom System.Messaging-Namespace verarbeitet:

string inputQueueName = threadDefinition.InputQueue;
string errorQueueName = threadDefinition.ErrorQueue;
if (!MessageQueue.Exists(inputQueueName) ||
   !MessageQueue.Exists(errorQueueName))
{
   // queue does not exist so through an error
   throw new ArgumentException("The Input/Error Queue does not Exist");
}
// try and open the input queue and set the default properties
inputQueue = new MessageQueue(inputQueueName);
inputQueue.MessageReadPropertyFilter.Body = true;
inputQueue.MessageReadPropertyFilter.AppSpecific = true;
// open the error queue
errorQueue = new MessageQueue(errorQueueName);
// set the formatter to be ActiveX if using COM to load messages
inputQueue.Formatter = new ActiveXMessageFormatter();
errorQueue.Formatter = new ActiveXMessageFormatter();

Nachdem die Nachrichtenwarteschlangen geöffnet wurden, müssen ihre Transaktionszustände auf Konsistenz überprüft werden. Eine Transaktionseingabewarteschlange erfordert einen Transaktionsfehler und eine Ausgabewarteschlange und umgekehrt. Wenn festgestellt wird, dass eine MessageQueueTransaction für die Sende- und Empfangsfunktionen erforderlich ist, wird eine erstellt:

if (workerInstance.WorkerInfo.Transactions == WorkerFormatter.SFTransactions.NotRequired)
{
   transactionalQueue = false;
}
else
{
   transactionalQueue = true;
}
if ((inputQueue.Transactional != transactionalQueue)
   || (errorQueue.Transactional != transactionalQueue))
{
   throw new ApplicationException
      ("Queues do not have Consistent Transactional Status");
}
// if require transactions create a message queue transaction
if (transactionalQueue)
{
   queueTransaction = new MessageQueueTransaction();
}
else
{
   queueTransaction = null;
}

Nachdem die Nachrichtenwarteschlangenverweise und -transaktionen definiert sind, wird ein Thread namens ProcessMessages für die eigentliche Verarbeitungsfunktion erstellt. Innerhalb des .NET Framework ist das Threading mit dem System.Threading-Namespace problemlos möglich:

messageProcessor = new Thread(new ThreadStart(ProcessMessages));
messageProcessor.Start();

Die ProcessMessages-Funktion ist eine Verarbeitungsschleife , die auf einem booleschen Wert basiert. Wenn sie auf false festgelegt ist, wird die Prozessschleife beendet. Daher legt die StopService-Methode des Threadobjekts nur diesen booleschen Wert fest und verknüpft den Thread dann mit dem Standard Thread, zusätzlich zum Schließen der geöffneten Nachrichtenwarteschlangen:

workerRunning = false;
// join the service thread and the processing thread
if (messageProcessor != null)
{
   messageProcessor.Join();
}
if (transactionalQueue)
{
   queueTransaction.Dispose();
}
inputQueue.Close();
errorQueue.Close();

Die PauseService-Methode legt lediglich einen booleschen Wert fest, der den Verarbeitungsthread für eine halbe Sekunde in den Ruhezustand versetzt:

if (workerPaused) 
   Thread.Sleep(500);

Schließlich rufen die Methoden StartService, StopService, PauseService und ContinueService die abstrakten Methoden OnStart, OnStop, OnPause und OnContinue auf. Diese abstrakten Methoden stellen die Hooks für implementierte Klassen bereit, um erforderliche Ressourcen zu erfassen und freizugeben.

Die ProcessMessages-Schleife weist die folgende grundlegende Struktur auf:

  • Erhalten Sie eine Nachricht.
  • Wenn eine Nachricht einen erfolgreichen Empfang aufweist, rufen Sie die abstrakte ProcessMessage-Methode auf.
  • Wenn die Receive- oder ProcessMessage-Methode fehlschlägt, senden Sie die Nachricht in eine Fehlerwarteschlange.

Der Inhalt dieser Verarbeitungsschleife lautet wie folgt:

// if a a transaction required create one and receive the message
MessageTransactionStart();
MessageReceive();

// once have a message call the process message abstract method
// any error at this point will force a send to the error queue
if (mInput != null)
{
   // if a terminate error is caught the transaction is aborted
   try
   {
      // call the method to process the message
      ProcessMessage();
   }
   // catch error thrown where exception status known
   catch (WorkerThreadException ex)
   {
      ProcessError(ex.Terminate, ex.Message);
   }
   // catch an unknown exception and call terminate
   catch (Exception ex)
   {
      ProcessError(true, ex.Message);
   }
   // successfully completed a processing of a message
   // this includes writing to the error queue
   MessageTransactionComplete(true);
}

Die Methoden MessageTransactionStart und MessageTransactionComplete behandeln die Transaktionsanforderungen des Messagings. Wenn die Nachrichtenwarteschlangen transaktional sind, wird eine MessageQueueTransaction initiiert und sowohl für den Empfangs- als auch für den Sendevorgang verwendet. Abhängig vom Ergebnis dieser Vorgänge wird die Transaktion dann committet oder abgebrochen. Die ProcessError-Methode sendet die fehlerhafte Nachricht an die Fehlerwarteschlange. Darüber hinaus kann eine Ausnahme ausgelöst werden, um den Thread abnormal zu beenden. Diese Aktion würde ausgeführt, wenn von der ProcessMessage-Methode ein Beendigungsfehler vom Typ WorkerThreadException ausgelöst wurde. Der Code für die Funktionen, die in der ProcessMessage-Methode aufgerufen werden, lautet wie folgt:

// method to process the failed message
private void ProcessError(bool terminateProcessing, string logMessage)
{
   // attempt to send the failed error to the error queue
   // upon failure to write to the error queue log the error
   try
   {
      MessageSend(errorQueue);
   }
   catch (Exception ex)
   {
      LogError
         ("Message error : " + inputMessage.Label + " : " + ex.Message);
      // as one cannot write to error queue terminate the thread
      terminateProcessing = true;
   }
   // if required terminate the thread and associated worker
   if (terminateProcessing)
   {
      // abort transaction as cannot place message into the error queue
      MessageTransactionComplete(false);
      LogError("Thread Terminated Abnormally : " + logMessage);
      // an error here should also terminate the processing thread
      workerInstance.StopOnError();
      throw new ApplicationException("Terminate Thread");
   }
}

// message queue send method that will honour the transaction in process
// protected to allow derived classes to send to remote queues
protected void MessageSend(MessageQueue messageQueue)
{
   if (queueTransaction == null)
   {
      messageQueue.Send(inputMessage);
   }
   else
   {
      messageQueue.Send(inputMessage, queueTransaction);
   }
}

// message queue receive method that will honour transaction in process
// protected to allow derived classes to read from other queues
protected void MessageReceive()
{
   try 
   {
      if (queueTransaction == null)
      {
         inputMessage = inputQueue.Receive(queueTimeout);
      }
      else
      {
         inputMessage =
            inputQueue.Receive(queueTimeout, queueTransaction);
      }
   }
   catch (MessageQueueException ex)
   {
      // set the message to null as not to be processed
      inputMessage = null;
      // as message has not been read terminate the transaction
      MessageTransactionComplete(false);
      // look at the error code and see if there was a timeout
      // if not a timeout throw an error and log the error number
      if (ex.MessageQueueErrorCode != MessageQueueErrorCode.IOTimeout)
      {
         LogError("Error : " + ex.Message);
         // an error here should also terminate processing thread
         workerInstance.StopOnError();
         throw ex;
      }
   }
}

// start any required message queue transaction
private void MessageTransactionStart()
{
   try
   {
      if (transactionalQueue)
      {
         queueTransaction.Begin();
      }
   }
   catch(Exception ex)
   {
      LogError("Cannot Create Message Transaction " +
         perfCounterThreadName + ": " + ex.Message);
      // an error here should also terminate processing thread
      workerInstance.StopOnError();
      throw ex;
   }
}

// complete the message queue transaction
// based on the existence of transaction and it success requirement
private void MessageTransactionComplete(bool transactionSuccess)
{
   if (transactionalQueue)
   {
      try
      {
         // committing or aborting a transactions must be successful
         if (transactionSuccess)
         {
            queueTransaction.Commit();
         }
         else
         {
            queueTransaction.Abort();
         }
      }
      catch (Exception ex)
      {
         LogError("Cannot Complete Message Transaction " +
            perfCounterThreadName + ": " + ex.Message);
         // an error here should also terminate processing thread
         workerInstance.StopOnError();
         throw ex;
      }
   }
}

Um die Transaktionskonsistenz der Nachrichten "Empfangen" und "Senden" besser zu verarbeiten, stellt die abstrakte Workerklasse die Funktionen MessageReceive und MessageSend bereit. Der Zweck dieser Funktionen besteht darin, die Nachricht zu verarbeiten und so transaktionsbasierte Anforderungen zu erfüllen.

Die abgeleiteten WorkerThread-Klassen

Jede Klasse, die von WorkerThread erbt, muss die Methoden OnStart, OnStop, OnPause, OnContinue und ProcessMessage überschreiben. Der Zweck der OnStart - und OnStop-Methoden besteht im Abrufen und Freigeben von Verarbeitungsressourcen. Die Optionen OnPause und OnContinue werden bereitgestellt, um die vorübergehende Freigabe und erneute Nutzung dieser Ressourcen zu ermöglichen. Die ProcessMessage-Methode sollte eine einzelne Nachricht verarbeiten und beim Auftreten eines Fehlers eine WorkerThreadException-Ausnahme auslösen.

Da der WorkerThread-Konstruktor Laufzeitparameter definiert, müssen die abgeleiteten Klassen den Basisklassenkonstruktor aufrufen:

public WorkerThreadRoundRobin(WorkerInstance workerInstance, WorkerThreadFormatter workerThreadFormatter)
   : base (workerInstance, workerThreadFormatter) {}

Abgeleitete Klassen werden für drei Arten der Verarbeitung bereitgestellt: Nachrichtenweiterleitung an eine einzelne Warteschlange, Nachrichtenweiterleitung an eine Warteschlangenliste und Aufrufen einer Komponentenmethode für jede Nachricht. Die beiden Implementierungen, die Nachrichten an eine einzelne Warteschlange weiterleiten, verwenden eine Roundrobin-Technik oder einen Anwendungsoffset, der in der AppSpecific-Eigenschaft der Nachricht gespeichert ist, als bestimmenden Faktor für die zu verwendende Warteschlange. Die Konfigurationsdatei in diesem Szenario sollte eine Liste von Warteschlangenpfaden enthalten. Die implementierten Methoden OnStart und OnStop sollten einen Verweis auf diese Warteschlangen öffnen und schließen:

queueCount = threadDefinition.OutputName.Length;
outputQueues = new MessageQueue[queueCount];
// open each output queue
for (int idx=0; idx<queueCount; idx++)
{
   outputQueues[idx] = new
      MessageQueue(threadDefinition.OutputName[idx]);
   outputQueues[idx].Formatter = new ActiveXMessageFormatter();
   // validate the transactional property of the queue
   if (outputQueues[idx].Transactional != transactionalQueue)
   {
      throw new ApplicationException
         ("Queues do not have consistent Transactional status");
   }
}

In diesen Szenarien ist die Verarbeitung der Nachricht einfach: Senden Sie die Nachricht mithilfe der funktion MessageSend an die erforderliche Ausgabewarteschlange. In einer Roundrobin-Situation wäre dieser Prozess:

try
{
   // attempt to send the message to the output queue
   MessageSend(outputQueues[queueNext]);
}
catch (Exception ex)
{
   // if an error force an error and terminate thread and worker
   throw new WorkerThreadException(ex.Message, true);
}
// calculate the next queue number
queueNext++;
queueNext %= queueCount;

Die Implementierung, die Nachrichten an eine Warteschlangenliste weiterleitet, ähnelt der vorherigen Implementierung sehr, mit der Ausnahme, dass die Nachricht an alle Ausgabewarteschlangen weitergeleitet wird, anstatt an eine deterministische einzelne Warteschlange:

try
{
   // attempt to send the message to all the output queue
   for (int idx=0; idx<queueCount; idx++)
   {
      MessageSend(outputQueues[idx]);
   }
}
catch (Exception ex)
{
   // if an error force an error and terminate thread and worker
   throw new WorkerThreadException(ex.Message, true);
}

Die letztere Implementierung, das Aufrufen einer Komponentenmethode mit den Nachrichtenparametern, ist etwas interessanter. Mithilfe der IProcessMessage-Schnittstelle ruft die ProcessMessage-Methode eine .NET-Komponente auf. Mit den Methoden OnStart und OnStop wird ein Verweis auf diese Komponente abgerufen und freigegeben.

Die Konfigurationsdatei in diesem Szenario sollte zwei Elemente enthalten: den vollständigen Klassennamen und den Speicherort der Datei, in der sich die Klasse befindet. Die Process-Methode wird für die Komponente aufgerufen, wie in der IProcessMessage-Schnittstelle definiert.

Um den Objektverweis abzurufen, wird die Activator.CreateInstance-Methode verwendet. Die Funktion erfordert einen Assemblytyp, der in diesem Fall vom Pfad der Assemblydatei und dem Klassennamen abgeleitet ist. Sobald ein Objektverweis abgerufen wurde, wird er in die entsprechende Schnittstelle umgewandelt:

private IProcessMessage messageProcessor;
private string messageProcessorLocation, messageProcessorName;
// obtain the assembly path and type name
messageProcessorLocation = threadDefinition.OutputName[0];
messageProcessorName = threadDefinition.OutputName[1];
// obtain a reference to the required object 
Type typSample = Assembly.LoadFrom
      (messageProcessorLocation).GetType(messageProcessorName);
object messageProcessorObject = Activator.CreateInstance(typSample);
// cast to the required interface on the object
messageProcessor = (IProcessMessage)messageProcessorObject;

Wenn ein Objektverweis abgerufen wird, ruft die ProcessMessage-Methode die Process-Methode auf der IProcessMessage-Schnittstelle auf:

ProcessMessageReturn processReturnValue;
// attempt to call the required Process method
try
{
   // define the parameters for the method call
   string messageLabel = inputMessage.Label;
   string messageBody = (string)inputMessage.Body;
   int messageAppSpecific = inputMessage.AppSpecific;
   // call the method and catch the return code
   processReturnValue = messageProcessor.Process
      (messageLabel, messageBody, messageAppSpecific);
}
catch (InvalidCastException ex)
{
   // if an error casting message details force a non critical error
   throw new WorkerThreadException(ex.Message, false);
}
catch (Exception ex)
{
   // if an error calling assembly termiate the thread processing
   throw new WorkerThreadException(ex.Message, true);
}
// if no error review the return status of the object call
switch (processReturnValue)
{
   case ProcessMessageReturn.ReturnBad:
      throw new WorkerThreadException
         ("Unable to process message: Message marked bad", false);
   case ProcessMessageReturn.ReturnAbort:
      throw new WorkerThreadException
         ("Unable to process message: Process terminating", true);
   default:
      break;
}

Die bereitgestellte Beispielkomponente schreibt den Nachrichtentext in eine Datenbanktabelle. In diesem Fall kann das gewünschte Ergebnis eines schwerwiegenden Datenbankfehlers ein Abbruch der Verarbeitung sein, wodurch die Nachricht bei einem weiteren Fehler als fehlerhaft markiert wird.

Da die instance der klasse, die für dieses Beispiel erstellt wurde, teure Datenbankressourcen abrufen und enthalten kann, wird der Objektverweis von den Methoden OnPause und OnContinue freigegeben und erneut abgerufen.

Instrumentierung

Wie in allen guten Anwendungen wird instrumentiert, um die status der Anwendung zu überwachen. Die .NET Framework hat die Einbeziehung von Ereignisprotokollierung, Leistungsindikatoren und Windows Management Instrumentation (WMI) in Anwendungen erheblich vereinfacht. Die Messaginganwendung verwendet Ereignisprotokollierung und Leistungsindikatoren, beide aus der System.Diagnostics-Assembly.

Innerhalb der ServiceBase-Klasse kann die automatische Ereignisprotokollierung aktiviert werden. Darüber hinaus unterstützt das ServiceBase EventLog-Mitglied das Schreiben in das Anwendungsereignisprotokoll:

EventLog.WriteEntry(logMessage, EventLogEntryType.Information);

Damit eine Anwendung andere Ereignisprotokolle als das Anwendungsprotokoll schreibt, kann sie ganz einfach einen Verweis auf eine EventLog-Quelle erstellen und abrufen, wie dies in der WorkerInstance-Klasse geschieht, und dann die WriteEntry-Methode zum Aufzeichnen von Protokolleinträgen verwenden:

private EventLog eventLog;
private bool eventLogCreated = false;
private string eventLogName = "Application";
private string eventLogSource = ServiceControl.ServiceControlName;
// see if the source exists creating it if not
if (!EventLog.SourceExists(eventLogSource))
{
   eventLogCreated = true;
   EventLog.CreateEventSource(eventLogSource, eventLogName);
}
// create the log object and reference the now defined source
eventLog = new EventLog();
eventLog.Source = eventLogSource;
// write an entry to inform successful creation
eventLog.WriteEntry("Successfully Created", EventLogEntryType.Information);

Leistungsindikatoren wurden durch die .NET Framework erheblich vereinfacht. Diese Messaginganwendung stellt Leistungsindikatoren bereit, die die Gesamtzahl und Anzahl verarbeiteter Nachrichten pro Sekunde für jeden Verarbeitungsthread, den Worker, von dem der Thread abgeleitet wurde, und den Dienst als Ganzes nachverfolgen. Um diese Funktionalität bereitzustellen, muss man die Leistungsindikatorkategorien definieren und dann entsprechende Zählerinstanzen inkrementieren.

Innerhalb der Service OnStart-Methode werden Leistungsindikatorkategorien definiert. Diese Kategorien stellen die beiden Indikatoren dar; Gesamtanzahl von Nachrichten und Nachrichten, die eine Sekunde verarbeitet wurden:

string perfCounterCatName = "MSDN Message Service";
string perfCounterSecName = "Messages/Total";
string perfCounterTotName = "Messages/Second";
CounterCreationDataCollection messagePerfCounters =
   new CounterCreationDataCollection();
messagePerfCounters.Add(new CounterCreationData(perfCounterTotName,
   "Total Messages Processed", PerformanceCounterType.NumberOfItems64));
messagePerfCounters.Add(new CounterCreationData(perfCounterSecName,
   "Messages a Second", PerformanceCounterType.RateOfCountsPerSecond32));
PerformanceCounterCategory.Create(perfCounterCatName,
   "MSDN Message Service Sample Counters", messagePerfCounters);

Nachdem die Leistungsindikatorkategorien definiert wurden, wird ein PerformanceCounter-Objekt erstellt, um den Zugriff auf Zählerfunktionen instance zu ermöglichen. Das PerformanceCounter-Objekt erfordert den Namen der Kategorie und den Zähler sowie einen optionalen instance Namen. Für den Workerprozess, der den Prozessnamen aus der XML-Konfigurationsdatei verwendet, lautet der Code wie folgt:

string perfCounterWorkerName = threadDefinition.ProcessName;
perfCounterTotWorker = new PerformanceCounter(
   perfCounterCatName, perfCounterTotName, perfCounterWorkerName, false);
perfCounterSecWorker = new PerformanceCounter(
   perfCounterCatName, perfCounterSecName, perfCounterWorkerName, false);
perfCounterTotWorker.RawValue = 0;
perfCounterSecWorker.RawValue = 0;

Das Erhöhen der Indikatoren ist dann einfach eine Frage des Aufrufens der entsprechenden Methode:

perfCounterTotWorker.IncrementBy(1);
perfCounterSecWorker.IncrementBy(1);

Abschließend sollte die installierte Leistungsindikatorkategorie beim Beenden des Diensts aus dem System gelöscht werden:

PerformanceCounterCategory.Delete(perfCounterCatName);

Das Löschen der Leistungsindikatorkategorie beim Beenden des Diensts bedeutet, dass die Dienstleistungsindikatoren nur während der Ausführung des Diensts vorhanden sind. Der Grund dafür ist, dass der Dienststartvorgang ausgeführt wird, wenn die XML-Konfigurationsdatei verarbeitet wird und somit die Leistungsindikatorinstanzen bekannt sind. Das Beenden und Starten des Diensts erzwingt, dass die XML-Konfigurationsdatei erneut verarbeitet wird.

Sprachneutralität

Wie bereits erwähnt, kann man mithilfe der .NET Framework hochfunktionelle Fenster und Webanwendungen und Dienste mit C#, Visual Basic .NET oder Managed C++ (MC++) schreiben. Innerhalb dieser Messaging-Warteschlangendienstanwendung stammen alle Funktionen für die Implementierung eines Windows-Diensts, das Verwalten von Threads, die Verarbeitung von Nachrichtenwarteschlangen, die Leistungsüberwachung, Ereignisprotokollierung, Ausnahmebehandlung und XML-Verarbeitung aus dem .NET Framework.

Um diese Anweisung nachzuweisen, ist im herunterladbaren Code die Visual Basic .NET-Version der Messaging-Warteschlangendienstanwendung enthalten. Beispielsweise sieht die vollständige Implementierung der abgeleiteten Klasse, die Nachrichten verarbeitet, indem eine Komponente über die IProcessMessage-Schnittstelle aufgerufen wird, wie folgt aus:

' implementation that calls into an assembly
' that implements the IProcessMessage interface
Friend Class WorkerThreadAssembly
   Inherits WorkerThread

   ' refernece to the required object interface
   Private iwmSample As IProcessMessage

   ' reference to the assembly information
   Private sFilePath, sTypeName As String

   ' calls the base class constructor
   Public Sub New (ByVal cParent As WorkerInstance, _
       ByVal wfThread As WorkerThreadFormatter)
      MyBase.new(cParent, wfThread)
   End Sub

   ' when starting obtain reference to assembly and construct object
   Protected Overrides Sub OnStart()
      ' ensure have the assembly path and type name
      sFilePath = wfThread.OutputName(0)
      sTypeName = wfThread.OutputName(1)
      ' obtain a reference to the required object
      InitObject()
   End Sub

   ' when stopping release the resources
   Protected Overrides Sub OnStop()
      ReleaseObject()
   End Sub

   ' override the pause and continue methods
   Protected Overrides Sub OnPause()
      ' when pausing release the object as may be paused for a while
      ReleaseObject()
   End Sub
   Protected Overrides Sub OnContinue()
      ' after a pause re-initialize the object
      InitObject()
   End Sub

   ' method to initialize the object
   Private Sub InitObject()
      ' obtain a reference to the required object 
      Dim asmSample As [Assembly] = [Assembly].LoadFrom(sFilePath)
      Dim typSample As [Type] = asmSample.GetType(sTypeName)
      Dim objSample As Object = Activator.CreateInstance(typSample)
      ' cast to the required interface on the object
      iwmSample = CType(objSample, IProcessMessage)
   End Sub
   ' method to release the obejct
   Private Sub ReleaseObject()
      iwmSample.Release()
      iwmSample = Nothing
   End Sub

   ' method to perform the processing of the message
   Protected Overrides Sub ProcessMessage()
      ' return value from the process call
      Dim wbrSample As ProcessMessageReturn
      ' attempt to call the required Process method
      Try
         ' define the parameters for the method call
         Dim sLabel As String = mInput.Label
         Dim sBody As String = mInput.Body
         Dim iAppSpecific As Integer = mInput.AppSpecific

         ' call the method and catch the return code
         wbrSample = iwmSample.Process(sLabel, sBody, iAppSpecific)
      Catch ex As InvalidCastException
         ' error casting message details force a non critical error
         Throw New WorkerThreadException(ex.Message, False)
      Catch ex As Exception
         ' error calling the assembly termiate the thread processing
         Throw New WorkerThreadException(ex.Message, True)
      End Try
      ' if no error review the return status of the object call
      Select Case wbrSample
         Case ProcessMessageReturn.ReturnBad
            Throw New WorkerThreadException _
               ("Unable to process message: Marked bad", False)
         Case ProcessMessageReturn.ReturnAbort
            Throw New WorkerThreadException _
               ("Unable to process message: Terminating", True)
         End Select
   End Sub

End Class

Um das Konzept der Sprachneutralität weiter zu erweitern, wird auch eine MC++-Beispielkomponente, die die IProcessMessage-Schnittstelle implementiert, mit dem herunterladbaren Code bereitgestellt. Daher kann aus der Visual Basic .NET-Implementierung des Windows-Diensts eine MC++-Komponente verwendet werden, die eine C#-Schnittstelle implementiert.

Die Beispielimplementierung der MC++-Komponente ist in einen Header und eine Quelldatei unterteilt. Die Klassendeklaration ist in der Headerdatei definiert:

#pragma once

#using <System.dll>
#using <System.Data.dll>
#using <MessageInterface.dll>

using namespace System;
using namespace System::Diagnostics;
using namespace System::Data;
using namespace System::Data::SqlClient;
using namespace System::Configuration;
using namespace MSDNMessageService::Interface;

namespace MSDNMessageService
{
   namespace MessageSample
   {

      public __gc class ExampleClass : public IProcessMessage
      {

      private:
         // the event logging class
         EventLog *eventLog;
         String *eventLogSource;
         String *eventLogName;
         Boolean eventLogCreated;

      public:
         ExampleClass()
         {
            // define the definition for the log class
            eventLogSource = S"MSDNMessageService.MessageExample";
            eventLogName = S"Application";
            // create the event log class
            CreateLogClass();
         };

         ~ExampleClass()
         {
            // delete the event log class
            DeleteLogClass();
         };

         ProcessMessageReturn Process(String *messageLabel, String *messageBody, int messageAppSpecific);
         void Release();

      private:
         void CreateLogClass();
         void DeleteLogClass();
         void LogInformation(String *logMessage);
         void LogError(String *logMessage);
         
      };

   };
}

Die tatsächliche Implementierung der Komponente ist in der Quelldatei enthalten:

#include "stdafx.h"
#include "MessageExampleMC.h"

namespace MSDNMessageService
{
   namespace MessageSample
   {
      // process the web message by posting it into the database
      ProcessMessageReturn ExampleClass::Process(String *messageLabel,
         String *messageBody, int messageAppSpecific)
      {
         // define the return variable
         ProcessMessageReturn returnCode =
            ProcessMessageReturn::ReturnGood;
         // try the database operation and if fails throw an error
         try
         {
            // connect to the database
            String *sqlConnection = ConfigurationSettings::
               AppSettings->get_Item("SqlConnection");
            SqlConnection *conNW = new SqlConnection(sqlConnection);
            // setup the command object
            SqlCommand *comNW = new SqlCommand
               ("usp_insert_messageorder", conNW);
            comNW->CommandType = CommandType::StoredProcedure;

            // define the message label parameter
            SqlParameter *labelParam = new SqlParameter
               ("@MessageLabel", SqlDbType::VarChar, 500);
            labelParam->Direction = ParameterDirection::Input;
            labelParam->Value = messageLabel;
            comNW->Parameters->Add(labelParam);

            // define the message body paramater
            SqlParameter *bodyParam = new SqlParameter
               ("@MessageBody", SqlDbType::VarChar, 5000);
            bodyParam->Direction = ParameterDirection::Input;
            bodyParam->Value = messageBody;
            comNW->Parameters->Add(bodyParam);

            // define the message App Specific paramater
            SqlParameter *appSpecificParam = new SqlParameter
               ("@AppSpecific", SqlDbType::Int);
            appSpecificParam->Direction = ParameterDirection::Input;
            appSpecificParam->Value = __box(messageAppSpecific);
            comNW->Parameters->Add(appSpecificParam);

            // execute the stored procedure
            conNW->Open();
            comNW->ExecuteNonQuery();

            // tidyup the database connection
            conNW->Close();
         }
         catch (SqlException &ex)
         {
            if (ex.Number > 16)  // severity test
            {
               LogError(ex.Message);
               returnCode = ProcessMessageReturn::ReturnAbort;
            }
            else
            {
               LogInformation(ex.Message);
               returnCode = ProcessMessageReturn::ReturnBad;
            }
         }
         catch (Exception &ex)
         {
            LogError(ex.Message);
            returnCode = ProcessMessageReturn::ReturnAbort;
         }

         // return the determined status code
         return returnCode;
      };

      // release all resources
      void ExampleClass::Release()
      {
         return;
      };

      // create a log class for writing to the event log
      void ExampleClass::CreateLogClass()
      {
         try
         {
            // create event log source if it does not exist
            if (!EventLog::SourceExists(eventLogSource)) 
            {
               EventLog::CreateEventSource(eventLogSource, eventLogName);
               eventLogCreated = true;
            }
            else
            {
               eventLogCreated = false;
            }
            // open the event log
            eventLog = new EventLog();
            eventLog->Source = eventLogSource;
         }
         catch (Exception&)
         {
            // if the log cannot be initialized null event object
            eventLog = 0;
         }
      };

      // delete the log class
      void ExampleClass::DeleteLogClass()
      {
         try
         {
            // close the event log
            eventLog->Close();
            // if event log source created then delete
            if (eventLogCreated) 
            {
               EventLog::DeleteEventSource(eventLogSource);
            }
         }
         catch (Exception&)
         {
            // nothing to do
         }
      };

      // write an informational message to the event log
      void ExampleClass::LogInformation(String *logMessage)
      {
         try
         {
            if (eventLog != 0)
            {
               eventLog->WriteEntry(logMessage,
                  EventLogEntryType::Information);
            }
         }
         catch (Exception&)
         {
            // nothing to do
         }
      };

      // write an error message to the event log
      void ExampleClass::LogError(String *logMessage)
      {
         try
         {
            if (eventLog != 0)
            {
               eventLog->WriteEntry(logMessage,
                  EventLogEntryType::Error);
            }
         }
         catch (Exception&)
         {
            // nothing to do
         }
      };

   };   
};

Beim Vergleich des verwalteten C++-Codes mit dem des C#-Beispiels gibt es nur sehr wenige Unterschiede, obwohl mc++ in einen Header und eine Quelldatei unterteilt ist. Neben der Verwendung von Zeigern und dem Domänenlöser ist der Standard Unterschied die Verwendung der __box Schlüsselwort (keyword). Dieser Boxvorgang erstellt ein verwaltetes Objekt, das von System.ValueType, einem __value Klassenobjekt, abgeleitet ist.

Dies ist erforderlich, um der SqlParameter-Werteigenschaft Werttypen zuzuweisen. Der INT-Werttyp erfordert boxing, im Gegensatz zum String-Typ, bei dem es sich bereits um ein verwaltetes Objekt handelt, das von System.Object abgeleitet wird.

Installation

Vor Abschluss wird eine kurze Erwähnung über die Installation und ein Hilfsprogramm namens installutil.exe garantiert. Da es sich bei dieser Anwendung um einen Windows-Dienst handelt, muss sie mit diesem Hilfsprogramm installiert werden. Um dies zu erleichtern, ist eine Klasse erforderlich, die die Installer-Klasse von der System.Configuration.Install-Assembly erbt:

[RunInstaller(true)]
public class ServiceRegister: Installer
{
   private ServiceInstaller serviceInstaller;
   private ServiceProcessInstaller processInstaller;

   public ServiceRegister()
   {      
      // define and create the service installer
       serviceInstaller = new ServiceInstaller();
      serviceInstaller.StartType = ServiceStartMode.Manual;
      serviceInstaller.ServiceName = ServiceControl.ServiceControlName;
      serviceInstaller.DisplayName = ServiceControl.ServiceControlDesc;
       Installers.Add(serviceInstaller);

      // define and create the process installer
      processInstaller = new ServiceProcessInstaller();
      #if RUNUNDERSYSTEM
         processInstaller.Account = ServiceAccount.LocalSystem;
      #else
         // prompt for user and password on install
         processInstaller.Account = ServiceAccount.User;
         processInstaller.Username = null;
         processInstaller.Password = null;
      #endif
       Installers.Add(processInstaller);
   }
}

Wie diese Beispielklasse zeigt, ist für einen Windows-Dienst ein Installationsprogramm für den Dienst und ein weiteres für den Dienstprozess erforderlich, um das Konto zu definieren, unter dem der Dienst ausgeführt wird. In diesem instance ist das Dienstkonto localSystem. Wenn das Benutzerkonto ausgewählt wird, fordert das Installationsprogramm während der Installation zur Eingabe eines Benutzernamens und kennworts auf, es sei denn, beide werden angegeben. Andere Installationsprogramme ermöglichen die Registrierung von Ressourcen wie Ereignisprotokollen und Leistungsindikatoren.

Schlussbemerkung

Wie aus diesem Beispiel .NET Framework Anwendung zu sehen ist, ist das, was bisher nur im Bereich der Visual C++-Programmierer war, jetzt in einem einfachen objektorientierten Programm möglich. Die neue .NET Framework ermöglicht die Entwicklung hochfunktional skalierbarer Windows-Anwendungen und -Dienste aus jeder Programmiersprache. Obwohl sich dieser Artikel auf C# konzentriert, konnte die gleiche Funktionalität problemlos mit Visual Basic .NET oder verwaltetem C++ erreicht werden.

Das neue Framework hat nicht nur die Programmiermöglichkeiten vereinfacht und erweitert, oft vergessene Anwendungsinstrumentation wie Leistungsmonitorindikatoren und Ereignisprotokollbenachrichtigungen lassen sich einfach in Anwendungen integrieren. Dies gilt auch für die Windows-Verwaltungsinstrumentation, obwohl diese in dieser Anwendung nicht verwendet wird.

Referenzen

Carl Nolan arbeitet in Nordkalifornien im Microsoft Technology Center Silicon Valley. Dieses Center konzentriert sich auf die Entwicklung von .NET-Lösungen unter Verwendung der Windows .NET-Plattform. Er ist erreichbar unter carlnol@microsoft.com.