Feeds:
Posts
Comments

Archive for the ‘TopShelf’ Category

The core application will be a simple calculator that can add, subtract, multiply and divide two double values. The output will be a JSON encoded POCO containing the calculated value OR an error message.

First the POCO:

[DataContract]
public class CalculationResult
{
	[DataMember] public double Answer;

	[DataMember] public string Message;
}

The attributes are used by WCF. We’ll be seeing a lot more of those. Next, the service contract:

[ServiceContract(SessionMode = SessionMode.NotAllowed)]
public interface ICalculator
{
	[WebGet(UriTemplate = "Add/{n1}/{n2}")]
	[OperationContract]
	CalculationResult Add(string n1, string n2);

	[WebGet(UriTemplate = "Subtract/{n1}/{n2}")]
	[OperationContract]
	CalculationResult Subtract(string n1, string n2);

	[WebGet(UriTemplate = "Multiply/{n1}/{n2}")]
	[OperationContract]
	CalculationResult Multiply(string n1, string n2);

	[WebGet(UriTemplate = "Divide/{n1}/{n2}")]
	[OperationContract]
	CalculationResult Divide(string n1, string n2);
}

The WebGet attribute configures WCF to let us call the method from an HTTP GET request, and the UriTemplate property lets us configure the structure of that URL for automatic parameter parsing.

We’re receiving strings but need to convert them to doubles, so here’s an extension method to do that:

public static class StringExtensions
{
	public static double? ToDouble(this string input)
	{
		double value1;
		return !Double.TryParse(input, out value1) ? (double?) null : value1;
	}
}

Next is the Calculator implementation:

[ServiceBehavior(IncludeExceptionDetailInFaults = false,
	AddressFilterMode = AddressFilterMode.Any,
	InstanceContextMode = InstanceContextMode.Single,
	ConcurrencyMode = ConcurrencyMode.Single)]
public class Calculator : ICalculator
{
	public CalculationResult Add(string n1, string n2)
	{
		Console.WriteLine("received request to Add " + n1 + " to " + n2);
		Func<double, double, double> add = (value1, value2)
		      => (value1 + value2);
		return Calculate(n1, n2, add);
	}

	public CalculationResult Subtract(string n1, string n2)
	{
		Console.WriteLine("received request to Subtract "+n2+" from "+n1);
		Func<double, double, double> subtract = (value1, value2)
		      => (value1 - value2);
		return Calculate(n1, n2, subtract);
	}

	public CalculationResult Multiply(string n1, string n2)
	{
		Console.WriteLine("received request to Multiply "+n1+" by "+n2);
		Func<double, double, double> multiply = (value1, value2)
		      => (value1 * value2);
		return Calculate(n1, n2, multiply);
	}

	public CalculationResult Divide(string n1, string n2)
	{
		Console.WriteLine("received request to Divide " + n1 + " by " + n2);
		Func<double, double, double> divide = (value1, value2)
		      => value2 == 0 ? Double.NaN : (value1 / value2);
		return Calculate(n1, n2, divide);
	}

	private static CalculationResult Calculate(
		string n1,
		string n2,
		Func<double, double, double> calculate)
	{
		var value1 = n1.ToDouble();
		if (!value1.HasValue)
		{
			return GetCouldNotConvertToDoubleResult(n1);
		}

		var value2 = n2.ToDouble();
		if (!value2.HasValue)
		{
			return GetCouldNotConvertToDoubleResult(n2);
		}

		double result = calculate(value1.Value, value2.Value);
		return new CalculationResult
			{
				Answer = result
			};
	}

	private static CalculationResult GetCouldNotConvertToDoubleResult(string input)
	{
		return new CalculationResult
			{
				Message = "Could not convert '" + input + "' to a double"
			};
	}
}

Now that we have a WCF attribute decorated service we need a way to spin it up and make it HTTP accessible. I’ve chosen to encapsulate that process so I can easily apply it to any service I want to expose. You can see that most of the code is error handling and logging related.

