Skip to content

Properties & Measuring Points

Properties and measuring points are how a logic block exposes its state. Service properties represent observable and optionally configurable values. Measuring points represent telemetry recorded over time. A single value can be both. A separate [Presentation] attribute controls how the value is grouped and rendered on the dashboard.

INFO

logic blocks do not read each other's service properties directly. Inter-block communication is done exclusively through logic interfaces (commands, request/response, state updates).

The [ServiceProperty] Attribute

[ServiceProperty] makes a C# property visible to the VION platform. It defines the schema-side facts about the value — its display name, unit, allowed range, and whether it is a secret.

csharp
[ServiceProperty(Title = "...", Description = "...", Unit = "...",
                 Minimum = ..., Maximum = ..., WriteOnly = ...)]
ParameterDescription
TitleDisplay name. Defaults to the C# property name.
DescriptionLong-form text for tooltips, search, and accessibility.
UnitUnit of measurement (e.g., "°C", "%", "kWh").
MinimumMinimum allowed value. Defaults to double.NegativeInfinity.
MaximumMaximum allowed value. Defaults to double.PositiveInfinity.
WriteOnlyMarks a string / string? property as a secret. See Secrets.

Basic Example

A user-configurable setpoint:

csharp
[ServiceProperty(Title = "Target Temperature", Unit = "°C", Minimum = 10, Maximum = 35)]
[Presentation(Group = PropertyGroup.Configuration)]
public double TargetTemp { get; set; } = 21.0;

Read-Only Properties

Use private set to make a property read-only from the dashboard. The logic block can still update the value internally.

csharp
[ServiceProperty(Title = "Power Consumption", Unit = "W")]
[Presentation(Group = PropertyGroup.Status)]
public double PowerConsumption { get; private set; }

Computed Properties

Properties with only a getter work as well. The value is evaluated each time the runtime reads it.

csharp
[ServiceProperty(Title = "Total Power", Unit = "W")]
[Presentation(Group = PropertyGroup.Status)]
public double TotalPower => HeaterPower + FanPower;

Supported Types

TypeNotes
bool
byte, short, ushort, int, uint, long
float, double
string
DateTime, TimeSpanFormatting via Presentation.Format.
enumSee Enum Properties.
T? (nullable)
ImmutableArray<T>See Complex Value Types.
readonly record structSee Complex Value Types.

Container types compose — ImmutableArray<Coordinates?> is a valid shape.

The [ServiceMeasuringPoint] Attribute

[ServiceMeasuringPoint] marks a property as telemetry. Measuring points are recorded in the time-series database for charts, exports, and analytics.

csharp
[ServiceMeasuringPoint(Title = "...", Description = "...", Unit = "...",
                       Minimum = ..., Maximum = ..., Kind = ...)]
ParameterDescription
TitleDisplay name. Defaults to the C# property name.
DescriptionLong-form text for tooltips and metadata.
UnitUnit of measurement.
MinimumLower bound. Defaults to double.NegativeInfinity.
MaximumUpper bound. Defaults to double.PositiveInfinity.
KindTime-series shape: Measurement (default), Total, or TotalIncreasing. See Measuring Point Kinds.

Basic Example

csharp
[ServiceMeasuringPoint(Title = "Room Humidity", Unit = "%", Minimum = 0, Maximum = 100)]
public double Humidity { get; private set; }

Measuring Point Kinds

The Kind field tells the chart engine what shape the time-series has. Picking the wrong one shows misleading averages or counter resets as drops.

KindUse For
Measurement (default)Instantaneous samples — temperature, voltage, RPM, state-of-charge percent. Can rise or fall freely.
TotalCumulative values that can both increase and decrease — stored energy, tank level, buffer fill.
TotalIncreasingMonotonically increasing counters with possible resets — imported energy, total runtime, packet count.
csharp
[ServiceMeasuringPoint(Title = "Power", Unit = "kW", Kind = MeasuringPointKind.Measurement)]
public double Power { get; private set; }

[ServiceMeasuringPoint(Title = "Stored Energy", Unit = "kWh", Kind = MeasuringPointKind.Total)]
public double StoredEnergy { get; private set; }

