Imagine a scenario where your .NET applications could effortlessly integrate with all your current systems, regardless of their technology! This is now a reality, thanks to interoperability that .NET offers. Whether a company uses native libraries, microservices, older systems, or mixed-language workloads, .NET offers adaptable ways to connect them without requiring a full rebuild. This blog provides a brief synopsis of how .NET works with other technologies and the reasons it is important for contemporary development. In simple words, interoperability refers to various systems or languages collaborating seamlessly.
Why interoperability matters?
Most environments are rarely designed around just one language or framework. In real life scenarios, you’ll typically encounter environments designed around the following languages and technologies:
- Native C/C++ libraries
- Java applications, Python, or Go services
- Shell scripts and command-line tools
- Legacy applications
- Modern microservices
Whenever you need to add new features or make changes, .NET interoperability lets you connect new parts with the old ones instead of rebuilding everything from scratch. This keeps the system stable, saves time, and makes it easier to modernize your application step by step.
How .NET interoperates?
The following are the ways in which .NET can interact with other components on the system:
- Calling native libraries using Platform Invocation Services (P/Invoke)
- Using defined processes
- Using APIs [such as Representational State Transfer (REST) or Google Remote Procedure Call (gRPC)]
- Working with system tools and scripts
- Using native Ahead-of-Time (AOT) for high-performance scenarios
Understanding the interoperability in action
In this blog, we’re going to go through an easy step-by-step example to see interoperability in action. Rather than jumping right into the theory, we’ll kick things off by building a small console application and then slowly linking it with the code from another language. Each step will build on the earlier, allowing you to clearly observe how two separate systems work together while still being loosely connected. By the time we finish this walkthrough, the idea of interoperability should seem practical and easy to grasp instead of just theoretical.
Prerequisites
Before you begin, make sure you have the following prerequisites fulfilled:
- .NET SDK installed
- A fundamental understanding of C#
- Access to the command line
- A compiler for native code (needed for P/Invoke examples)
- Internet access (required for API examples)
Create a .NET console application
Complete the following steps to create a new .NET console application.
Step 1: Create the Project.
- Create a new console project using following command.
dotnet new console -n DotNetInterDemo
- Copy the project into the project folder using following command.
cd DotNetInterDemo
Step 2: Run the project.
By default, a Hello World program is created. R un the application using the following command.
dotnet run
Step 3: Verify the result.
Verify if t he term inal display s following output :
Hello, World!
Approaches to verify interoperability in .NET
Following are some of the approaches for executing interoperability in .NET.
Approach 1: Calling the native libraries using Platform Invocation Services (P/Invoke)
If you already have C or C++ libraries, .NET can directly call them. This makes it easier to reuse existing logic rather than starting from scratch. P/Invoke allows you to use existing native libraries directly without the need to rewrite any code.
Let's go over a quick example of calling C++ from .NET:
Step 1: Create a native library.
- Create a main.cpp file using the vim editor or nano:
vi main.cpp
- In the main.cpp file, we include some C++ code that defines a simple function that takes two numbers and returns their sum:
main.cpp:
#include <iostream>
extern "C" {
int add_nos(int a, int b){
return a + b;
}
}
- To turn your C++ code into a shared library that .NET can use, run:
g++ -shared -fPIC main.cpp -o libadd.so
- To locate the new library run:
export LD_LIBRARY_PATH=$PWD:$LD_LIBRARY_PATH
This adds the current folder to the library search path so .NET can locate libadd.so.
Step 2: Call the Native Code from .NET.
Open the Program.cs file in your project DotNetInterDemo and replace its content with the following code:
using System;
using System.Runtime.InteropServices;
class NativeMethods
{
private const string LIB = "libadd.so";
[DllImport(LIB, CallingConvention = CallingConvention.Cdecl)]
public static extern int add_nos(int a, int b);
}
class Program
{
static void Main()
{
int result = NativeMethods.add_nos(10, 20);
Console.WriteLine($"Addition result from C++: {result}");
}
}
Step 3: Run the project.
dotnet run
Step 4: Verify the result.
Verify if the terminal display s following output.
Addition result from C++: 30
Use P/Invoke when your .NET app needs to connect with OS-level features, high-performance native code, old libraries, or functions that aren’t available in managed code. When you’re using P/Invoke, always think about long-term maintenance, cross-platform compatibility, and good memory management. Common scenarios include device drivers, unique or specialized algorithms written in native code, and legacy shared libraries.
Approach 2: Verifying process-level interoperability
Sometimes you already have working Java code and don’t want to rewrite it in .NET. The great thing is that .NET can interact with Java code in multiple ways. The easiest and most dependable method is to run Java as a separate process and grab the output. This is very common in real-time projects.
Let’s walk you through an example:
Step 1: Create and compile a Java program.
First, let us create a Java program inside the DotNetInterDemo directory and compile it.
- Create a new Java file, named, CallJava.java.
public class CallJava {
public static void main(String[] args) {
System.out.println("Hello from Java!!!");
}
}
- Compile the Java file.
javac CallJava.java
- Run the Java program:
java CallJava
- Verify if the terminal displays following output.
Hello from Java!!!"
Step 2: Modify Program.cs to Run the Java Code
After creating and compiling the Java program, the next action is updating Program.cs so the .NET application can launch that Java program (using process-level interoperability).
Program.cs:
using System;
using System.Diagnostics;
class Program
{
static void Main()
{
var process = new Process();
process.StartInfo.FileName = "java";
process.StartInfo.Arguments = "CallJava";
process.StartInfo.WorkingDirectory = "./";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.UseShellExecute = false;
process.Start();
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
Console.WriteLine("Output from Java code:");
Console.WriteLine(output);
}
}
In this step, we will use System.Diagnostics.Process in C# to start the Java program.
Step 3: Run the project
Run the application using the following command:
dotnet run
Step 4: Verify the result
Verify if the terminal displays following output.
Output from Java code:
Hello from Java!!!
Approach 3: Using APIs (REST or gRPC)
Sometimes applications don’t need to share code, they just need to communicate. This is where APIs come in. APIs provide a common language for applications to exchange data, making it possible for .NET to interact seamlessly with services written in other platforms.
Among the most widely used options are:
- REST : simple and widely used for basic services.
- gRPC : faster and ideal for internal or high‑performance services.
APIs are often used when .NET applications need to interact with Java, Python, or other platforms. Let's go over a quick example of a REST API in .NET. In this example we create a local REST API service in .NET.
Step 1: Create a local REST API service in .NET.
- To set up our REST API project, we’ll start by creating a new minimal web application using the .NET CLI:
dotnet new web -n lcoalRestApiService
- Copy the above project to the project folder.
cd lcoalRestApiService
We need to configure our project by updating Program.cs file. This is where we define the REST API endpoints that will handle incoming requests and return responses.
Program.cs:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/info", () =>
{
return new
{
message = "Hello this the local REST API service!!"
};
});
app.Run();
Step 2: Run the project.
Run the application using the following command:
dotnet run
Step 3: Verify the result.
Verify if the terminal displays following output:
{"message":"Hello this the local REST API service!!"}
Step 4: Modify the main program for REST API call.
Return to the DotNetInterDemo directory and modify the main Program.cs. so, it can send requests to the REST API and handle the responses.
Program.cs:
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var client = new HttpClient();
var response = await client.GetStringAsync("https:// localhost:5273/info");
Console.WriteLine(“Response from REST API service :”);
Console.WriteLine(response);
}
}
Step 5: Run the main project.
Run the application using the following command:
dotnet run
Step 6: Verify the result.
Verify if the terminal displays following output:
Response from REST API service :
{"message":"Hello this the local REST API service!!"}
Approach 4: Working with system tools and scripts
In a .NET application, we can run system commands and scripts. In recent times, a lot of automation is done through:
- Bash scripts
- Cron jobs
- Systemd services
Let's go through a brief example of a system command.
Step 1: Run a system command from Program.cs
To demonstrate process interoperability, let’s update Program.cs with a brief example that executes a system command. This shows how your .NET application can interact directly with the operating system.
Program.cs:
using System.Diagnostics;
class Program
{
static void Main()
{
var process = Process.Start("bash", "-c \"echo Hello from .NET, This is a example of system command!\"");
process.WaitForExit();
}
}
This launches a new bash shell from your .NET program and runs the command echo Hello from .NET, This is a example of system command!. The result is that the message is printed to the console, showing how .NET can execute system-level commands directly.
Step 2: Run the project.
Run the application using the following command:
dotnet run
Step 3: Verify the result.
Verify if the terminal displays following output:
Hello from .NET, This is a example of system command!
Conclusion
This blog explained the various instances of interoperability which plays a significant role in .NET. It has enabled the teams to gradually modernize; reuse current libraries; link .NET with Java, native code, or system tools; and create microservices that seamlessly integrate environment. The outcome is a versatile and effective platform where .NET serves as an additional powerful tool in the enterprise stack.