Advanced Scenarios and Integrations
Inter-Process Communication: The Host/Agent Model
For sophisticated inter-process communication (IPC), WitRPC offers a dedicated extension called OutWit.InterProcess. This extension implements a Host/Agent model - a powerful pattern for managing the lifecycle and communication of child processes from a main (host) application. Note that this extension is essentially a process management and orchestration layer built on top of the core WitRPC framework.
Architecture Overview
The model consists of two main components:
-
The Host: The main application process that launches, manages, and communicates with one or more agent processes.
-
The Agent: A separate child process launched by the Host. The agent exposes a WitRPC service that the host can consume.
This architecture is ideal for scenarios such as:
-
Task Offloading: Offloading long-running or resource-intensive tasks to a separate process to keep the main application's UI responsive.
-
Isolation: Running potentially unstable code in an isolated agent process. If an agent crashes, it will not take down the host.
-
Plugin Architectures: Loading modular functionality as separate agent processes.
Key Components
The OutWit.InterProcess extension provides several key classes for the Host/Agent model.
-
HostManager<TService>: A factory/manager on the host side. Responsible for launching new agents, tracking them, and shutting them down. -
IAgent<TService>: An interface representing a single agent instance from the host's perspective. Provides methods to initialize, stop, and access the agent's remote service. -
HostAgent<TService>: The host-side implementation ofIAgent<TService>used by theHostManager. Encapsulates the agent process (aSystem.Diagnostics.Process) and theWitClientused to communicate with it. -
AgentStartupParameters: A data class holding the configuration needed to launch an agent (IPC address, parent process ID, shutdown timeout, etc.). This object is serialized into command-line arguments passed to the agent process.
-
AgentApplication: The entry point class for the agent process. It parses the startup parameters, monitors the parent host process (shutting down if the host exits), and manages an automatic shutdown timer.
Platform Dependency Consideration
A crucial detail of the default AgentApplication is its dependency on the Windows desktop environment. The AgentApplication class inherits from System.Windows.Application and uses a System.Windows.Threading.DispatcherTimer for its timeout mechanism. This means that, out of the box, an agent project must reference WPF (<UseWPF>true in the project file) and can only run on Windows.
For cross-platform or console-based agents, you will need to create a custom agent entry point. This involves writing your own Main method to parse the AgentStartupParameters and using a cross-platform timer (such as System.Threading.Timer) instead of the WPF-dependent AgentApplication class.
Walkthrough: Host and Agent Interaction
The following is a step-by-step overview of how the host and agent interact (based on the InterProcess example):
-
Host Launches Agent:
-
The host application uses
HostManager<TService>.CreateClient(...)to launch a new agent. -
The HostManager creates an
AgentStartupParameters, specifying an IPC transport and a unique address (e.g., a new GUID for a pipe name). -
It calls
HostUtils.RunAgent(...), which starts the agent process (e.g., launching the Agent executable) and passes the serialized startup parameters via command-line arguments.
-
-
Agent Initializes Server:
-
The
AgentApplicationin the agent process starts up. -
It parses the command-line arguments back into an
AgentStartupParametersobject. -
It begins monitoring the parent host process (using the parent process ID). If the host exits, the agent will automatically shut down.
-
It uses the provided IPC address to start a WitRPC server, exposing its
TServiceimplementation.
-
-
Host Connects to Agent:
-
Back in the host, after launching the agent, the HostManager builds a
WitClient. -
The client is configured to use the same transport and address that were passed to the agent.
-
The host calls
client.ConnectAsync(...)to establish the IPC connection. -
Once connected, it calls
client.GetService<TService>()to obtain a proxy to the agent's service.
-
-
Communication and Lifecycle Management:
-
The host can now call methods on the service proxy; those calls execute within the agent process.
-
The host can explicitly shut down the agent by calling
agent.Shutdown(), which terminates the agent's process. -
If the host application closes or crashes, the agent detects that its parent process has exited and automatically terminates itself, preventing orphaned processes.
-
This robust lifecycle management is a key strength of the Host/Agent model, ensuring that resources are managed cleanly and reliably.
Dynamic Service Discovery
In many distributed systems - especially microservice architectures - hardcoding service addresses is impractical. Service discovery allows clients to dynamically find services on the network. WitRPC provides a simple but effective discovery mechanism based on UDP broadcasts.
Server Configuration
To make a WitRPC server discoverable, assign it a name and description and enable the discovery feature via the builder. For example:
var server = WitServerBuilder.Build(options =>
{
options.WithService(new MyService())
.WithTcp(port: 0) // Use port 0 to get an ephemeral port
.WithName("Billing Service")
.WithDescription("Handles all billing and invoicing tasks.")
.WithDiscovery(); // Enable the discovery feature
});
server.StartWaitingForConnection();
With discovery enabled, the server listens for UDP discovery requests. When it receives one, it responds with a DiscoveryMessage containing its name, description, and full transport details (e.g., its IP address and the actual port it's listening on).
Client Implementation
On the client side, create a discovery client to listen for service announcements. For example:
// Create a client dedicated to discovery
var discoveryClient = WitClientBuilder.Discovery(options => { });
// Collection to store info about discovered services
var discoveredServices = new List<DiscoveryMessage>();
// Handle discovery messages as they arrive
discoveryClient.MessageReceived += (sender, message) =>
{
Console.WriteLine($"Discovered service: {message.Name} at {message.TransportData["HostInfo"]}");
discoveredServices.Add(message);
};
// Start listening for announcements
discoveryClient.Start();
The Discovery Process
Service discovery involves two client instances:
-
Discovery Client: A long-lived client (created with
WitClientBuilder.Discovery()) that listens for UDP broadcast messages and collects service announcements via itsMessageReceivedevents. -
Service Client: A standard WitRPC client that you create when the user chooses a service from the discovered list. The crucial step is using the received
DiscoveryMessageto configure this new client's transport.
For example:
// Suppose 'selectedServiceMessage' is a DiscoveryMessage from the discovery client
var serviceClient = WitClientBuilder.Build(options =>
{
// Configure transport from the discovery message
options.WithTransport(selectedServiceMessage);
// Configure other options (serializer, security, etc.)
options.WithJson();
options.WithEncryption();
});
await serviceClient.ConnectAsync(...);
var service = serviceClient.GetService<IExampleService>();
// ... use the service
Here, options.WithTransport(selectedServiceMessage) automatically extracts the transport type, address, port, and other details from the discovery message. This means the client does not need to know those details upfront.
UI Framework Integration
WitRPC is well-suited as a communication layer for rich client applications built with modern UI frameworks. The framework has been integrated with Blazor WebAssembly, WPF, and WinUI, among others. While the core usage is similar, each environment has some unique considerations.
Blazor WebAssembly
Integrating WitRPC into a Blazor WebAssembly application requires handling the browser's sandboxed environment:
-
Cryptography Limitations: Blazor WebAssembly (which runs in the browser) cannot use the standard .NET
System.Security.CryptographyAPIs. This means WitRPC's default RSA/AES encryption handshake will not work. You have two options:-
Disable Encryption: Call
.WithoutEncryption()on both client and server. (Use this only if communication is already secured via HTTPS/WSS.) -
Custom Encryptor: Implement a custom client-side encryptor that uses JavaScript interop to leverage the browser's Web Crypto API (
window.crypto.subtle). TheEncryptorClientWebclass in the WitRPC Blazor example demonstrates this approach, usingIJSRuntimeto perform key generation and encryption/decryption in JavaScript.
-
-
AOT and Static Proxies: Blazor WebAssembly apps often use ahead-of-time (AOT) compilation for better performance. As discussed in section 3.5, AOT environments do not support runtime code generation, so you must use a static proxy for WitRPC services.�
-
UI Updates: In Blazor, after receiving a server callback or completing an asynchronous operation that changes component state, you must call
StateHasChanged()to tell the framework to re-render the UI with the updated state.
Desktop (WPF & WinUI)
Integrating WitRPC with desktop frameworks like WPF and WinUI is straightforward, but it requires careful thread management:
-
MVVM Best Practices: Use the Model-View-ViewModel pattern. Manage the
WitClientand its service proxy within a ViewModel, and expose properties and commands to the View (XAML UI) for data binding. -
Thread Marshalling: WitRPC client callbacks (including server-initiated events) occur on background threads. Attempting to update UI elements from a background thread will cause a cross-thread access error. You must marshal such updates back to the UI thread. For example, in WPF you can use the application dispatcher:
// In a ViewModel, handling a server event (e.g., progress update)
private void OnProgressChanged(double progress)
{
// 'Application.Current.Dispatcher' is for WPF.
// WinUI has a similar mechanism.
Application.Current.Dispatcher.Invoke(() =>
{
// This code is now running on the UI thread.
this.ProgressPercentage = progress;
});
}
Both the WPF and WinUI examples demonstrate this pattern of subscribing to service events in the ViewModel and using the dispatcher to safely update UI-bound properties.