How ASP.NET can call Windows Service

TL;DR: to use WCF over named pipes with just a few lines of code and zero configuration, look at WcfWrapper.cs. To use .NET Remoting of named pipes, look at RemotingWrapper.cs.  TL;DR

This article describes a complete sample how to call a Windows Service from an ASP.NET Web API application using WCF or .NET Remoting over named pipes, and also explains why it is a good idea. Included parts are

  • HTML client (JQuery)
  • ASP.NET Web API 2 web server
  • Windows Service
  • Console client for teting purposes

WCF sample on Github: https://github.com/ikriv/AspNetCallsWinService

Remoting sample: https://github.com/ikriv/AspNetCallsWinService/tree/remoting

Table of Contents

Why ASP.NET needs to call a Windows service?

ASP.NET needs to call an external process to handle long-running background tasks. IIS worker process (w3wp.exe) may be killed at any time; handling background tasks directly within it is not recommended. IIS worker process may die because of an unhandled exception in a background thread, because of inactivity timeout, manual or automatic app pool recycling, etc.

For the record, Windows services are popular, but not the only possible solution, see "Alternatives to Windows services" below.

Installing and Running the Example

The examples allows a user to create deferred requests, specifying how many seconds each request should be deferred for, and query for the state of the completed requests. There are two interfaces into the system: web based interface (looks pretty) and a console application (good for debugging).

Screenshot

To run the example you should have Visual Studio 2015 and local IIS installed.

  1. Choose which IPC method you want to use: WCF or Remoting.
  2. Clone the repository:
    git clone https://github.com/ikriv/AspNetCallsWinService
    or git clone -b remoting https://github.com/ikriv/AspNetCallsWinService
  3. Run Visual Studio 2015 as administrator (important).
  4. Open AspNetCallsWinService.sln.
  5. Build the solution.
  6. Open administrative command prompt and navigate to MyWindowsService\bin\Debug.
  7. Run the following commands:
    icacls . /grant "NETWORK SERVICE":(OI)(CI)RX /T
  8. MyWindowsService.exe install
    net start IkrivSampleWindowsServiceWcf or net start IkrivSampleWindowsServiceRem
  9. Go back to Visual Studio and make MyWebSite the startup project.
  10. Go to MyWebSite's properties, select "Web" and click on "Create Virtual Director"
  11. Press F5 or select Debug→Start Debugging from the menu.

Enter the text to process and the processing delay. New pending request will be created in yellow. After the specified delay the request will turn green. More than one user can create pending requests, they will be displayed in order of creation.

Architecture and code structure

WCF and Remoting examples are very similar, and share the same general architecture:

High level architecture

In terms of project structure, both client projects (MyWebSite and MyConsoleClient) and the server project (MyWindowsService) depend on MyInterfaces that contains communication interface, and on MyCommunication that knows hwo to create the client and the server. The client/server technology is largely encapsulated in the MyCommunication project, and this is where WCF and remoting branches differ the most.

Projects

ASP.NET server talks to the Windows service using this interface:

public interface IRequestHandler
{
    AcceptedRequest CreateRequest(Request req);
    AcceptedRequest GetRequest(long id);
    RequestList GetAllRequests(long sinceRevision); // use sinceRevision = 0 to get all available requests
}

CreateRequest() creates a new request, GetRequest() retrieves status of the particular request, and GetAllRequests() returns status of multiple requests. Every time anything changes in the requests universe, we increment a 64-bit revision. The client may request status of all requests (sinceRevision=0) or of requests that changed after particular revision. When the client polls the server periodically for chanegs, this mechanism allows to send only the deltas since the last observed changed instead of re-sending the entire list of requests on every poll.

Object Lifetimes

Our ASP.NET server is implemented using WebAPI technology. Web requests are processed using the RequestsController class. New, short lived instance of the controller is created for every web request, and the controller in turns creates a short lived instance of a WCF or remoting client object.

The object on the server side of the WCF/remoting channel is stable: it is created upon start of the Windows service and is destroyed only when the Windows service stops. Thus, we have multiple short lived client objects and a single long lived server object. This is true whether client/server interaction is done via WCF or via Remoting.

Objects lifetime

Windows Service implementation

MyWindowsService implements a Windows Service that hosts the RequestHandler object responsible for processing long-running client requests. Program.cs implements standard Windows Service routines such as running as a service, running as a console program, installing, and uninstalling.

RequestHandler class keeps a dictionary of all known requests, along with their state. All access to the dictionary is synchronized using lock statements. When new request is created, it is processed using Task Parallel Library:

var delay = rawRequest.RequestedProcessingTimeMs;
Task.Delay(delay).ContinueWith((task, state) => CompleteRequest(request), null);

There is some debate (StackOverflow) on how efficient this solution is, but it seems certain that pending requests rely on a timer and don't hold up any threads.