[Persistent]
[ServiceMeasuringPoint(Title = "Energy Imported", Unit = "kWh", Kind = MeasuringPointKind.TotalIncreasing)]
public double EnergyImported { get; private set; }

MeasuringPointKind lives in Vion.Dale.Sdk.Core, the same namespace as [ServiceMeasuringPoint] and the other authoring attributes — a single using Vion.Dale.Sdk.Core; covers it, with no separate wire- or schema-namespace import.

Combining Service Properties and Measuring Points

A property can be both. This is common for live values shown on a dashboard tile and recorded over time.

csharp
[ServiceProperty(Title = "Current Temperature", Unit = "°C")]
[ServiceMeasuringPoint]
[Presentation(Group = PropertyGroup.Status, Importance = Importance.Primary)]
public double CurrentTemp { get; private set; }

Cross-Fill

When both attributes are present, the runtime cross-fills any shared field set on only one side. Title, Description, Unit, Minimum, and Maximum all cross-fill, so declare them once.

csharp
[ServiceProperty(Title = "Power", Unit = "kW")]
[ServiceMeasuringPoint]
public double Power { get; private set; }

[ServiceProperty(Title = "Charge", Unit = "%")]
[ServiceMeasuringPoint(Kind = MeasuringPointKind.Total)]
public double StateOfCharge { get; private set; }

WriteOnly is property-only. Kind is measuring-point-only.

When to Use Which

ScenarioUse
User-configurable setting[ServiceProperty] with public setter
Read-only runtime state[ServiceProperty] with private setter
Telemetry recorded over time[ServiceMeasuringPoint]
Dashboard value and time-series recordingBoth attributes
Cumulative counter charted as a delta[ServiceMeasuringPoint(Kind = MeasuringPointKind.TotalIncreasing)]
Secret string the operator sets but never reads back[ServiceProperty(WriteOnly = true)]

Complex Value Types

The Dale SDK supports three composable container shapes: nullables, immutable arrays, and readonly record struct. These can be combined — ImmutableArray<Coordinates?> is a valid measuring-point type.

Nullable Values

Append ? to any supported primitive, enum, or struct type. null means "unknown" and is distinct from a zero or default value.

csharp
[ServiceProperty(Title = "Optional Setpoint", Unit = "kW")]
public double? OptionalTarget { get; set; }

Arrays (ImmutableArray<T>)

Use ImmutableArray<T> to expose a sequence of values. Immutability is required so snapshots can be safely shared across the runtime boundary.

csharp
using System.Collections.Immutable;

[ServiceMeasuringPoint(Title = "Histogram Buckets", Unit = "A")]
public ImmutableArray<double> HistogramBuckets { get; private set; }
    = ImmutableArray<double>.Empty;

Initialize the property with ImmutableArray<T>.Empty — the default default(ImmutableArray<T>) is not iterable. Assign a new array to replace contents:

csharp
ScheduleHours = ImmutableArray.Create(6, 12, 18);

Custom Structs

Bundle related scalars into a readonly record struct. Each field can carry its own unit and range via [StructField].

csharp
public readonly record struct Coordinates(
    [StructField(Unit = "deg", Minimum = -90, Maximum = 90)]
    double Lat,
    [StructField(Unit = "deg", Minimum = -180, Maximum = 180)]
    double Lon);

[StructField] accepts Title, Description, Unit, Minimum, and Maximum.

Structs used as service-element values must be flat:

  • Declare as readonly record struct. Mutable or non-record structs are rejected.
  • Fields must be primitives, enums, string, DateTime, TimeSpan, or nullable of those.
  • No nested structs and no arrays inside a struct. Use ImmutableArray<MyStruct> at the property level.

The [Presentation] Attribute

[Presentation] carries the UI-side hints that the schema-side attributes do not. [ServiceProperty] says what the value is; [Presentation] says how it is shown.

csharp
[Presentation(DisplayName = "...", Group = ..., Order = ..., Importance = ...,
              StatusIndicator = ..., Decimals = ..., UiHint = ..., Format = ...)]
