Building Office 2003 Research Services That Work Offline

 

John Murray
MedDataSolutions

Chris Kunicki
OfficeZealot.com

June 2004

Applies to:
    Microsoft® Office System

Summary: In this article, we explore a practical approach to develop and deploy research services for offline access by exposing a custom research service locally on the end user computer. This gives users the benefit of research services whether they are in their office plugged into the network or are writing a proposal on a plane without network connectivity. (17 printed pages)

Download odc_RSOfflineResearchServices.EXE.

Download the odc_RSOfflineResearchServices.EXE file. (143 KB)

Contents

Introduction
Research Service Overview
Implementation of an Offline Research Service
Security Considerations
Expanding on the Concept
Conclusion

Introduction

The Microsoft® Office System introduces a new technology called Research services. Research services take advantage of network connectivity to allow users to access relevant research information based on the context of their work in the Microsoft Office System, and to take action on that information. Depending on the Research service, this information may come from confidential corporate servers, fee-based value-add research data from independent software vendor (ISVs), and other free sources from the Internet.

Corporations and ISVs quickly responded to the opportunities made possible by research services. They began to integrate research services into internal applications to allow easier access for employees to critical customer, product, and service information. ISVs in finance, healthcare, and legal services are using research services to make information more integrated and accessible to both their internal and external users. Some even started to tap research services as an additional way to integrate their fee-based services into the Microsoft Office System.

An important advantage of research services is its minimal deployment and administration. Users access research services from a remote server by using Web services using a documented XML schema-based protocol. This XML protocol is invisible to the user, but handles the user's research request and displays the response in a user-friendly way.

Not all applications work well in a connected only world. There are times when users do not have connections to the corporate network or the Internet. In this article, we explore a practical approach to develop and deploy research services that work in an offline mode. This gives users the benefit of research services whether they are in their office plugged into the network or are writing a proposal on a plane without network connectivity.

To solve this problem we expose a research service through a custom Web server running locally on the end-user computer. This approach is relatively simple, and achieved through assembling existing software components with a small amount of code.

To work through this demonstration, you need Microsoft Visual Studio® .NET 2003. With Visual Studio .NET, we use the wizards to build a simple, custom Microsoft Windows® service that launches Cassini, a managed code Web server that we describe here shortly, on the local computer. This Web server hosts the research provider consumed by the research task pane in Office 2003 editions. The code samples are provided in Microsoft Visual C#® development language.

Research Service Overview

Research services are accessed in the Microsoft Office System (and Microsoft Internet Explorer, once you install the Microsoft Office System) through the research task pane (see Figure 1.) By typing search terms in the text box, you can search for results from any of a set of providers. Additional research services are available through custom Web services that implement specific interfaces by using XML. Although the specifics are fascinating, they are beyond the scope of this document, and are well covered in the Microsoft Office 2003 Research Services SDK.

Figure 1. Many useful online services are available through a single Research pane

As mentioned previously, one of the strongest features of the Research platform is also its Achilles heel—you can expose custom research providers only through a Web service. This requires an HTTP connection between the Office application and the research provider. One solution could be to host the research providers on the end-user computer, using Internet Information Services (IIS), but this further limits the potential audience of consumers, to only those who have IIS available and a computer capable of running IIS. Deployment of IIS also brings with it a host of other issues including the need to keep the server patched and secured, which may be outside the threshold of comfort for many end users.

Implementation of an Offline Research Service

Our solution is to deploy a custom Windows service that exposes a limited Web server on the end-user computer. Cassini, a downloadable, managed code server that by default only accepts connections from the local host, already nicely packages the functionality of this Web server. Cassini has more conservative system requirements than IIS. Cassini, in turn, hosts an ASP.NET process responsible for exposing the Web service to serve content to the research task pane in the Microsoft Office System.

Parts of the Solution

There are four parts to the sample solution: the Cassini Web server, a custom Windows service, the IBuySpy sample research service from the Research Service SDK, and registration code to inform the Microsoft Office System of the new provider.

Cassini

Cassini is a free, 100% managed code, community-supported Web server written entirely in C#. Cassini developed out of Microsoft's Web Matrix project and the source is available at the following: Download ASP.NET Cassini Sample Web Server. The Cassini server has a smaller memory footprint than IIS and is configured to accept connections only on the local computer by default.

