Dev Create

A Named Pipes Approach to IPC

Revit_Named_Pipes

How we built reliable request/response communication with Revit using Named
Pipes</strong >


The Challenge

Building automation around Revit presents a unique problem:
How do you reliably communicate with a running Revit process?

Revit runs as a desktop application with strict threading requirements. The
Revit API must be called from specific contexts, and any automation needs to
respect the ExternalEvent pattern. This creates challenges:

  • Asynchronous by nature – You can’t just “call” Revit
    synchronously
  • Threading constraints – Revit API is single-threaded
  • Response timing – Need to know when Revit finishes
    processing
  • Data exchange – How to pass complex requests and get
    results back

We needed a communication pattern that worked with Revit’s
architecture, not against it.

The Solution: Named Pipes for IPC

After evaluating several approaches, we settled on
Named Pipes – a Windows inter-process communication (IPC)
mechanism that’s perfect for local service-to-service communication.

Why Named Pipes?

We considered several options:

  • File watching – High latency, polling overhead, race
    conditions
  • HTTP API inside Revit – Complex threading, stability
    concerns
  • COM/RPC – Heavy, complex registration requirements
  • Named Pipes ← Our choice: Fast, reliable, built into
    Windows

Named Pipes give us:

  • Sub-second communication between processes
  • Request/response pattern – Clean, predictable flow
  • Binary serialization – Efficient data transfer
  • Windows security – ACL-based access control
  • No network exposure – Stays local to the machine
The Communication Pattern

Our implementation follows a clean request/response model:

1. The Request Model
public class PipeRequest
{
    public string RequestId { get; set; }        // Unique identifier
    public string Action { get; set; }           // "RevitToJson" or "JsonToRevit"
    public string InputFilePath { get; set; }    // Absolute path to input file
    public string OutputFolderPath { get; set; } // Where to write results
    public Dictionary<string, string> Parameters { get; set; } // Optional params
}

Simple, focused, and contains everything Revit needs to process the job.

2. The Response Model
public class PipeResponse
{
    public string RequestId { get; set; }        // Matches the request
    public bool Success { get; set; }            // Did it work?
    public string Message { get; set; }          // Human-readable result
    public Dictionary<string, string> OutputFiles { get; set; } // Result file paths
}

Clean separation: request contains instructions,
response contains results.

Implementation: Client Side (Queue Service)

The client establishes a connection and sends requests to Revit:

public class RevitPipeClient
{
    private readonly string _pipeName = "RevitAutomationPipe";
    public async Task<PipeResponse> SendRequestAsync(PipeRequest request)
    {
        using var pipeClient = new NamedPipeClientStream(
            ".", 
            _pipeName, 
            PipeDirection.InOut,
            PipeOptions.Asynchronous
        );
        // Connect with timeout
        await pipeClient.ConnectAsync(5000);
        // Serialize and send request
        var requestJson = JsonConvert.SerializeObject(request);
        var requestBytes = Encoding.UTF8.GetBytes(requestJson);
        await pipeClient.WriteAsync(BitConverter.GetBytes(requestBytes.Length));
        await pipeClient.WriteAsync(requestBytes);
        await pipeClient.FlushAsync();
        // Read response
        var lengthBytes = new byte[4];
        await pipeClient.ReadAsync(lengthBytes, 0, 4);
        var responseLength = BitConverter.ToInt32(lengthBytes, 0);
        var responseBytes = new byte[responseLength];
        await pipeClient.ReadAsync(responseBytes, 0, responseLength);
        var responseJson = Encoding.UTF8.GetString(responseBytes);
        return JsonConvert.DeserializeObject<PipeResponse>(responseJson);
    }
}

Key points:

  • Length-prefixed messages (4-byte int + payload)
  • JSON serialization for readability
  • Async/await for non-blocking I/O
  • Clean timeout handling
Implementation: Server Side (Revit Add-in)

The Revit add-in listens on the pipe and processes requests:

