Skip navigation

Automate Web Service Calls With Windows Services

Speed up end user data access by calling Web services on a timed basis.

Related: "Web Services Client Authentication" and "Web Service Essentials."

Although the .NET platform makes it easy to call Web services from within an ASP.NET Web form, a call across the Internet might cause an undesirable delay to end users depending upon the service's response time. Developers have come up with several different solutions to handle this problem, ranging from Windows Script Host (WSH) files used in conjunction with a Windows scheduler to employing Global.asax events and caching tricks. Although these solutions can get the job done, another solution exists in .NET. It relies on Windows Services to access remote data on a timed basis and then caches the data in a database or XML file, and it can be especially useful when the data only changes every few minutes.

Windows Services (formally known as NT Services) are processes that run behind the scenes on a Windows server or workstation. When they're set up properly, they can start automatically, without human intervention, when a computer is rebooted. Given this, they work well in situations where a long-running program must access remote data on a regular basis. In this article, I'll walk you through the process of creating a dynamic Windows Service timer application that you can plug different components into by simply dropping .NET assemblies into an application folder.

A Windows Service application is useful when a Web service needs to be called on a frequent basis to get weather, news, or stock information, or when a Web page needs to be screen-scraped frequently to extract data. It can also be used for tasks such as checking an FTP site for a new file drop. The application in this article's download calls a weather Web service on a recurring basis and stores the results as XML. If the main Web service fails, the application switches automatically to a backup. The accompanying application also calls a stock market index Web service on a timed basis and stores the results as XML. If this service fails, the application resorts to screen-scraping a Web page. The screen-scraping service is provided to show that the Windows Service can be used for a variety of tasks.

 

Create a Windows Service

In the "old" days (you know, back in 2001), developers relied on languages such as C++ to write a Windows Service. Today, the .NET platform makes writing these services a snap because developers are able to use languages such as C# or VB .NET. Simply select the "Windows Service" template Visual Studio .NET provides and you're in business (see Figure 1).


Figure 1. Visual Studio .NET provides excellent support for creating Windows Services using different .NET languages.

The Windows Service class created for this article (creatively named WindowsService.cs) inherits from a class named ServiceBase located in the System.ServiceProcess namespace. ServiceBase provides several virtual protected methods (methods that can be overridden by derived classes) such as OnStart, OnStop, OnContinue, and OnPause. These methods allow direct access to the Windows Service processing pipeline, which means you can detect when a service starts, stops, pauses, or continues, and can cause specific code to execute in response to these events (see Figure 2).

protected override void OnStart(string[] args) {

  StartServices();

}

 

protected override void OnContinue() {

  StartServices();

}

 

protected override void OnStop() {

  StopServices();

}

 

protected override void OnPause() {

  StopServices();

}

 

public void StartServices() {

  if (dispatcher == null) {

    dispatcher = new ServiceDispatcher();

  }

  dispatcher.StartServices();

}

 

public void StopServices() {

  if (dispatcher != null) {

    dispatcher.StopServices();

  }

}

 

public void ResetServices() {

  // Stop and then start

  StopServices();

  System.Threading.Thread.Sleep(5000); //pause 5 seconds

  StartServices();

}

Figure 2. The ServiceBase class acts as a wrapper class for Windows Service functionality and allows developers to tie into the process of starting, stopping, continuing, and pausing a service. It relies on the ServiceDispatcher class (covered later) for starting and stopping timers contained within classes.

The WindowsService class is only used to start and stop the service and its associated processes. It relies on functionality available in a helper class named ServiceDispatcher, which I'll discuss in a moment. One of the WindowsService class' interesting features is its use of the FileSystemWatcher class (located in the System.IO namespace). This class is instantiated in the WindowsService class' InitializeComponent method and is used to constantly monitor a folder for changes. When change events fire, the tasks the Windows Service is performing are restarted so that new .NET assemblies can automatically be discovered and run. Take a look at the configuration file monitoring code (see Figure 3).

