Migrating ASP.NET Web Services to WCF

I recently had to migrate a common ASP.NET web service over to WCF, making sure that clients of the former would still be able to use the latter. There were a couple of things I stumbled across, so I am blogging about the minimal steps I had to perform to get clients of the old ASP.NET web service running with the new WCF one. Let’s use the following simple ASP.NET web service for this tiny tutorial.

[WebService(Namespace = "http://foo.bar.com/Service/Math")]
public class MathAddService : WebService
{
    [WebMethod]
    public int Add(int x, int y)
    {
        // Let's ignore overflows here ;-)
        return x + y;
    }
}

The first thing we need to do is create a new interface which offers the same methods as the web service did and mark it as a service contract. This is required because the WCF endpoints are contract based, i.e. they need such an interface. So we extract the public web service interface of the MathAddService class and decorate it with the WCF attributes:

[ServiceContract(Namespace = "http://foo.bar.com/Service/Math")]
[XmlSerializerFormat]
public interface IMathAddService
{
    [OperationContract(Action = "http://foo.bar.com/Service/Math/Add")]
    int Add(int x, int y);
}

The ServiceContract attribute tells WCF to use the same namespace for the web service as ASP.NET did. If you don’t do this, your clients will not be able to use the migrated service because the namespaces don’t match. The XmlSerializerFormat attribute is used to make sure that WCF uses the standard SOAP format for messages. If you don’t specify this, your clients will likely see strange error messages of mismatching operations / messages. Then, for each method you exposed in the former web service, you need to add the exact same signature here, plus make sure that the OperationContract attribute for each method has the Action property set to ‘/’ . Without this, you’ll get another set of exceptions like ‘operation not defined’.

Now the next step is to implement this interface in a class, but we basically already have this in the former MathAddService class. So we just adapt the class’ definition as follows.

[WebService(Namespace = "http://foo.bar.com/Service/Math")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
[ServiceBehavior(Namespace = "http://foo.bar.com/Service/Math")]
public class MathAddService : WebService, IMathAddService
{
    [WebMethod]
    public int Add(int x, int y)
    {
        // Let's ignore overflows here ;-)
        return x + y;
    }
}

As you can see, we’re also adding two new attributes. AspNetCompatibilityRequirements are used to make sure that the new WCF service is really capable of serving old clients. The ServiceBehavior attribute is used to make sure that the WCF hosted service really uses the correct namespace, i.e. the same as the old ASP.NET service used. By the way, you should find all the additional attributes in the System.ServiceModel and System.ServiceModel.Activation namespaces (from the System.ServiceModel assembly).

Now lets get to the configuration of endpoints and bindings for the web service. The following block shows you the new sections in the web.config file for the virtual directory which hosts the WCF service.

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
    <system.web>
        <!-- ... -->
    </system.web>
    <system.serviceModel>
        <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
        <services>
            <service name="MathAddService" behaviorConfiguration="MathAddServiceBehavior">
                <endpoint address=""
                          binding="basicHttpBinding"
                          bindingConfiguration="httpsIwa"
                          bindingNamespace="http://foo.bar.com/Service/Math"
                          contract="IMathAddService"/>
            </service>
        </services>
        <bindings>
            <basicHttpBinding>
                <binding name="httpsIwa">
                    <security mode="Transport">
                        <transport clientCredentialType="Windows" />
                    </security>
                </binding>
            </basicHttpBinding>
        </bindings>
        <behaviors>
            <serviceBehaviors>
                <behavior name="MathAddServiceBehavior">
                    <serviceMetadata httpsGetEnabled="true" />
                    <serviceDebug httpsHelpPageEnabled="true" includeExceptionDetailInFaults="true" />
                </behavior>
            </serviceBehaviors>
        </behaviors>
    </system.serviceModel>
</configuration>

As you can see on lines 19 and 20, we are using HTTPS and IWA for this particular binding, but you should of course make it the same as you had for your ASP.NET service. If you served all requests without HTTP based authentication and without SSL/TLS, then you should stick to that so you don’t break your clients :). You have to make sure that you are offering at least one basicHttpBinding, because that’s what closest matches the ASP.NET SOAP interface.

Finally, we add a new file called ‘MathAddService.svc’ in the virtual directory on IIS with the following contents.

<%@ ServiceHost Service="MathAddService" %>

This will use the implementation of the MathAddService class to serve the request for the IMathAddService interface. Of course your clients will have to be updated to use the new URL now (or you can try a 302 redirect but depending on the client’s policies, this may fail). In case your requests to the new SVC file produce strange results (or send you back the above contents of the file), in the IIS administrative tools make sure that the .svc extension is mapped properly. If it isn’t, you can run the aspnet_regiis.exe tool from the .NET framework to get that done.

Using the Agent WebService of the Response Group Service

Your Office Communications Server 2007 R2, Response Group Service deployment comes with a tiny but nice little addition: the Agent WebService. It basically offers exactly the same data and functionality as the Agent OC tab but does this through a SOAP interface. If you have RGS deployed on a pool with the FQDN ‘ocs-pool-01.contoso.com’, you’ll find the Agent OC tab at https://ocs-pool-01.contoso.com/Rgs/Clients/Tab.aspx. The Agent WebService is then located at https://ocs-pool-01.contoso.com/Rgs/Clients/ProxyService.asmx. If you are running the OCS WebComponents on other machines than the front ends, then the host name is the FQDN of the WebComponents machine/farm. So here’s how you can write your own client to sign in/out with RGS Agent Groups. The TechNet article Deploying Response Group Clients gives more information about deploying RGS Clients, with focus on the Agent OC Tab.

