Unix Domain Sockets in .NET 6 — Basics and Real-World Examples
Using Unix Domain Sockets in .NET 6 server and client apps
11 min read
Creating cross-platform capable APIs using .NET became easier with the evolution towards .NET 6. APIs built on ASP.NET typically serve the app via TCP/IP using a specific interface and port. That makes sense due to APIs interacting over a network regularly. Have you already thought about changing this behavior?
In some cases, more performant solutions exist, especially when applications need to exchange data and run on the same machine. If you are faced with this type of scenario, Unix Domain Sockets might be a suitable alternative to the default behavior. This article shows how to use .NET applications to serve and consume APIs based on Unix Domain Sockets. Use cases at the end of the article clarify the usage based on practical examples.
What is a Unix Domain Socket?
Before diving into code samples, let’s have a brief look at Unix Domain Sockets (or Unix Socket) and how they differ from a TCP/IP socket.
A Unix Domain Socket is an inter-process communication mechanism that allows bidirectional data exchange between multiple applications. Like almost everything on a Unix-based operating system, Unix Sockets are file-based. Hence, access can be controlled via file system permissions and communication is limited to applications running on the same machine.
When applications are running on the same Unix-based host, Unix Domain Sockets should be preferred because they are more lightweight and faster than TCP/IP sockets.
In contrast, TCP/IP sockets allow communication between applications over a network. By using the loopback interface, communication can take place on the same machine too. Because of the purpose of inter-machine communication, TCP/IP sockets have to consider operations like routing, among others. That’s why they are not as fast and lightweight as Unix Domain Sockets.
Serving a .NET Minimal API over Unix Sockets
Applications based on ASP.NET are served via the build-in cross-platform web server named Kestrel. By default, Kestrel is set up based on a TCP/IP Socket using the parameters specified within different environment variables, for example using ASPNETCORE_URLS for interface and port bindings. That makes sense because APIs are usually created for communication between applications over a network.
But it’s just the default. Adjusting the configuration via the well-designed interface is very easy. With a few lines of code, you can build and run an API that uses Unix Domain Sockets instead of TCP/IP Sockets based on ASP.NET. We will look at some practical use cases later in this article.
But for now, we will walk through server and client application code which demonstrates how to work with Unix Sockets. To get started with our first server application we create a new minimal API using the dotnet CLI:
dotnet new webapi -minimal -o SocketDemo
Then we replace the entire content of the Program.cs file using the next code snippet. Within this code extract, the constant UnixSocketPath defines the path that is used to create the socket on the file system. After some sanity checks, Kestrel is advised to use the defined socket file via ListenUnixSocket With this simple line of code all pre-defined registrations of TCP/IP stack-related settings are replaced. The remaining parts of the snippet follow the common syntax when creating minimal APIs using .NET
To run the example on Windows-based systems, I can recommend WSL 2. Visual Studio and Visual Studio Code come with excellent remote integrations so you can run the code directly from the development environment.
The screenshot below shows the previously presented sample executed within a Visual Studio Code WSL 2 remote session. The left terminal window shows the running ASP.NET application executed via
dotnet run, confirming that we are serving the API using the defined Unix Socket.
The right terminal window shows a client request using the curl command
curl -w "\n" --unix-socket /tmp/foo.sock [http://localhost/](http://localhost/) The output represents the expected response from our server application.
Consuming an API using Unix Domain Sockets
With the command line tool curl we just saw one of many possibilities to request data from our server application. In this section, you will learn how to build .NET clients that can consume data from our Unix Socket-based backend. We will discuss several approaches that all lead to the same result.
Using a .NET 6 based console application
In our first example, we start with a simple console application representing the “hard way” to request data from the backend. It uses the Socket class that is included in the framework.
The reason why I call this the “hard way” is shown in the following code block. After creating the endpoint using UnixDomainSocketEndpoint class, we have to build the required request message from scratch before sending it using the previously instantiated Socket.
But the .NET framework does provide even more possibilities to request data based on sockets. The second console application represents the “elegant way” using the SocketsHttpHandler class.
With this approach, we are using a higher level of abstraction. To get started we have to create an instance of the SocketsHttpHandler first. Via the ConnectCallback property, the connection to our Unix Domain Socket can be established. For this, a NetworkStream is created using the UnixDomainSocketEndpoint class.
By the use SocketsHttpHandler, the requests are processed using the HTTP messaging pipeline. Each message handler in the pipeline receives an HTTP request and returns an HTTP response. A pipeline contains typically multiple handlers that are chained together. HTTP message handlers provide the possibility to benefit from the advantages of the HttpClient interface. So there is no need to build the messages manually before sending a request. As shown in the snippet above, we can easily request data from the backend by using the
GetAsyncmethod of the HttpClient.
Using an ASP.NET application with Refit
In the case of web applications developed with ASP.NET, we can benefit from both embedded dependency injection and Refit. This combination allows centralized configuration of our endpoints, whereby no API-specific knowledge is required during the use of the external API over Unix Sockets.
Refit is an automatic type-safe REST library for .NET Core, Xamarin and .NET that turns your REST API into a live interface. (https://github.com/reactiveui/refit)
Within the next code snippet, we will have a look at the configuration and usage of a Unix Socket-based API with ASP.NET and Refit. The snippet covers the following steps:
- Create an interface representing the API
APIs are described by an interface. Methods represent single endpoints configured via additional attributes. In our example, the
IFooApiinterface represents our Minimal API backend. Using the attribute
[Get("/")]the specific endpoint of the API is mapped to the interface method.
- Register the Refit client embedded dependency injection (DI) container
By the call of the extension method
AddRefitClientall required dependencies are registered within the DI container. For this the NuGet package Refit.HttpClientFactory must be installed previously.
- Configure the primary HTTP message handler
With this next step, Refit is advised to use the SocketsHttpHandler class as the primary HTTP message handler, which is the same implementation as in our console application example before. Hence, all API calls using the registered interface are processed using the HTTP messaging pipeline.
- Configure the HTTP client
Finally, the set-up of the HTTP client should take place. In the example below, the configuration is reduced to the base address of our backend. Since we are focusing on Unix Domain Sockets, of course, this is localhost.
With this basic configuration, the API can be called via the interface
IFooApi. The interface can be injected using the mechanisms of ASP.NET, which means in every instance created by DI, for example, Controllers.
Practical Use Cases
In the sections above you have seen some approaches how to deal with Unix Sockets in .NET-based applications. So now it’s time for some real-world examples where this can be very useful.
Use Case #1 — Exchange data between .NET and Python
Think about an application to predict some amazing things. General frontend and backend parts are implemented in ASP.NET. Some prediction algorithms are Python-based. So basically both components can be used separately from each other or like in this use case shipped together in one Linux-based Docker image.
Out there, many ways exist to exchange data between applications. Pure implementations using named pipes, sockets, files or specific in the .NET context Python.NET. Another approach is based on gRPC. This technology allows exchange between both application parts using a well-designed contract. gRPC easily enables various ways of communication like unary calls, client streaming, server streaming or the combination of both bidirectional streamings.
Due to the whole application being shipped within the same Docker Image, Unix Domain Sockets are the lightweight and fastest option to spin up a running application.
The basics how to build and use gRPC for .NET and Python can be found within the article I’ve linked above. For this specific use case, I would like to state that gRPC can be used besides TCP/IP Sockets with Unix Domain Sockets as well. The following example shows a Python-based gRPC server using a Unix Socket. Within a .NET-based gRPC client a unary call is executed. Of course, the roles can also be reversed.
As shown in the ASP.NET with Refit sample the gRPC client also relies on SocketsHttpHandler. With this handler, the gRPC communication channel is created. In the last step, the unary call is executed against the gRPC server hosted in Python. In the sample, the code is wrapped within a .NET Minimal API so it can easily be triggered (that’s just for the demo — never recreate the whole communication stack for a single request).
When running all mentioned parts of the sample within a Visual Studio Code WSL 2 remote session, we can query against the API using curl. The result is displayed within the next figure.
Use Case #2— Request data from ctrlX CORE ultra-compact control
ctrlX CORE is an ultra-compact control system for automation provided by Bosch Rexroth. One feature is the possibility to develop third-party applications and distribute them over the ctrlX Store. These apps can be developed using various programming languages and are packed using Snap technology. The platform offers various REST interfaces for seamless integration with user management or the licensing system, among others. To benefit from platform features these APIs must be used.
Of course, the REST interfaces can be addressed using regular HTTP calls. In the case of ctrlX CORE all endpoints enforce HTTPS and require a Bearer token to prevent unallowed access. For non-interactive scenarios, for example, an app needs to request license data, this token is not available. But how to realize integration in these scenarios? The solution is almost obvious.
Snap containers can be linked to each other via slots and plugs. The licensing service on ctrlX CORE offers such a plug:
plugs: licensing-service: interface: content content: licensing-service target: $SNAP\_DATA/licensing-service
Thereby, third-party apps can request data from the license system API via the Unix Domain Socket
$SNAP_DATA/licensing-service/licensing-service.sock. This socket is accessible only to applications running on the same system. Hence, a request does not need to use HTTPS and a Bearer token is no longer required.
Let’s have a look at some details about realizing the use case. We will not discuss all aspects required to create a .NET app for ctrlX CORE. Maybe I will write about that in a future article ;-)
A Snap is described by a file named snapcraft.yaml. This file is the main entry point for Snapcraft (please see official documentation for further details). To access the Unix Socket provided by another Snap, this file must be extended. In the case of ctrlX CORE this could look like below:
After adding the plug to the list of known plugs inside our snapcraft.yaml file, we can now access the specific Unix Domain Socket. Since we are still within our technology stack, the approaches mentioned above can be used in this case also. The next code extracts show the application of this stack for the described use case. It consists mainly of
A class that is responsible for putting all applications parts together. This covers for example the registration and configuration of the components within the dependency injection container. The procedure for socket-related interfaces and classes follows the scheme that we have learned before.
This interface describes the licensing API. Each method is decorated with a Refit-specific attribute. In the sample code, only the method to acquire a license is exemplarily shown.
A class that takes responsibility for application-specific logic concerning license management. It uses the ICtrlXLicenseApi interface to access the API hosted on ctrlX CORE using the configuration provided via Startup class.
In this article, the basics of using Unix Domain Sockets in .NET-based applications are explained. Practical examples further demonstrate the usage. We can state that integrated mechanisms, like HTTP message handlers, make it very simple to establish lightweight and fast communication for services running on the same machine. In combination with other libraries such as Refit, most communication-related initialization can be decoupled from productive code.
On GitHub, a full working sample application is available. It covers client and server applications using Unix Domain Socket for communication. In addition, the code for the gRPC sample is available. (Example how to work with Unix Sockets using .NET 6)
Thank you for taking the time to read my article. 😄
If you enjoyed it and want to see more coding-related content then follow me on my social profiles. Don't hesitate to like, comment or share this post. I appreciate any kind of feedback on this article.
References & Further links
Did you find this article valuable?
Support Fabian Zankl by becoming a sponsor. Any amount is appreciated!