Tuesday, 21 December 2010

A WCF Hosting story

I have a VS2010 project that has a WCF service that is ready to be deployed. I now want to setup IIS7 to use WAS to host the WCF components. Here are the steps needed to do this

1. Goto Control Panel/Administrative Tools/Internet Information Services (IIS) Manager
2. Browse to Sites/Default Website
3. Right mouse click and "Add Virzual Directory..."
4. In the Add Virtual Directory
   - Alias : 05
   - Physical path: C:\InetPub\wwwroot\wmi\2010\12\05

(The objective is: To have a simple URL. Have the ability to version both the service and the namespaces. Monitor when decommissioning a service so as to prevent breaking applications [In our case done via logging in a base class])

5. Browse to Application Pools
6. Right mouse click on the list box and Add Application Pool....
7. Add Application Pool
   - Name :Wmi
   - .Net Framework v4.0.30319
   - Managed pipeine mode : Integrated
8. Right mouse click on the newly created WMI service account and select Advanced Settings
9. Under Process Model click on the "..." button next to Identity
10. Select Custom account and enter the service account details

At this point you could try and publish...
i Right mouse click on the ASP.NET project containing the WCF service
ii Enter
   - Publish profile: BulsburgProfile
   - Publish method: File Sysetm
   - Target location \\chbbpresxxx\wwwroot$\wmi\2010\12\05
Then if you enter the url to the service http://chbbpresxxx/wmi/Disk.svc you will get the following error

Server Error in '/' Application.
________________________________________
Runtime Error
Description: An application error occurred on the server. The current custom error settings for this application prevent the details of the application error from being viewed remotely (for security reasons). It could, however, be viewed by browsers running on the local server machine.

Details: To enable the details of this specific error message to be viewable on remote machines, please create a <customErrors> tag within a "web.config" configuration file located in the root directory of the current web application. This <customErrors> tag should then have its "mode" attribute set to "Off".

<!-- Web.Config Configuration File -->

<configuration>
    <system.web>
        <customErrors mode="Off"/>
    </system.web>
</configuration>

Notes: The current error page you are seeing can be replaced by a custom error page by modifying the "defaultRedirect" attribute of the application's <customErrors> configuration tag to point to a custom error page URL.

<!-- Web.Config Configuration File -->

<configuration>
    <system.web>
        <customErrors mode="RemoteOnly" defaultRedirect="mycustompage.htm"/>
    </system.web>
</configuration>

So now we add the following to Web.Config and republish
    <system.web>
        <customErrors mode="Off"/>
    </system.web>

Then you will get the following error:


Server Error in '/' Application.
________________________________________
The type 'Ccp.Wcf.Wmi.Disk', provided as the Service attribute value in the ServiceHost directive, or provided in the configuration element system.serviceModel/serviceHostingEnvironment/serviceActivations could not be found.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

The solution is to
11. Go back to the Internet Information Services (IIS) Manager
12. Right mouse click on the newly created virtual directory and select "Convert to Application"
13. - Alias: wmi
    - Application pool: wmi
    - Physical path: C:\InetPub\wwwroot\wmi\2010\12\05
Then it works...


Next I want to add the finishing touches to the web.config
    <system.serviceModel>
      <services>
        <service behaviorConfiguration="Ccp.Wcf.Wmi.DiskBehavior"
          name="Ccp.Wcf.Wmi.Disk">
          <endpoint address="" binding="wsHttpBinding" contract="Ccp.Wcf.Wmi.IDisk">
            <identity>
              <dns value="localhost" />
            </identity>
          </endpoint>
          <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
          <host>
            <baseAddresses>
              <!-- We don't need a base address when deploying to production -->
              <!--add baseAddress="http://localhost:8731/Design_Time_Addresses/Ccp.Wcf.Wmi/Disk/" /-->
            </baseAddresses>
          </host>
        </service>
      </services>

If you happen to try the same thing on IIS6 there are some differences:

1. Inside the Internet Information Services (IIS) Manager
2. Browse to Web Service Extentions and enable ASP.NET v4.0.30319
3. Repeat the excersize of creating the application pool and virtual directory
4. Copy the bin and the web.config file from C:\InetPub\wwwroot\wmi\2010\12\05 to C:\InetPub\wwwroot\

Then when you try to do some special WMI calls you will end up with some errors like : Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED)). In our case the IIS6 Server is a fringe server that will be updated to Server 2008

So now taking our system administrator hat off and putting our developer hat back on. We have an client app.config file that looks like:
            <endpoint address="http://chbbpresxxx.global.partnerre.net/Wmi/2010/12/05/Disk.svc"
                binding="wsHttpBinding" bindingConfiguration="DiskwsHttp"
                contract="WmiDeployedServiceReference.IDisk" name="DiskwsHttpBalsburg">
                <identity>
                    <dns value="localhost" />
                </identity>
            </endpoint>

NB the dns entry is necessary otherwise the service will not be found

The corresponding code is as follows:


        [TestMethod]
        public void GetFreeSpaceInGBWCfDeployed_CheckDiskCapacityOnC_StringContainingDiskCapacity()
        {
            WmiDeployedServiceReference.DiskClient proxy = new WmiDeployedServiceReference.DiskClient("DiskwsHttpBalsburg");

            string diskCapacityString = "";
            string serverName = Environment.MachineName;
            string driveLetter = "C";

            diskCapacityString = proxy.GetFreeSpaceInGB(serverName, driveLetter);

            long AvailableSpaceOnC = 0;

            DriveInfo d = new DriveInfo(driveLetter);
            AvailableSpaceOnC = d.AvailableFreeSpace;
            AvailableSpaceOnC = AvailableSpaceOnC / 1024 / 1024 / 1024;

            long diskCapacityLong = Convert.ToInt64(Convert.ToDouble(diskCapacityString));

            Assert.IsFalse(diskCapacityString == "");
            Assert.IsTrue(diskCapacityLong == AvailableSpaceOnC);

        }

Now I want to use NetTcp because it is more efficient than wsHttp.
In some early iterations after adding the netTcp binding when I try http://chbbpresxxx/wmi/2010/12/05/Disk.svc
I get the error like an error like:


Server Error in '/Wmi/2010/12/05' Application.
--------------------------------------------------------------------------------

Could not find a base address that matches scheme net.tcp for the endpoint with binding NetTcpBinding. Registered base address schemes are [http].
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

To fix this error you need to add the net.tcp binding to the binding of the application in IIS.
1. Right click the virtual directory/application in IIS -> Manage application -> Advanced settings.
2. On the Enabled Protocols : http,net.tcp

I tried out the WCF configuration editor to setup a simple net-Tcp binding to the existing contract and I publish it. Then when I open the http://chbbpresxxx/wmi/2010/12/05/Disk.svc I got the error:


Cannot load the X.509 certificate identity specified in the configuration.

The solution in my case was to comment out the certificate in the web.config


          <endpoint binding="wsHttpBinding" name="DiskwsHttp" contract="Ccp.Wcf.Wmi.IDisk"
            listenUriMode="Explicit">
            <identity>
              <dns value="localhost" />
              <!--certificateReference storeName="My" storeLocation="LocalMachine"
                x509FindType="FindBySubjectDistinguishedName" /-->
            </identity>
          </endpoint>
          <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"
            listenUriMode="Explicit">
            <identity>
              <!--certificateReference storeName="My" storeLocation="LocalMachine"
                x509FindType="FindBySubjectDistinguishedName" /-->
            </identity>
          </endpoint>

Alternatively you can follow the instructions in the following url to setup the certificate

http://wcfsecurity.codeplex.com/wikipage?title=How%20To%20-%20Create%20and%20Install%20Temporary%20Certificates%20in%20WCF%20for%20Message%20Security%20During%20Development&ProjectName=wcfsecurity


After some other edits for the wsHttp binding works but the net-tcp binding gives an error like

Could not connect to net.tcp://chbbpresxxx.global.partnerre.net:8731/Wmi/2010/12/05/Disk.svc. The connection attempt lasted for a time span of 00:00:01.1986602. TCP error code 10061: No connection could be made because the target machine actively refused it 10.102.65.150:8731.

This error had me stuck for a while, I tried a lot of things, all failed. Here is a list:
- Look at the Advanced settings of wwwroot and add http,net.tcp to the enabled protocols (as in the Virtual directory)
- Right mouse click on wwwroot and add Binding.
   - Modified existing binging from 808:* to 9* to 9001 and back
   - Adding net-tcp with 9001
- Under Server Manager/Configuration/Microsoft Fire Wall with.../ look at the properties and set Firewall status to off at doamin, private and public levels

The problem was the setup of Features where different. To fix this we have to put on our system administrator hat on and do the following:

1. Open the Server Manager
2. Browse to Server Manager/features
3. Add Under .Net Framework 3.0 Features (Installed)
       Under WCF Activation
       + HTTP Activation
       + Non HTTP Activations
4. The installation took some time

Now if we try the test client by:
1. D:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\WcfTestClient.exe
2. File/ Add Service
3. http://chbbpresxxx.global.partnerre.net/Wmi/2010/12/05/Disk.svc
We get the error


Error: Cannot obtain Metadata from http://chbbpresxxx.global.partnerre.net/Wmi/2010/12/05/Disk.svc
If this is a Windows (R) Communication Foundation service to which you have access, please check that you have enabled metadata publishing at the specified address.  For help enabling metadata publishing, please refer to the MSDN documentation at http://go.microsoft.com/fwlink/?LinkId=65455.WS-Metadata Exchange etc etc

I found a link that describes what is happening

http://blogs.msdn.com/b/webtopics/archive/2010/04/28/system-typeloadexception-for-system-servicemodel-activation-httpmodule-in-asp-net-4.aspx

Installation of .NET 3.5.1 WCF HTTP activation feature adds a global module for the 3.0 framework’s 'System.ServiceModel’ assembly for the type 'System.ServiceModel.Activation.HttpModule'. Since the application pool’s runtime version is v4.0, this assembly is tried to be loaded from the .NET 4 assemblies folder. Since, the definition of the 'System.ServiceModel.Activation.HttpModule’ is now moved to the “System.ServiceModel.Activation” assembly, it fails.

Following the instructions the to fix this open a command prompt:

 
C:\>cd C:\Windows\Microsoft.NET\Framework\v4.0.30319
C:\Windows\Microsoft.NET\Framework\v4.0.30319>aspnet_regiis -iru
Start installing ASP.NET (4.0.30319).
..................................
Finished installing ASP.NET (4.0.30319).

Then reboot the server...
By the way the following error happens when I tried adding a Binding in the IIS Manager for net-tcp port 9001

The protocol binding '9001' does not conform to the syntax for 'net.tcp'. The following is an example of valid 'net.tcp' protocol bindings: '808:*'.

The solution is to remove the added net-tcp binding from the IIS Server Manager.


The Final Solution with our developer hat on


We use baseAddress="net.tcp://localhost:8080/Wmi/2010/12/05/Disk.svc" in the Web.config that looks like:

<?xml version="1.0"?>
<configuration>
    <system.web>
        <compilation debug="true" targetFramework="4.0" />
      <customErrors mode="Off"/>
    </system.web>

    <system.serviceModel>
      <services>
        <service behaviorConfiguration="Ccp.Wcf.Wmi.DiskBehavior" name="Ccp.Wcf.Wmi.Disk">
          <clear />
          <endpoint binding="wsHttpBinding" name="DiskwsHttp" contract="Ccp.Wcf.Wmi.IDisk"
            listenUriMode="Explicit">
            <identity>
              <dns value="localhost" />
            </identity>
          </endpoint>
          <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"
            listenUriMode="Explicit">
            <identity>
            </identity>
          </endpoint>
          <endpoint address=""
            binding="netTcpBinding"  name="DisknetTcp" contract="Ccp.Wcf.Wmi.IDisk" >
            <identity>
              <dns value="localhost" />
            </identity>
          </endpoint>
          <host>
            <baseAddresses>
              <!-- We don't need a base address when deploying to production -->
              <add baseAddress="net.tcp://localhost:8080/Wmi/2010/12/05/Disk.svc" />
            </baseAddresses>
          </host>

        </service>
      </services>
      <behaviors>
        <serviceBehaviors>
          <behavior name="Ccp.Wcf.Wmi.DiskBehavior">
            <!-- To avoid disclosing metadata information,
          set the value below to false and remove the metadata endpoint above before deployment -->
            <serviceMetadata httpGetEnabled="True"/>
            <!-- To receive exception details in faults for debugging purposes,
          set the value below to true.  Set to false before deployment
          to avoid disclosing exception information -->
            <serviceDebug includeExceptionDetailInFaults="true" />
          </behavior>
        </serviceBehaviors>
      </behaviors>
      <bindings>
        <netTcpBinding>
          <binding name="ReliableSessionBinding">
            <reliableSession ordered="false" inactivityTimeout="00:10:00" enabled="true" />
          </binding>
          <binding name ="DisknetTcp">
          </binding>
        </netTcpBinding>
      </bindings>
      <client>
        <endpoint address="net.tcp://services-tst.global.partnerre.net/Group/Common/Logging/2009/02/11/LogService.svc"
             binding="netTcpBinding" bindingConfiguration="ReliableSessionBinding"
             contract="LogServiceReference.ILogService" >
          <identity>
            <servicePrincipalName value="host/CHBBPRES563.global.partnerre.net" />
          </identity>
        </endpoint>
      </client>
    </system.serviceModel>
</configuration>

We use address="net.tcp://chbbpresxxx.global.partnerre.net/Wmi/2010/12/05/Disk.svc"in the App.config, that ends up looking like:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <client>
            <endpoint address="http://chbbpresxxx.global.partnerre.net/Wmi/2010/12/05/Disk.svc"
                binding="wsHttpBinding" bindingConfiguration="DiskwsHttp"
                contract="WmiDeployedServiceReference.IDisk" name="DiskwsHttpBalsburg">
                <identity>
                    <dns value="localhost" />
                </identity>
            </endpoint>
            <endpoint address="http://localhost:8732/Design_Time_Addresses/Ccp.Wcf.Wmi/Disk/"
                binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IDisk"
                contract="WmiServiceReference.IDisk" name="WSHttpBinding_IDisk">
                <identity>
                    <dns value="localhost" />
                </identity>
            </endpoint>
            <endpoint address="net.tcp://localhost:8731/Wmi/2010/12/05/Disk"
                binding="netTcpBinding" contract="WmiServiceReference.IDisk"
                name="NetTcpBinding_IDisk">
                <identity>
                    <dns value="localhost" />
                </identity>
            </endpoint>
            <endpoint address="http://chbbpresxxx.global.partnerre.net/Wmi/2010/12/05/Disk.svc"
                binding="wsHttpBinding" bindingConfiguration="DiskwsHttp1"
                contract="WmiDeployedServiceReference.IDisk" name="DiskwsHttp">
                <identity>
                    <dns value="localhost" />
                </identity>
            </endpoint>
            <endpoint address="net.tcp://chbbpresxxx.global.partnerre.net/Wmi/2010/12/05/Disk.svc"
                binding="netTcpBinding"
                contract="WmiDeployedServiceReference.IDisk" name="DisknetTcp">
                <identity>
                    <dns value="localhost" />
                </identity>
            </endpoint>
        </client>
    </system.serviceModel>
</configuration>

And the unit test looks like:
        [TestMethod]
        public void GetFreeSpaceInGBWCfDeployedTcp_CheckDiskCapacityOnC_StringContainingDiskCapacity()
        {
            WmiDeployedServiceReference.DiskClient proxy = new WmiDeployedServiceReference.DiskClient("DisknetTcp");

            string diskCapacityString = "";
            string serverName = Environment.MachineName;
            string driveLetter = "C";

            diskCapacityString = proxy.GetFreeSpaceInGB(serverName, driveLetter);

            long AvailableSpaceOnC = 0;

            DriveInfo d = new DriveInfo(driveLetter);
            AvailableSpaceOnC = d.AvailableFreeSpace;
            AvailableSpaceOnC = AvailableSpaceOnC / 1024 / 1024 / 1024;

            long diskCapacityLong = Convert.ToInt64(Convert.ToDouble(diskCapacityString));

            Assert.IsFalse(diskCapacityString == "");
            Assert.IsTrue(diskCapacityLong == AvailableSpaceOnC);

        }