Generating the Client Proxy

The first thing you typically want to do is to actually generate the proxy code which you will compile into your own client. You do so by calling

wsdl.exe /namespace:RgsAgentService /language:cs /out:RgsAgentService.cs https://ocs-pool-01.contoso.com/Rgs/Clients/ProxyService.asmx?wsdl

This will generate the RgsAgentService.cs file which you can include into your project and use right away. Web Services Description Language Tool (Wsdl.exe) on MSDN has more info on wsdl.exe if needed.

Using the Web Service

Now you’re already good to go and use the web service. The code below shows a sample console application which does the following.

  • Create a new instance of ProxyService (the generated class from the step above) which points to the service on the pool you’re interested in.
  • Query the web service if the current user is an agent or not. This requires you to authenticate with the user’s credentials.
  • If the current user is an agent
    • Determine some basic information (e.g. the name and SIP address of the agent).
    • Retrieve the list of agent groups the agent is a member of in the connected pool.
    • If there are formal agent groups to which the agent is not signed in, the user is asked if he wants to sign in.
      • If he choses to sign in, tries to sign the agent in to those formal groups.
using System;
using System.Collections.Generic;

using RgsAgentService;

namespace RgsClient
{
    class Program
    {
        static void Main(string[] args)
        {
            string poolFqdn = "ocs-pool-01.contoso.com";

            if (args.Length > 0)
            {
                poolFqdn = args[0];
            }

            ProxyService service = ConnectToPool(poolFqdn);

            // First, figure out if the current user is an Agent.
            if (!service.IsAgent())
            {
                Console.WriteLine("You are not an agent in Pool '{0}'.", poolFqdn);
                return;
            }

            // Now get some information about the Agent (i.e. the current User).
            AcdAgent self = service.GetAgent();

            Console.WriteLine("You were authenticated as agent '{0}' ('{1}') in pool '{2}'.",
                self.DisplayName, self.SipAddress, poolFqdn);
            Console.WriteLine();

            // Finally, determine which Agent Groups this Agent belongs to.
            AcdGroup[] agentGroups = service.GetGroups();

            Console.WriteLine("Agent Group Name                  Formal?   Signed In?  # Agents");
            Console.WriteLine("----------------------------------------------------------------");

            Dictionary<string, Guid> agentGroupIdsForSignIn = new Dictionary<string, Guid>();

            for (int i = 0; i < agentGroups.Length; i++)
            {
                Console.WriteLine("{0,-32}  {1,-8}  {2,-10}  {3,8}",
                    agentGroups[i].Name, agentGroups[i].CanSignIn,
                    agentGroups[i].IsSignedIn, agentGroups[i].NumberOfAgents);

                if (agentGroups[i].CanSignIn &&
                    !agentGroups[i].IsSignedIn)
                {
                    agentGroupIdsForSignIn.Add(agentGroups[i].Name, agentGroups[i].Id);
                }
            }

            // If the Agent is not signed in to all his formal groups, then offer
            // him to do so now.
            if (agentGroupIdsForSignIn.Count > 0)
            {
                Console.WriteLine();
                Console.WriteLine("You are not currently signed in to {0} agent group(s):",
                    agentGroupIdsForSignIn.Count);

                foreach (string agentGroupName in agentGroupIdsForSignIn.Keys)
                {
                    Console.WriteLine("    {0}", agentGroupName);
                }

                Console.WriteLine();
                Console.Write("Do you want to sign in to these groups now? [y/n] ");

                ConsoleKeyInfo key = Console.ReadKey();
                while (key.KeyChar != 'y' && key.KeyChar != 'n')
                {
                    key = Console.ReadKey();
                }

                if (key.KeyChar == 'n')
                {
                    return;
                }

                Console.WriteLine();

                if (service.SignInMultiple(agentGroupIdsForSignIn.Values.ToArray()))
                {
                    Console.WriteLine("You have successfully signed in.");
                }
                else
                {
                    Console.WriteLine("Sign-in to at leat one agent group has failed.");
                }
            }
        }

        private static ProxyService ConnectToPool(string poolFqdn)
        {
            ProxyService service = new ProxyService();

            service.Url = String.Format("https://{0}/Rgs/Clients/ProxyService.asmx", poolFqdn);
            service.UseDefaultCredentials = true;

            return service;
        }
    }
}

Running this program will yield something similar to the following.

You were authenticated as agent 'Bob' ('bob@contoso.com') in pool 'rgs-pool-01.contoso.com'.

Agent Group Name                  Formal?   Signed In?  # Agents
----------------------------------------------------------------
Payroll Questions                 False     True               5
General HR Questions              True      False              8

You are not currently signed in to 1 agent group(s):
    General HR Questions

Do you want to sign in to these groups now? [y/n] y
You have successfully signed in.

More Methods

The WebService has a few more methods. These are in particular

  • SignIn(Guid groupId) – Tries to sign the current agent in to the agent group with the given ID
  • SignOut(Guid groupId) – Tries to sign the current agent out of the agent group with the given ID
  • SignOutMultiple(Guid[] groupIds) – Tries to sign the current agent out of all the groups identified with their respective IDs

These methods all return a boolean which indicates success (true) or failure (false).

Summary

You can integrate the RGS Agent Services into your own application by using the corresponding web service which is installed with the Response Group Service. This allows you to offer the same information and functionality as the RGS Agent OC Tab in a customizable manner.