Nicolás de Ory

About meProjectsBlog

Simple IPC using Named Pipes in .NET Core

Aug 18, 2020

Inter-Process Communication (IPC) is the technical term for whenever you want to have two or more separate processes communicate. It has always been a problem with different solutions, using native APIs in different languages, with different approaches. From file-based communication to TCP servers, I have found named pipes to be the simplest solution for my use case, especially if you're willing to use a lightweight library that abstracts away the boilerplate code and low-level system calls.

What are Named Pipes?

Essentially, it's a pipe with a label. It's a high level implementation of IPC provided by Windows, as well as other operating systems. A process might ask "Hey OS could you plug me into the pipe called 'example' so I can send and listen for messages?". When another process makes the same request, the OS internally "binds" them and redirects messages accordingly, until one of the two disconnects. It's important to know that a pipe is only usable for two processes. If you have a server and multiple clients, you will need to have different pipes for each client-server connection.

    It's recommended to have unique pipe names every time. If the client disconnects, and then tries connecting to the same named pipe again, it will probably not work as expected.

Invoking remote methods asynchronously, both ways

I have been using the PipeMethodCalls library, which is written for .NET Core. It essentially lets you invoke methods on the other end of the pipe, passing arguments and maintaining type-safety via interfaces. Method calls and parameters are passed via JSON, with some limitations, such as properties or methods with ref and out parameters. You can choose to use two-way communication, or use a client-server configuration. In this example we're going to achieve the former.

Throughout the guide we're going to use client and server to distinguish between the parent and child processes, but since the communication is two-way, both have full functionality. If you have a backend/frontend configuration, you could have the frontend querying the backend, or have the backend send commands to the frontend.

The first thing we need to figure out is what functionality is going to be exposed via IPC. We can define what operations are supported by the client and the server using an interface.

For example, let's say you wanted the server to provide calculator-like functionality. We'd like to be able to ask the server what's 1+1, or 4-2. We can define our interface IServerCalculator with the methods Add and Substract.

public interface IServerCalculator {
    int Add(int a, int b);
    int Substract(int a, int b);
}

Note that since the server is the one doing the calculations, the client doesn't need to know anything about the implementation. It just needs to know that the server is able to add and substract, but it doesn't need to know how. The interface is enough information for the client to make requests to the server.

Let's say the client also wants to expose some functionality to be called by the server. We'd like to make it capable of concatenating strings.

So let's define our IClientConcatenator interface, with only one method Concatenate.

public interface IClientConcatenator {
    string Concatenate(string a, string b);
}

We have now established what each process is capable of, so we have type-safety and won't have to worry about malformed data or any other problems that may arise.

Implementing both interfaces

In order to have two-way communication, both client and server need to know what the other can do, as well as what they can do themselves. So you would need to have the interface files in both projects. You can use shared projects, or you could just copy the files.

However, as we mentioned earlier, we only need to have the implementation in their respective project. So you would have the IServerCalculator implementation inside the server project, and the IClientConcatenator one inside the client.

Let's implement both classes.

public class ServerCalculator : IServerCalculator {
    public int Add(int a, int b) {
        return a + b;
    }
    
    public int Substract(int a, int b) {
        return a - b;
    }
}
public class ClientConcatenator: IClientConcatenator {
    public string Concatenate(string a, string  b) {
        return a + b;
    }
}

Now that we have an implementation for each interface, we're ready for the last step: establish communication and make remote method calls.

Designing the pipeline loop

Why would we need to make a loop? With the library we're using, we only have to connect once and make as many calls as needed.

However, I wanted to handle reconnections in my case. What would happen if the client disconnects? Could it reconnect to the server?

Playing with named pipes for quite some time, I found that the best way to handle reconnections is to agree on a different pipe name every time.