Each request contains a revision: every time the request is created or changed, it is assigned a new revision. This allows the handler to quickly retrieve requests that have changed after a certain point in time.

WCF or Remoting? TCP or Named Pipes?

Both technologies will do the job. I was a big Remoting fan, but after implementing the examples, I slightly incline towards WCF. WCF implementation is slightly less verbose, it is more modern technology, allegedly more performant (but most of the time it probably does not matter), and the documentation is easier to find. See below for more specifics.

Both WCF and Remoting can use TCP or Named Pipes as a transport protocol. For inter-machine communication TCP is the transport protocol of choice; frequently it is the only choice. However, for local interprocess communication we are better off using named pipes. Unlike TCP, named pipe server does not require a numeric port number. This may not seem like a big deal, but there is only several thousands port numbers available, and if you pick one at random, sooner or later (probably sooner) your service will collide with some other service. Also, named pipes communication is guaranteed to be limited to the local machine and thus presents lower security risk.

Why not use raw named pipes then? NET Framework does provide classes like NamedPipeServerStream, but if both sides of the communication are .NET programs, it makes little sense to send raw bytes over named pipe. Someone will have to translate .NET objets to bytes and then back to objects, and usualy there is no reason to do that manually when WCF or Remoting can do it automatically for you.

This allows us to do nice things like var list = handler.GetAllRequests(42) instead of manually packing bytes into a stream writer and then manually interpreting the bytes that come back.

WCF specific notes

WCF Interfaces

All interfaces involved in WCF communication must be decorated with the [ServiceContract] attribute, and all methods of those interfaces must be decorated with the [OperationContract] attribute.

This means that the interface must be written specifically for WCF, must include WCF-specific attributes, and must reference the System.ServiceModel assembly. You cannot reuse an existing interface created for some other purpose. Having to wriet [OperationContract] on every line is also a little annoying, but this is a requirement.

WCF version of our communication interface looks like this:

