Skip navigation

Improve Data Exchange

Parse XML data feeds with a Windows service.

XtremeData

LANGUAGES: C#

TECHNOLOGIES: XML | Data Exchange | Parsing | Windows Services

 

Improve Data Exchange

Parse XML data feeds with a Windows service.

 

By Dan Wahlin

 

If you've integrated XML into applications before, you know there's more to it than first meets the eye. To use XML effectively, XML programmers must master a wide range of skill sets, including document architecture, parsing, validation with Document Type Definitions (DTDs) or schemas, and eXtensible Stylesheet Language Transformation (XSLT).

 

In this article, I'll introduce you to another skill you might need when using XML for data exchange between companies or even between internal departments. You'll learn how to create a Windows service that can watch a folder for new XML documents, parse the files, and move the data they contain into a database. First, you'll need to know what a Windows service is. You'll also need to create an XML parsing class, a FileSystemWatcher wrapper class, and a Windows service class, and you'll need to install the Windows service.

 

Before jumping into any code, read the sidebar, "What is a Windows Service?" to find out why you might want to create a Windows service in the first place.

 

Create an XML Parsing Class

The class responsible for parsing the XML documents dropped into the xmlFeeds folder (see sidebar) is named SQLGenerator. It contains two public methods - CreateSQLStatement and ExecuteNonQuery - that parse the XML document, create SQL INSERT statements, and execute the statements against a database.

 

The XmlTextReader class is used to parse the XML in a forward-only manner. You could use several other alternatives instead, such as the dataset or bulk XML import functionality built into databases such as SQL Server 2000. But XmlTextReader provides a fast and efficient API for parsing XML. It's easy to use once you understand its different properties and methods. Although the SQLGenerator class does not validate the XML document with the XmlValidatingReader class before moving its data into the database, you certainly can add this type of functionality as necessary.

 

Figure 1 shows a portion of the XML document (named customers.xml) the SQLGenerator class will parse.

 

    

        

            

                Windows Inc.

                

                    title="Sales Rep" />

            

            

                

                    city="Tempe" state="AZ"

                    zip="85244" country="USA" />

                

                    

                         mobile="480 123-5555" />

                    

                

            

        

    

    

Figure 1. The customers.xml document marks up customer data, including address and phone information. The Windows service I'll discuss later in this article will use the SQLGenerator class to parse the document and create SQL statements to insert the data into a database.

 

Figure 2 shows a portion of the CreateSQLStatement method, which demonstrates how you can use the XmlTextReader class (located within the System.Xml namespace) to parse the customers.xml document shown in Figure 1 and generate SQL INSERT statements dynamically. (The complete code is available in the download files associated with this article.) This code contains several comments to explain what the code is doing at each step.

 

public SQLInfo CreateSQLStatement(string xmlPath) {

    XmlTextReader reader = null;

    //Hashtable used to hold field names and values.

    //This data will be used to construct the SQL.

    Hashtable fieldNamesValues = new Hashtable();

    //StringBuilder holds multiple SQL statements.

    StringBuilder sqlStatements = new StringBuilder();

    bool error = false;

    //Create return object for this method.

    SQLInfo sqlInfo = new SQLInfo();

    try {

      reader = new XmlTextReader(xmlPath);

      //Read through XML tokens in the stream.

      while (reader.Read()) {

         //Stop parsing if problem is encountered.  

        if (!error) {   

          if (reader.NodeType == XmlNodeType.Element) {

            //Get the name of the XML token.

            switch (reader.Name.ToLower()) {

                case "customer":   //Handle customer node.

                  //Read customerID attribute.

                  if (reader.HasAttributes) {

                    string customerID =

                      reader.GetAttribute("id");

                    if (customerID != String.Empty) {

                      fieldNamesValues.Add("CustomerID","'"

                       + customerID + "'");

                    } else {

                      //We need customerID for database,

                      //so end process if it is empty.

                       sqlInfo.Status = false;

                      sqlInfo.StatusMessage =

                       "ID attrib. empty on cust. element";

                      sqlInfo.SQL = null;

                      error = true;

                    }

                   } else {  

                    //We need customerID for database,

                    //so end process.               

                    sqlInfo.Status = false;

                    sqlInfo.StatusMessage =

                     "No attributes on customer element";

                    sqlInfo.SQL = null;

                    error = true;

                  }

                  break;

                  //Other case stmts ommitted for brevity.

            } //End switch.

          } //XmlNodeType check.

        } else {

          break;

        }

        if (reader.NodeType == XmlNodeType.EndElement) {

          if (reader.Name.ToLower() == "customer") {

            string[] FVArray =

             AddSeparator(fieldNamesValues,',');

            string fields = FVArray[0];

            string fieldVals = FVArray[1];

            sqlStatements.Append("INSERT INTO Customers ("

              + fields + ") VALUES (" + fieldVals + ");");

            //Clear out ArrayLists.

            fieldNamesValues.Clear();

          }

        }

      } //End While.

      if (!error) {  

        sqlInfo.Status = true;

        sqlInfo.StatusMessage = String.Empty;

        sqlInfo.SQL = sqlStatements.ToString();

      }

      return sqlInfo;   //Return SQLInfo object from method.

    } catch (Exception exp) {

      sqlInfo.Status = false;

      sqlInfo.StatusMessage = exp.Message +

       "\n\n" + exp.StackTrace;

      sqlInfo.SQL = null;

      return sqlInfo;

    }

    finally { if (reader != null) reader.Close(); }

}