private void InitializeComponent() {

  components = new System.ComponentModel.Container();

  this.ServiceName = "EBCWindowsService";

 

  string appPath =

    Assembly.GetExecutingAssembly().Location;

  appPath = appPath.Substring(0,appPath.LastIndexOf(@"\"));

 

  watcher = new FileSystemWatcher();

  watcher.Path = appPath;

  watcher.NotifyFilter = NotifyFilters.LastWrite;

 

 

  // Add event handlers

  watcher.Changed +=

    new FileSystemEventHandler(File_OnChanged);

  watcher.Created +=

    new FileSystemEventHandler(File_OnChanged);

 

  // Begin watching for creations/changes

  watcher.EnableRaisingEvents = true;

}

 

// FileSystemWatcher Event Handler

public void File_OnChanged

   (object source, FileSystemEventArgs e){

 

  // Trick to prevent multiple calls from

  // being made on a single folder change

  TimeSpan span = DateTime.Now.Subtract(lastChangeTime);

  if (span.Seconds >= 2) {

    ResetServices();

    lastChangeTime = DateTime.Now;

  }

}

Figure 3. The FileSytemWatcher class is useful when folders and files need to be monitored. The code shown here detects changes to the application's folder contents and restarts the service's tasks automatically as new files are added.

 

Leverage Reflection and Interfaces

The custom ServiceDispatcher class is the workhorse of the Windows Service application. This class is responsible for loading assemblies from a configuration file and calling the StartTimer and StopTimer methods that are defined in the IService interface:

public interface IService {

 void StartTimer;

 void StopTimer;

}

Any class that implements the IService interface can have a timer started and stopped through an instance of the ServiceDispatcher class.

By leveraging interfaces and polymorphism, any assembly containing a class that implements IService can be dropped into the Windows Service's application folder and automatically have its timer started and stopped without any code recompilation. This makes the application dynamic and allows new components to be deployed quickly and easily.

So how does all of this work? First, a class must be written that implements the IService interface. This class is responsible for starting a timer and gathering data (or performing other activities) when the StartTimer method is called. When StopTimer is called, the class is responsible for stopping all activity. The WeatherService class (covered later) included in this article's download code is an example of a class that implements IService. When the StartTimer method is called, a Web service proxy is used to call a weather Web service asynchronously. When the call returns, the results are stored as XML on the file system (the results could just as easily be stored in a database or alternate data store). When StopTimer is called, the proxy stops all Web service calls.

For the WeatherService class to be instantiated and used by ServiceDispatcher, it must be defined in the App.config configuration file, which lives at the root of the Windows Service application. Within the configuration file is a section named Services that contains the name of the assembly and namespace qualified class name within the assembly that should be called by ServiceDispatcher (see Figure 4).



  

    

    

  

Figure 4. The App.config file defines the assemblies that implement the IService interface. The configuration values are used to load the defined assemblies and types using reflection.

When the Windows Service is started, the ServiceDispatcher class is instantiated by calling the service's StartServices method (refer back to Figure 2). ServiceDispatcher's StartServices method is then invoked. This method reads different settings in the App.config file and uses reflection to load and instantiate classes that implement the IService interface. Each instantiated object is added to an ArrayList to track all of the objects that are currently running. When the objects need to be stopped (due to a configuration file change or someone stopping the Windows Service manually, for instance), the ServiceDispatcher class' StopServices method is called, which calls the StopTimer method of each object that implements IService. I've included the complete code for the ServiceDispatcher class (see Figure 5).

public class ServiceDispatcher {

  ArrayList servicesArray = null;

 

  public ServiceDispatcher() {

    servicesArray = new ArrayList();

  }

 

  public void StartServices() {

    try {

      string appFullPath =

        Assembly.GetCallingAssembly().Location;

      string appPath =

       appFullPath.Substring(0,

         appFullPath.LastIndexOf("\\"));

      string configPath = appFullPath + ".config";

      //Load config data

      ConfigurationLookup serviceConfig =

        new ConfigurationLookup(configPath);

      serviceConfig.SectionGroup = "ServicesConfiguration";

 

      //Get ServicesConfiguration data

      serviceConfig.Section = "Services";

      XmlNodeList keyNodes = serviceConfig.GetKeyNodes();

 

      foreach (XmlNode node in keyNodes) {

        string classType = node.Attributes["value"].Value;

        //Load Assembly

        Assembly assembly = Assembly.LoadFrom(appPath +

         "\\" + node.Attributes["key"].Value);

        //Create Type instance

        object obj =

          assembly.CreateInstance(classType,true);

        //Cast object to IService

        IService service = (IService)obj;

        //Call StartTimer() - part of IService interface

        service.StartTimer();

        servicesArray.Add(service);

      }

    } catch (Exception exp) {

      Logger.WriteToLog("EBCServices","EBCServicesLog",

        "EBCSvc failed to start: " + exp.Message + "\n\n" +

        exp.StackTrace);

    }

  }

 

  public void StopServices() {

    try {

      IEnumerator enumerator =

        servicesArray.GetEnumerator();

      while (enumerator.MoveNext()) {

        IService service = (IService)enumerator.Current;

        if (service != null) {

          service.StopTimer();

        }        

      }

      servicesArray.Clear();

    } catch (Exception exp) {

      Logger.WriteToLog("EBCServices","EBCServicesLog",

        "EBCSvc failed to stop: " + exp.Message +

        "\n\n" + exp.StackTrace);

    }

  }

}

Figure 5. The ServiceDispatcher class uses reflection to dynamically load assemblies defined in the App.config configuration file. Any class that implements IService can be started and stopped by ServiceDispatcher.

 

Implement IService

Now that you've seen the building blocks of the Windows Service application, let's examine the WeatherService class I mentioned earlier. This class is used to call a weather Web service asynchronously and store the results in an XML file. The portion of the class that implements IService is shown in Figure 6.

public class WeatherService : IService {

 

...

 

  public void StartTimer() {

    weatherTimer = new Timer();

    weatherTimer.Interval = Double.Parse(weatherInterval);

    weatherTimer.Elapsed +=

      new ElapsedEventHandler(Timer_WeatherElapsed);

    weatherTimer.Start();

  }

 

  public void StopTimer() {

    weatherTimer.Stop();

  }

 

...

 

}

Figure 6. This code demonstrates how the IService interface can be implemented. Because the WeatherService class implements this interface, the ServiceDispatcher class can count on it exposing the StartTimer and StopTimer methods.

When ServiceDispatcher calls StartTimer, the Timer class (located in the System.Timers namespace) is instantiated and its Elapsed event is hooked up to a method named Timer_WeatherElapsed using the ElapsedEventHandler delegate. The Timer_WeatherElapsed method is responsible for making an asynchronous call to the desired weather Web service (see Figure 7).

private void Timer_WeatherElapsed(object sender,

  ElapsedEventArgs e) {

  this.GetWeather1();

}

 

private void GetWeather1() {

  //Call weather Web service

  if (zipCode == null) {

    Logger.WriteToLog("EBCServices","EBCServicesLog",

    "ZipCode not provided in configuration file.");

  } else {

    if (logDetails)

      Logger.WriteToLog("EBCServices","EBCServicesLog",

      "Getting weather data.");

    try {

      weatherProxy1 = new WeatherFetcherProxy();

      //Handle going through proxy server

      if (proxy != null) {

        ICredentials credential =

          new NetworkCredential(uid, password, domain);

        IWebProxy proxyServer  = new WebProxy(proxy, true);

        proxyServer.Credentials = credential;

        weatherProxy1.Proxy = proxyServer;

      }

      //Make Asynchronous call

      AsyncCallback callBack =

       new AsyncCallback(this.WeatherProxy1_AsyncCallBack);

      weatherProxy1.BeginGetLicWeather(zipCode,

       weatherWSKey, callBack,null);

    } catch (Exception e) {

      weatherProxy1.Abort();

      Logger.WriteToLog("EBCServices","EBCServicesLog",

        "Error getting weather data: " + e.Message +

        "\n\n" + e.StackTrace);

      //Try backup service

      this.GetWeather2();

    }

  }

}

 

//Callback method for async call

private void WeatherProxy1_AsyncCallBack

   (IAsyncResult result) {

  try {

    Weather weather =

      (Weather)weatherProxy1.EndGetWeather(result);

    if (weather.Temperature != null &&

        weather.Temperature != String.Empty) {

      this.GenerateWeatherXml1(weather);

    } else {

      this.GetWeather2();

    }

  } catch (Exception e) {

    this.GetWeather2();

      Logger.WriteToLog("EBCServices","EBCServicesLog",

      "Weather retrieval failed:   " + e.Message +

      "\n\n" + e.StackTrace);

  }

}

Figure 7. After the timer elapses, the Timer_WeatherElapsed method is called automatically. This causes the Web service proxy to be instantiated and used to make an asynchronous call. If the Web service call fails, a backup method named GetWeather2 is called.

After the Web service call is made, the results are captured and stored as XML using the System.Xml namespace's XmlTextWriter (see Figure 8).

private void GenerateWeatherXml1(Weather weather) {

  XmlTextWriter writer = null;

  try {

    writer =

      new XmlTextWriter(weatherDocumentPath,

      Encoding.UTF8);

    writer.Formatting = Formatting.Indented;

    writer.WriteStartDocument();

    writer.WriteStartElement("root");

    writer.WriteStartElement("weatherData");

    writer.WriteElementString("Skies",weather.Conditions);

    writer.WriteElementString

       ("Temperature",weather.Temperature);

    writer.WriteElementString("Humidity",weather.Humidity);

 

    writer.WriteElementString("Wind",weather.Wind);

    writer.WriteElementString("updateDateTime",

      DateTime.Now.ToShortDateString() + " " +

      weather.Time);

    writer.WriteEndElement();

    writer.WriteEndElement(); //root

    if (logDetails) Logger.WriteToLog("EBCServices",

      "EBCServicesLog","Weather data saved.");

  }

  catch (Exception e) {

    if (logDetails) Logger.WriteToLog("EBCServices",

       "EBCServicesLog",

      "Error occurred saving weather data " +

      "(GenerateWeatherXml1): " + e.Message + "\n\n" +

      e.StackTrace);

  }

  finally {

    writer.Close();

  }

}

Figure 8. The XmlTextWriter provides a fast streaming model for generating XML documents.

The XmlTextWriter provides a fast, forward-only stream model that is extremely efficient when XML files need to be created from scratch. The generated XML file can be used as a cache dependency in the ASP.NET caching model; this gives users fast and efficient access to different types of Web service data (such as weather, market indices, news, and so on). See Figure 9 for an example of the XML that's generated.





  

    clear sky; no ceiling

    102

    12

    

      prevailing wind is 2.058 m/s from NW(310')

    

    9/29/2003 8:07 PM

  

Figure 9. The XML the WeatherService class generates can be used to hook into the ASP.NET cache mechanism, which can result in better page scalability and performance.

Another class included in the downloadable code named StockQuoteService implements IService and is used to start and stop a timer. When the timer elapses, two different objects are called depending on whether the XML file is generated successfully. The first object (StockQuoteServiceProxy) attempts to call a stock quote Web service asynchronously. If the call fails, another class (StockQuoteRegEx) is instantiated that screen-scrapes an HTML page providing stock market index data using the RegEx class located in the System.Text.RegularExpressions namespace. I've included an example of the XML generated by calling these classes (see Figure 10).





  

    $INDU

    Dow Jones Ind Average

    9380.24

     +67.16

    +0.7

    9395.32

    9293.22

    205M

  

  

    $TRAN

    Dow Jones Trans Average

    2710.29

     +46.46

    +1.7

    2710.42

    2660.66

    21.3M

  

Figure 10. The XML generated by the StockQuoteService class contains different market index data.

The application included with this article's download supports calling remote servers and Web services through proxies, variable timer settings for each component, automatic logging to the event log, custom XML configuration files, and more. To try the Windows Service on your system, run the included MSI file, install it, go to Services in the Control Panel, then start the service named EBCWindowsService (see the .NET SDK if you'd like more information on creating Windows Service installers). If you go through a proxy server, you'll need to add the necessary proxy information to the configuration file (EBCWindowsService.exe.config) installed with the Windows Service. The configuration file is full of comments explaining the purpose of each configuration setting.

Although you can certainly use alternatives to Windows Services to aggregate data on a regular basis, the solution shown here provides a robust mechanism for handling this task and has the added benefit of starting automatically after server reboots. Now that .NET directly supports creating Windows Services, you can build them using your preferred .NET language.

The sample code in this article is available for download.

 

Dan Wahlin (Microsoft Most Valuable Professional for ASP.NET and XML Web services) is the 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's also a corporate trainer and speaker, and teaches XML and .NET training courses around the U.S. Dan co-authored Professional Windows DNA (Wrox, 2000) and ASP.NET: Tips, Tutorials and Code (Sams, 2001), and authored XML for ASP.NET Developers (Sams, 2001).

 

 

 

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