Using Cassini offers three challenges: there is no caching mechanism for high volume requests; it is unable to handle authentication above what ASP.NET provides; and, most significantly, it wasn't subjected to the same rigorous analysis and testing as IIS. Even with these potential limitations, Cassini does everything we need: it offers a local HTTP connection without requiring IIS or another complicated Web server installation on the user's desktop in a relatively secure manner.

The Custom Windows Service

Cassini is a code library exposed as a managed DLL. Like all well-behaving libraries, you need something to instantiate it. Cassini's source code comes with an executable for configuring and instantiating the library. The drawback to using this application is that is requires users to run a separate application before the research task pane can access the content providers.

To make your research service available with a minimal demand for user interaction, we can write a custom Windows service to handle this instantiation. In our case, this is done using C#, with a little help from Visual Studio .NET and its Windows Service Wizard. We also discuss how to install this new Windows service. In addition to our Windows service, we add an installer class that modifies the registry to tell the Microsoft Office System how to query our offline research service.

IBuySpy Research Provider

The entire research service infrastructure is only as good as the content it provides. For our solution, we needed an existing research service and decided to use the IBuySpy research service sample included with the Microsoft Office 2003 Research Services SDK. You need to download this SDK separately and review the IBuySpy research service sample.

Our sample uses c:\root\cassini_root as the root directory for the research service. Copy the IBuySpy sample to this directory, or modify the ResearchServiceDemo.OnStart() method (listed below) to point the solution to the IBuySpy sample code directory.

Building a Local Research Service

For the IBuySpy sample to run, you must expose it through ASP.NET, which is hosted by Cassini. As both Cassini and our custom code depend on the .NET Framework, you must install the .NET Framework. You also want to download Cassini and build it based on the ReadMe.txt file included with the Cassini download. This build process, using the build.bat script, compiles the binaries, configures ASP.NET for you (if it is not already configured,) and registers the Cassini library into the Global Assembly Cache (GAC). You may find it simpler to run the build.bat script from within the Visual Studio .NET command window. To open the command window, click Start, point to Program Files, point to Visual Studio .NET, point to Visual Studio .NET Tools, and then click Visual Studio .NET Command Prompt. This command window provides the necessary path references to the compiler.

Code Walkthrough

The download accompanying this article contains a sample Visual Studio .NET project that implements the technique we are discussing. If you want to, open the sample project in Visual Studio .NET and follow along. Note that the article download does not include the Research Service SDK or the IBuySpy sample; these must be downloaded separately.

First, we create a Windows service project. To do this, select a new Windows Service project from the New Project menu in Visual Studio .NET. For the name of the project, use ResearchServiceDemo. Once completed, this provides two class files: AssemblyInfo.cs and the class for your Windows service, Service1.cs. The design surface for the new Windows service is visible.

Visual Studio .NET creates a basic Windows service that you must modify. We change the Windows service name to ResearchServiceDemo in both the (Name) and the ServiceName properties. Next, we confirm that the AutoLog property is set to the value of True. When AutoLog is set to True, the Windows service automatically writes events to the Windows event log each time the service starts and stops. The default service configuration also has CanStop set to True; CanHandlePowerEvent,CanPauseAndShutdown, and CanShutdown are all set to False (see Figure 2).

Figure 2. Set your properties to match these

We also add a reference to the Cassini.dll that we created when we ran the build.bat file in the Cassini download. With a reference to the Cassini class library, we are now ready to modify the Windows service created by the Visual Studio .NET wizard.

In the source file for the Windows service, we make a few minor modifications to the generated code. In Visual Studio .NET, in the solution explorer window, if you right click the ResearchServiceDemo.cs file, and select View Code, you can view and directly edit the generated source code. The first edit is in the Main() method of the ResearchServiceDemo class. The Main() method executes when the service first loads and then it creates instances of each class (derived from System.ServiceProcess.ServiceBase) to provide the functionality of the service. This method is intended to allow multiple services to run; however, for brevity, we reduce the code to instantiate only the code responsible for exposing Cassini. Listed below is the new Main() method.

static void Main()
{
   try
   {
   System.ServiceProcess.ServiceBase.Run(new ResearchServiceDemo());
   }
   catch
   {   }
}