public class PipeListener
{
    private readonly ExternalEvent _externalEvent;
    private readonly JobHandler _jobHandler;
    private CancellationTokenSource _cts;
    public async Task StartListeningAsync()
    {
        _cts = new CancellationTokenSource();
        while (!_cts.Token.IsCancellationRequested)
        {
            using var pipeServer = new NamedPipeServerStream(
                "RevitAutomationPipe",
                PipeDirection.InOut,
                1,
                PipeTransmissionMode.Byte,
                PipeOptions.Asynchronous
            );
            await pipeServer.WaitForConnectionAsync(_cts.Token);
            try
            {
                // Read request
                var lengthBytes = new byte[4];
                await pipeServer.ReadAsync(lengthBytes, 0, 4);
                var requestLength = BitConverter.ToInt32(lengthBytes, 0);
                var requestBytes = new byte[requestLength];
                await pipeServer.ReadAsync(requestBytes, 0, requestLength);
                var requestJson = Encoding.UTF8.GetString(requestBytes);
                var request = JsonConvert.DeserializeObject<PipeRequest>(requestJson);
                // Process via ExternalEvent (Revit's threading model)
                _jobHandler.SetCurrentJob(request);
                _externalEvent.Raise();
                // Wait for completion
                var response = await _jobHandler.WaitForCompletionAsync();
                // Send response
                var responseJson = JsonConvert.SerializeObject(response);
                var responseBytes = Encoding.UTF8.GetBytes(responseJson);
                await pipeServer.WriteAsync(BitConverter.GetBytes(responseBytes.Length));
                await pipeServer.WriteAsync(responseBytes);
                await pipeServer.FlushAsync();
            }
            catch (Exception ex)
            {
                // Send error response
                var errorResponse = new PipeResponse
                {
                    RequestId = "unknown",
                    Success = false,
                    Message = $"Error: {ex.Message}"
                };
                var errorJson = JsonConvert.SerializeObject(errorResponse);
                var errorBytes = Encoding.UTF8.GetBytes(errorJson);
                await pipeServer.WriteAsync(BitConverter.GetBytes(errorBytes.Length));
                await pipeServer.WriteAsync(errorBytes);
            }
        }
    }
}

Key points:

  • Continuous listening loop
  • Single connection at a time (simple, reliable)
  • ExternalEvent for Revit API access
  • Error handling with structured responses
The ExternalEvent Bridge

Revit’s API requires calls from the main UI thread. ExternalEvent provides the
bridge:

public class JobHandler : IExternalEventHandler
{
    private PipeRequest _currentRequest;
    private TaskCompletionSource<PipeResponse> _completionSource;
    public void SetCurrentJob(PipeRequest request)
    {
        _currentRequest = request;
        _completionSource = new TaskCompletionSource<PipeResponse>();
    }
    public async Task<PipeResponse> WaitForCompletionAsync()
    {
        return await _completionSource.Task;
    }
    public void Execute(UIApplication app)
    {
        try
        {
            var response = _currentRequest.Action switch
            {
                "RevitToJson" => HandleRevitToJson(app, _currentRequest),
                "JsonToRevit" => HandleJsonToRevit(app, _currentRequest),
                _ => new PipeResponse 
                { 
                    Success = false, 
                    Message = $"Unknown action: {_currentRequest.Action}" 
                }
            };
            response.RequestId = _currentRequest.RequestId;
            _completionSource.SetResult(response);
        }
        catch (Exception ex)
        {
            _completionSource.SetResult(new PipeResponse
            {
                RequestId = _currentRequest.RequestId,
                Success = false,
                Message = ex.Message
            });
        }
    }
    public string GetName() => "RevitAutomationJobHandler";
}

This pattern elegantly handles Revit’s threading requirements while
maintaining async communication.

Real-World Usage Example

From the queue service perspective, using this is simple:

var pipeClient = new RevitPipeClient();
var request = new PipeRequest
{
    RequestId = Guid.NewGuid().ToString(),
    Action = "RevitToJson",
    InputFilePath = @"C:Jobsbuilding.rvt",
    OutputFolderPath = @"C:Jobsoutput",
    Parameters = new Dictionary<string, string>
    {
        { "includeGeometry", "true" },
        { "exportFormat", "json" }
    }
};
var response = await pipeClient.SendRequestAsync(request);
if (response.Success)
{
    Console.WriteLine($"Success! Output: {response.OutputFiles["output.json"]}");
}
else
{
    Console.WriteLine($"Failed: {response.Message}");
}

Total round-trip time: ~5 seconds for typical Revit
processing.

Benefits of This Approach
1. Simplicity

No message brokers, no queues, no complex infrastructure. Just two processes
talking directly.

2. Performance

Named Pipes are extremely fast for local communication. Sub-millisecond
connection times, minimal serialization overhead.

3. Reliability

Windows Named Pipes are battle-tested. They handle process crashes gracefully
– the pipe closes, client gets notification.

4. Security

Pipes stay local to the machine. No network sockets, no firewall rules, no TLS
certificates to manage.

5. Debugging

JSON serialization makes debugging trivial. Log the request/response JSON and
you have complete visibility.

6. Scalability

Want to scale? Run multiple Revit instances on separate machines with their
own pipe names. Simple load distribution.

Technical Lessons Learned
1. Length-Prefixed Messages Are Essential

Without knowing message length upfront, you can’t reliably read from a stream.
The 4-byte length prefix pattern is simple and bulletproof:

// Write: [4 bytes: length][N bytes: payload]
await stream.WriteAsync(BitConverter.GetBytes(payload.Length));
await stream.WriteAsync(payload);
// Read: [4 bytes: length][N bytes: payload]
var lengthBytes = new byte[4];
await stream.ReadAsync(lengthBytes, 0, 4);
var length = BitConverter.ToInt32(lengthBytes, 0);
var payload = new byte[length];
await stream.ReadAsync(payload, 0, length);
2. ExternalEvent Is Your Friend