FieldTypePurpose
DisplayNamestring?Override the label. Falls back to Title or the C# property name. Useful for enum-/struct-typed properties where the schema title names the type itself.
Groupstring?Section the property is rendered in. Use a PropertyGroup constant or your own. See Groups.
OrderintSort hint within a group. Lower values render first; negatives are allowed. Unset falls back to base-to-derived hierarchy order.
ImportanceImportanceTile-vs-detail visibility: Primary, Secondary, Normal (default), Hidden.
StatusIndicatorboolMarks an enum-typed property as an operational status.
DecimalsintDisplay precision for numeric values. Unset uses sensible per-type defaults.
UiHintstring?Routing key for non-default renderers. Use a UiHints constant or your own.
Formatstring?Format-token string for date/duration rendering. Use a Formats constant or a moment.js / day.js token.

The attribute is not sealed — see Preset Attributes.

Groups

The dashboard renders all properties with the same Group key in one section. The platform ships well-known constants in PropertyGroup; integrators may use their own string keys.

ConstantUse For
PropertyGroup.IdentityManufacturer, model, serial number, firmware version.
PropertyGroup.StatusLive read-only operational state.
PropertyGroup.ConfigurationAnything the operator can write — settings, runtime controls, triggers.
PropertyGroup.MetricCounters, totals, accumulated values.
PropertyGroup.DiagnosticsTroubleshooting and health — last error, response time, connectivity.
PropertyGroup.AlarmActive alarm state, fault codes.
PropertyGroup.NoneUngrouped (fallback bucket).
csharp
[ServiceProperty(Title = "Target Temperature", Unit = "°C")]
[Presentation(Group = PropertyGroup.Configuration)]
public double TargetTemp { get; set; } = 21.0;

Use one of the PropertyGroup constants. Custom string keys are accepted, but unknown keys carry no UI guarantees — they're rendered verbatim today, and that may change. Block-level section ordering is set on [LogicBlock(Groups = [...])] — see Section Ordering.

UI Hints

UiHint routes a property to a non-default renderer. The platform ships well-known constants in UiHints; integrators may supply their own keys.

ConstantPurpose
UiHints.StatusIndicatorAuto-emitted when StatusIndicator = true. Do not set directly.
UiHints.TriggerWritable bool rendered as a button. See Action Triggers.
UiHints.SparklineInline sparkline for numeric arrays.
UiHints.MultilineWritable string as a multi-line textarea.
UiHints.JsonWritable string as a JSON code editor.
UiHints.SliderBounded numeric as a slider control. Requires both Minimum and Maximum.
csharp
[ServiceProperty(Title = "Operator Notes")]
[Presentation(UiHint = UiHints.Multiline)]
public string Notes { get; set; } = "";

Unrecognized hints fall back to the default renderer.

Format

Format controls how date and duration values are rendered. The dashboard interprets the value as a moment.js / day.js compatible format token. Two reserved sentinels short-circuit the token interpreter:

ConstantTokenRenders As
Formats.Relative"relative"Auto-updating "3 minutes ago" (locale-aware). Requires DateTime.
Formats.Humanize"humanize"Humanized duration: "3 hours". Requires TimeSpan.
Formats.LocaleFull"LLLL""Wednesday, May 13, 2026 2:32 PM".
Formats.LocaleLong"LLL""May 13, 2026 2:32 PM".
Formats.Iso"YYYY-MM-DD HH:mm:ss"Explicit ISO-ish date-time.
Formats.IsoMillis"YYYY-MM-DD HH:mm:ss.SSS"With millisecond precision.
Formats.Clock"HH:mm:ss"Clock-style duration.
Formats.ClockMillis"HH:mm:ss.SSS"Clock-style duration with milliseconds.

Formats carries additional locale-aware shortcuts (LocaleShort, LocaleDate, LocaleTime). Any moment-compatible token string works directly.

csharp
[ServiceMeasuringPoint(Title = "Last Sample")]
[Presentation(Group = PropertyGroup.Diagnostics, Format = Formats.Relative)]
public DateTime LastSampleAt { get; private set; } = DateTime.UtcNow;

[ServiceMeasuringPoint(Title = "Uptime")]
[Presentation(Group = PropertyGroup.Diagnostics, Format = Formats.Humanize)]
public TimeSpan Uptime { get; private set; }

Format is orthogonal to UiHint and Decimals — the three control different aspects of rendering.

Property Ordering