[ServiceContract]
public interface IRequestHandler
{
    [OperationContractAcceptedRequest CreateRequest(Request req);
    [OperationContractAcceptedRequest GetRequest(long id);
    [OperationContractRequestList GetAllRequests(long sinceRevision); // use sinceRevision = 0 to get all available requests
}

WCF Data Objects

Data objects passed between the client and the server may be decorated with [DataContract] and [DataMember] attributes, but this is optional. This allows to reuse POD objects created for other purposes.

WCF Server Object

There are no specific requirements for the server object, but in order to work properly, its cluss must be decorated with certain WCF specific attributes. I am not aware of the way to specify service behavior in code without attributes.

[ServiceBehavior(
    ConcurrencyMode = ConcurrencyMode.Multiple,
    InstanceContextMode = InstanceContextMode.Single,
    IncludeExceptionDetailInFaults=true)]
public class RequestHandler : IRequestHandler

Creating WCF server and client

On the server side we create a WCF host that is tied to the long-lived server object. The host has one or more endpoints. Each endpoint has a URL, a binding that describes the transport protocol, and .NET type, which is typically an interface.

Creating WCF host is implemented in WcfWrapper.cs:

_serviceHost = new ServiceHost(serverObject, new Uri(url));
_serviceHost.AddServiceEndpoint(typeof(IServerInterface), new NetNamedPipeBinding(NetNamedPipeSecurityMode.None), "");
_serviceHost.Open();

Creating a WCF client is implemented in the same file:

var address = new EndpointAddress(url);
var channelFactory = new ChannelFactory<IServerInterface>(new NetNamedPipeBinding(NetNamedPipeSecurityMode.None));
var client = channelFactory.CreateChannel(address);

WCF Security

You can see that we don't use transport security here, and this is a concious choice: named pipe communication is entirely local to the machine, and the transport security is arguably not needed.

One of the problems when communicating between an ASP.NET server and a Windows Service is that they typically run under two different user accounts. ASP.NET runs under IIS AppPool\DefaultAppPool user, while the Windows service typically runs as NETWORK SERVICE.

Fortunately, when communicating between ASP.NET client and Windows Service server, WCF pipe security works out of the box. Unfortunately, under other circumstances weird security issues may occur, and WCF does not provide an obvious solution to those. In particular, I discovered that if both client and server are run as console programs from Visual Studio Debugger, they cannot communicate properly.

Remoting specific notes

Remoting Interfaces

Remoting requires the server to does not require the interface to have any specific attributes. Remoting version of our communication interface does not mention Remoting at all:

public interface IRequestHandler
{
    AcceptedRequest CreateRequest(Request req);
    AcceptedRequest GetRequest(long id);
    RequestList GetAllRequests(long sinceRevision); // use sinceRevision = 0 to get all available requests
}

Remoting Data Objects

All data objects involved in Remoting communication must be [Serializable]. This creates an unexpected issue with WebAPI: WebAPI does not like [Serializable] classes. This behavior of WebAPI can be turned off, but this must be done separately for JSON and XML formatters, and requires quite a few lines of code in Global.asax.cs:

var formatters = GlobalConfiguration.Configuration.Formatters;
 
formatters.JsonFormatter.SerializerSettings = new JsonSerializerSettings
{
    NullValueHandling = NullValueHandling.Ignore,
    ContractResolver = new DefaultContractResolver
    {
        IgnoreSerializableAttribute = true,
    }
};
 
formatters.XmlFormatter.UseXmlSerializer = true;

Remoting server object

The only requirement is that remoting server object must derive from MarshalByRefObject.

Creating Remoting server and client

Creating server and client object is trickier than in WCF. First one must register a remoting channel, and this must be done only once per process. Each side needs both server and client provider, since the communication is bidirectional:

var serverProvider = new BinaryServerFormatterSinkProvider { TypeFilterLevel = TypeFilterLevel.Full };
var clientProvider = new BinaryClientFormatterSinkProvider();
 
var properties = new Hashtable
{
    ["portName"] = channelName,
    ["authorizedGroup"] = GetAccountName(WellKnownSidType.AuthenticatedUserSid)
};
 
var channel = new IpcChannel(properties, clientProvider, serverProvider);
ChannelServices.RegisterChannel(channel, false);

Once the channel is registered, creating a client is quite simple:

return (T)Activator.GetObject(typeof(T), url);

Creating the server for a URL requires parsing the URL, that takes a form of ipc://{channelname}/{objectName}. Once this is done, the server can be registered in one line:

RemotingServices.Marshal(mbrObj, objectName, typeof(TInterface));

Remoting security

The trickiest part is assigning "AuthorizedGroup" property of the IpcChannel. This property designates the group of accounts that have a write to access the remoting pipe. Without setting this property, calling from ASP.NET to Windows service does not work. We want allow access to the "Authenticated Users" group, which, besides real users, includes all service accounts such as app pool identity and NETWORK SERVICE. The trouble is, the property takes display name of the group, which will be localized in language-specific editions of Windows. The group is called "Authenticated Users" in English Windows, but it is "Authentifizierte Benutzer" in German Windows, and something else in French and Japanese Windows. To get around it, we need to perform an extra of converting internal group ID to a localized group name:

properties["authorizedGroup"] = GetAccountName(WellKnownSidType.AuthenticatedUserSid);

private static string GetAccountName(WellKnownSidType wellKnownSid)
{
    var sid = new SecurityIdentifier(wellKnownSid, null);
    var account = sid.Translate(typeof(NTAccount)) as NTAccount;
    return account?.Value;
}

However, after this is done, there is no more surprises: remoting communication works in all scenarios, including the case when client and server run under debugger in Visual Studio.

Documentation

For both WCF and Remoting, most of the documentation is focused on the TCP transport, and Named Pipes are mentioned only in passing. Finding complete examples that work with named pipes is a challenge: this is exactly why this article was born.

Being a newer technology, WCF documentation is easier to find. Remoting documentation often hides under older versions of .NET Framework, and under warnings like "This documentation is archived and is not being maintained".

The most difficult part was to find documentation for the IpcChannel properties. It actually hides in the documentation for the Server Channel Properties. There is also a couple of surprises. First, the property for the pipe name is called "portName" - probably a greeting from our TCP cousins. Second, the documentation says that "the default is to allow access to all authorized users". I am not sure what exactly "authorized users" means, but it is not the "Authenticated Users" group.

The documentation about WCF named pipe security is also muddy. WCF uses some intersting memory sharing protocol to determine the pipe name, and does not give easy access to the pipe handle. This protocol was designed before Windows Vista days, and sometimes breaks, e.g. when a process with elevated privileges tries to call a process which runs under the same user, but with regular privileges.

The good thing about both technologies is they are quite mature, and whatever works today is unlikely to change, unless Microsoft does a major redesign of the Windows platform.

Alternatives to Windows Services

Windows Services are popular, but not the only possible solution for processing long requests. For example, Hangfire library fights volatility of the worker process by persisting scheduled tasks to an external data source. However, Hangfire won't help if the worker process is killed and not restarted due to inactivity: see this StackOverflow post.

You need full admin rights on the machine to install and start Windows services. Therefore, they are not available in the shared hosting scenario. In this case Hangfire or a message queue may be your only option.

Conclusion and feedback

We now know how to call a Windows Service from an ASP.NET project. Obviously, this is just a sample, please don't treat it as production code. There are many things that I ignored that should be taken care of in a production system. E.g. I would not recommend a production web site to poll the server every 500ms: this will easily bring the server to its knees with just a couple of thousand users. Using web sockets to push notifications may be a better idea. The project lacks logging and user session management, and probably quite a few other things I did not think of.

If you have questions or comments, feel free to leave feedback.


Copyright (c) Ivan Krivyakov. Last updated: March 25, 2017