When the Service Control Manager (SCM) is ready to start the service, the OnStart method of the object(s) instantiated in **Main()**executes. To expose Cassini when the service is started, we modify the OnStart() method of our ResearchServiceDemo class. From within this method, we could choose to start an instance of Cassini directly; however, for finer control we add another class called CassiniStarter that is responsible for interacting with the Cassini code library. Essentially this is because debugging a Windows service presents some challenges that are outside the scope of this article, and using CassiniStarter allows us to create and debug a majority of the code outside of the service. CassiniStarter exposes properties that allow setting of the network port that Cassini monitors for research requests as well as the directory where the Cassini object looks for the ASP.NET project that exposes the content providing Web service.

Note   You do not need to use port 80. For example, we are using 1968 in our solution.

The main portion of the work is done in the Start() and Stop() methods of CassiniStarter. In the Start() method of CassiniStarter, we start with some initial checks of the provided properties to ensure they are valid. We also check to ensure that there is not an instance of Cassini already running. From here, it's just a matter of instantiating the Cassini object, which you should register in the Global Assembly Cache (GAC) during the build process and then indicate to the Cassini object to start.

public void Start()
{   
   // Let's do a cursory check to make sure 
   // that the appropriate parameters are set
   if(_portNumber < 1)
   {
      throw new InvalidOperationException("The port "
         + "number for this instance is not "
         + "specified and prevents starting the "
         + "server.");
   }

   if((_appDirectory.Length == 0) || 
      (false == Directory.Exists(_appDirectory)))
   {
      throw new InvalidOperationException("The "
         + "application directory specified "
         + "does not exist, or has not been "
         + "set and prevents the server from "
         + "starting.");
   }

   // If we already have an instance 
   // of Cassini, we assume that it 
   // is running, so will call Stop, 
   // which sets the variable to null.
   if(_server != null)
   {
      
      Stop();
   }

   // Create the server and start it.  
   // We are depending on the caller 
   // to handle exceptions that might 
   // be thrown from this operation.
   _server = new Cassini.Server(_portNumber, 
                  virtualRoot, 
                  appDirectory);
   _server.Start();
}

Start() has an evil twin and this, of course, is the Stop() method where, if we have an instance of Cassini already running, we attempt to shut it down nicely and set its variable to null.

public void Stop()
{
   if(_server != null)
   {
      try
      {
         _server.Stop();
      }
      catch(Exception ex)
      {
         throw new Exception("Unable to "
            + "stop server instance: " 
            + ex.Message);
      }
      finally
      {
         // Regardless of how this 
         // function ends up, we 
         // want to get rid of the 
         // current instance of the server.
         _server = null;
      }
   }         
}

Assuming that you now have the complete code in place for CassiniStarter, we just need to create an instance of it in the OnStart method of the ResearchService demo class and call its public Start method. In the constructor for CassiniStarter, we hand it a static location to look for our ASP.NET Web service as a literal string. If you have the IBuySpy research task pane sample stored at a different location, you want to modify this startup directory. You can also modify the code to derive this value dynamically, possibly from an application configuration file. If CassiniStarter.Start() throws an exception, we write an entry to the Windows event log, and gracefully exit. This log entry is important, because the current implementation of Windows services in Visual Studio .NET does not allow us to return a value from OnStart() indicating that startup failed. There are some ways to work around this, but it is outside the scope of this discussion. As a result, if there is a failure in OnStart(), the service appears to start, but cannot serve content. Having the entry in the event log provides some additional information on any problems that may occur.

protected override void OnStart(string[] args)
{
   try
   {
   _cassiniStarter = new CassiniStarter(@"c:\temp\cassini_root", 
                     1968);
   _cassiniStarter.Start();
   }
   catch(Exception e)
   {
      try
      {
         EventLog.WriteEntry("An error occured attempting "
            + "to start the service: " + e.Message);
         throw e;
      } 
      catch { }
   }
}

The OnStop method is called when the SCM decides it's time to shutdown the running service. In our code, this means that we need to ensure that we have a running instance of CassiniStarter (remember, the OnStart method can fail, and the SCM still indicates that the service is running) and if we do, call its Stop method. This, of course, is wrapped in some generic error handling:

