AdvancedRPC is a remote procedure call library for .NET. It differs from common solutions like REST, GRPC or WebSockets in that it supports an object hierarchy similar to .NET Remoting. I wrote the library mainly as a replacement for .NET Remoting to make our corporate application ready for .NET Core. It relies heavily on the ability to make remote procedure calls on objects.
- Communication via TCP and Named Pipes
- Support for impersonation with Named Pipes
- Deep object hierarchies
- Events and callbacks
- No need for serialization annotations, just publish an interface
- Support for multiple clients with notification on connection and disconnection
- .NET 8, .NET Framework 4.8, .NET Standard 2.0 and .NET Standard 2.1
- Communication between .NET Framework, modern .NET and Unity apps (not tested on Xamarin yet, but should work there too)
- Very easy to setup: No need to start a web service or define proto files. Just define an interface that is shared between applications and you are ready.
AdvancedRPC 2.0 replaces BinaryFormatter with a versioned MessagePack wire format. This removes the unsafe runtime type loading performed by BinaryFormatter, but the wire protocol and some serialization behavior are intentionally not compatible with AdvancedRPC 1.x.
AdvancedRPC 1.x and 2.x clients and servers cannot communicate with each other. Upgrade every process that shares an RPC channel in the same deployment. The 2.0 wire payload contains an explicit protocol version and rejects incompatible payloads with a serialization error.
The .NET Core 3.1, .NET 5, and .NET 6 targets have been replaced by .NET 8. Applications using one of these targets must move to .NET 8 to consume the framework-specific build. The library continues to provide .NET Framework 4.8, .NET Standard 2.0, and .NET Standard 2.1 builds for compatible consumers.
The basic serializer setup is unchanged:
var serializer = new BinaryRpcSerializer();BinaryRpcSerializer no longer exposes or uses CreateBinaryFormatter. Applications that derived from the serializer to configure a BinaryFormatter, binder, surrogate, or formatter setting must move that behavior to DTO contracts or a custom IRpcSerializer implementation.
Concrete [Serializable] parameter and result types are deserialized from the locally declared RPC method type and need no additional setup. When an RPC contract declares an interface or abstract base type but transfers a concrete serializable DTO by value, explicitly register that concrete type under a stable contract identifier on both client and server:
var dtoTypes = new RpcSerializationTypeRegistry()
.Register<OrderDto>("orders.order.v1")
.Register<OrderLineDto>("orders.order-line.v1");
var serializer = new BinaryRpcSerializer(dtoTypes);Contract identifiers are application protocol identifiers. Keep them stable across assembly renames and version changes, and never derive them from untrusted input. A polymorphic DTO whose contract identifier is missing or not registered is rejected instead of being resolved through Type.GetType.
DTO members must themselves use concrete MessagePack-compatible types. Interface- or abstract-typed members require an application-defined DTO shape or custom IRpcSerializer; registering the outer DTO does not enable typeless serialization for nested members.
Non-built-in object arrays should use a concrete array type in the RPC interface. Primitive and string arrays carry their safe element type in the 2.0 wire format and also work when the declared parameter type is object or a compatible interface.
Common framework exception types such as ArgumentException, InvalidOperationException, TimeoutException, OperationCanceledException, and IOException retain their type. Other remote exception types are represented by RemoteRpcException; inspect its RemoteTypeName and RemoteStackTrace properties for diagnostics. This avoids constructing an arbitrary exception type selected by a remote peer.
MessagePack deserialization uses MessagePackSecurity.UntrustedData, and CLR assembly-qualified names are no longer used to select serialized DTO or exception types. Event delegate proxies are released after the final matching unsubscription; registering the same handler more than once or on multiple events no longer releases a still-active callback.
Common interface definition
public interface IRpcServer
{
IRpcObject CreateObject(string name);
}
public interface IRpcObject
{
string Name { get; }
void ChangeName(string name);
event NameChanged;
}Server implementation
class RpcServer : IRpcServer
{
IRpcObject CreateObject(string name)
{
return RpcObjectImpl(name);
}
}
class RpcObjectImpl : IRpcObject
{
public RpcObjectImpl(string name)
{
Name = name;
}
string Name { get; private set; }
void ChangeName(string name)
{
Name = name;
NameChanged?.Invoke(this, EventArgs.Empty);
}
event NameChanged;
}
class Program
{
static async Task Main(string[] args)
{
var server = new NamedPipeRpcServerChannel(new BinaryRpcSerializer(),
new RpcMessageFactory(), "myipcchannelname");
server.ObjectRepository.RegisterSingleton<RpcServer>();
await server.ListenAsync();
Console.WriteLine("Press key to quit");
Console.ReadKey();
}
}Client implementation
class Program
{
static async Task Main(string[] args)
{
var client = new NamedPipeRpcClientChannel(new BinaryRpcSerializer(),
new RpcMessageFactory(), "myipcchannelname");
await client.ConnectAsync(TimeSpan.FromSeconds(5));
var rpcServerObj = await client.GetServerObjectAsync<IRpcServer>();
var nameObj = rpcServerObj.CreateObject("Jon Doe")
nameObj.NameChanged += (sender, e) => Console.WriteLine(((IRpcObject)sender).Name);
// This calls the method on the server and invokes
// the event NameChanged on the client.
nameObj.ChangeName("Jane Doe");
Console.WriteLine("Press key to quit");
Console.ReadKey();
}
}See unit tests for more advanced scenarios.
For platforms that do not support dynamic code generation (i.e. Unity, Xamarin iOS) it is necessary to generate the proxy code in advance. AdvancedRpcLib supports this by annotating RPC interface definitions with the AotRpcObjectAttribute and using the package AdvancedRpc.MSBuild. The package will then generate the proxy files and the class AotRpcObjects during build.
The generation of proxy objects is only supported for RPC clients. It might work in some scenarios for servers as well though, if you do not need events or delegates.
To use the generated proxies, use AotRpcObjectRepository instead of the default RpcObjectRepository. An examle would look like this:
[AdvancedRpcLib.AotRpcObject]
public interface IRpcServer
{
void DoSometing();
}
class Main
{
static async Task Main()
{
IPAddress ip = ...
int port = ...
var rpcClientChannel = new TcpRpcClientChannel(
new BinaryRpcSerializer(),
new RpcMessageFactory(),
ip,
port,
new AotRpcObjectRepository(true, AotRpcObjects.GetImplementationTypes()),
() => new AotRpcObjectRepository(false, AotRpcObjects.GetImplementationTypes()));
await rpcClientChannel.ConnectAsync();
var rpcServer = await rpcClientChannel.GetServerObjectAsync<IRpcServer>();
// rpcServer will be the pregenerated proxy type
// from here everything is like normal...
}
}- If you return a plain static object that doesn't need to know about server changes, use the
Serializableattribute on the implementation. In that case the object will be serialized and copied to the client or server without creating a proxy object. Polymorphic DTOs also need a sharedRpcSerializationTypeRegistryentry as described in the migration guide. This can be more efficient for data objects if you have a lot of properties and deep hierarchies. This behaves like a REST call. - Do not return or pass IEnumerable, as this will result in a remote call for every
MoveNextwhen iterating over it. Instead, use an array in those cases. - Watch out for memory leaks. AdvancedRPC handles a lot of scenarios for you but take care to remove your event listeners.
- CAREFUL! Every remote call can throw an exception if the server goes down. The same is true for events, if the clients disconnects.
- Method overloads with same parameter count are not supported (yet). Overloads with different parameter count are possible though.
- Named Pipe impersonation limitations:
- doesn't work with .NET Standard 2.0 (for now)
- only works on Windows
IEnumerabledoesn't work for .NET Core (the interfaces use ByRef Values). There might be a workaround to support this.