public class WcfServiceWrapper<TServiceImplementation, TServiceContract>
	: ServiceBase
	where TServiceImplementation : TServiceContract
{
	private readonly string _serviceUri;
	private ServiceHost _serviceHost;

	public WcfServiceWrapper(string serviceName, string serviceUri)
	{
		_serviceUri = serviceUri;
		ServiceName = serviceName;
	}

	protected override void OnStart(string[] args)
	{
		Start();
	}

	protected override void OnStop()
	{
		Stop();
	}

	public void Start()
	{
		Console.WriteLine(ServiceName + " starting...");
		bool openSucceeded = false;
		try
		{
			if (_serviceHost != null)
			{
				_serviceHost.Close();
			}

			_serviceHost = new ServiceHost(typeof(TServiceImplementation));
		}
		catch (Exception e)
		{
			Console.WriteLine("Caught exception while creating " + ServiceName + ": " + e);
			return;
		}

		try
		{
			var webHttpBinding = new WebHttpBinding(WebHttpSecurityMode.None);
			_serviceHost.AddServiceEndpoint(typeof(TServiceContract), webHttpBinding, _serviceUri);

			var webHttpBehavior = new WebHttpBehavior
				{
					DefaultOutgoingResponseFormat = WebMessageFormat.Json
				};
			_serviceHost.Description.Endpoints[0].Behaviors.Add(webHttpBehavior);

			_serviceHost.Open();
			openSucceeded = true;
		}
		catch (Exception ex)
		{
			Console.WriteLine("Caught exception while starting " + ServiceName + ": " + ex);
		}
		finally
		{
			if (!openSucceeded)
			{
				_serviceHost.Abort();
			}
		}

		if (_serviceHost.State == CommunicationState.Opened)
		{
			Console.WriteLine(ServiceName + " started at " + _serviceUri);
		}
		else
		{
			Console.WriteLine(ServiceName + " failed to open");
			bool closeSucceeded = false;
			try
			{
				_serviceHost.Close();
				closeSucceeded = true;
			}
			catch (Exception ex)
			{
				Console.WriteLine(ServiceName + " failed to close: " + ex);
			}
			finally
			{
				if (!closeSucceeded)
				{
					_serviceHost.Abort();
				}
			}
		}
	}

	public new void Stop()
	{
		Console.WriteLine(ServiceName + " stopping...");
		try
		{
			if (_serviceHost != null)
			{
				_serviceHost.Close();
				_serviceHost = null;
			}
		}
		catch (Exception ex)
		{
			Console.WriteLine("Caught exception while stopping " + ServiceName + ": " + ex);
		}
		finally
		{
			Console.WriteLine(ServiceName + " stopped...");
		}
	}
}

Lastly we’ll use TopShelf to turn the whole thing into a Windows Service. One great feature of TopShelf is our program can be a simple console application and therefore easy to debug.

internal class Program
{
	private static void Main(string[] args)
	{
		const string serviceUri = "http://localhost:10000/calc";
		var host = HostFactory.New(c =>
			{
				c.Service<WcfServiceWrapper<Calculator, ICalculator>>(s =>
					{
						s.SetServiceName("CalculatorService");
						s.ConstructUsing(x => 
							new WcfServiceWrapper<Calculator, ICalculator>("Calculator", serviceUri));
						s.WhenStarted(service => service.Start());
						s.WhenStopped(service => service.Stop());
					});
				c.RunAsLocalSystem();

				c.SetDescription("Runs CalculatorService.");
				c.SetDisplayName("CalculatorService");
				c.SetServiceName("CalculatorService");
			});

		Console.WriteLine("Hosting ...");
		host.Run();
		Console.WriteLine("Done hosting ...");
	}
}

That’s it for the code. Let’s try it out. First run it as a console application (you’ll need to do this as an Administrator otherwise WCF won’t have security rights to listen on the port).

>Calculator.exe
Hosting ...
Calculator starting...
Calculator started at http://localhost:10000/calc

Now bring up a browser and ask the service to add two numbers:

http://localhost:10000/calc/Add/4.7/5.2

On the console we see

received request to Add 4.7 to 5.2

and in the browser we see the result

{"Answer":9.9,"Message":null}

Hit Control-C in the console window to shut down the calculator service.

Calculator stopping...
Calculator stopped...
Done hosting ...

Next let’s install the calculator as a Windows Service.

>Calculator.exe install
Hosting ...

Running a transacted installation.

Beginning the Install phase of the installation.
Installing service CalculatorService...
Service CalculatorService has been successfully installed.
Creating EventLog source CalculatorService in log Application...

The Install phase completed successfully, and the Commit phase is beginning.

The Commit phase completed successfully.

The transacted install has completed.
Done hosting ...

Now open the Windows Services snap-in and you’ll see CalculatorService in the list. Start it up then hit the service with your browser again.

http://localhost:10000/calc/Add/4.7/5.3

with response

{"Answer":10,"Message":null}

Success!

To uninstall the CalculatorService run it again from a command window with the uninstall argument. You don’t even have to stop the server first as TopShelf will do that for you.

>Calculator.exe uninstall
Hosting ...


The uninstall is beginning.
Removing EventLog source CalculatorService.
Service CalculatorService is being removed from the system...
Service CalculatorService was successfully removed from the system.
Attempt to stop service CalculatorService.

The uninstall has completed.
Done hosting ...

Enjoy!

30 Jun 2011 – Updated to DSL used in TopShelf version 2.2.1.1

Read Full Post »