Figure 2. The XmlTextReader class contains several methods you can use to parse an XML document's elements and attributes in order to access data. The code you see in this listing looks for specific node names in the stream of XML tokens the XmlTextReader class creates. As different nodes are found, the names and associated values are added into a HashTable object used to construct one or more SQL INSERT statements.

 

In addition to parsing the customers.xml document, the SQLGenerator class handles executing SQL statements it generates against a database through its ExecuteNonQuery method. This method simply wraps database connection and command-object functionality (see Figure 3).

 

public SQLInfo ExecuteNonQuery(string sql) {

  string connStr =

   ConfigurationSettings.AppSettings["DBConnectionString"];

  SqlConnection conn = null;

  SQLInfo info = new SQLInfo();

  try {

    conn = new SqlConnection(connStr);

    SqlCommand cmd = new SqlCommand(sql,conn);

    conn.Open();

    cmd.ExecuteNonQuery();

    info.Status = true;

  }

  catch (Exception exp) {

    info.Status = false;

    info.StatusMessage = exp.Message;

  }

  finally {

    if (conn.State != ConnectionState.Closed) conn.Close();

  }

  return info;

}

Figure 3. The ExecuteNonQuery method handles executing SQL statements against a database. It wraps the SqlConnection and SqlCommand managed-provider classes.

 

Now that you've seen how XML documents can be parsed and moved into a database, I'll explain the class responsible for watching the xmlFeeds folder and calling the SQLGenerator class's methods.

 

Create a Wrapper Class

You can use the .NET platform in several ways to watch for changes to directories or files. Fortunately, the .NET platform provides a built-in class named FileSystemWatcher that handles these types of operations automatically with little effort on your part. When XML documents are dropped into the folder being watched, you can use the FileSystemWatcher class to detect the file and trigger the parsing process you saw in the previous section. To use the FileSystemWatcher class this way, either you assign the directory and filename you want to watch to the FileSystemWatcher class's Path or Filter properties, respectively, or you pass these values into one of its overloaded constructors:

 

public FileSystemWatcher(

   string path,     //the directory to watch

   string filter   //the file (or files) to watch for

);

 

Because the directory- and file-watching capabilities are called from within a Windows service, a custom class named XmlFileWatcher is created to handle these tasks. Here is this class' shell:

 

public class XmlFileWatcher {

    FileSystemWatcher watcher = null;

    string _directory;

    string _filter;

 

    public XmlFileWatcher(string directory,string filter) {

        _directory = directory;   //the directory to watch

        _filter = filter; //The file(s) to watch for

    }

}

 

The XmlFileWatcher class contains a method named StartXmlFileWatcher, which handles instantiating the FileSystemWatcher class, identifying the directory and file to watch, and hooking up events (such as the Created event) to event handlers. The Created event is fired any time a file is created in the folder the class is watching. FileSystemWatcher exposes several other events that aren't used in this application, such as Changed, Deleted, and Renamed. Figure 4 shows the code for the StartXmlFileWatcher method, as well as the event-handler method for the Created event.

 

public void StartXmlFileWatcher() {

   //WriteToLog() method available in article's

   //downloadable code.

   WriteToLog("Starting XmlImporter file watcher.");

 

   //If a directory is not specified, exit program.

   if (_directory == String.Empty || _directory == null) {

      WriteToLog("XmlImporter: No directory specified " +

      "to watch. Update the application config file");

      throw new Exception("No directory specified");

   }

 

   //Create a new FileSystemWatcher.

   watcher = new FileSystemWatcher(_directory,_filter);

   //Watch for changes in LastAccess and LastWrite times,

   //and the renaming of files or directories.

   watcher.NotifyFilter = NotifyFilters.LastAccess |

      NotifyFilters.LastWrite

      | NotifyFilters.FileName | NotifyFilters.DirectoryName;

 

   //Add event handler to see if a new file is created in

   //the directory being watched. The

   //FileSystemEventHandler delegate is used to hook up

   //Created to a method named File_OnChanged.

   watcher.Created +=

    new FileSystemEventHandler(File_OnChanged);

 

   //Begin watching for XML file...enable events.

   watcher.EnableRaisingEvents = true;

}

 