Don’t fight Revit’s threading model. Use ExternalEvent +
TaskCompletionSource to bridge async pipe communication with
Revit’s synchronous API:

// Set up the job
_completionSource = new TaskCompletionSource<PipeResponse>();
_externalEvent.Raise();
// Wait asynchronously
var response = await _completionSource.Task;
// In Execute() method (called on Revit's main thread):
var result = ProcessRevitFile(); // Revit API calls
_completionSource.SetResult(result); // Unblock awaiter
3. Single Connection at a Time

We experimented with
NamedPipeServerStream.MaxNumberOfServerInstances = -1
(unlimited). It added complexity without benefit.

Keep it simple: One connection at a time. If multiple clients
need access, queue them at a higher level.

4. Timeouts Are Critical

Always set connection timeouts. A crashed client shouldn’t block the server
forever:

await pipeClient.ConnectAsync(5000); // 5-second timeout

On the server side, use CancellationToken for graceful shutdown:

await pipeServer.WaitForConnectionAsync(cancellationToken);
5. JSON vs Binary Serialization

We chose JSON for debuggability. The performance difference is negligible for
our use case (typical messages are < 10KB).

If you’re transferring large geometry data, consider:

  • File paths instead of inline data
  • MessagePack or Protocol Buffers for binary efficiency
  • Compression for large JSON payloads
Architecture Diagram
┌─────────────────────────────────────────────────────────┐
│                    Queue Service                        │
│                                                         │
│  ┌──────────────────────────────────────────────────┐  │
│  │  RevitPipeClient                                 │  │
│  │                                                  │  │
│  │  1. Create PipeRequest                          │  │
│  │  2. Connect to "RevitAutomationPipe"            │  │
│  │  3. Send JSON request                           │  │
│  │  4. Await response                              │  │
│  │  5. Return PipeResponse                         │  │
│  └──────────────────────────────────────────────────┘  │
└──────────────────┬──────────────────────────────────────┘
                   │
                   │ Named Pipe
                   │ "RevitAutomationPipe"
                   │
┌──────────────────▼──────────────────────────────────────┐
│                 Revit Add-in                            │
│                                                         │
│  ┌──────────────────────────────────────────────────┐  │
│  │  PipeListener                                    │  │
│  │                                                  │  │
│  │  1. Listen on named pipe                        │  │
│  │  2. Receive PipeRequest                         │  │
│  │  3. Queue to ExternalEvent                      │  │
│  │  4. Execute on Revit thread                     │  │
│  │  5. Return PipeResponse                         │  │
│  └──────────────────────────────────────────────────┘  │
│                                                         │
│  ┌──────────────────────────────────────────────────┐  │
│  │  JobHandler : IExternalEventHandler             │  │
│  │                                                  │  │
│  │  Execute(UIApplication app)                     │  │
│  │  {                                              │  │
│  │      // Safe Revit API access here              │  │
│  │      OpenDocument(inputPath);                   │  │
│  │      ExportToJson(outputPath);                  │  │
│  │      return PipeResponse                        │  │
│  │  }                                              │  │
│  └──────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
When to Use This Pattern

Perfect for:

  • Desktop application automation (Revit, AutoCAD, etc.)
  • Local service-to-service communication
  • Request/response workflows
  • Single-machine deployments

Not ideal for:

  • Network communication (use HTTP/gRPC)
  • Pub/sub patterns (use message brokers)
  • Cross-platform scenarios (Named Pipes are Windows-specific)

That’s it. ~200 lines of code for robust IPC.

The Bottom Line

Named Pipes are an elegant solution for communicating with desktop
applications like Revit.</strong >

Key takeaways:

  • Simple to implement (no heavy frameworks needed)
  • Fast and reliable for local communication
  • Works beautifully with Revit’s threading model
  • Production-ready with minimal code
  • Easy to debug and maintain

If you’re building automation around Revit, AutoCAD, or any Windows desktop
application, consider Named Pipes before reaching for more complex solutions.


Resources

Technologies Used:

  • System.IO.Pipes (.NET)
  • Newtonsoft.Json for serialization
  • Revit API 2024
  • ExternalEvent pattern

Further Reading:


Have you used Named Pipes for IPC? What patterns worked well for you? Share
your experiences in the comments!</em >
👇

Disclaimer- I do not claim to be an expert in this area. We have used this for the first time and thought of sharing. If you have any suggestions which can help, please feel free to share.


Tags: #Revit #NamedPipes #IPC #DotNet #BIM #Automation
#SoftwareArchitecture #CSharp

Leave a Reply

Your email address will not be published. Required fields are marked *