Typical high-level flow would be as follows:

  1. Server process starts, and waits for a connection on the agreed-on pipe name example-001.
  2. Client starts, and tries to connect to the pipe example-001.
  3. Connection is established. Client can send commands to the server at any time, and vice versa.
  4. Client disconnects.
  5. Server detects the disconnection, and starts waiting for another connection, on a different pipe name, example-002

And so on. Note that if you do want the client to be able to handle server reconnections, this loop must be present on both processes, so it works either way if the server or the client disconnect. In our case, the server never disconnects before the client, so on the client side we only need to care about connecting to the server.

    But wait, but if this loop is meant to achieve communication between the two processes in the first place, how can both agree on a unique pipe name without being able to talk to each other?

There are several solutions for this. What I did in my case was to use a temporary file in a shared directory, accessible by the two. Only the server would write to this file, and the client would read it. Because the server always started first, and the client never runs without the server, it would be the one deciding the pipe name. Once the connection is established, the server deletes the file. When the client disconnects, the server creates the file again, waiting for the client to reconnect.

I'm fairly sure that there's a more elegant solution, but after many headaches I decided that this would just work for my use case. Of course, if you don't need to handle reconnections, you can always use the same pipe name, but the server should shut down as soon as the client does. It might seem counter-intuitive to do it this way, but Windows doesn't like repeated pipe names.

Finally, we can implement this in our .NET Core app.

I strongly recommend running this code inside a separate thread or task, so your app isn't stuck waiting on the main thread.

Here's the code for the server:

public static async void Main(string[] args)
{
    PipeServerWithCallback<IUIController, IBackendController> pipeServer;
    Random random = new Random();
    while (true)
    {
        try
        {
            // We choose a random pipe name and write it to a file
            int randomNum = random.Next(0, 1000000);
            string pipeName = "example-" + randomNum.ToString();
            using (StreamWriter f = File.CreateText("pipename"))
            {
                f.Write(pipeName);
            };

            // We create the pipe server. 
            // The first type parameter is the client interface - what the client can do
            // The second one is the server interface - what the server can do
            // Lastly, we pass the pipe name and an instance of our calculator implementation
            pipeServer = new PipeServerWithCallback<IClientConcatenator, IServerCalculator>(pipeName, () => new ServerCalculator());

            // We wait for a connection (this could be a long time).
            await pipeServer.WaitForConnectionAsync();
            // If execution continues, that means we have a connection.
            Debug.WriteLine("Pipeline connected.");
            // Delete the pipename file, since we don't need it anymore
            File.Delete("pipename");
            
            // Wait until somebody closes the pipe.
            await pipeServer.WaitForRemotePipeCloseAsync();
            // Clean up
            pipeServer.Dispose();
            Debug.WriteLine("Pipeline disconnected.");
        }
        catch (Exception e)
        {
            Debug.WriteLine("Something bad happened");
            Debug.WriteLine(e.ToString());
        }

    }
}

And here's the code for the client. In my case it was simple because, as mentioned above, it didn't need to handle server disconnections:

public static async void Main(string[] args)
{
    PipeClientWithCallback<IServerCalculator, IClientConcatenator> pipeServer;
    // We read the pipe name from the file
    string pipeName = File.ReadAllText("pipename");
    
    // Initialize the pipe client. Note that the type parameters are exactly
    // the same passed in the server example, but in reverse order.
    // The first type parameters indicates what the other process can do,
    // while the second one indicates the functionality this process exposes.
    pipeClient = new PipeClientWithCallback<IServerCalculator, IClientConcatenator>(pipeName, () => new ClientConcatenator());
    
    // We wait until the connection is established
    await pipeClient.ConnectAsync();

    // We're connected now.
    // Let's ask the server to do 1+2, and wait for the result
    int result = await pipeClient.InvokeAsync(x => x.Add(1,2));
    Debug.WriteLine("Server said 1+2 is " + result);

    // Always clean up if you're going to disconnect
    pipeClient.Dispose();
    
}

That should be enough to get yourself started with named pipes in .NET Core.

If you have any questions, please post a comment or you can also send me an email.

Back to top