Skip to content

Dependency Injection

Belay.NET provides first-class support for dependency injection using Microsoft.Extensions.DependencyInjection. This enables clean architecture, testability, and seamless integration with ASP.NET Core, Worker Services, and other .NET applications.

Quick Setup

Console Application

csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Belay.Extensions;

var host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddBelay(config => {
            config.Device.DefaultConnectionTimeoutMs = 5000;
            config.Communication.Serial.DefaultBaudRate = 115200;
        });
    })
    .Build();

var deviceFactory = host.Services.GetRequiredService<IDeviceFactory>();
using var device = deviceFactory.CreateSerialDevice("COM3");
await device.ConnectAsync();

var result = await device.ExecuteAsync<float>("sensor.read_temp()");
Console.WriteLine($"Temperature: {result}°C");

ASP.NET Core

csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddBelay(config => {
    config.Device.DefaultConnectionTimeoutMs = 10000;
    config.Communication.Serial.MaxConcurrentConnections = 5;
});

builder.Services.AddControllers();
var app = builder.Build();

app.MapControllers();
app.Run();
csharp
// Controllers/SensorController.cs
[ApiController]
[Route("api/[controller]")]
public class SensorController : ControllerBase
{
    private readonly IDeviceFactory _deviceFactory;
    private readonly ILogger<SensorController> _logger;

    public SensorController(IDeviceFactory deviceFactory, ILogger<SensorController> logger)
    {
        _deviceFactory = deviceFactory;
        _logger = logger;
    }

    [HttpGet("temperature")]
    public async Task<ActionResult<float>> GetTemperature()
    {
        try
        {
            using var device = _deviceFactory.CreateSerialDevice("COM3");
            await device.ConnectAsync();
            
            var temperature = await device.ExecuteAsync<float>("sensor.read_temp()");
            _logger.LogInformation("Temperature reading: {Temperature}°C", temperature);
            
            return Ok(temperature);
        }
        catch (DeviceConnectionException ex)
        {
            _logger.LogError(ex, "Failed to connect to temperature sensor");
            return StatusCode(503, "Sensor unavailable");
        }
    }
}

Configuration

Configuration Options

Belay.NET uses a hierarchical configuration system:

csharp
services.AddBelay(config => {
    // Device configuration
    config.Device.DefaultConnectionTimeoutMs = 10000;
    config.Device.MaxRetryAttempts = 3;
    config.Device.EnableAutoReconnect = true;
    
    // Communication configuration
    config.Communication.Serial.DefaultBaudRate = 115200;
    config.Communication.Serial.MaxConcurrentConnections = 10;
    config.Communication.Serial.ConnectionPooling = true;
    
    // Session configuration  
    config.Session.MaxConcurrentSessions = 50;
    config.Session.SessionTimeoutMs = 300000; // 5 minutes
    config.Session.EnableSessionCaching = true;
    
    // Executor configuration
    config.Executor.EnableMethodCaching = true;
    config.Executor.CacheTimeoutMs = 60000;
    config.Executor.MaxConcurrentExecutions = 20;
    
    // Exception handling
    config.ExceptionHandling.EnableDetailedStackTraces = true;
    config.ExceptionHandling.LogDeviceErrors = true;
});

Configuration from appsettings.json

json
{
  "Belay": {
    "Device": {
      "DefaultConnectionTimeoutMs": 10000,
      "MaxRetryAttempts": 3,
      "EnableAutoReconnect": true
    },
    "Communication": {
      "Serial": {
        "DefaultBaudRate": 115200,
        "MaxConcurrentConnections": 10,
        "ConnectionPooling": true
      }
    },
    "Session": {
      "MaxConcurrentSessions": 50,
      "SessionTimeoutMs": 300000,
      "EnableSessionCaching": true
    }
  }
}
csharp
// Load from configuration
services.AddBelay(builder.Configuration.GetSection("Belay"));

