December 15 2025
dotnet add package NATS.Client.Core
public static class NatsConnectionFactory
{
public static NatsConnection Create()
{
return new NatsConnection(new NatsOpts
{
Url = "nats://localhost:4222",
Name = "smart-agriculture",
Reconnect = true,
MaxReconnect = 10,
ReconnectWait = TimeSpan.FromSeconds(1),
});
}
}
public record SoilMoistureReading(
string SensorId,
string FieldId,
double MoisturePercent,
DateTime TimestampUtc);
public record IrrigationCommand(
string FieldId,
bool EnableIrrigation,
double TargetMoisturePercent,
DateTime TimestampUtc);
public sealed class SoilSensorSimulator
{
private NatsConnection _connection;
private readonly string _sensorId;
private readonly string _fieldId;
private readonly Random _random = new();
public SoilSensorSimulator(NatsConnection connection, string sensorId, string fieldId)
{
_connection = connection;
_sensorId = sensorId;
_fieldId = fieldId;
}
public async Task RunAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var reading = new SoilMoistureReading(
SensorId: _sensorId,
FieldId: _fieldId,
MoisturePercent: 15 + _random.NextDouble() * 20, // 15–35%
TimestampUtc: DateTime.UtcNow);
var subject = $"sensors.soil.moisture.{_fieldId}";
await _connection.PublishAsync(subject, reading, cancellationToken: ct);
Console.WriteLine($"[Sensor {_sensorId}] {reading.MoisturePercent:F1}% in field {reading.FieldId}");
await Task.Delay(500, ct);
}
}
}
using Microsoft.Extensions.Hosting;
using NATS.Client.Core;
public sealed class SoilAnalyticsWorker : BackgroundService
{
private NatsConnection _connection;
public SoilAnalyticsWorker(NatsConnection connection)
{
_connection = connection;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Subscribe to all soil moisture readings from all fields
await foreach (var msg in _connection.SubscribeAsync<SoilMoistureReading>(
subject: "sensors.soil.moisture.*",
cancellationToken: stoppingToken))
{
var reading = msg.Data;
Console.WriteLine(
$"[Analytics] Field={reading.FieldId}, Sensor={reading.SensorId}, Moisture={reading.MoisturePercent:F1}%");
// Here you could:
// - update an in-memory average
// - push to a time-series DB
// - feed a dashboard, etc.
}
}
}
public sealed class IrrigationControllerWorker : BackgroundService
{
private NatsConnection _connection;
private const string QueueGroupName = "irrigation-controllers";
public IrrigationControllerWorker(NatsConnection connection)
{
_connection = connection;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var msg in
_connection.SubscribeAsync<SoilMoistureReading>(
subject: "sensors.soil.moisture.*",
queueGroup: QueueGroupName,
cancellationToken: stoppingToken))
{
var reading = msg.Data;
Console.WriteLine(
$"[Controller] Field={reading.FieldId}, Moisture={reading.MoisturePercent:F1}%");
if (reading.MoisturePercent < 20)
{
var command = new IrrigationCommand(
FieldId: reading.FieldId,
EnableIrrigation: true,
TargetMoisturePercent: 25,
TimestampUtc: DateTime.UtcNow);
await _connection.PublishAsync(
"irrigation.commands",
command,
stoppingToken);
Console.WriteLine(
$"[Controller] Issued irrigation command for field {reading.FieldId}");
}
}
}
}
public sealed class IrrigationDeviceWorker : BackgroundService
{
private NatsConnection _connection;
private readonly string _deviceId;
public IrrigationDeviceWorker(NatsConnection connection, string deviceId)
{
_connection = connection;
_deviceId = deviceId;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var msg in _connection.SubscribeAsync<IrrigationCommand>(
subject: "irrigation.commands",
cancellationToken: stoppingToken))
{
var command = msg.Data;
Console.WriteLine(
$"[Device {_deviceId}] Apply irrigation → Field={command.FieldId}, Enable={command.EnableIrrigation}, Target={command.TargetMoisturePercent}%");
// Here you'd talk to actual hardware:
// - open valve
// - configure timer
// - log to local storage, etc.
}
}
}
public record FieldStatusRequest(string FieldId);
public record FieldStatusResponse(
string FieldId,
double AverageMoisturePercent,
DateTime LastUpdatedUtc);
public interface IFieldStatusCache
{
(double AverageMoisture, DateTime LastUpdatedUtc) GetStatusForField(string fieldId);
}
public sealed class FieldStatusResponder : BackgroundService
{
private NatsConnection _connection;
private IFieldStatusCache _cache;
public FieldStatusResponder(NatsConnection connection, IFieldStatusCache cache)
{
_connection = connection;
_cache = cache;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var msg in
_connection.SubscribeAsync<FieldStatusRequest>(
subject: "fields.status.get",
cancellationToken: stoppingToken))
{
var (average, lastUpdated) = _cache.GetStatusForField(msg.Data.FieldId);
var response = new FieldStatusResponse(
FieldId: msg.Data.FieldId,
AverageMoisturePercent: average,
LastUpdatedUtc: lastUpdated);
await msg.ReplyAsync(response, stoppingToken);
}
}
}
public sealed class FieldStatusClient
{
private NatsConnection _connection;
public FieldStatusClient(NatsConnection connection)
{
_connection = connection;
}
public async Task<FieldStatusResponse?> GetStatusAsync(string fieldId, CancellationToken ct = default)
{
var request = new FieldStatusRequest(fieldId);
var msg = await _connection.RequestAsync<FieldStatusRequest, FieldStatusResponse>(
subject: "fields.status.get",
data: request,
cancellationToken: ct);
return msg.Data;
}
}
app.MapGet("/fields/{fieldId}/status", async (string fieldId, FieldStatusClient client, CancellationToken ct) =>
{
var status = await client.GetStatusAsync(fieldId, ct);
return status is null ? Results.NotFound() : Results.Ok(status);
});
1. Design Patterns that Deliver
This isn’t just another design patterns book. Dive into real-world examples and practical solutions to real problems in real applications.Check out it here.
Go-to resource for understanding the core concepts of design patterns without the overwhelming complexity. In this concise and affordable ebook, I've distilled the essence of design patterns into an easy-to-digest format. It is a Beginner level. Check out it here.
Every Monday morning, I share 1 actionable tip on C#, .NET & Arcitecture topic, that you can use right away.
Join 18,000+ subscribers to improve your .NET Knowledge.
Subscribe to the TheCodeMan.net and be among the 18,000+ subscribers gaining practical tips and resources to enhance your .NET expertise.