Automate Web Service Calls With Windows Services
Speed up end user data access by calling Web services on a timed basis.
October 30, 2009
Related: "Web Services Client Authentication" and "Web Service Essentials."
Although the .NET platform makesit easy to call Web services from within an ASP.NET Web form, a call across theInternet might cause an undesirable delay to end users depending upon theservice's response time. Developers have come up with several differentsolutions to handle this problem, ranging from Windows Script Host (WSH) filesused in conjunction with a Windows scheduler to employing Global.asax eventsand caching tricks. Although these solutions can get the job done, anothersolution exists in .NET. It relies on Windows Services to access remote data ona timed basis and then caches the data in a database or XML file, and it can beespecially useful when the data only changes every few minutes.
Windows Services (formally knownas NT Services) are processes that run behind the scenes on a Windows server orworkstation. When they're set up properly, they can start automatically,without human intervention, when a computer is rebooted. Given this, they workwell in situations where a long-running program must access remote data on aregular basis. In this article, I'll walk you through the process of creating adynamic Windows Service timer application that you can plug differentcomponents into by simply dropping .NET assemblies into an application folder.
A Windows Service application isuseful when a Web service needs to be called on a frequent basis to getweather, news, or stock information, or when a Web page needs to bescreen-scraped frequently to extract data. It can also be used for tasks suchas checking an FTP site for a new file drop. The application in this article'sdownload calls a weather Web service on a recurring basis and stores theresults as XML. If the main Web service fails, the application switchesautomatically to a backup. The accompanying application also calls a stock marketindex Web service on a timed basis and stores the results as XML. If thisservice fails, the application resorts to screen-scraping a Web page. Thescreen-scraping service is provided to show that the Windows Service can beused for a variety of tasks.
Create a Windows Service
In the "old" days (you know, backin 2001), developers relied on languages such as C++ to write a WindowsService. Today, the .NET platform makes writing these services a snap becausedevelopers 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 .NETprovides excellent support for creating Windows Services using different .NETlanguages.
The Windows Service class createdfor this article (creatively named WindowsService.cs) inherits from a classnamed ServiceBase located in the System.ServiceProcess namespace. ServiceBaseprovides several virtual protected methods (methods that can be overridden by derivedclasses) such as OnStart, OnStop, OnContinue, and OnPause. These methods allowdirect access to the Windows Service processing pipeline, which means you candetect when a service starts, stops, pauses, or continues, and can causespecific 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. TheServiceBase class acts as a wrapper class for Windows Service functionality andallows 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 usedto start and stop the service and its associated processes. It relies onfunctionality available in a helper class named ServiceDispatcher, which I'lldiscuss in a moment. One of the WindowsService class' interesting features isits use of the FileSystemWatcher class (located in the System.IO namespace).This class is instantiated in the WindowsService class' InitializeComponentmethod and is used to constantly monitor a folder for changes. When changeevents fire, the tasks the Windows Service is performing are restarted so thatnew .NET assemblies can automatically be discovered and run. Take a look at theconfiguration 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 Handlerpublic 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. TheFileSytemWatcher class is useful when folders and files need to be monitored.The code shown here detects changes to the application's folder contents andrestarts the service's tasks automatically as new files are added.
Leverage Reflection andInterfaces
The custom ServiceDispatcher classis the workhorse of the Windows Service application. This class is responsiblefor loading assemblies from a configuration file and calling the StartTimer andStopTimer methods that are defined in the IService interface:
public interface IService { void StartTimer; void StopTimer;}
Any class that implements theIService interface can have a timer started and stopped through an instance ofthe ServiceDispatcher class.
By leveraging interfaces andpolymorphism, any assembly containing a class that implements IService can bedropped into the Windows Service's application folder and automatically haveits timer started and stopped without any code recompilation. This makes theapplication dynamic and allows new components to be deployed quickly andeasily.
So how does all of this work?First, a class must be written that implements the IService interface. Thisclass is responsible for starting a timer and gathering data (or performingother activities) when the StartTimer method is called. When StopTimer iscalled, the class is responsible for stopping all activity. The WeatherServiceclass (covered later) included in this article's download code is an example ofa class that implements IService. When the StartTimer method is called, a Webservice proxy is used to call a weather Web service asynchronously. When thecall returns, the results are stored as XML on the file system (the resultscould just as easily be stored in a database or alternate data store). WhenStopTimer is called, the proxy stops all Web service calls.
For the WeatherService class to beinstantiated and used by ServiceDispatcher, it must be defined in theApp.config configuration file, which lives at the root of the Windows Serviceapplication. Within the configuration file is a section named Services thatcontains the name of the assembly and namespace qualified class name within theassembly that should be called by ServiceDispatcher (see Figure 4).
Figure 4. TheApp.config file defines the assemblies that implement the IService interface.The configuration values are used to load the defined assemblies and typesusing reflection.
When the Windows Service isstarted, the ServiceDispatcher class is instantiated by calling the service'sStartServices method (refer back to Figure 2). ServiceDispatcher'sStartServices method is then invoked. This method reads different settings inthe App.config file and uses reflection to load and instantiate classes thatimplement the IService interface. Each instantiated object is added to anArrayList to track all of the objects that are currently running. When the objectsneed to be stopped (due to a configuration file change or someone stopping theWindows Service manually, for instance), the ServiceDispatcher class'StopServices method is called, which calls the StopTimer method of each objectthat implements IService. I've included the complete code for theServiceDispatcher 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 + "" + 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 + "" + exp.StackTrace); } }}
Figure 5. The ServiceDispatcherclass uses reflection to dynamically load assemblies defined in the App.configconfiguration file. Any class that implements IService can be started andstopped by ServiceDispatcher.
Implement IService
Now that you've seen the buildingblocks of the Windows Service application, let's examine the WeatherServiceclass I mentioned earlier. This class is used to call a weather Web serviceasynchronously and store the results in an XML file. The portion of the classthat 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. Thiscode demonstrates how the IService interface can be implemented. Because theWeatherService class implements this interface, the ServiceDispatcher class cancount on it exposing the StartTimer and StopTimer methods.
When ServiceDispatcher callsStartTimer, the Timer class (located in the System.Timers namespace) isinstantiated and its Elapsed event is hooked up to a method named Timer_WeatherElapsedusing the ElapsedEventHandler delegate. The Timer_WeatherElapsed method isresponsible 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 + "" + e.StackTrace); //Try backup service this.GetWeather2(); } }} //Callback method for async callprivate 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 + "" + e.StackTrace); }}
Figure 7. Afterthe timer elapses, the Timer_WeatherElapsed method is called automatically.This causes the Web service proxy to be instantiated and used to make anasynchronous call. If the Web service call fails, a backup method namedGetWeather2 is called.
After the Web service call ismade, the results are captured and stored as XML using the System.Xmlnamespace'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 + "" + e.StackTrace); } finally { writer.Close(); }}
Figure 8. The XmlTextWriter provides a fast streaming model forgenerating XML documents.
The XmlTextWriter provides a fast,forward-only stream model that is extremely efficient when XML files need to becreated from scratch. The generated XML file can be used as a cache dependencyin the ASP.NET caching model; this gives users fast and efficient access todifferent types of Web service data (such as weather, market indices, news, andso 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 XMLthe WeatherService class generates can be used to hook into the ASP.NET cachemechanism, which can result in better page scalability and performance. Another class included in thedownloadable code named StockQuoteService implements IService and is used tostart and stop a timer. When the timer elapses, two different objects arecalled depending on whether the XML file is generated successfully. The firstobject (StockQuoteServiceProxy) attempts to call a stock quote Web serviceasynchronously. If the call fails, another class (StockQuoteRegEx) isinstantiated that screen-scrapes an HTML page providing stock market index datausing the RegEx class located in the System.Text.RegularExpressions namespace.I've included an example of the XML generated by calling these classes (seeFigure 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 XMLgenerated by the StockQuoteService class contains different market index data. The application included with thisarticle's download supports calling remote servers and Web services throughproxies, variable timer settings for each component, automatic logging to theevent log, custom XML configuration files, and more. To try the Windows Serviceon your system, run the included MSI file, install it, go to Services in theControl Panel, then start the service named EBCWindowsService (see the .NET SDKif you'd like more information on creating Windows Service installers). If yougo through a proxy server, you'll need to add the necessary proxy informationto the configuration file (EBCWindowsService.exe.config) installed with theWindows Service. The configuration file is full of comments explaining thepurpose of each configuration setting. Although you can certainly usealternatives to Windows Services to aggregate data on a regular basis, thesolution shown here provides a robust mechanism for handling this task and hasthe added benefit of starting automatically after server reboots. Now that .NETdirectly supports creating Windows Services, you can build them using yourpreferred .NET language. The samplecode in this article is available for download. Dan Wahlin(Microsoft Most Valuable Professional for ASP.NET and XML Web services) is thepresident of Wahlin Consulting and founded the XML for ASP.NET Developers Website (http://www.XMLforASP.NET), whichfocuses on using XML and Web services in Microsoft's .NET platform. He's also acorporate trainer and speaker, and teaches XML and .NET training courses aroundthe U.S. Dan co-authored Professional Windows DNA (Wrox, 2000) and ASP.NET: Tips, Tutorialsand Code (Sams, 2001), and authored XML for ASP.NET Developers (Sams, 2001).
Read more about:
MicrosoftAbout the Author
You May Also Like