// Or combine with code configuration
services.AddBelay(builder.Configuration.GetSection("Belay"), config => {
    // Override specific settings
    config.Device.EnableAutoReconnect = false;
});

Factory Patterns

Device Factory

Create devices with consistent configuration:

csharp
public class IoTService
{
    private readonly IDeviceFactory _deviceFactory;

    public IoTService(IDeviceFactory deviceFactory)
    {
        _deviceFactory = deviceFactory;
    }

    public async Task<float> ReadTemperatureAsync(string port)
    {
        using var device = _deviceFactory.CreateSerialDevice(port);
        await device.ConnectAsync();
        return await device.ExecuteAsync<float>("sensor.read_temp()");
    }

    public async Task<List<SensorReading>> ReadMultipleSensorsAsync(string[] ports)
    {
        var tasks = ports.Select(async port => {
            using var device = _deviceFactory.CreateSerialDevice(port);
            await device.ConnectAsync();
            var temp = await device.ExecuteAsync<float>("temp_sensor.read()");
            var humidity = await device.ExecuteAsync<float>("humidity_sensor.read()");
            return new SensorReading(port, temp, humidity);
        });

        return (await Task.WhenAll(tasks)).ToList();
    }
}

public record SensorReading(string Port, float Temperature, float Humidity);

Communication Factory

Create different communication types:

csharp
public class DeviceManager
{
    private readonly ICommunicatorFactory _communicatorFactory;
    private readonly ILogger<DeviceManager> _logger;

    public DeviceManager(ICommunicatorFactory communicatorFactory, ILogger<DeviceManager> logger)
    {
        _communicatorFactory = communicatorFactory;
        _logger = logger;
    }

    public async Task<Device> ConnectToDeviceAsync(string connectionString)
    {
        var communicator = connectionString switch
        {
            var s when s.StartsWith("COM") => _communicatorFactory.CreateSerial(s),
            var s when s.StartsWith("/dev/") => _communicatorFactory.CreateSerial(s),
            var s when s.StartsWith("subprocess:") => _communicatorFactory.CreateSubprocess(s[11..]),
            _ => throw new ArgumentException($"Unsupported connection string: {connectionString}")
        };

        var device = new Device(communicator);
        await device.ConnectAsync();
        
        _logger.LogInformation("Connected to device via {Connection}", connectionString);
        return device;
    }
}

Custom Device Registration

Register Custom Device Classes

csharp
public interface ITemperatureSensor
{
    Task<float> ReadTemperatureAsync();
    Task<bool> CalibrateAsync();
}

public class TemperatureSensorDevice : Device, ITemperatureSensor
{
    public TemperatureSensorDevice(IDeviceCommunication communication, ILogger<TemperatureSensorDevice> logger)
        : base(communication, logger) { }

    [Setup]
    public async Task InitializeAsync()
    {
        await ExecuteAsync("from machine import ADC, Pin; sensor = ADC(Pin(26))");
    }

    [Task(Cache = true)]
    public async Task<bool> CalibrateAsync()
    {
        return await ExecuteAsync<bool>("sensor.calibrate()");
    }

    [Task]
    public async Task<float> ReadTemperatureAsync()
    {
        return await ExecuteAsync<float>("sensor.read_temp()");
    }
}

// Register custom device
services.AddBelay();
services.AddTransient<ITemperatureSensor, TemperatureSensorDevice>();

// Factory for custom devices
services.AddTransient<Func<string, ITemperatureSensor>>(serviceProvider => 
    port => {
        var communicator = serviceProvider.GetRequiredService<ICommunicatorFactory>()
            .CreateSerial(port);
        return new TemperatureSensorDevice(communicator, 
            serviceProvider.GetRequiredService<ILogger<TemperatureSensorDevice>>());
    });

Use Custom Devices

csharp
public class SensorService
{
    private readonly Func<string, ITemperatureSensor> _sensorFactory;