Within a group, properties render in this order:

  1. Properties with an explicit [Presentation(Order = N)] value sort ascending. Negative values are allowed.
  2. Properties without an explicit Order follow base-to-derived hierarchy order: interface defaults first, base classes next, the most-derived class last. Within each level, source declaration order is preserved.
csharp
public interface IMeter
{
    // Renders first (inherited, no Order).
    [ServiceProperty(Title = "Manufacturer")]
    [Presentation(Group = PropertyGroup.Identity)]
    string Manufacturer { get; }
}

public class ElectricityMeter : LogicBlockBase, IMeter
{
    public string Manufacturer { get; private set; } = "ACME";

    // Renders BEFORE Manufacturer — explicit negative Order wins.
    [ServiceProperty(Title = "Model")]
    [Presentation(Group = PropertyGroup.Identity, Order = -10)]
    public string Model { get; private set; } = "EM-100";

    // Renders AFTER Manufacturer — no Order, derived in source order.
    [ServiceProperty(Title = "Firmware Version")]
    [Presentation(Group = PropertyGroup.Identity)]
    public string FirmwareVersion { get; private set; } = "1.2.0";
}

Status Indicators

A status indicator is an enum-typed property that drives a status badge on the dashboard tile. Set StatusIndicator = true on [Presentation] and decorate the enum members with [EnumLabel] (display label) and [Severity] (severity level).

csharp
public enum DeviceStatus
{
    [EnumLabel("Connected")]
    [Severity(StatusSeverity.Success)]
    Connected,

    [EnumLabel("Connecting")]
    [Severity(StatusSeverity.Warning)]
    Connecting,

    [EnumLabel("Disconnected")]
    [Severity(StatusSeverity.Error)]
    Disconnected,
}

[ServiceProperty(Title = "Connection Status")]
[Presentation(Group = PropertyGroup.Alarm, StatusIndicator = true, Importance = Importance.Primary)]
public DeviceStatus Status { get; private set; } = DeviceStatus.Disconnected;

StatusSeverity values: Success, Info, Warning, Error, Neutral.

A block may declare more than one status indicator — distinct status dimensions like operating mode, connection state, and activity status all surface on the tile.

Action Triggers

A writable bool with UiHint = UiHints.Trigger renders as a button. Click commits true; the property's getter must always return false so the button visually returns to its resting state.

csharp
[ServiceProperty(Title = "Reset Statistics")]
[Presentation(Group = PropertyGroup.Configuration, UiHint = UiHints.Trigger)]
[Persistent(Exclude = true)]
public bool ResetStats
{
    get => false;
    set
    {
        if (value)
        {
            TimesGreeted = 0;
        }
    }
}

The getter-always-false pattern is a transitional bridge until a first-class [ServiceAction] primitive ships. Combine with [Persistent(Exclude = true)] so the trigger does not re-fire on every restart.

Secrets

A string or string? property with WriteOnly = true is a secret. The Dale runtime strips the actual value at the publish boundary and replaces it with a redaction sentinel before anything leaves the gateway. Operators can set, clear, and overwrite the value — but they cannot read it back.

csharp
[ServiceProperty(Title = "API Key", WriteOnly = true)]
[Presentation(Group = PropertyGroup.Configuration)]
public string? ApiKey { get; set; }

The setter inside the logic block receives the real value — cache it, hand it to your client library, or persist it normally. Three states are distinguishable through the dashboard: never set, cleared, and set-and-hidden.

WriteOnly is restricted to string / string? in v1 — the redaction sentinel is itself a string literal. Persistence works as usual; opt out with [Persistent(Exclude = true)] if a secret should be re-entered on every restart.

WARNING

At-rest encryption is out of scope. The gateway's persistence store keeps the real value on disk. WriteOnly protects the wire path, not the local disk.

Enum Properties

Enums are a natural fit for properties with a fixed set of values. The dashboard renders them as dropdowns by default, or as status badges when marked as indicators.

csharp
public enum OperatingMode
{
    [EnumLabel("Automatic")]
    Auto,

    [EnumLabel("Manual Override")]
    Manual,

    [EnumLabel("Energy Saving")]
    Eco,

    [EnumLabel("Off")]
    Off,
}

