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:

bash
# For WitRPC servers
dotnet add package OutWit.Communication.Server.DependencyInjection

# For WitRPC clients
dotnet add package OutWit.Communication.Client.DependencyInjection

These 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:

csharp
// 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:

csharp
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:

csharp
// 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
);
csharp
// 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
);
csharp
// 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:

csharp
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:

csharp
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.

csharp
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:

csharp
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:

csharp
// 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:

csharp
// 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:

csharp
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:

csharp
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:

csharp
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:

csharp
// 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:

json
{
  "WitRpc": {
    "Servers": {
      "main": {
        "Port": 5000,
        "MaxClients": 100,
        "Serializer": "MessagePack",
        "EnableEncryption": true
      }
    },
    "Clients": {
      "backend": {
        "Host": "backend-service",
        "Port": 5000,
        "Serializer": "MessagePack",
        "AccessToken": "secret-token"
      }
    }
  }
}
csharp
// 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.

csharp
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:

csharp
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:

csharp
// 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

csharp
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

csharp
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:

csharp
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 AddWitRpcServer and AddWitRpcServerWithServices
  • Client registration with AddWitRpcClient and typed service injection
  • Factory patterns via IWitServerFactory and IWitClientFactory
  • 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.