    public SensorService(Func<string, ITemperatureSensor> sensorFactory)
    {
        _sensorFactory = sensorFactory;
    }

    public async Task<List<float>> ReadAllTemperaturesAsync(string[] ports)
    {
        var tasks = ports.Select(async port => {
            using var sensor = _sensorFactory(port);
            await ((Device)sensor).ConnectAsync();
            return await sensor.ReadTemperatureAsync();
        });

        return (await Task.WhenAll(tasks)).ToList();
    }
}

Health Checks

Belay.NET includes built-in health checks for monitoring device connectivity:

csharp
services.AddBelay(config => {
    config.Device.EnableHealthChecks = true;
});

services.AddHealthChecks()
    .AddBelay() // Adds system health checks
    .AddBelayDevice("main-sensor", "COM3") // Specific device health check
    .AddBelayDevice("backup-sensor", "COM4");

// ASP.NET Core health check endpoint
app.MapHealthChecks("/health");

Custom Health Checks

csharp
public class CustomSensorHealthCheck : IHealthCheck
{
    private readonly IDeviceFactory _deviceFactory;
    private readonly string _port;

    public CustomSensorHealthCheck(IDeviceFactory deviceFactory, string port)
    {
        _deviceFactory = deviceFactory;
        _port = port;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, 
        CancellationToken cancellationToken = default)
    {
        try
        {
            using var device = _deviceFactory.CreateSerialDevice(_port);
            await device.ConnectAsync(cancellationToken);
            
            // Test basic functionality
            var version = await device.ExecuteAsync<string>("sys.version");
            var freeMemory = await device.ExecuteAsync<int>("gc.mem_free()");
            
            var data = new Dictionary<string, object>
            {
                ["port"] = _port,
                ["version"] = version,
                ["free_memory"] = freeMemory
            };

            return freeMemory > 1000 
                ? HealthCheckResult.Healthy($"Sensor on {_port} is healthy", data)
                : HealthCheckResult.Degraded($"Low memory on {_port}", null, data);
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy($"Sensor on {_port} is unavailable", ex);
        }
    }
}

// Register custom health check
services.AddHealthChecks()
    .Add(new HealthCheckRegistration(
        "custom-sensor",
        sp => new CustomSensorHealthCheck(
            sp.GetRequiredService<IDeviceFactory>(), 
            "COM3"),
        HealthStatus.Degraded,
        new[] { "sensors", "hardware" },
        TimeSpan.FromSeconds(30)));

Testing with Dependency Injection

Mock Services for Testing

csharp
public class MockDeviceFactory : IDeviceFactory
{
    public Device CreateSerialDevice(string portName, SerialConnectionSettings? settings = null)
    {
        var mockCommunicator = new Mock<IDeviceCommunication>();
        
        // Setup mock behavior
        mockCommunicator
            .Setup(c => c.ExecuteAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync("mock_response");

        return new Device(mockCommunicator.Object);
    }

    public Device CreateSubprocessDevice(string executablePath, string[]? arguments = null)
    {
        throw new NotImplementedException("Not needed for this test");
    }
}

// Test setup
[TestMethod]
public async Task SensorService_ReadTemperature_ReturnsExpectedValue()
{
    // Arrange
    var services = new ServiceCollection();
    services.AddTransient<IDeviceFactory, MockDeviceFactory>();
    services.AddTransient<SensorService>();
    
    var serviceProvider = services.BuildServiceProvider();
    var sensorService = serviceProvider.GetRequiredService<SensorService>();

    // Act
    var temperature = await sensorService.ReadTemperatureAsync("COM3");

    // Assert
    Assert.AreEqual(23.5f, temperature);
}

Integration Testing

csharp
public class IntegrationTestFixture : IDisposable
{
    public IServiceProvider ServiceProvider { get; }

    public IntegrationTestFixture()
    {
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddBelay(config => {
            config.Device.DefaultConnectionTimeoutMs = 5000;
        });

        ServiceProvider = services.BuildServiceProvider();
    }