//Event Handler for Changed event. This will be called

//if a file named customers.xml is created in the

//directory being monitored.

public void File_OnChanged(object source,

 FileSystemEventArgs e) {

   //Pause briefly to allow FileSystemWatcher file handle

   //to be released.

   System.Threading.Thread.Sleep(1000);

 

   //Get path of file that was created.

   string filePath = e.FullPath;

 

   //Call XML import object to move XML into db.

   SQLGenerator gen = new SQLGenerator();

   SQLInfo info = gen.CreateSQLStatement(filePath);

 

   if (info.Status) { //Keep going if SQL generated OK.

      WriteToLog(filePath + " parsed successfully!");

 

      //Execute SQL statement against database.

      SQLInfo dbInfo = gen.ExecuteNonQuery(info.SQL);

      if (dbInfo.Status) {

         WriteToLog(filePath + " data updated " +

                 "successfully in database!");

      } else {

         WriteToLog(filePath + " not updated in db " +

                 "successfully.   Error: " +

                  dbInfo.StatusMessage);

      }

   } else {

      WriteToLog(filePath + " not parsed successfully. " +

       "Error: " + info.StatusMessage);

   }

 

   //Rename the file so we can refer to it if needed

   //in the future.

   File.Move(filePath,filePath + "." +

      Guid.NewGuid().ToString() + ".old");

}

Figure 4. The Windows service calls the StartXmlFileWatcher method. The service uses the StartXmlFileWatcher method to create a new FileSystemWatcher class, specify the directory and file to watch, and handle events thrown by the FileSystemWatcher class, such as the Created event.

 

Now that you've seen how you can use .NET to watch for XML documents and how you can handle events to trigger the parsing of the XML documents, I'll show you how to create a Windows service that leverages the SQLGenerator and XmlFileWatcher classes.

 

Create a Windows Service Class

Using .NET to create a Windows service is surprisingly simple, especially if you have access to Visual Studio .NET. Even if you don't have VS .NET installed, you can create a Windows service relatively quickly in a simple editor such as Notepad.

 

I named the Windows service class created for this article XmlImporter. It inherits functionality from a base class named ServiceBase, located within the System.ServiceProcess namespace. XmlImporter contains a main entry point for the service, the Main method; an overridden Dispose method used to clean up the service; and an overridden OnStart method called when the service is started. You also can override the OnStop, OnPause, OnContinue, and OnShutDown methods if you choose.

 

I chose to use VS .NET to create the XmlImporter Windows service class because it creates the different members I mentioned automatically. It does this by creating a new Windows service VS .NET project. This is the code VS .NET added into the Main method:

 

// The main entry point for the process.

static void Main() {

  System.ServiceProcess.ServiceBase[] ServicesToRun;

 

      //Identify XmlImporter class as service to run.

      //Multiple services could be identified here if needed

  ServicesToRun =

        new System.ServiceProcess.ServiceBase[]

         {new XmlImporter()};

 

      //Run XmlImporter service by calling Run() method

  System.ServiceProcess.ServiceBase.Run(ServicesToRun);

}

 

After the Main method is executed and the service is started through the Control Panel's Services manager (see your system's help file for information on starting and stopping services), the OnStart method is called. The XmlImporter service uses this method to instantiate the XmlFileWatcher class I mentioned earlier, which handles watching a given directory for XML file drops. Here's the OnStart method:

 

protected override void OnStart(string[] args) {

 //Get directory and file information from config file.

 string directory =

  ConfigurationSettings.AppSettings

    ["XmlWatchDirectory"];

 string file =

  ConfigurationSettings.AppSettings

    ["XmlWatchDirectoryFile"];

 

 //Fire up the XmlFileWatcher by passing in the

 //directory and file to watch for.

 watcher = new XmlFileWatcher(directory,file);

 watcher.StartXmlFileWatcher();

}

 

Notice that the directory and file to watch are located within an application-configuration file. This allows different directories and files to be watched without having to recompile and redeploy the Windows service.

 

After creating a Windows service class, an installer class that inherits from System.Configuration.Install.Installer also must be created to help with installing resources associated with the service. This class handles setting up important details such as the username and password (the security context) under which the service runs and the name by which the service will appear in Windows. For this application, the service name will be XmlImportService. If you need the service to run immediately after a reboot, you can specify that the service should run under the local System account. Because the XmlImporter service needs to run constantly, its installer (named XmlImportServiceProjectInstaller) is coded to run under this account.

 

Figure 5 shows the complete installer code the XmlImporter Windows service uses.

 

