fix(s7): validate span-read options at parse time; document the over-PDU degradation

This commit is contained in:
Joseph Doherty
2026-08-17 03:45:14 -04:00
parent f06cbe16d9
commit acb3772426
3 changed files with 135 additions and 10 deletions
@@ -56,6 +56,26 @@ public static class S7DriverFactoryExtensions
throw new InvalidOperationException(
$"S7 driver config for '{driverInstanceId}' missing required Host");
// Block-read grouping knobs. There is no IValidateOptions machinery for S7, so this parse is
// the single choke point every deployed config passes through — validate here or a nonsensical
// value is accepted at deploy time and only misbehaves later, inside the read loop.
var spanReadMaxGapBytes = dto.SpanReadMaxGapBytes ?? S7DriverOptions.DefaultSpanReadMaxGapBytes;
var spanReadMaxSpanBytes = dto.SpanReadMaxSpanBytes ?? S7DriverOptions.DefaultSpanReadMaxSpanBytes;
if (spanReadMaxSpanBytes < 1)
throw new InvalidOperationException(
$"S7 driver config for '{driverInstanceId}' has SpanReadMaxSpanBytes {spanReadMaxSpanBytes}; " +
"must be >= 1 — it caps a single planned block read, so a non-positive cap admits none. " +
"There is no upper bound: a value above the negotiated PDU is legal and degrades to " +
"multi-request chunking inside S7.Net.");
// -1 is the documented escape hatch (span grouping disabled — S7Driver enables spans on >= 0)
// and 0 means "tolerate no gap", i.e. merge only strictly adjacent tags. Both stay legal.
if (spanReadMaxGapBytes < -1)
throw new InvalidOperationException(
$"S7 driver config for '{driverInstanceId}' has SpanReadMaxGapBytes {spanReadMaxGapBytes}; " +
"must be >= -1 — -1 disables span grouping and 0 merges only strictly adjacent tags.");
return new S7DriverOptions
{
Host = dto.Host!,
@@ -69,11 +89,11 @@ public static class S7DriverFactoryExtensions
// tags. The driver maps each RawTagEntry's TagConfig blob through S7TagDefinitionFactory
// at Initialize; the legacy pre-declared DTO tag path (Name/Address → BuildTag) is retired.
RawTags = dto.RawTags is { Count: > 0 } ? [.. dto.RawTags] : [],
// Block-read grouping knobs. Mapped explicitly like every other key on this DTO — the
// options object is never deserialised directly — so omitting them here would leave the
// documented "-1 disables grouping" escape hatch unreachable from a deployed config.
SpanReadMaxGapBytes = dto.SpanReadMaxGapBytes ?? S7DriverOptions.DefaultSpanReadMaxGapBytes,
SpanReadMaxSpanBytes = dto.SpanReadMaxSpanBytes ?? S7DriverOptions.DefaultSpanReadMaxSpanBytes,
// Mapped explicitly like every other key on this DTO — the options object is never
// deserialised directly — so omitting them here would leave the documented
// "-1 disables grouping" escape hatch unreachable from a deployed config.
SpanReadMaxGapBytes = spanReadMaxGapBytes,
SpanReadMaxSpanBytes = spanReadMaxSpanBytes,
Probe = new S7ProbeOptions
{
Enabled = dto.Probe?.Enabled ?? true,
@@ -128,7 +148,8 @@ public static class S7DriverFactoryExtensions
/// <summary>Gets the probe configuration.</summary>
public S7ProbeDto? Probe { get; init; }
/// <summary>Gets the largest hole the read planner reads through when merging tags into one
/// block request; any value below zero disables block-read grouping. Null keeps the default.</summary>
/// block request; <c>-1</c> disables block-read grouping and anything below it is rejected.
/// Null keeps the default.</summary>
public int? SpanReadMaxGapBytes { get; init; }
/// <summary>Gets the largest single planned block read in bytes. Null keeps the default.</summary>
public int? SpanReadMaxSpanBytes { get; init; }