    public void Dispose()
    {
        (ServiceProvider as IDisposable)?.Dispose();
    }
}

[TestClass]
public class DeviceIntegrationTests : IClassFixture<IntegrationTestFixture>
{
    private readonly IntegrationTestFixture _fixture;

    public DeviceIntegrationTests(IntegrationTestFixture fixture)
    {
        _fixture = fixture;
    }

    [TestMethod]
    [TestCategory("Integration")]
    public async Task DeviceFactory_CreateSerialDevice_CanConnect()
    {
        // Skip if no hardware available
        if (!HardwareDetection.IsAvailable()) 
            return;

        var factory = _fixture.ServiceProvider.GetRequiredService<IDeviceFactory>();
        
        using var device = factory.CreateSerialDevice("COM3");
        await device.ConnectAsync();
        
        var result = await device.ExecuteAsync<string>("'Hello World'");
        Assert.AreEqual("Hello World", result);
    }
}

Advanced Configuration

Multiple Device Configurations

csharp
services.AddBelay("primary", config => {
    config.Device.DefaultConnectionTimeoutMs = 5000;
    config.Communication.Serial.DefaultBaudRate = 115200;
});

services.AddBelay("secondary", config => {
    config.Device.DefaultConnectionTimeoutMs = 10000;
    config.Communication.Serial.DefaultBaudRate = 9600;
});

// Inject named configurations
public class MultiDeviceService
{
    private readonly IDeviceFactory _primaryFactory;
    private readonly IDeviceFactory _secondaryFactory;

    public MultiDeviceService(
        [FromKeyedServices("primary")] IDeviceFactory primaryFactory,
        [FromKeyedServices("secondary")] IDeviceFactory secondaryFactory)
    {
        _primaryFactory = primaryFactory;
        _secondaryFactory = secondaryFactory;
    }
}

Environment-Specific Configuration

csharp
services.AddBelay(config => {
    if (Environment.IsDevelopment())
    {
        config.Communication.EnableDebugLogging = true;
        config.Device.DefaultConnectionTimeoutMs = 30000; // Longer for debugging
    }
    else if (Environment.IsProduction())
    {
        config.Communication.Serial.ConnectionPooling = true;
        config.Session.EnableSessionCaching = true;
        config.Device.EnableAutoReconnect = true;
    }
});

Best Practices

Service Lifetime Management

csharp
// Correct: Register factories as Singleton (they're stateless)
services.AddSingleton<IDeviceFactory, DeviceFactory>();

// Correct: Register custom devices as Transient (they have state)
services.AddTransient<ITemperatureSensor, TemperatureSensorDevice>();

// Correct: Register session managers as Scoped for web apps
services.AddScoped<IDeviceSessionManager, DeviceSessionManager>();

Resource Management

csharp
public class ResourceManagedService : IDisposable
{
    private readonly List<Device> _devices = new();
    private readonly IDeviceFactory _deviceFactory;

    public ResourceManagedService(IDeviceFactory deviceFactory)
    {
        _deviceFactory = deviceFactory;
    }

    public async Task<Device> GetDeviceAsync(string port)
    {
        var device = _deviceFactory.CreateSerialDevice(port);
        await device.ConnectAsync();
        _devices.Add(device);
        return device;
    }

    public void Dispose()
    {
        foreach (var device in _devices)
        {
            device?.Dispose();
        }
        _devices.Clear();
    }
}

// Register with proper lifetime
services.AddScoped<ResourceManagedService>();

Configuration Validation

csharp
services.AddBelay(config => {
    config.Device.DefaultConnectionTimeoutMs = 5000;
})
.Validate(config => config.Device.DefaultConnectionTimeoutMs > 0, "Timeout must be positive")
.Validate(config => config.Communication.Serial.DefaultBaudRate >= 9600, "Baud rate too low");

Next Steps

Released under the Apache License 2.0.