Preface
Presented here are 2 strategies for leveraging the Visual Studio development server, both as an out of process exe and in process library for integration testing ASP.NET based applications including Silverlight and MVC.
The former is provided more as an exercise in completion of a previous iteration. The latter, WebHostServer is the leanest and likely the preferred method.
Introduction
In a previous post, I presented a class that enables programmatic control of the Visual Studio 2008 development server, WebDev.WebServer.exe. The use case for this functionality as presented here is in the interest of testing web applications and endpoints.
Refactored
While the previous implementation is capable in the context of interactive test runners, there were a few resource management issues affecting the usability in more autonomous scenarios such as continuous integration.
Amongst these were the use of static port assignment and the lack of a means to shut down an instance, specifically the ability to start and stop an instance within the scope of a single test fixture.
In this implementation, these issues have been resolved by providing a more capable means of identifying and controlling running instances of WebDev.WebServer.exe using WMI data and the polling of port use to enable dynamic port assignment.
The only outstanding issue is the inability to remove the notification icon that is orphaned in the tray area when programmatically closing an instance.
This may be dealt with in the future as an academic, but this implementation is being provided simply as a verification that it is possible to fully control shelled instances of the executable.
There is a another way to get where we want to go, but first let's update the WebDev.WebServer.exe wrapper.
WebDevServer
Using WebDevServer with NUnit
using System.Net;
using NUnit.Framework;
namespace Salient.Excerpts
{
[TestFixture]
public class WebDevServerFixture : WebDevServer
{
[TestFixtureSetUp]
public void TestFixtureSetUp()
{
StartServer(@"..\..\..\..\TestSite");
}
[TestFixtureTearDown]
public void TestFixtureTearDown()
{
StopServer();
}
[Test]
public void Test()
{
string html = new WebClient().DownloadString(NormalizeUri("Default.aspx"));
}
}
}
WebDevServer.cs
#region
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Management;
using System.Net;
using System.Net.NetworkInformation;
using System.Text.RegularExpressions;
using System.Threading;
#endregion
namespace Salient.Excerpts
{
public class WebDevServer
{
public WebDevServer()
{
}
public string ApplicationPath { get; private set; }
public string HostName { get; private set; }
public int Port { get; private set; }
public string VirtualPath { get; private set; }
public string RootUrl
{
get { return string.Format(CultureInfo.InvariantCulture,
"http://{0}:{1}{2}", HostName, Port, VirtualPath); }
}
private int ProcessId { get; set; }
private static string ProgramFilesx86
{
get
{
if (8 == IntPtr.Size ||
(!String.IsNullOrEmpty(Environment.GetEnvironmentVariable
("PROCESSOR_ARCHITEW6432"))))
{
return Environment.GetEnvironmentVariable("ProgramFiles(x86)");
}
return Environment.GetEnvironmentVariable("ProgramFiles");
}
}
public virtual Uri NormalizeUri(string relativeUrl)
{
return new Uri(RootUrl + relativeUrl);
}
public void StartServer(string applicationPath)
{
StartServer(applicationPath, GetAvailablePort
(8000, 10000, IPAddress.Loopback, true), "/", "localhost");
}
public void StartServer(string applicationPath, int port,
string virtualPath, string hostName)
{
applicationPath = Path.GetFullPath(applicationPath);
hostName = string.IsNullOrEmpty(hostName) ? "localhost" : hostName;
virtualPath = String.Format("/{0}/",
(virtualPath ?? string.Empty).Trim('/')).Replace("//", "/");
if (GetRunningInstance(applicationPath, port, virtualPath) != null)
{
return;
}
IPAddress ipAddress = IPAddress.Loopback;
if (!IsPortAvailable(ipAddress, port))
{
throw new Exception(string.Format("Port {0} is in use.", port));
}
string arguments = String.Format
(CultureInfo.InvariantCulture, "/port:{0} /path:\"{1}\" /vpath:\"{2}\"",
port, applicationPath, virtualPath);
using (Process proc = new Process())
{
proc.StartInfo = new ProcessStartInfo
{
FileName = GetWebDevExecutablePath(),
Arguments = arguments,
CreateNoWindow = true
};
bool started = proc.Start();
if (!started)
{
throw new Exception("Error starting server");
}
ProcessId = proc.Id;
}
ApplicationPath = applicationPath;
Port = port;
VirtualPath = virtualPath;
HostName = hostName;
}
public void StopServer()
{
try
{
WebDevServer instance = GetRunningInstance
(ApplicationPath, Port, VirtualPath);
if (instance != null)
{
using (Process process = Process.GetProcessById(instance.ProcessId))
{
if (process.MainWindowHandle != IntPtr.Zero)
{
process.CloseMainWindow();
}
process.Kill();
process.WaitForExit(100);
}
}
}
catch
{
}
}
public void Dispose()
{
StopServer();
}
#region Instance Management
private const string WebDevPath =
@"Common Files\Microsoft Shared\DevServer\9.0\WebDev.WebServer.exe";
private static readonly Regex RxPath =
new Regex(@"/path:""(?<path>[^""]*)", RegexOptions.ExplicitCapture);
private static readonly Regex RxPort =
new Regex(@"/port:(?<port>\d*)", RegexOptions.ExplicitCapture);
private static readonly Regex RxVPath =
new Regex(@"/vpath:""(?<vpath>[^""]*)", RegexOptions.ExplicitCapture);
private WebDevServer(string commandLine, int processId)
{
ProcessId = processId;
Port = Int32.Parse(RxPort.Match(commandLine).Groups["port"].Value);
ApplicationPath = RxPath.Match(commandLine).Groups["path"].Value;
VirtualPath =
String.Format("/{0}/", RxVPath.Match(commandLine).Groups
["vpath"].Value.Trim('/')).Replace("//", "/");
HostName = "localhost";
}
public static WebDevServer GetRunningInstance(string applicationPath,
int port, string virtualPath)
{
return GetRunningInstances().FirstOrDefault(s =>
string.Compare(s.ApplicationPath, applicationPath,
StringComparison.OrdinalIgnoreCase) == 0 &&
string.Compare(s.VirtualPath, virtualPath,
StringComparison.OrdinalIgnoreCase) == 0 &&
s.Port == port);
}
public static List<WebDevServer> GetRunningInstances()
{
List<WebDevServer> returnValue = new List<WebDevServer>();
const string query = "select CommandLine,ProcessId from
Win32_Process where Name='WebDev.WebServer.EXE'";
using (ManagementObjectSearcher searcher =
new ManagementObjectSearcher(query))
{
using (ManagementObjectCollection results = searcher.Get())
{
foreach (ManagementObject process in results)
{
returnValue.Add(
new WebDevServer(process["CommandLine"].ToString(),
int.Parse(process["ProcessId"].ToString())));
process.Dispose();
}
}
}
return returnValue;
}
public static string GetWebDevExecutablePath()
{
string exePath = Path.Combine(ProgramFilesx86, WebDevPath);
if (!File.Exists(exePath))
{
throw new FileNotFoundException(exePath);
}
return exePath;
}
public static bool IsPortAvailable(IPAddress ipAddress, int port)
{
bool portAvailable = false;
for (int i = 0; i < 5; i++)
{
portAvailable = GetAvailablePort(port, port, ipAddress, true) == port;
if (portAvailable)
{
break;
}
Thread.Sleep(100);
}
return portAvailable;
}
public static int GetAvailablePort(int rangeStart,
int rangeEnd, IPAddress ip, bool includeIdlePorts)
{
IPGlobalProperties ipProps = IPGlobalProperties.GetIPGlobalProperties();
Func<IPAddress, bool> isIpAnyOrLoopBack = i => IPAddress.Any.Equals(i) ||
IPAddress.IPv6Any.Equals(i) ||
IPAddress.Loopback.Equals(i) ||
IPAddress.IPv6Loopback.
Equals(i);
List<ushort> excludedPorts = new List<ushort>();
excludedPorts.AddRange(from n in ipProps.GetActiveTcpConnections()
where
n.LocalEndPoint.Port >= rangeStart &&
n.LocalEndPoint.Port <= rangeEnd && (
isIpAnyOrLoopBack(ip) ||
n.LocalEndPoint.Address.Equals(ip) ||
isIpAnyOrLoopBack(n.LocalEndPoint.Address)) &&
(!includeIdlePorts || n.State
!= TcpState.TimeWait)
select (ushort)n.LocalEndPoint.Port);
excludedPorts.AddRange(from n in ipProps.GetActiveTcpListeners()
where n.Port >= rangeStart && n.Port <= rangeEnd && (
isIpAnyOrLoopBack(ip) ||
n.Address.Equals(ip) || isIpAnyOrLoopBack(n.Address))
select (ushort)n.Port);
excludedPorts.AddRange(from n in ipProps.GetActiveUdpListeners()
where n.Port >= rangeStart && n.Port <= rangeEnd && (
isIpAnyOrLoopBack(ip) ||
n.Address.Equals(ip) || isIpAnyOrLoopBack(n.Address))
select (ushort)n.Port);
excludedPorts.Sort();
for (int port = rangeStart; port <= rangeEnd; port++)
{
if (!excludedPorts.Contains((ushort)port))
{
return port;
}
}
return 0;
}
#endregion
}
}
Redesigned
While there may be scenarios in which using an out of process executable would be the best strategy, ideally, use of an assembly referenced type in the process of the calling code, tests in this case, would provide better control of the server and eliminate much of the WMI and Process management code found in WebDevServer.
Reflecting on WebDev.WebServer.exe, it becomes clear that it is simply a wrapper for Microsoft.VisualStudio.WebHost.Server
. WebHost.Server
exposes all of the methods we need to capably and cleanly control an instance of a web server.
As an added bonus, since the web server is directly instantiated in code, we have the ability to F5 debug the code making the HTTP requests, our tests, and the site under test, just as we would be able to if using the development server directly from Visual Studio.
The subtle difference here is that we are in full control of the server instance, making it more suitable for headless scenarios such as in continuous integration and eliminating the necessity of juggling configurations between interactive development and check-in/test.
The bottom line is: Unless you have a compelling reason to use the out-of-process WebDevServer
, you will have a better experience all around using the in-process WebHostServer
.
Note: The Microsoft.VisualStudio.WebHost
namespace is contained in the file WebDev.WebHost.dll. This file is in the GAC, but it is not possible to add a reference to this assembly from within Visual Studio.
To add a reference, you will need to open your .csproj file in a text editor and add the reference manually.
Look for the ItemGroup
that contains the project references, and add the following element:
For ASP.Net 2.0-3.5 using .Net Framework 3.5
<Reference Include="WebDev.WebHost, Version=9.0.0.0,
Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=x86">
<Private>False</Private>
</Reference>
For ASP.Net 2.0-3.5 using .Net Framework 4.0
<Reference Include="WebDev.WebHost20, Version=10.0.0.0,
Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=x86">
<Private>False</Private>
</Reference>
For ASP.Net 4.0 using .Net Framework 4.0
<Reference Include="WebDev.WebHost40, Version=10.0.0.0,
Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=x86">
<Private>False</Private>
</Reference>
You may open WebHostServer.csproj in a text editor for an example.
WebHostServer
Using WebHostServer with NUnit
using System.Net;
using NUnit.Framework;
namespace Salient.Excerpts
{
[TestFixture]
public class WebHostServerFixture : WebHostServer
{
[TestFixtureSetUp]
public void TestFixtureSetUp()
{
StartServer(@"..\..\..\..\TestSite");
}
[TestFixtureTearDown]
public void TestFixtureTearDown()
{
StopServer();
}
[Test]
public void Test()
{
string html = new WebClient().DownloadString(NormalizeUri("Default.aspx"));
}
}
}
WebHostServer.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Threading;
using Microsoft.VisualStudio.WebHost;
namespace Salient.Excerpts
{
public class WebHostServer
{
private Server _server;
public string ApplicationPath { get; private set; }
public string HostName { get; private set; }
public int Port { get; private set; }
public string VirtualPath { get; private set; }
public string RootUrl
{
get { return string.Format(CultureInfo.InvariantCulture,
"http://{0}:{1}{2}", HostName, Port, VirtualPath); }
}
public virtual Uri NormalizeUri(string relativeUrl)
{
return new Uri(RootUrl + relativeUrl);
}
public void StartServer(string applicationPath)
{
StartServer(applicationPath, GetAvailablePort
(8000, 10000, IPAddress.Loopback, true), "/", "localhost");
}
public void StartServer(string applicationPath, int port,
string virtualPath, string hostName)
{
if (_server != null)
{
throw new InvalidOperationException("Server already started");
}
IPAddress ipAddress = IPAddress.Loopback;
if(!IsPortAvailable(ipAddress, port))
{
throw new Exception(string.Format("Port {0} is in use.", port));
}
applicationPath = Path.GetFullPath(applicationPath);
virtualPath = String.Format("/{0}/", (virtualPath ??
string.Empty).Trim('/')).Replace("//", "/");
_server = new Server(port, virtualPath, applicationPath, false, false);
_server.Start();
ApplicationPath = applicationPath;
Port = port;
VirtualPath = virtualPath;
HostName = string.IsNullOrEmpty(hostName) ? "localhost" : hostName;
}
public void StopServer()
{
if (_server != null)
{
_server.Stop();
_server = null;
Thread.Sleep(100);
}
}
public void Dispose()
{
StopServer();
}
public static bool IsPortAvailable(IPAddress ipAddress, int port)
{
bool portAvailable = false;
for (int i = 0; i < 5; i++)
{
portAvailable = GetAvailablePort(port, port, ipAddress, true) == port;
if (portAvailable)
{
break;
}
Thread.Sleep(100);
}
return portAvailable;
}
public static int GetAvailablePort(int rangeStart,
int rangeEnd, IPAddress ip, bool includeIdlePorts)
{
IPGlobalProperties ipProps = IPGlobalProperties.GetIPGlobalProperties();
Func<IPAddress, bool> isIpAnyOrLoopBack = i => IPAddress.Any.Equals(i) ||
IPAddress.IPv6Any.Equals(i) ||
IPAddress.Loopback.Equals(i) ||
IPAddress.IPv6Loopback.
Equals(i);
List<ushort> excludedPorts = new List<ushort>();
excludedPorts.AddRange(from n in ipProps.GetActiveTcpConnections()
where
n.LocalEndPoint.Port >= rangeStart &&
n.LocalEndPoint.Port <= rangeEnd && (
isIpAnyOrLoopBack(ip) ||
n.LocalEndPoint.Address.Equals(ip) ||
isIpAnyOrLoopBack(n.LocalEndPoint.Address)) &&
(!includeIdlePorts || n.State
!= TcpState.TimeWait)
select (ushort)n.LocalEndPoint.Port);
excludedPorts.AddRange(from n in ipProps.GetActiveTcpListeners()
where n.Port >= rangeStart && n.Port <= rangeEnd && (
isIpAnyOrLoopBack(ip) || n.Address.Equals(ip)
|| isIpAnyOrLoopBack(n.Address))
select (ushort)n.Port);
excludedPorts.AddRange(from n in ipProps.GetActiveUdpListeners()
where n.Port >= rangeStart && n.Port <= rangeEnd && (
isIpAnyOrLoopBack(ip) || n.Address.Equals(ip)
|| isIpAnyOrLoopBack(n.Address))
select (ushort)n.Port);
excludedPorts.Sort();
for (int port = rangeStart; port <= rangeEnd; port++)
{
if (!excludedPorts.Contains((ushort)port))
{
return port;
}
}
return 0;
}
}
}