[RunInstaller(true)]

public class XmlImportServiceProjectInstaller :

  System.Configuration.Install.Installer {

    private System.ServiceProcess.ServiceProcessInstaller

      serviceProcessInstaller;

    private System.ServiceProcess.ServiceInstaller

      serviceInstaller;

    private System.ComponentModel.Container components = null;

 

    public XmlImportServiceProjectInstaller()  {

        InitializeComponent();

    }

 

    private void InitializeComponent() {

        this.serviceProcessInstaller = new

          System.ServiceProcess.ServiceProcessInstaller();

        this.serviceInstaller = new

          System.ServiceProcess.ServiceInstaller();

 

         //Specify that service will run under local System

        //account.

        this.serviceProcessInstaller.Account =

          System.ServiceProcess.ServiceAccount.LocalSystem;

        this.serviceProcessInstaller.Password = null;

        this.serviceProcessInstaller.Username = null;

 

        //Establish service name. This will show up in the

        //Services manager in the Control Panel.

        this.serviceInstaller.ServiceName =

         "XmlImportService";

 

        //Set service start-up type to Automatic.

        this.serviceInstaller.StartType =

          System.ServiceProcess.ServiceStartMode.Automatic;

 

        this.Installers.AddRange(

          new System.Configuration.Install.Installer[] {

               this.serviceProcessInstaller,

                 this.serviceInstaller});

    }

}

Figure 5. The XmlImportServiceProjectInstaller class is necessary to run the XmlImporter class as a Windows service. XmlImportServiceProjectInstaller contains functionality that allows a username, password, and service name to be set so you can install and run the service. The service name given to this application is XmlImportService.

 

Install the Windows Service

After compiling the SQLGenerator, XmlFileWatcher, XmlImporter, and XmlImportServiceProjectInstaller classes, an .exe file is generated. You cannot install the XmlImporter service directly into Windows by executing this file, however, even though you created an installer class for it. Instead, you must create a custom installer project (see the .NET SDK for details on doing this in VS .NET) or use a command-line utility named InstallUtil.exe. Using this utility is extremely straightforward. For example, to install a Windows service named yourWindowsService.exe, type this on the command line:

 

installutil yourWindowsService.exe

 

Uninstalling the service involves using the /u switch:

 

installutil /u yourWindowsService.exe

 

After you run this utility to install the XmlImporter service, it shows up in the Windows Services manager (see Figure 6). After you start the service, files dropped automatically into the xmlFeeds folder are processed using the SQLGenerator class discussed earlier.

 


Figure 6. After the XmlImporter service class is compiled and installed using InstallUtil.exe, it shows up in the Windows Service manager where you can start, stop, or pause it as needed.

 

Windows services provide an excellent way to encapsulate code for long-running applications that must run even if a user isn't logged into the system. In this article, you learned how several classes can be aggregated to create a Windows service that watches a folder for newly created XML files, parses those files, creates dynamic SQL statements, and executes the statements against a database. By writing this type of service, XML data exchange between different systems can become less labor-intensive.

 

The files referenced in this article are available for download.

 

Dan Wahlin received Microsoft's Most Valuable Professional award in the ASP.NET category. He is president of Wahlin Consulting and founded the XML for ASP.NET Developers Web site (http://www.XMLforASP.NET), which focuses on using XML and Web Services in Microsoft's .NET platform. He also is a corporate trainer and speaker, and he teaches XML and ASP.NET training courses around the United States. Dan co-authored Professional Windows DNA (Wrox) and wrote XML for ASP.NET Developers (Sams). E-mail Dan at mailto:[email protected].

 

What is a Windows Service?

The term Windows service was introduced with the release of the .NET platform and replaces the older term NT service, which you might have heard in the past. A Windows service is quite useful in situations where you need a program to run for a long time and start up automatically when a computer is rebooted (even if a user has not logged into the system).

 

Before .NET, creating Windows services was an involved process and required a labor-intensive language such as C++. By using the framework classes .NET provides, you now can create Windows services using intrinsic languages such as C# or VB .NET.

 

Examples of Windows services include performance-monitoring applications, servers, or XML document-parsing services. To see other types of services, locate the Services icon in your computer's Control Panel (Start | Settings | Control Panel | Administrative Tools | Services on Windows 2000) and view the different services available.

 

The Windows service I'll introduce you to in the main article watches a specific folder, named xmlFeeds, for XML documents that are dropped into the folder (through FTP, copy and paste, XCopy, etc.). The FileSystemWatcher class in the System.IO namespace watches the folder, and when XML files are dropped into the folder, FileSystemWatcher instantiates a custom class that uses the XmlTextReader class to parse the XML data.

 

Tell us what you think! Please send any comments about this article to [email protected]. Please include the article title and author.

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish