Saturday 29 August 2009

A story of pifalls in hosting webservices or WCF Services on IIS6

It’s been a while since I made some WCF Services and even longer thatI had to do much with IIS. Here is a log of what I experienced as I was implementing some WCF functions in a LAN. Often I found web sites that had exactly the same problems that I had faced but no solution.

I have a number of servers on which I need a WCF Service. But I don’t want to enable IIS on all these servers, so I decided to host the WCF service in a Windows Service. Here are some links about this:

All informations about Windows Services:
http://msdn.microsoft.com/en-us/library/aa984074(VS.71).aspx

Walkthrough: Creating a Windows Service Application
http://msdn.microsoft.com/en-us/library/aa984464(VS.71).aspx

How to: Host a WCF Service in IIS
http://msdn.microsoft.com/en-us/library/ms733766.aspx

Hosting and Consuming WCF Services
http://msdn.microsoft.com/en-us/library/bb332338.aspx


Here are the steps I took to create a Windows Service:

1. Open Visual Studio and Create a Windows Service Project
2. Click in the design pain of Service1.cs
3. In the properties
Name = NigelsService
ServiceName = NigelsService
4. Drag and drop Eventlog from the toolbox into the design pain of Service1.cs
5. Edit the following code:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;

namespace WindowsService1
{
public partial class NigelsService : ServiceBase
{
public NigelsService()
{
InitializeComponent();
// Create the source, if it does not already exist.
if (!EventLog.SourceExists("MySource"))
{
//An event log source should not be created and immediately used.
//There is a latency time to enable the source, it should be created
//prior to executing the application that uses the source.
//Execute this sample a second time to use the new source.
EventLog.CreateEventSource("MySource", "MyNewLog");
}

// Create an EventLog instance and assign its source.
this.eventLog1.Source = "MySource";

// This is exected on Power up
// Instanciate WCF service here


}

protected override void OnStart(string[] args)
{
// This is executed when the service starts
eventLog1.WriteEntry("Service Start");
// Start WCF service here
}

protected override void OnStop()
{
// This is executed when the service Stops
// (Power down is the garbage collector

eventLog1.WriteEntry("Service Stop");
}
}
}

6. Right mouse click in the design pain of Service1.cs and select Add Installer
7. Click the ServiceInstaller1 and change the following properties
Name = NigelsServiceInstaller1
ServiceName = NigelsService
StartType = Automatic
8. Click on ServiceProcessInstaller1 and change the following properties
Account = LocalSystem
9. Click File / add / New Project
10. Under Setup and Deployment select Setup Project
11. Right mouse click on the project root Setup1
12. Add / Project Output ...
13. Project = WindowsService1
Primary Output
Configuration Active
OK
14. Right mouse click on the project root Setup1
15. View / Custom Actions
16. Right Mouse click on the root of Custom items and select Add Custom Action..
17. Look in : Application Folder
Primary output from WindowsService1 (Active)
OK
18. Build the project

When installing the service you will notice that the Product name is used
When the service is started and stopped corresponding entries are made in the Application Event log

This worked very well because the service did not have much to do. I also had a service that had quite a lot of work to do. For this service I did not want to use a Windows Service because I wanted to make use of IIS’s ability to look after memory management and restarting the service if something goes wrong. I had an IIS6 server on to which I could deploy my application

Firstly I created a solution with a WCF Solution and for old times sake I created an additional WebService project.

Starting with WebService hosted on ASP.NET. Visual studio made a sample asmx file which I quickly modified to include all the functions that I needed. And it worked. So I renamed the Service1.asmx to something more appropriate and I got the following error

Parser Error
Description: An error occurred during the parsing of a resource required to service this request. Please review the following specific parse error details and modify your source file appropriately.
Parser Error Message: Could not create type 'PRECEDWebService.Service1'.
Source Error:
Line 1: WebService Language="VB" CodeBehind="PRECED.asmx.vb" Class="PRECEDWebService.Service1"
Source File: /PRECEDWebService/PRECED.asmx Line: 1

So the fix for this was to change
WebService Language="VB" CodeBehind="PRECED.asmx.vb" Class="PRECEDWebService.Service1"
to:
WebService Language="VB" CodeBehind="PRECED.asmx.vb" Class="PRECEDWebService.PRECED"

The next step was I wanted to deploy this to my IIS6 Server. Its been about 7 years since the last time I really made something on IIS. So I was a little rusty. First on the server I setup a Virtuel directory in the IIS Manager and published my new webservice into it. But when I tried to invoke the Webservice I get a get an error.
By default all Web service extensions are disabled. I found some instructions in the following link:
http://www.microsoft.com/technet/prodtechnol/WindowsServer2003/Library/IIS/de6171d8-704c-431a-8117-915e02fac30b.mspx?mfr=true

To enable and disable a Web service extension
1. In IIS Manager, expand the local computer, and then click Web Service Extensions.
2. In the details pane, click the Web service extension that you want to enable or disable.
3.To enable a disabled Web service extension, click Allow.
4.To disable an enabled Web service extension, click Prohibit.
A message box with a list of applications that will be prevented from running on the IIS Web server displays.
5.Click OK to disable the Web service extension.
You will need to enable
- All Unkenown ISAPI Extentions
- Active Server Pages
- ASP.NET v2.0.50727

This time when trying to add a WebService I got the following error
The current identity (NT AUTHORITY\NETWORK SERVICE) does not have write access to 'C:\WINDOWS\Microsoft.NET\Framework64\v2.0.50727\Temporary ASP.NET Files'.

To fix this
1. Goto IIS
2. Open the application Pools
3. Click on the properties of DefaultAppPool
4. Click on Identity
5. Change Predefined to Local System
6. Restart web server
Also I granted IUSR_ServerName access rights to ../Windows/Microsoft.NET/Framework/version
In addition I found that if the IIS had not been correctly configured the following command line statement would reinstall a number of components
C:\WINDOWS\Microsoft.NET\Framework\v3.0\Windows Communication Foundation>service
modelreg.exe –r

This worked quite nicely, but if the server is internet facing for security reasons I would not use IIS7 and do some research over security issues.
So next I looked again at the WCF solution to see if I could do the same. Starting off by trying to consume a WCF with the solution. The first thing I did was to change the binding type to basicHttpBinding. The first time I did this I used a capital B in the web.config file and got an error about an unrecognized binding type. I wish the compiler would also correct my config files! The web.config file ended up looking like:

client
endpoint address="http://localhost:1271/PRECED.svc" binding="basicHttpBinding"
bindingConfiguration="EndPointHTTP" contract="ServiceReference4.IPRECED"
name="EndPointHTTP"
/client

The corresponding code to call this with app.config looked like
Dim iPRECEDClient As New ServiceReference5.PRECEDClient
Console.WriteLine(vbLf & "Calling Service..." & vbLf)
Dim str As String
str = iPRECEDClient.HelloWorld
Console.WriteLine(str)
Console.ReadLine()
Alternatively without app.config

Dim proxy As IPRECED = ChannelFactory(Of IPRECED).CreateChannel(New BasicHttpBinding(), New EndpointAddress("http://localhost:1271/PRECED.svc"))
Console.WriteLine(vbLf & "Calling Service..." & vbLf)
Console.WriteLine(proxy.ValidateUser())

Console.ReadLine()

The final step was to publish this to the web server. I replaced http://localhost:1271/PRECED.svc with http://servername/VirtuelDir/PRECED.svc%20in%20both%20the%20Web.config and the app.config and everything worked well
For completeness I added a MEX endpoing as shown below

system.serviceModel
services
service behaviorConfiguration="PRECEDBehaviors" name="PRECEDWcfService.PRECED"
endpoint address="http://localhost:13243/" binding="basicHttpBinding"
name="EndPointHTTP" contract="PRECEDWcfService.IPRECED" /
/service
service behaviorConfiguration="PRECEDBehaviors" name="PRECEDWcfService.PRECEDMex"
endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /
/service
/services
behaviors
serviceBehaviors
behavior name="PRECEDBehaviors"
serviceMetadata httpGetEnabled="true" /
/behavior
/serviceBehaviors
/behaviors
/system.serviceModel
This was quite a long and painful exercise but that was because I been working on thick clients apps and not web apps in the last 7 years. One nice trick I learned on the way was to use the browse function on the design time webserver because when you double click on the svc files you get a good error messages.