A single WitRPC server can host several service interfaces at once. Clients connect once and request a proxy for any registered interface over that one connection. This keeps interfaces small and focused without multiplying servers, ports, and deployments.
Why composite services
An application that handles users, orders, and notifications can structure its API three ways: one large combined interface, three separate servers, or one server hosting three interfaces. The composite option avoids the weaknesses of the other two.
A combined IECommerceService with fifty methods across unrelated concerns is hard to maintain, test, and understand. Separate servers keep interfaces clean but triple the operational surface: processes, ports, deployments, monitoring. A composite server keeps each interface focused while everything ships and runs as one unit. Clients hold a single connection for all services, and one security configuration (encryption, tokens) covers everything: a client authenticates once and reaches every registered interface.
Registering services on the server
WithServices() opens a composite registration chain; Build() closes it and returns to the main options:
var server = WitServerBuilder.Build(options =>
{
options.WithServices()
.AddService<IUserService>(new UserService())
.AddService<IOrderService>(new OrderService())
.AddService<INotificationService>(new NotificationService())
.Build();
options.WithTcp(5000, maxNumberOfClients: 100);
options.WithJson();
options.WithEncryption();
});
server.StartWaitingForConnection();The generic parameter of AddService<TInterface> names the interface clients will request; the argument is its implementation. Registration accepts three shapes:
// An existing instance
.AddService<IUserService>(new UserService())
// A factory, for lazy construction or constructor dependencies
.AddService<IUserService>(() => new UserService(configuration))
// Interface and implementation types stated explicitly
.AddService<IUserService, UserServiceImpl>(new UserServiceImpl())Services can also come from a dependency injection container instead of manual construction; that path, including automatic startup, is covered in Dependency Injection →.
Assembly matching
By default, WitRPC matches methods strictly, including assembly identity, which assumes client and server reference the same contract assembly. When the two sides compile against different assemblies containing structurally identical interfaces, relax the match:
options.WithServices(isStrongAssemblyMatch: false)
.AddService<IUserService>(new UserService())
.Build();Accessing services from the client
The client connects exactly as it would to a single-service server, then requests a proxy per interface:
var client = WitClientBuilder.Build(options =>
{
options.WithTcp("localhost", 5000);
options.WithJson();
options.WithEncryption();
});
await client.ConnectAsync(TimeSpan.FromSeconds(5), CancellationToken.None);
var userService = client.GetService<IUserService>();
var orderService = client.GetService<IOrderService>();
var notificationService = client.GetService<INotificationService>();
var user = await userService.GetUserAsync(userId);
var orders = await orderService.GetOrdersForUserAsync(userId);
await notificationService.SendNotificationAsync(user.Email, "Your orders are ready!");All proxies share the one underlying connection; requesting another interface adds no connection overhead.
Events across services
Each interface can declare its own events, and all of them travel over the same connection. A client subscribes on whichever proxy declares the event:
public interface INotificationService
{
event Action<string, string> NotificationReceived;
void SendNotification(string userId, string message);
void BroadcastNotification(string message);
}
var notificationService = client.GetService<INotificationService>();
notificationService.NotificationReceived += (userId, message) =>
{
Console.WriteLine($"Notification for {userId}: {message}");
};One practical implication: subscribe on the proxy for the interface that declares the event. In a setup with several interfaces it is easy to hold the wrong proxy and wonder why events never arrive.
Organizing composite services
Keep each interface to one domain. IUserService for users, IOrderService for orders. If an interface starts absorbing a second concern, split it; adding the new interface to the server costs one AddService line.
Avoid method-name collisions across interfaces. If two registered interfaces both declare Delete(int id), rename toward the domain: DeleteUser, DeleteOrder.
Keep contracts in their own project. A typical layout:
MyApp.Contracts/ interfaces + DTOs, referenced by both sides
MyApp.Server/ implementations + hosting
MyApp.Client/ client codeThe contracts project holds only interfaces and models, as described in Service Contracts →.
One server or several?
| Situation | Choice |
|---|---|
| Services share security context and authentication | Composite |
| Services scale very differently | Separate servers |
| Minimizing operational complexity matters most | Composite |
| Different teams own deployment of different services | Separate servers |
| Services have different availability requirements | Separate servers |
The pattern in short: composite services suit logically related functionality that benefits from shared infrastructure; independent scaling, deployment, or lifecycle needs justify separate servers.