[ServiceProperty(Title = "Operating Mode")]
[Presentation(Group = PropertyGroup.Configuration, Importance = Importance.Secondary)]
public OperatingMode Mode { get; set; } = OperatingMode.Auto;

[EnumLabel] provides a human-readable display name for each member. Without it, the C# identifier is used.

Preset Attributes

[Presentation], [ServiceProperty], and [ServiceMeasuringPoint] are all un-sealed. Subclass any of them to pre-fill the constructor and stay DRY across a logic block library:

csharp
public class Kilowatts : ServicePropertyAttribute
{
    public Kilowatts() { Unit = "kW"; Minimum = 0; }
}

public class StateMetric : PresentationAttribute
{
    public StateMetric()
    {
        Group = PropertyGroup.Status;
        Importance = Importance.Primary;
        Decimals = 1;
    }
}

Apply them like the platform attributes; per-property arguments override preset defaults:

csharp
[Kilowatts]
[StateMetric]
public double ActivePower { get; private set; }

[Kilowatts(Minimum = -50)] // Allow negative — battery discharging.
public double NetPower { get; private set; }

See Declarative Presentation for the cascade rules and the cross-cutting model.

Persistence

Writable service properties (public setter) are automatically persisted across restarts. The gateway restores the last-known value before the logic block enters the Ready state.

Opt a writable property out of persistence with [Persistent(Exclude = true)]:

csharp
[ServiceProperty(Title = "Reset Statistics")]
[Presentation(Group = PropertyGroup.Configuration, UiHint = UiHints.Trigger)]
[Persistent(Exclude = true)]
public bool ResetStats { get => false; set { if (value) DoReset(); } }

Read-only properties (private setter) are not persisted by default. Add [Persistent] to keep cumulative counters across restarts:

csharp
[Persistent]
[ServiceProperty(Title = "Total Energy", Unit = "kWh")]
[ServiceMeasuringPoint(Kind = MeasuringPointKind.TotalIncreasing)]
[Presentation(Group = PropertyGroup.Metric)]
public double TotalEnergy { get; private set; }

For more detail, see Persistence.

Service Interfaces

Service interfaces standardize the data surface of your logic blocks. Define a C# interface decorated with [ServiceInterface] and apply the attributes to its members. Logic blocks that implement the interface inherit the metadata.

csharp
[ServiceInterface]
public interface IClimateService
{
    [ServiceProperty(Title = "Temperature", Unit = "°C")]
    [ServiceMeasuringPoint]
    [Presentation(Group = PropertyGroup.Status, Importance = Importance.Primary)]
    double Temperature { get; }

    [ServiceProperty(Title = "Target Temperature", Unit = "°C", Minimum = 10, Maximum = 35)]
    [Presentation(Group = PropertyGroup.Configuration)]
    double TargetTemperature { get; set; }
}

A logic block implements the interface directly:

csharp
[LogicBlock(Name = "Room Climate Controller")]
public class RoomClimateBlock : LogicBlockBase, IClimateService
{
    public RoomClimateBlock(ILogger logger) : base(logger) { }

    public double Temperature { get; private set; }
    public double TargetTemperature { get; set; } = 21.0;

    protected override void Ready() { }
}

[Presentation] cascades per-field — the interface declares defaults; the class can override any single field without re-declaring the rest. Schema attributes cascade as a whole — overriding [ServiceProperty] on the class replaces the interface's declaration entirely. See Declarative Presentation.

Service Relations

Use [ServiceRelation] to declare directional relationships between service interfaces.

csharp
[ServiceInterface]
[ServiceRelation("PingPong", ServiceRelationDirection.Outwards, typeof(IPongService))]
public interface IPingService
{
    [ServiceProperty]
    [ServiceMeasuringPoint]
    int PingsPerSecond { get; }
}

The matching interface must declare the same relationType with the opposite direction (Inwards).

Using the Dale CLI

Add a service property:

