November, 2008

Posts
  • CarlosAg Blog

    Calling Web Services from Silverlight using IIS 7.0 and ARR

    • 10 Comments

    During this PDC I attended Ian's presentation about WPF and Silverlight where he demonstrated the high degree of compatibility that can be achieved between a WPF desktop application and a Silverlight application. One of the differences that he demonstrated was when your application consumed Web Services since Silverlight applications execute in a sandboxed environment they are not allowed to call random Web Services or issue HTTP requests to servers that are not the originating server, or a server that exposes a cross-domain manifest stating that it is allowed to be called by clients from that domain.

    Then he moved to show how you can work around this architectural difference by writing your own Web Service or HTTP end-point that basically gets the request from the client and using code on the server just calls the real Web Service. This way the client sees only the originating server and it allows the call to succeed, and the server can freely call the real Web Service. Funny enough while searching for a Quote Service I ran into an article from Dino Esposito in MSDN magazine  where he explains the same issue and also exposes a "Compatibility Layer" which again is just code (more than 40 lines of code) to act as proxy to call a Web Service (except he uses the JSON serializer to return the values).

    The obvious disadvantage is that this means you have to write code that only forwards the request and returns the response acting essentially as a proxy. Of course this can be very simple, but if the Web Service you are trying to call has any degree of complexity where custom types are being sent around, or if you actually need to consume several methods exposed by it, then it quickly becomes a big maintenance nightmare trying to keep them in sync when they change and having to do error handling properly, as well as dealing with differences when reporting network issues, soap exceptions, http exceptions, etc.

    So after looking at this, I immediately thought about ARR (Application Request Routing) which is a new extension for IIS 7.0 (see http://www.iis.net/extensions) that you can download for free from IIS.NET for Windows 2008, that among many other things is capable of doing this kind of routing without writing a single line of code.

    This blog tries to show how easy it is to implement this using ARR. Here are the steps to try this: (below you can find the software required), note that if you are only interested in what is really new just go to 'Enter ARR' section below to see the configuration that fix the Web Service call.

    1. Create a new Silverlight Project (linked to an IIS Web Site)
      1. Launch Visual Web Developer from the Start Menu
      2. File->Open Web Site->Local IIS->Default Web Site. Click Open
      3. File->Add->New Project->Visual C#->Silverlight->Silverlight Application
      4. Name:SampleClient, Locaiton:c:\Demo,  Click OK
      5. On the "Add Silverlight Application" dialog choose the "Link this Silverlight control into an existing Web site", and choose the Web site in the combo box.
      6. This will add a SampleClientTestPage.html to your Web site which we will run to test the application.
    2. Find a Web Service to consume
      1. In my case I searched using http://live.com for a Stock Quote Service which I found one at http://www.webservicex.net/stockquote.asmx
    3. Back at our Silverlight project, add a Service Reference to the WSDL
      1. Select the SampleClient project in the Solution Explorer window
      2. Project->Add Service Reference and type http://www.webservicex.net/stockquote.asmx in the Address and click Go
      3. Specify a friendly Namespace, in this case StockQuoteService
      4. Click OK
    4. Add a simple UI to call the Service
      1. In the Page.xaml editor type the following code inside the <UserControl></UserControl> tags:
      2.     <Grid x:Name="LayoutRoot" Background="Azure">
               
        <Grid.RowDefinitions>
                   
        <RowDefinition Height="30" />
                    <
        RowDefinition Height="*" />
                </
        Grid.RowDefinitions>
               
        <Grid.ColumnDefinitions>
                   
        <ColumnDefinition Width="50" />
                    <
        ColumnDefinition Width="*" />
                    <
        ColumnDefinition Width="50" />
                </
        Grid.ColumnDefinitions>
               
        <TextBlock Grid.Column="0" Grid.Row="0" Text="Symbol:" />
                <
        TextBox Grid.Column="1" Grid.Row="0" x:Name="_symbolTextBox" />
                <
        Button Grid.Column="2" Grid.Row="0" Content="Go!" Click="Button_Click" />
                <
        ListBox Grid.Column="0" Grid.Row="1" x:Name="_resultsListBox" Grid.ColumnSpan="3"
                         ItemsSource
        ="{Binding}">
                   
        <ListBox.ItemTemplate>
                       
        <DataTemplate>
                           
        <StackPanel Orientation="Horizontal">
                               
        <TextBlock Text="{Binding Path=Name}" FontWeight="Bold" Foreground="DarkBlue" />
                                <
        TextBlock Text=" = " />
                                <
        TextBlock Text="{Binding Path=Value}" />
                            </
        StackPanel>
                       
        </DataTemplate>
                   
        </ListBox.ItemTemplate>
               
        </ListBox>
           
        </Grid>
      3. Right click the Button_Click text above and select the "Navigate to Event Handler" context menu.
      4. Enter the following code to call the Web Service
      5.     private void Button_Click(object sender, RoutedEventArgs e)
           
        {
               
        var service = new StockQuoteService.StockQuoteSoapClient();
               
        service.GetQuoteCompleted += service_GetQuoteCompleted;
               
        service.GetQuoteAsync(_symbolTextBox.Text);
           
        }
      6. Now, since we are going to use XLINQ to parse the result of the Web Service which is an XML then we need to add the reference to System.Xml.Linq by using the Project->Add Reference->System.Xml.Linq.
      7. Finally, add the following function to handle the result of the Web Service
      8.     void service_GetQuoteCompleted(object sender, StockQuoteService.GetQuoteCompletedEventArgs e)
           
        {
               
        var el = System.Xml.Linq.XElement.Parse(e.Result);
               
        _resultsListBox.DataContext = el.Element("Stock").Elements();
           
        }
    5. Compile the application. Build->Build Solution.
    6. At this point we are ready to test our application, to run it just navigate to http://localhost/SampleClientTestPage.html or simply select the SampleClientTestPage.html in the Solution Explorer and click View In Browser.
    7. Enter a Stock Symbol (say MSFT) and press Go!, Verify that it breaks. You will see a small "Error in page" with a Warning icon in the status bar. If you click that and select show details you will get a dialog with the following message:
    8. Message: Unhandled Error in Silverlight 2 Application An exception occurred during the operation, making the result invalid. 

    Enter Application Request Routing and IIS 7.0

    1. Ok, so now we are running into the cross-domain issue, and unfortunately we don't have a cross-domain here is where ARR can help us call the service without writing more code
    2. Modify the Web Service configuration to call a local Web Service instead
      1. Back in Visual Web Developer, open the file ServiceReferences.ClientConfig
      2. Modify the address="http://www.webservicex.net/stockquote.asmx" to be instead address="http://localhost/stockquote.asmx", it should look like:
      3.     <client>
               
        <endpoint address="http://localhost/stockquote.asmx"
                    binding
        ="basicHttpBinding" bindingConfiguration="StockQuoteSoap"
                    contract
        ="StockQuoteService.StockQuoteSoap" name="StockQuoteSoap" />
            </
        client>
    3. This will cause the client to call the Web Service in the same originating server, now we can configure ARR/URL Rewrite rule to route the Web Service requests to the original end-point
      1. Add a new Web.config to the http://localhost project (Add new item->Web.config)
      2. Add the following content:
      3. <?xml version="1.0" encoding="UTF-8"?>
        <configuration>
           
        <system.webServer>
               
        <rewrite>
                   
        <rules>
                       
        <rule name="Stock Quote Forward" stopProcessing="true">
                           
        <match url="^stockquote.asmx$" />
                            <
        action type="Rewrite" url="http://www.webservicex.net/stockquote.asmx" />
                        </
        rule>
                   
        </rules>
               
        </rewrite>
           
        </system.webServer>
        </configuration>
    4. This rule basically uses regular expression to match the requests for StockQuote.asmx and forwards them to the real Web Service.
    5. Compile everything by running Build->Rebuild Solution
    6. Back in your browser refresh the page to get the new, enter MSFT in the symbol and press Go!
    7. And Voila!!! everything works.

    Summary

    One of the features offered by ARR is to provide proxy functionality to forward requests to another server. One of the scenarios where this functionality is useful is when using it from clients that cannot make calls directly to the real data, this includes Silverlight, Flash and AJAX applications. As shown in this blog, by just using a few lines of XML configuration you can enable clients to call services in other domains without having to write hundreds of lines of code for each method. It also means that I get the original data and that if the WSDL were to change I do not need to update any wrappers. Additionally if using REST based services you could use local caching in your server relying on Output Cache and increase the performance of your applications significantly (again with no code changes).

    Software used

    Here is the software I installed to do this sample(amazing that all of it is completely free):

    1. Install Visual Web Developer 2008 Express
    2. Install Silverlight Tools for Visual Studio 2008 SP 1
    3. Install Application Request Routing for IIS 7.
  • CarlosAg Blog

    Creating a Setup Project for IIS Extensions using Visual Studio 2008

    • 2 Comments

    Introduction

    IIS 7 provides a rich extensibility model, whether extending the server or the user interface, one critical thing is provide a simple setup application that can install all the required files, add any registration information required, and modify the server settings as required by the extension.
    Visual Studio 2008 provides a set of project types called Setup and Deployment projects specifically for this kind of applications. The output generated for these projects is an MSI that can perform several actions for you, including copying files, adding files to the GAC, adding registry keys, and many more.
    In this document we will create a setup project to install a hypothetical runtime Server Module that also includes a User Interface extension for IIS Manager.
    Our setup will basically perform the following actions:
    •    Copy the required files, including three DLL’s and an html page.
    •    Add a couple of registry keys.
    •    Add the managed assemblies to the GAC
    •    Modify applicationHost.config to register a new module
    •    Modify administration.config to register a new UI extensibility for InetMgr
    •    Create a new sample application that exposes the html pages
    •    Finally, we will remove the changes from both configuration files during uninstall

    Creating the Setup Project

    Start Visual Studio 2008. In the File Menu, select the option New Project.
    In the New Project Dialog, expand the Other Project Types option in the Project type tree view.
    Select the option Setup and Deployment type and select the option Setup Project. Enter a name for the Project and a location. I will use SampleSetup as the name.

    image

    Adding Files to the Setup

    • Select the menu View->Editor->File System. This will open the editor where you can add all the files that you need to deploy with your application. In this case I will just add an html file that I have created called readme.htm.
    • To do that, right click the Application Folder directory in the tree view and select the option Add File. Browse to your files and select all the files you want to copy to the setup folder (by default <Program Files>\<Project Name>.

    Adding assemblies to the GAC

    Adding assemblies to the setup is done in the same File System editor, however it includes a special folder called Global Assembly Cache that represents the GAC in the target system.
    In our sample we will add to the GAC the assemblies that have the runtime server module and the user interface modules for IIS Manager. I have created the following set of projects:

    1. SampleModule.dll that includes the runtime module on it.
    2. SampleModuleUI.dll that contains the server-side portion of the IIS Manager extension (ModuleProvider, ModuleService, etc).
    3. SampleModuleUIClient.dll that contains the client side portion of the IIS Manager extension (Module, ModulePage, TaskLists, etc).


    Back in Visual Studio,

    • Select the menu option View->Editor->File System
    • Right-click the root node in the Tree view titled File System on Target Machine and select the option Add Special Folder.
      Select the option Global Assembly Cache Folder.
      Right click the newly added GAC folder and choose the option Add File and browse to the DLL and choose OK. Another option is using the Add Assembly and use the "Select Component" dialog to add it.
      Visual Studio will recognize the dependencies that the assembly has, and will try to add them to the project automatically. However, certain assemblies such as Microsoft.Web.Administration, or any other System assemblies should be excluded because they will already be installed in the target machine.
    • To ensure that you don't ship system assemblies, in the Solution Explorer expand the Detected Dependencies folder and right click each of the assemblies that shouldn't be packaged and select the option Exclude. (In our case we will exclude Microsoft.Web.Administration.dll, Microsoft.Web.Management.dll, Microsoft.ManagementConsole.dll and MMCFxCommon.dll)
      After completing this, the project should look as follows:

    image

    Adding Registry Keys

    Visual Studio also includes a Registry editor that helps you adding any registry keys in the target machine. For this sample I will just add a registry key in HKEY_LOCAL_MACHINE\Software\My Company\Message. For that:
    Select the menu option View->Editor->Registry.
    Expand the HKEY_LOCAL_MACHINE node and drill down to Software\[Manufacturer].
    [Manufacturer] is a variable that holds the name of the company, and can be set by selecting the SampleSetup node in Solution Explorer and using the Property Grid to change it. There are several other variables defined such as Author, Description, ProductName, Title and Version that helps whenever dynamic text is required.
    Right click [Manufacturer] and select the option new String Value. Enter Message as the name. To set the value you can select the item in the List View and use the Property Grid to set its value.
    After completing this, the project should look as follows:

    clip_image002

    Executing Custom Code

    To support any custom code to be executed when running the setup application, Visual Studio (more explicitly MSI) supports the concept of Custom Actions. These Custom Actions include running an application, a script or executing code from a managed assembly.
    For our sample, we will create a new project where we will add all the code  to read and change configuration.
    Select the option File->Add->New Project.
    Select the Class Library template and name it SetupHelper.

    image

    • Since we will be creating a custom action, we need to add a reference to System.Configuration.Install to be able to create the custom action. Use the Project->Add Reference. And in the .NET Tab select the System.Configuration.Install and press OK.
    • Since we will also be modifying server configuration (for registering the HTTP Module in ApplicationHost.config and the ModuleProvider in administration.config) using Microsoft.Web.Administration we need to add a reference to it as well. Again use the Project->Add Reference, and browse to <windows>\system32\inetsrv and select Microsoft.Web.Administration.dll
    • Rename the file Class1.cs file to be named SetupAction.cs and make the class name SetupAction. This class needs to inherit from System.Configuration.Install.Installer which is the base class for all custom actions and it has several methods that you can override to add custom logic to the setup process. In this case we will add our code in the Install and the Uninstall method.
    using System;
    using System.ComponentModel;
    using System.Configuration.Install;

    namespace SetupHelper {
       
    [RunInstaller(true)]
       
    public class SetupAction : Installer {
           
    public override void Install(System.Collections.IDictionary stateSaver) {
               
    base.Install(stateSaver);

               
    InstallUtil.AddUIModuleProvider(
                    "SampleUIModule"
    ,
                    "SampleUIModule.SampleModuleProvider, SampleUIModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=12606126ca8290d1"
               
    );

               
    // Add a Server Module to applicationHost.config
                InstallUtil.AddModule(
                    "SampleModule"
    ,
                    "SampleModule.SampleModule, SampleModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=12606126ca8290d1"
               
    );

               
    // Create a web application
                InstallUtil.CreateApplication(
                    "Default Web Site"
    ,
                    "/SampleApp"
    ,
                   
    Context.Parameters["TargetDir"]
               
    );
           
    }

           
    public override void Uninstall(System.Collections.IDictionary savedState) {
               
    base.Uninstall(savedState);

               
    InstallUtil.RemoveUIModuleProvider("SampleUIModule");
               
    InstallUtil.RemoveModule("SampleModule");
               
    InstallUtil.RemoveApplication("Default Web Site", "/SampleApp");
           
    }
       
    }
    }
       

    As you can see the code above is actually really simple, it just calls helper methods in a utility class called InstallUtil that is shown at the end of this entry. You will also need to add the InstallUtil class to the project to be able to compile it. The only interesting piece of code above is how we pass the TargetDir from the Setup project to the Custom action through the Parameters property of the InstallContext type.

    Configuring the Custom Action

    To be able to use our new Custom Action we need to add the SetupHelper output to our setup project, for that:
    Select the option View->Editor->File System
    Right-click the Application Folder node and select the option Add Project Output... and select the SetupHelper project in the Project drop down.

    image

    After doing this, the DLL will be included as part of our setup.

    Adding the Install Custom Action

    Select the option View->Editor->Custom Actions
    Right-click the Install node and select the option Add Custom Action… drill down into the Application Folder and select the Primary output from SetupHelper.

    image

    Click OK and type a name such as InstallModules

    Now, since we want to pass the TargetDir variable to be used as the physical path for the web application that we will create within our Installer derived-class, select the custom action and go to the Property Grid. There is a property called CustomActionData. This property is used to pass any data to the installer parameters class, and uses the format “/<name>=<value>”. So for our example we will set it to: /TargetDir="[TARGETDIR]\"

    image

    Adding the Uninstall Custom Action

    In the same editor, right-click the Uninstall node and select the option Add Custom Action…, again drill down into the Application Folder and select the Primary output from SetupHelper.
    Press OK and type a name such as UninstallModules.
    After doing this the editor should look as follows:

    image

    Building and Testing the Setup

    Finally we can build the solution by using the Build->Rebuild Solution menu option.
    This will create a file called SampleSetup.msi, in the folder SampleSetup\SampleSetup\Debug\SampleSetup.msi
    You can now run this MSI and it will walk through the process of installing. The user interface that is provided by default can also be configured to add new steps or remove the current steps. You can also provide a Banner logo for the windows and many more options from the View->Editor->User Interface.

    clip_image002[4]clip_image002[6]

    Visual Studio provides different packaging mechanisms for the setup application. You can change it through the Project Properties dialog where you get the option to use:
    1)    As loose uncompressed files. This option packages all the files by just copying them into a file system structure where the files are copied unchanged. This is a good packaging option for CD or DVD based setups
    2)    In setup file. This option packages all the files within the MSI file
    3)    In cabinet files. This option creates a set of CAB files that can be used in scenarios such as diskette based setup.

    You can also customize all the setup properties using the property grid, such as DetectNewerInstalledVersion which will warn users if a newer version is already installed or RemovePreviousVersion that will automatically remove older versions for the user whenever he tries to install a new one.

     

    64-bit considerations

    Turns out that the managed code custom action will fail under 64-bit platform due to it being executed as a 32-bit custom action the following blog talks about the details and shows how you can fix the issue:

    http://blogs.msdn.com/heaths/archive/2006/02/01/64-bit-managed-custom-actions-with-visual-studio.aspx

     

     

    Summary

    Visual Studio 2008 provides a simple option to easily create Setup applications that can perform custom code through Custom actions. In this document we created a simple custom action to install modules and InetMgr extensions through this support.

    For the latest information about IIS 7.0, see the IIS 7 Web site at http://www.iis.net

    InstallUtil

    This is the class that is used from the SetupHelper class we created to do the actual changes in configuration. As you can see it only has six public methods, AddModule, AddUIModuleProvider, CreateApplication, RemoveApplication, RemoveModule, and RemoveUIModule. The other methods are just helper methods to facilitate reading configuration.

    using System;
    using Microsoft.Web.Administration;

    namespace SetupHelper {

       
    public static class InstallUtil {

           
    /// <summary>
            /// Registers a new Module in the Modules section inside ApplicationHost.config
            /// </summary>
            public static void AddModule(string name, string type) {
               
    using (ServerManager mgr = new ServerManager()) {
                   
    Configuration appHostConfig = mgr.GetApplicationHostConfiguration();
                   
    ConfigurationSection modulesSection = appHostConfig.GetSection("system.webServer/modules");
                   
    ConfigurationElementCollection modules = modulesSection.GetCollection();

                   
    if (FindByAttribute(modules, "name", name) == null) {
                       
    ConfigurationElement module = modules.CreateElement();
                       
    module.SetAttributeValue("name", name);
                       
    if (!String.IsNullOrEmpty(type)) {
                           
    module.SetAttributeValue("type", type);
                       
    }

                       
    modules.Add(module);
                   
    }

                   
    mgr.CommitChanges();
               
    }
           
    }

           
    public static void AddUIModuleProvider(string name, string type) {
               
    using (ServerManager mgr = new ServerManager()) {

                   
    // First register the Module Provider 
                    Configuration adminConfig = mgr.GetAdministrationConfiguration();

                   
    ConfigurationSection moduleProvidersSection = adminConfig.GetSection("moduleProviders");
                   
    ConfigurationElementCollection moduleProviders = moduleProvidersSection.GetCollection();
                   
    if (FindByAttribute(moduleProviders, "name", name) == null) {
                       
    ConfigurationElement moduleProvider = moduleProviders.CreateElement();
                       
    moduleProvider.SetAttributeValue("name", name);
                       
    moduleProvider.SetAttributeValue("type", type);
                       
    moduleProviders.Add(moduleProvider);
                   
    }

                   
    // Now register it so that all Sites have access to this module
                    ConfigurationSection modulesSection = adminConfig.GetSection("modules");
                   
    ConfigurationElementCollection modules = modulesSection.GetCollection();
                   
    if (FindByAttribute(modules, "name", name) == null) {
                       
    ConfigurationElement module = modules.CreateElement();
                       
    module.SetAttributeValue("name", name);
                       
    modules.Add(module);
                   
    }

                   
    mgr.CommitChanges();
               
    }
           
    }

           
    /// <summary>
            /// Create a new Web Application
            /// </summary>
            public static void CreateApplication(string siteName, string virtualPath, string physicalPath) {
               
    using (ServerManager mgr = new ServerManager()) {
                   
    Site site = mgr.Sites[siteName];
                   
    if (site != null) {
                       
    site.Applications.Add(virtualPath, physicalPath);
                   
    }
                   
    mgr.CommitChanges();
               
    }
           
    }

           
    /// <summary>
            /// Helper method to find an element based on an attribute
            /// </summary>
            private static ConfigurationElement FindByAttribute(ConfigurationElementCollection collection, string attributeName, string value) {
               
    foreach (ConfigurationElement element in collection) {
                   
    if (String.Equals((string)element.GetAttribute(attributeName).Value, value, StringComparison.OrdinalIgnoreCase)) {
                       
    return element;
                   
    }
               
    }

               
    return null;
           
    }

           
    public static void RemoveApplication(string siteName, string virtualPath) {
               
    using (ServerManager mgr = new ServerManager()) {
                   
    Site site = mgr.Sites[siteName];
                   
    if (site != null) {
                       
    Application app = site.Applications[virtualPath];
                       
    if (app != null) {
                           
    site.Applications.Remove(app);
                           
    mgr.CommitChanges();
                       
    }
                   
    }
               
    }
           
    }

           
    /// <summary>
            /// Removes the specified module from the Modules section by name
            /// </summary>
            public static void RemoveModule(string name) {
               
    using (ServerManager mgr = new ServerManager()) {
                   
    Configuration appHostConfig = mgr.GetApplicationHostConfiguration();
                   
    ConfigurationSection modulesSection = appHostConfig.GetSection("system.webServer/modules");
                   
    ConfigurationElementCollection modules = modulesSection.GetCollection();
                   
    ConfigurationElement module = FindByAttribute(modules, "name", name);
                   
    if (module != null) {
                       
    modules.Remove(module);
                   
    }

                   
    mgr.CommitChanges();
               
    }
           
    }


           
    /// <summary>
            /// Removes the specified UI Module by name
            /// </summary>
            public static void RemoveUIModuleProvider(string name) {
               
    using (ServerManager mgr = new ServerManager()) {
                   
    // First remove it from the sites
                    Configuration adminConfig = mgr.GetAdministrationConfiguration();
                   
    ConfigurationSection modulesSection = adminConfig.GetSection("modules");
                   
    ConfigurationElementCollection modules = modulesSection.GetCollection();
                   
    ConfigurationElement module = FindByAttribute(modules, "name", name);
                   
    if (module != null) {
                       
    modules.Remove(module);
                   
    }

                   
    // now remove the ModuleProvider
                    ConfigurationSection moduleProvidersSection = adminConfig.GetSection("moduleProviders");
                   
    ConfigurationElementCollection moduleProviders = moduleProvidersSection.GetCollection();
                   
    ConfigurationElement moduleProvider = FindByAttribute(moduleProviders, "name", name);
                   
    if (moduleProvider != null) {
                       
    moduleProviders.Remove(moduleProvider);
                   
    }

                   
    mgr.CommitChanges();
               
    }
           
    }
       
    }
    }
Page 1 of 1 (2 items)