Modern .NET applications embrace dependency injection as a core architectural pattern. Services are registered in a container, resolved automatically, and their lifetimes managed by the framework. WitRPC fits naturally into this model.
Today, we'll explore how to integrate WitRPC clients and servers with ASP.NET Core's dependency injection system. Whether you're building a web API that communicates with backend services or hosting RPC endpoints alongside your REST API, these patterns will help you write clean, testable, maintainable code.
The DI Packages
WitRPC provides two NuGet packages for DI integration:
# For WitRPC servers
dotnet add package OutWit.Communication.Server.DependencyInjection
# For WitRPC clients
dotnet add package OutWit.Communication.Client.DependencyInjectionThese packages provide extension methods for IServiceCollection that feel native to ASP.NET Core development.
Server-Side Integration
Let's start with hosting a WitRPC server in an ASP.NET Core application.
Basic Server Registration
The simplest registration uses AddWitRpcServer:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Register your service implementation
builder.Services.AddSingleton<ITaskService, TaskService>();
// Register WitRPC server
builder.Services.AddWitRpcServer<ITaskService, TaskService>(
"task-server",
options =>
{
options.WithJson();
},
autoStart: true // Start when application starts
);
// Configure transport separately
builder.Services.Configure<TcpServerTransportOptions>("task-server", opt =>
{
opt.Port = 5000;
opt.MaxNumberOfClients = 100;
});
var app = builder.Build();
app.Run();The autoStart: true parameter registers a hosted service that starts the WitRPC server when the application starts and stops it gracefully on shutdown.
Server with Service Provider Access
Sometimes you need access to other services when configuring WitRPC:
builder.Services.AddWitRpcServer(
"task-server",
(options, serviceProvider) =>
{
// Access configuration
var config = serviceProvider.GetRequiredService<IConfiguration>();
var token = config["WitRpc:AccessToken"];
// Access services
var taskService = serviceProvider.GetRequiredService<ITaskService>();
options.WithService(taskService);
options.WithAccessToken(token);
options.WithMessagePack();
});Composite Services with DI
For multiple services, use AddWitRpcServerWithServices:
// Option 1: Services already registered in DI
builder.Services.AddSingleton<IUserService, UserService>();
builder.Services.AddSingleton<IOrderService, OrderService>();
builder.Services.AddSingleton<INotificationService, NotificationService>();
builder.Services.AddWitRpcServerWithServices(
"api-server",
options =>
{
options.WithMessagePack();
options.WithEncryption();
},
services =>
{
// These are resolved from DI
services.AddService<IUserService>();
services.AddService<IOrderService>();
services.AddService<INotificationService>();
},
autoStart: true
);// Option 2: Register and add in one step
builder.Services.AddWitRpcServerWithServices(
"api-server",
options =>
{
options.WithMessagePack();
},
services =>
{
// Registers in DI AND adds to composite
services.AddService<IUserService, UserServiceImpl>();
services.AddService<IOrderService, OrderServiceImpl>();
},
autoStart: true
);// Option 3: Factory functions for complex construction
builder.Services.AddWitRpcServerWithServices(
"api-server",
options =>
{
options.WithJson();
},
services =>
{
services.AddService<IUserService>(sp => new UserService(
sp.GetRequiredService<IUserRepository>(),
sp.GetRequiredService<ILogger<UserService>>()
));
services.AddService<IOrderService>(sp => new OrderService(
sp.GetRequiredService<IOrderRepository>(),
sp.GetRequiredService<IUserService>() // Depends on another service
));
});Using IWitServerFactory
For programmatic control, inject IWitServerFactory:
public class ServerManagementService
{
private readonly IWitServerFactory _factory;
private readonly ILogger<ServerManagementService> _logger;
public ServerManagementService(
IWitServerFactory factory,
ILogger<ServerManagementService> logger)
{
_factory = factory;
_logger = logger;
}
public async Task StartServerAsync(string name)
{
var server = _factory.GetServer(name);
server.StartWaitingForConnection();
_logger.LogInformation("Server {Name} started", name);
}
public void StopServer(string name)
{
var server = _factory.GetServer(name);
server.StopWaitingForConnection();
_logger.LogInformation("Server {Name} stopped", name);
}
}Client-Side Integration
Now let's look at consuming WitRPC services from an ASP.NET Core application.
Basic Client Registration
Register a named client:
builder.Services.AddWitRpcClient(
"backend-service",
options =>
{
options.WithWebSocket("ws://backend:5000");
options.WithJson();
options.WithEncryption();
options.WithAccessToken("secret-token");
});Typed Service Registration
The most convenient pattern: register the service interface directly.
builder.Services.AddWitRpcClient<ITaskService>(
"task-service",
options =>
{
options.WithTcp("task-server", 5000);
options.WithMessagePack();
},
autoConnect: true,
connectionTimeout: TimeSpan.FromSeconds(10)
);
// Now ITaskService can be injected anywhere!
public class TaskController : ControllerBase
{
private readonly ITaskService _taskService;
public TaskController(ITaskService taskService)
{
_taskService = taskService;
}
[HttpPost]
public async Task<IActionResult> CreateTask([FromBody] CreateTaskRequest request)
{
var task = await _taskService.CreateTaskAsync(request.Name);
return Ok(task);
}
}The autoConnect: true parameter starts a background service that connects when the application starts and maintains the connection.
Using IWitClientFactory
For more control or multiple services:
builder.Services.AddWitRpcClient(
"multi-service",
options =>
{
options.WithTcp("backend", 5000);
options.WithMessagePack();
});
public class OrderController : ControllerBase
{
private readonly IWitClientFactory _factory;
public OrderController(IWitClientFactory factory)
{
_factory = factory;
}
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
{
// Get service proxies from factory
var orders = _factory.GetService<IOrderService>("multi-service");
var inventory = _factory.GetService<IInventoryService>("multi-service");
// Check inventory
var available = await inventory.CheckAvailabilityAsync(request.ProductId);
if (!available)
{
return BadRequest("Product not available");
}
// Create order
var order = await orders.CreateOrderAsync(request);
return Ok(order);
}
}Multiple Clients
Register multiple clients for different backend services:
// User service on one server
builder.Services.AddWitRpcClient<IUserService>(
"user-service",
options =>
{
options.WithTcp("user-server", 5001);
options.WithMessagePack();
},
autoConnect: true
);
// Order service on another server
builder.Services.AddWitRpcClient<IOrderService>(
"order-service",
options =>
{
options.WithTcp("order-server", 5002);
options.WithMessagePack();
},
autoConnect: true
);
// Analytics service via WebSocket
builder.Services.AddWitRpcClient<IAnalyticsService>(
"analytics-service",
options =>
{
options.WithWebSocket("ws://analytics:8080");
options.WithJson();
},
autoConnect: true
);
// All three can be injected independently
public class DashboardController : ControllerBase
{
private readonly IUserService _users;
private readonly IOrderService _orders;
private readonly IAnalyticsService _analytics;
public DashboardController(
IUserService users,
IOrderService orders,
IAnalyticsService analytics)
{
_users = users;
_orders = orders;
_analytics = analytics;
}
[HttpGet]
public async Task<IActionResult> GetDashboard()
{
var user = await _users.GetCurrentUserAsync();
var recentOrders = await _orders.GetRecentOrdersAsync(user.Id, count: 5);
var stats = await _analytics.GetUserStatsAsync(user.Id);
return Ok(new DashboardViewModel(user, recentOrders, stats));
}
}Real-World Integration Patterns
Let's look at common patterns for integrating WitRPC in ASP.NET Core applications.
Pattern 1: API Gateway
Your ASP.NET Core app acts as an API gateway, forwarding requests to WitRPC backend services:
// Program.cs
builder.Services.AddWitRpcClient<IUserService>("users", opts =>
opts.WithTcp("user-service", 5001).WithMessagePack());
builder.Services.AddWitRpcClient<IOrderService>("orders", opts =>
opts.WithTcp("order-service", 5002).WithMessagePack());
builder.Services.AddWitRpcClient<IProductService>("products", opts =>
opts.WithTcp("product-service", 5003).WithMessagePack());
// Controllers expose REST API, internally use WitRPC
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
public UsersController(IUserService userService)
{
_userService = userService;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
try
{
var user = await _userService.GetUserAsync(id);
return Ok(user);
}
catch (WitExceptionFault ex)
{
// Translate WitRPC exceptions to HTTP responses
return StatusCode(500, new { error = ex.Message });
}
}
[HttpPost]
public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest request)
{
var user = await _userService.CreateUserAsync(request);
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}
}Pattern 2: Background Service Consumer
A background service that reacts to WitRPC events:
public class OrderNotificationService : BackgroundService
{
private readonly IWitClientFactory _factory;
private readonly IEmailService _email;
private readonly ILogger<OrderNotificationService> _logger;
public OrderNotificationService(
IWitClientFactory factory,
IEmailService email,
ILogger<OrderNotificationService> logger)
{
_factory = factory;
_email = email;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var client = _factory.GetClient("order-service");
// Ensure connected
while (!stoppingToken.IsCancellationRequested)
{
try
{
if (!client.IsConnected)
{
await client.ConnectAsync(TimeSpan.FromSeconds(5), stoppingToken);
}
var orders = client.GetService<IOrderService>();
// Subscribe to events
orders.OrderCreated += async order =>
{
_logger.LogInformation("New order {OrderId}", order.Id);
await _email.SendOrderConfirmationAsync(order);
};
orders.OrderShipped += async order =>
{
_logger.LogInformation("Order {OrderId} shipped", order.Id);
await _email.SendShippingNotificationAsync(order);
};
// Keep running until cancelled
await Task.Delay(Timeout.Infinite, stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in notification service");
await Task.Delay(5000, stoppingToken); // Retry after delay
}
}
}
}
// Registration
builder.Services.AddHostedService<OrderNotificationService>();Pattern 3: Minimal API Integration
WitRPC works great with .NET 6+ minimal APIs:
var builder = WebApplication.CreateBuilder(args);
// Register WitRPC clients
builder.Services.AddWitRpcClient<ITaskService>("tasks", options =>
{
options.WithTcp("task-service", 5000);
options.WithMessagePack();
}, autoConnect: true);
var app = builder.Build();
// Minimal API endpoints
app.MapGet("/tasks", async (ITaskService tasks) =>
{
var allTasks = await tasks.GetAllTasksAsync();
return Results.Ok(allTasks);
});
app.MapGet("/tasks/{id}", async (int id, ITaskService tasks) =>
{
var task = await tasks.GetTaskAsync(id);
return task is not null ? Results.Ok(task) : Results.NotFound();
});
app.MapPost("/tasks", async (CreateTaskRequest request, ITaskService tasks) =>
{
var task = await tasks.CreateTaskAsync(request.Name, request.Priority);
return Results.Created($"/tasks/{task.Id}", task);
});
app.MapDelete("/tasks/{id}", async (int id, ITaskService tasks) =>
{
var deleted = await tasks.DeleteTaskAsync(id);
return deleted ? Results.NoContent() : Results.NotFound();
});
app.Run();Pattern 4: Hybrid Server (REST + WitRPC)
Host both REST API and WitRPC on the same application:
var builder = WebApplication.CreateBuilder(args);
// REST API services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Business services (used by both REST and WitRPC)
builder.Services.AddSingleton<IOrderService, OrderService>();
builder.Services.AddSingleton<IInventoryService, InventoryService>();
// WitRPC server (exposes same services via RPC)
builder.Services.AddWitRpcServerWithServices(
"rpc-server",
options =>
{
options.WithMessagePack();
options.WithEncryption();
},
services =>
{
services.AddService<IOrderService>();
services.AddService<IInventoryService>();
},
autoStart: true
);
var app = builder.Build();
// REST API middleware
app.UseSwagger();
app.UseSwaggerUI();
app.MapControllers();
app.Run();Now the same services are accessible via REST (for web clients) and WitRPC (for .NET clients).
Pattern 5: Health Checks Integration
Monitor WitRPC connection health:
// Custom health check for WitRPC client
public class WitRpcClientHealthCheck : IHealthCheck
{
private readonly IWitClientFactory _factory;
private readonly string _clientName;
public WitRpcClientHealthCheck(IWitClientFactory factory, string clientName)
{
_factory = factory;
_clientName = clientName;
}
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var client = _factory.GetClient(_clientName);
if (client.IsConnected)
{
return Task.FromResult(HealthCheckResult.Healthy($"{_clientName} connected"));
}
return Task.FromResult(HealthCheckResult.Degraded($"{_clientName} disconnected"));
}
catch (Exception ex)
{
return Task.FromResult(HealthCheckResult.Unhealthy($"{_clientName} error", ex));
}
}
}
// Registration
builder.Services.AddHealthChecks()
.AddCheck("witrpc-orders", sp =>
new WitRpcClientHealthCheck(
sp.GetRequiredService<IWitClientFactory>(),
"order-service"))
.AddCheck("witrpc-users", sp =>
new WitRpcClientHealthCheck(
sp.GetRequiredService<IWitClientFactory>(),
"user-service"));
// Endpoint
app.MapHealthChecks("/health");Configuration from appsettings.json
Load WitRPC settings from configuration:
{
"WitRpc": {
"Servers": {
"main": {
"Port": 5000,
"MaxClients": 100,
"Serializer": "MessagePack",
"EnableEncryption": true
}
},
"Clients": {
"backend": {
"Host": "backend-service",
"Port": 5000,
"Serializer": "MessagePack",
"AccessToken": "secret-token"
}
}
}
}// Read configuration
var witRpcConfig = builder.Configuration.GetSection("WitRpc");
builder.Services.AddWitRpcClient(
"backend",
options =>
{
var config = witRpcConfig.GetSection("Clients:backend");
options.WithTcp(
config["Host"],
config.GetValue<int>("Port")
);
if (config["Serializer"] == "MessagePack")
options.WithMessagePack();
else
options.WithJson();
if (!string.IsNullOrEmpty(config["AccessToken"]))
options.WithAccessToken(config["AccessToken"]);
},
autoConnect: true
);Testing with DI
DI makes testing easier: mock the WitRPC services.
public class OrderControllerTests
{
[Fact]
public async Task CreateOrder_WithValidRequest_ReturnsCreated()
{
// Arrange
var mockOrderService = new Mock<IOrderService>();
mockOrderService
.Setup(s => s.CreateOrderAsync(It.IsAny<CreateOrderRequest>()))
.ReturnsAsync(new Order { Id = 1, Status = "Created" });
var controller = new OrderController(mockOrderService.Object);
// Act
var result = await controller.CreateOrder(new CreateOrderRequest
{
ProductId = "SKU-123",
Quantity = 2
});
// Assert
var createdResult = Assert.IsType<CreatedAtActionResult>(result);
var order = Assert.IsType<Order>(createdResult.Value);
Assert.Equal(1, order.Id);
}
}For integration tests, use the test server:
public class OrderApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public OrderApiIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Replace real WitRPC client with mock
services.RemoveAll<IOrderService>();
services.AddSingleton<IOrderService>(new MockOrderService());
});
});
}
[Fact]
public async Task GetOrders_ReturnsSuccessStatusCode()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/orders");
response.EnsureSuccessStatusCode();
}
}Best Practices
1. Use Named Registrations
Always name your clients and servers for clarity:
// Good: named registration
builder.Services.AddWitRpcClient("order-service", ...);
builder.Services.AddWitRpcClient("user-service", ...);
// Avoid: generic names
builder.Services.AddWitRpcClient("client1", ...);2. Enable Auto-Reconnect for Production
builder.Services.AddWitRpcClient(
"backend",
options =>
{
options.WithTcp("backend", 5000);
options.WithAutoReconnect(reconnect =>
{
reconnect.MaxAttempts = 10;
reconnect.InitialDelay = TimeSpan.FromSeconds(1);
reconnect.MaxDelay = TimeSpan.FromMinutes(1);
});
},
autoConnect: true
);3. Log WitRPC Events
builder.Services.AddWitRpcClient(
"backend",
options =>
{
var logger = sp.GetRequiredService<ILogger<Program>>();
options.WithTcp("backend", 5000);
options.WithLogger(new WitRpcLogger(logger));
});4. Graceful Shutdown
The DI integration handles graceful shutdown automatically when autoStart is used. For manual control:
app.Lifetime.ApplicationStopping.Register(() =>
{
var factory = app.Services.GetRequiredService<IWitServerFactory>();
factory.GetServer("main").StopWaitingForConnection();
});Conclusion
WitRPC's DI integration brings RPC into the modern .NET ecosystem:
- Server registration with
AddWitRpcServerandAddWitRpcServerWithServices - Client registration with
AddWitRpcClientand typed service injection - Factory patterns via
IWitServerFactoryandIWitClientFactory - Auto-connect/auto-start for production deployments
- Configuration binding from
appsettings.json - Health checks for monitoring
- Testing support through mockable interfaces
Whether you're building an API gateway, a microservices backend, or a hybrid REST+RPC application, WitRPC integrates smoothly with ASP.NET Core's patterns and practices.
Next up: Performance Tuning WitRPC: Transports and Serializers, squeezing the most out of transport and serializer choices.
This is part 8 of a series on WitRPC. See previous posts: Introducing WitRPC, Getting Started, Real-World Benefits, WitRPC vs. gRPC, Migrating to WitRPC, Under the Hood, and Composite Services.