protected override void OnStop()
{
   if(_cassiniStarter != null)
   {
      try
      {
         _cassiniStarter.Stop();
      }
      catch(Exception e)
      {
         try
         {
            EventLog.WriteEntry("An error occured "
               +" attempting to stop the service: " 
               + e.Message);
         } 
         catch { }
      }
   }
}

If all you did was compile the code at this point, you produced an exemplary collection of useless bits because the Windows service is not intended to run stand-alone, and is intended to be controlled by the Service Control Manager (SCM) on the client computer. Fortunately, Visual Studio .NET comes to the rescue, by providing a wizard to embed installer instructions in the Windows service executable. This allows you to use the InstallUtil executable (described later) to correctly install and remove the service from the command line so that the SCM is aware of it. To use this wizard, you want to have the design surface visible in Visual Studio .NET. To do so, right click the ResearchServiceDemo.cs source file in Solution Explorer and click View Designer. Once the design surface is visible, you can right click anywhere in the design surface window and click Add Installer from the menu. (see Figure 3).

Figure 3. Add components to your new Windows service

Click Add Installer to add a new file to your project, named ProjectInstaller.cs, and provide a new design surface that looks like Figure 4:

Figure 4. The new file is quite simple in its design view

On the ProjectInstaller design surface, you see two objects: serviceProcessInstaller1 and serviceInstaller1. The serviceProcessInstaller1 item, on the design surface, installs the process that runs the service. By means of review, remember that each Windows service is hosted from within its own process, while that process can host multiple services. In our case, it's academic because we are hosting one service from within one process. Thinking of the service process in terms of the static method that instantiates the ResearchServiceDemo class and the service as the individual instance of the class may also help to clarify the relationship.

The serviceInstaller1 item is responsible for installation tasks specific to the ResearchDemoService service we just wrote. One important item to note is that by default, this installer configures the service to require manual starting by the user, as compared to automatic startup with the system. Using automatic startup during the development phase is discouraged because any errors in the code run the risk of making your system unstable and difficult to recover. To verify this setting, click the serviceInstaller1 object on the design surface and view the StartType property in the properties window. Ensure that this is set to Manual.

Because the research task pane only needs you to indicate the existence of the new, local research provider the first time (the information is stored in the registry), this installer provides an ideal location for the code that tells the research task pane about our new service. Double-click serviceInstaller1 on the design surface to view the associated code. By default, there is a method called serviceInstaller1_AfterInstall that runs after the installation completes. This provides a convenient place to put the code that provides the details of our research service provider into the Windows registry. While a bit on the long side, the code listed below is a relatively straightforward addition of new values to the registry.

private void serviceInstaller1_AfterInstall(object sender, 
      System.Configuration.Install.InstallEventArgs e)
{
   // Get the root registry -- uncomment the line 
   // for Local Machine, and comment the one for 
   // current user if you want to register this 
   // service for everyone 
   // RegistryKey root = Registry.LocalMachine;
   RegistryKey root = Registry.CurrentUser;
   
   //Create the provider entries
   RegistryKey result = root.CreateSubKey(
      @"Software\Microsoft\Office\11.0\Common\Research\Sources\"
      + "{5A1AAF54-87F0-4F1D-A2D4-AB3E5D080DE1}");
   
   result.SetValue("ProviderName",
      "Offline IBuySpyStore.com Research Pane");
   result.SetValue("QueryPath",
      "http://localhost:1968//Query.asmx");
   result.SetValue("Type","SOAP");
   result.SetValue("Status","Enabled");
   result.SetValue("Revision",0);
   result.SetValue("UpdateStatus",1);


   //Create the Service Entries
   RegistryKey ServiceKey = result.CreateSubKey(
      "{1698075D-E2F5-4254-87B2-7FC9E9AB0780}");
   ServiceKey.SetValue("ServiceName", 
      "IBuySpy.com Offline Product Information");
   
   ServiceKey.SetValue("Description", "Congratulations, "
      + "you have successfully infiltrated "
      + "IBuySpy.com Product Information "
      + "Research Pane! IBuySpy is a fictitious "
      + "\"click and mortar\" retailer that "
      + "sells ultra-cool spy gear on its Web "
      + "site. This tool provides information "
      + "on our broad product line.");

   ServiceKey.SetValue("TermsOfUse", 
      "All content Copyright (c) 2003.");
   ServiceKey.SetValue("CategoryID", 0x64000000);
   ServiceKey.SetValue("SortOrder", 0x0);
   ServiceKey.SetValue("Status", "Enabled");
   ServiceKey.SetValue("Display", "On");
   ServiceKey.SetValue("Parental", "Unsupported");
}

