Contracts Analyzers
Voltimax.Iot.Contracts.Analyzers contains two Roslyn incremental source generators that produce the platform's core metric and device asset types at compile time.
Metrics Source Generator
Transforms JSON metric schemas into strongly-typed C# code. The JSON files are the single source of truth for all metric definitions across the platform.
Setup
<ItemGroup>
<PackageReference Include="Voltimax.Iot.Contracts.Analyzers"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="schemas/metrics/metrics.json" />
<AdditionalFiles Include="schemas/metrics/registries/*.json" />
</ItemGroup>Input Schema
The generator reads from two kinds of JSON files:
metrics.json - global configuration and enum definitions:
{
"registryPath": "./registries",
"enums": [
{
"name": "Unit",
"description": "Units of measurement",
"values": [
{ "value": "Watt", "description": "Watt (W)" }
]
}
]
}Registry files (e.g., registries/battery.json) - domain-specific metric definitions:
{
"id": "battery",
"name": "Battery",
"description": "Battery pack measurements",
"metrics": [
{
"name": "PackVoltage",
"key": "pack_voltage",
"unit": "Volt",
"description": "Pack DC voltage"
}
]
}See Data Model for the full schema reference.
Generated Output
The generator produces the following types in namespace Voltimax.Iot.Contracts.Metrics:
Enums
Unit and MetricProfile enums from the enums array in metrics.json:
public enum Unit
{
Unknown,
Watt,
Volt,
Ampere,
// ...
}MetricDomain
Static class with domain name constants:
public static class MetricDomain
{
public const string Battery = "battery";
public const string Energy = "energy";
// ...
public static readonly string[] AllDomains = [...];
}{Domain}MetricDefinitions
Static field per metric, carrying all metadata:
public static class BatteryMetricDefinitions
{
public static readonly Metric PackVoltage = new(...);
public static readonly Metric PackCurrent = new(...);
// ...
}{Domain}MetricValues
Wide-format POCO with JSON serialization attributes for telemetry:
public class BatteryMetricValues
{
[JsonPropertyName("pack_voltage")]
public double? PackVoltage { get; set; }
[JsonPropertyName("pack_current")]
public double? PackCurrent { get; set; }
// ...
public IEnumerable<MetricValue> ToEnumerable() { ... }
}{Domain}MetricCatalog
Lookup structures using frozen collections:
public static class BatteryMetricCatalog
{
public static FrozenSet<Metric> All { get; }
public static FrozenDictionary<string, Metric> ById { get; }
public static bool TryGetById(string id, out Metric metric);
public static int Count => 45;
public static string Category => "Battery";
}MetricCatalog (Global)
Aggregates all domain catalogs:
public static class MetricCatalog
{
public static FrozenSet<Metric> All { get; }
public static FrozenDictionary<string, FrozenSet<Metric>> ByCategory { get; }
public static FrozenDictionary<string, Metric> ById { get; }
public static bool TryGetById(string id, out Metric metric);
public static FrozenSet<Metric> GetByCategory(string category);
public static IReadOnlyList<string> Categories { get; }
}TelemetryFrame
Top-level telemetry container with a property per domain:
public class TelemetryFrame
{
public DateTime Timestamp { get; set; }
public string GatewayId { get; set; }
public AssetInfo Asset { get; set; }
public BatteryMetricValues? Battery { get; set; }
public EnergyMetricValues? Energy { get; set; }
// ...
}Plus TelemetryFrameExtensions for format conversion.
Adding a New Metric
- Edit the appropriate registry file in
schemas/metrics/registries/(e.g.,battery.json) - Add the metric definition with
name,key,unit,description - Optionally add
scopes,profile, orcounter - Build - the generator picks up the change automatically
{
"name": "CellTemperatureMax",
"key": "cell_temperature_max",
"unit": "DegreeCelsius",
"description": "Maximum cell temperature",
"profile": "Standard"
}No CLI command needed. The generated code updates on the next build.
Device Asset Source Generator
Transforms YAML device definitions into abstract C# base classes for Modbus devices. Each YAML file describes a physical device's register layout and produces a base class with typed read/write methods.
INFO
The Contracts Analyzers project handles the schema-level asset generation. The more protocol-specific generators (with full ReadAsync/WriteAsync implementations, scaling, and enum casting) live in Voltimax.Edge.Control.Analyzers.
Setup
<ItemGroup>
<AdditionalFiles Include="schemas/assets/modbus/*.yaml" />
</ItemGroup>Input Schema
YAML device definitions in schemas/assets/:
device:
id: EastronSdm630
manufacturer: Eastron
model: SDM630-Modbus
description: Three-phase energy meter
assetTypes: [Grid, Building, Solar]
protocol:
type: modbus
addressing: oneBased
registerPacking: wordHighFirst
byteOrder: bigEndian
registers:
input:
voltages:
- id: phase1Voltage
address: 30001
type: float32
access: read
unit: voltGenerated Output
For each device YAML file, the generator produces an abstract base class:
- Register address constants
- Typed read/write method signatures for each register
- Device-specific enum types from
enumReffields - XML documentation with address, unit, and scaling metadata
Adding a New Device
- Create a YAML file in
schemas/assets/modbus/following the schema format - Build - the generator creates the base class automatically
- Create a concrete implementation extending the generated base class