bash
dale add serviceproperty TargetTemp --type double --to ThermostatBlock
FlagDescription
--type / -t (required)C# type (double, string, bool, etc.)
--toTarget logic block class (auto-detected if only one exists)
--setterpublic or private (default: private)
--default-nameSets the Title field on the emitted attribute
--persistentAdds [Persistent]
--group[Presentation] group — a PropertyGroup name (Status, Configuration, Metric, Diagnostics, Identity, Alarm) or an arbitrary raw key
--importance[Presentation] importance — Primary, Secondary, Normal, or Hidden
--decimals[Presentation] numeric display precision
--format[Presentation] date/duration/numeric format token

The four presentation flags emit a [Presentation(...)] attribute alongside [ServiceProperty]; supplying none omits the attribute entirely. A known --group name renders as the PropertyGroup constant, any other value as a raw string key:

bash
dale add serviceproperty TargetTemp --type double --to ThermostatBlock --group Configuration --importance Secondary --decimals 1

This generates:

csharp
[ServiceProperty(Title = "TargetTemp")]
[Presentation(Group = PropertyGroup.Configuration, Importance = Importance.Secondary, Decimals = 1)]
public double TargetTemp { get; private set; }

Add a measuring point:

bash
dale add measuringpoint CurrentTemp --type double --to ThermostatBlock --kind Total

Measuring points take the same flags except --setter (always private set), plus --kind (Measurement, Total, or TotalIncreasing), emitted inside the attribute as [ServiceMeasuringPoint(Kind = MeasuringPointKind.…)]. The command adds the using Vion.Dale.Sdk.Core; for MeasuringPointKind automatically.

For a value that is both a property and a measuring point, run the CLI for one and add the other attribute by hand — the CLI refuses to add to an existing property.

Complete Example

A thermostat block exercising properties, measuring points, presentation, status indicators, and persistence:

csharp
[LogicBlock(Name = "Smart Thermostat", Icon = "temp-cold-line",
            Groups = new[]
                     {
                         PropertyGroup.Alarm,
                         PropertyGroup.Status,
                         PropertyGroup.Metric,
                         PropertyGroup.Configuration,
                         PropertyGroup.Identity,
                     })]
public class SmartThermostatBlock : LogicBlockBase
{
    public enum ThermostatStatus
    {
        [EnumLabel("Heating")]
        [Severity(StatusSeverity.Warning)]
        Heating,

        [EnumLabel("Cooling")]
        [Severity(StatusSeverity.Info)]
        Cooling,

        [EnumLabel("Idle")]
        [Severity(StatusSeverity.Success)]
        Idle,

        [EnumLabel("Fault")]
        [Severity(StatusSeverity.Error)]
        Fault,
    }

    [ServiceProperty(Title = "State")]
    [Presentation(Group = PropertyGroup.Alarm, StatusIndicator = true,
                  Importance = Importance.Primary)]
    public ThermostatStatus State { get; private set; } = ThermostatStatus.Idle;

    [ServiceProperty(Title = "Current Temperature", Unit = "°C")]
    [ServiceMeasuringPoint]
    [Presentation(Group = PropertyGroup.Status, Importance = Importance.Primary)]
    public double CurrentTemp { get; private set; }

    [ServiceProperty(Title = "Target Temperature", Unit = "°C", Minimum = 10, Maximum = 35)]
    [Presentation(Group = PropertyGroup.Configuration, UiHint = UiHints.Slider)]
    public double TargetTemp { get; set; } = 21.0;

    [Persistent]
    [ServiceProperty(Title = "Total Runtime", Unit = "h")]
    [ServiceMeasuringPoint(Kind = MeasuringPointKind.TotalIncreasing)]
    [Presentation(Group = PropertyGroup.Metric)]
    public double TotalRuntime { get; private set; }

    [ServiceMeasuringPoint(Title = "Last Service")]
    [Presentation(Group = PropertyGroup.Diagnostics, Format = Formats.Relative)]
    public DateTime LastServiceAt { get; private set; } = DateTime.UtcNow.AddDays(-7);

    [ServiceProperty(Title = "Reset Statistics")]
    [Presentation(Group = PropertyGroup.Configuration, UiHint = UiHints.Trigger)]
    [Persistent(Exclude = true)]
    public bool ResetStats
    {
        get => false;
        set
        {
            if (value)
            {
                TotalRuntime = 0;
            }
        }
    }
}

Run dale dev to preview the block in the local DevHost — it renders presentations the same way the production dashboard does, giving fast feedback during authoring.