If you use a research provider other than the IBuySpy sample, you may need to modify some of these values including ServiceKey, ServiceProvider and ServiceName. The Microsoft Office 2003 Research Services SDK provides documentation for each of these items.

Installation

Now it's time to install and start the Windows service. Using the Visual Studio .NET command prompt, you can navigate to the directory in which your Windows service was built and type the command InstallUtil ResearchServiceDemo.exe. This prompts you for the user name and password of the account with which to run this Windows service. This prompt doesn't allow you to specify that you want to run the Windows service under the Windows System account, but you can modify it to use the System credentials from within Microsoft Management Console (MMC) once the service is installed. Additionally, the user name used to install the window service may require the computer or domain name. For example, my_computer\local_account_name.

Once installed, you want to go into MMC to start the new service. Once started, you can check the event log for the computer to verify if there are any problems starting the service. To remove this service, you can use the command line InstallUtil /u ResearchServiceDemo.exe. If everything works to this point, you can now open the Microsoft Office System and access the Research Services pane as well as see and query the new provider.

Security Considerations

The scope of this article is to demonstrate the concept of developing a local research service provider using a minimum of resources. To this point, we generally glossed over a number of security issues that rightfully you need to consider. Depending on how you intend to distribute this service, the specific security requirements can change dramatically. If you were an ISV providing this service to clients, there may be concerns about protecting your intellectual property, while for a corporate administrator, you may be primarily concerned with ensuring that only authorized users can access the service. For both of these audiences, restricting the ability of ill-intentioned people to do harm to the end user or to change the behavior of the service is probably of paramount concern. With this in mind, what follows is a generalized discussion of some of the security measures that you can integrate into a solution of this type.

Network Security

By default, Cassini only accepts connections on the local host, and as long as the Cassini source remains unmodified, we are not aware of any methods for circumventing this. With that in mind, it's always the threats we can't think of that keep us awake at nights. Standard network security including border routers or personal firewalls that prevent unauthorized traffic from making it to the end-user computer can be useful in addressing this type of security threat.

Client Computer Security

As Cassini only runs on Microsoft Windows Server™ 2003, Microsoft Windows 2000, and Microsoft Windows XP, you have the option of using the NTFS file system to secure the code. This allows you to prevent unauthorized users from editing or viewing your deployed code. For the directory where you install cassini.dll, you can assign Read-Only permissions to the account running the Windows service. The account ASP.NET and the account that runs Cassini must have access to the Web service directory where you install the research service provider. Some of these options, obviously, may not be available for ISVs.

Code Security

From a code security perspective, the primary concern is preventing modification of the behavior of existing code, and goes well beyond restrictions for securing the code on the hard drive. Microsoft provides a fantastic reference for this: Secure Coding Guidelines for the .NET Framework.

An obvious risk is the use of untrusted code. You can take advantage of evidence-based security in the .NET Framework to ensure that your classes can only be instantiated by trusted code when you add the following attributes to each of your classes:

[System.Security.Permissions.PermissionSetAttribute(
System.Security.Permissions.SecurityAction.InheritanceDemand, Name="FullTrust")]

[System.Security.Permissions.PermissionSetAttribute(
   System.Security.Permissions.SecurityAction.LinkDemand, 
   Name="FullTrust")]

The InheritanceDemand attribute in this bit of code allows the base class to demand that any class deriving from it have the requested permissions, which, in our case, is FullTrust. The LinkDemand attribute applies to any methods calling your class and demands that the methods have the required permissions. Note that while the InheritanceDemand attribute applies to the entire call stack, the LinkDemand only applies to the direct caller. It should also be noted that in strong named assemblies, the LinkDemand attribute is applied by default.

You could also prevent derived classes from inheriting classes in your library by using the sealed modifier on the class declarations. In addition, use strong names for classes and sign them with a unique key. Strong names also provide the additional advantage of allowing the common language runtime (CLR) to verify that a given assembly, perhaps your research service executable, was not tampered with since you created it.

When you deploy Cassini, you can create a different key pair from the one included with the assembly. This allows you to ensure the code integrity of the version that you publish (when you first place the code in the GAC rather than each time it executes) but also prevents the Cassini you distribute from conflicting with other installed versions that your users might use. By signing your Windows service executable, you can apply the StrongNameIdentityPermission attribute to the Cassini.dll to ensure that only your Windows service (or other applications signed by your private key) can use the objects contained. The example below shows a statement that you can add just before the class declaration of the Cassini server object implementing this permission.

[ StrongNameIdentityPermission(
SecurityAction.LinkDemand,PublicKey=". . ." ) ]
public class Server : MarshalByRefObject { . . . }

There isn't any support for including multiple keys in this demand as yet, and this restriction appears to apply only to calls, even from within the same assembly. The result is that if you are going to apply this restriction to Cassini, you also must sign the Cassini.dll with the same key used to sign the Windows service, or you get an exception similar to this:

Unhandled Exception: System.Security.SecurityException: 
Request for the permission of type System.Security.Permissions.StrongNameIdentityPermission, 
mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 failed.

Expanding on the Concept

The sample should be easy to implement with your own research services provider. If you already have a content provider, some simple configuration changes can be made to expose them as a local only provider. To do so, copy your ASP.NET Web service project to the directory where you indicated for Cassini (in the code for the Windows service). If so inclined, you can make some simple modifications to the Windows service to instantiate multiple Cassini instances, each running on a different port and each using a different ASP.NET Web service project.

It should be noted that the registration information that we applied through our code in the installer has some information specific to each research service. When implementing a different custom provider exposed through this Windows service instance, you probably want to modify the code (QueryPath, ServiceKey and so on) as appropriate to your own service.

The solution presented here works effectively for a static data source, in a limited environment, but as mentioned previously, this simple concept provides some powerful opportunities to make it much more robust. Listed below are two possible scenarios where this could apply.

Cached Server

Rather than exposing an existing, complete service, on the user desktop, you could modify the Windows service to provide caching services. While the user is connected, the service could cache requests and responses. Then, if a later request is made that is already cached, or if the user is offline, the local service can attempt to provide the results from its local cache. This could be used to reduce the resource requirements (bandwidth and processing power) on other parts of the enterprise infrastructure. You could implement this caching either using a newly implemented ASP.NET layer, or through changes to the Cassini source code or some other combination.

The power of this modification is dependant on the usage patterns of the end users. Typically, caching is useful when users repeat the same query, on the same keywords, multiple times. A specific example of where this may be useful would be a corporate phone directory. Typically, users of this type of application request the same information repeatedly over a span of time. By modifying this solution to examine each request, serving appropriate requests from the cache and forwarding the non-cached responses to the corporate directory you can reduce network traffic and load. In addition, the cache can also serve responses while the user is disconnected.

Local Store, With Replicated Information:

A slight modification on the caching scenario might be where an ISV wants to distribute information that doesn't need to be completely dynamic but should have the ability to be updated when certain conditions (such as a network connection) exist. You could implement the Windows service to wait for changes to a network connection. When that connection appears, the service could query the information provider for changes, and update the local data store.

With the user's permission, the Windows service could also communicate back to the information provider to offer usage statistics or to confirm that licensing for this client is still valid. You can do this type of modification without ever having to modify the online-based version of the research service provider, or the code that instantiates Cassini from within the Windows service process.

Conclusion

Research services are a powerful new addition to the already immense set of features included in the Microsoft Office System, and with a few simple modifications to existing code and a little new code, your users can access content in ways that are terrifically convenient, even when they are offline.

About the authors

John Murray is an experienced software architect and hands on developer with more than 12 years experience delivering client facing solutions. Currently, John is the Vice President of Development for MedDataSolutions, a company providing data collection software to the emergency medical industry. On his downtime, John typically can be found in front of the keyboard working on independent development projects.

Christopher P. Kunicki is a longtime enthusiast of Office development and has been evangelizing Office as an important platform for building solutions for many years. As the founder of OfficeZealot.com, a leading website on Office solutions development, Chris builds enterprise solutions, designs tools for developers, delivers presentations, and writes extensively on the topic of the Microsoft Office System, with the goal of helping developers take control of the world's most powerful software. Get more on Chris and Office at OfficeZealot.com or e-mail him at chris@officezealot.com.

© Microsoft Corporation. All rights reserved.