Files
Marc Schier b2b4dbb13f [Server] Part 13 Aggregates compliance fixes, full test coverage and docs (#3863)
# Description

Brings the server-side **OPC UA Part 13 (Aggregates) v1.05.07** support
to full conformance: fixes the three genuine non-compliances found by a
spec review, completes value-validated test coverage for **all 37
standard aggregate functions**, and adds documentation. The existing
aggregate engine is a faithful port and was already largely compliant;
this PR closes the remaining gaps.

## Compliance fixes

- **`TreatUncertainAsBad` default (§4.2.1.2).** The server-default
`AggregateConfiguration` (used when a `ReadProcessedDetails` request
sets `UseServerCapabilitiesDefaults = true`) now defaults
`TreatUncertainAsBad` to **`True`** as the spec mandates, in both
`AggregateManager.GetDefaultConfiguration` and the `HistorianDispatcher`
fallback. Behavior change noted in `MigrationGuide.md`.
- **`AnnotationCount` (§5.4.3.20).** Previously counted raw data values.
It now counts **Annotations** per interval, computed in
`HistorianDispatcher.DispatchProcessedReadAsync` from
`IHistorianAnnotationProvider` (endTime-exclusive bucketing;
reverse-time and `ProcessingInterval == 0` handled). Returns
`Bad_AggregateNotSupported` when the provider has no annotation history.
- **StandardDeviation/Variance use all n Good raw values
(§5.4.3.37/.39).** `ComputeStdDev` previously computed over the N−1
sub-regions returned by `GetRegionsInValueSet`, silently dropping the
last raw value. It now computes the mean/variance directly over all Good
raw values in the interval (UseBounds = None), differing only by the
divisor (sample n−1, population n). The population spec-example tests
that used to add a duplicate trailing value as a workaround were updated
to the natural 5-value datasets matching §A.35/.37.

## Tests

- **Calculator coverage is complete for all 37 aggregates** — added
`StatusAggregateCalculatorTests` (DurationGood/Bad, PercentGood/Bad,
WorstQuality/WorstQuality2), the Min/Max `*2` simple-bound variants
(`Minimum2`/`Maximum2`/`Range2`/`MinimumActualTime2`/`MaximumActualTime2`),
and `Interpolative`.
- **End-to-end integration** in `HistoryClientIntegrationTests` over the
live in-process ReferenceServer: value-validated
`Average`/`Minimum`/`Maximum` processed reads and an **AnnotationCount**
write→read round-trip.
- **Dispatcher-level** `AnnotationCount` tests (counts per interval;
`Bad_AggregateNotSupported` without an annotation provider).
- **Tightened `AggregateBaseTests`**: dropped `allowAnyResult` from
`StandardDeviationSample`/`StandardDeviationPopulation`/`NumberOfTransitions`
basecases (now deterministic on seeded data after the D3 fix).
- **Validation tests** for `AggregateConfiguration` per §4.2.1.2:
`PercentDataBad`/`PercentDataGood > 100` and the invalid relationship
`PercentDataGood < (100 - PercentDataBad)`.
- **Cross-cutting**: reversed time-range test (start > end).

## Verification

- Builds: **0 warnings / 0 errors**.
- Aggregate + historian dispatcher + History integration suites green on
**.NET 10** and **.NET Framework 4.8** (cross-TFM).
- Black-box (MCP) check against a standalone `ConsoleReferenceServer`:
both `Server.ServerCapabilities.AggregateFunctions` and
`HistoryServerCapabilities.AggregateFunctions` advertise all 37
functions (§4.2.2.1).

## Notes / out of scope

- AnnotationCount advertisement remains the full standard set with
per-node `Bad_AggregateNotSupported`. This is spec-acceptable: §4.2.2.1
lists what the server *can* support; per-node refinement is the optional
`HistoricalConfiguration.AggregateFunctions`. Conditional advertisement
could be added later as a quality refinement.
- A native-vs-framework-fallback parity test would be useful once a
third-party `IHistorianProcessedProvider` implementation exists; the
bundled `InMemoryHistorianProvider` uses only the framework fallback
today.

## Related Issues

_No existing tracking issue; this addresses OPC 10000-13 (Part 13,
Aggregates) v1.05.07 conformance gaps. Maintainers: please link or open
a tracking issue if one is required._

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. _(Ran the
aggregate/historian/integration suites on net48 + net10; full-solution
run pending CI.)_
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-10 08:34:09 +02:00

6.7 KiB

Aggregates (OPC UA Part 13)

Overview

The .NET Standard stack implements OPC UA Part 13 (OPC 10000-13) v1.05.07 Aggregates on the server side, computed over historical data retrieved through Historical Access (Part 11). All 37 standard aggregate functions are supported and advertised through the address space.

Aggregates are requested with the HistoryRead service using ReadProcessedDetails (a startTime, endTime, ProcessingInterval, one or more AggregateType NodeIds, and an optional AggregateConfiguration). The server divides the time range into intervals of ProcessingInterval milliseconds and produces one aggregate value per interval.

Supported aggregate functions

Category Functions
Interpolative Interpolative
Averages / integrals Average, TimeAverage, TimeAverage2, Total, Total2
Extrema Minimum, Maximum, MinimumActualTime, MaximumActualTime, Range, Minimum2, Maximum2, MinimumActualTime2, MaximumActualTime2, Range2
Counts Count, AnnotationCount, DurationInStateZero, DurationInStateNonZero, NumberOfTransitions
Start / end Start, End, Delta, StartBound, EndBound, DeltaBounds
Quality / time-in-state DurationGood, DurationBad, PercentGood, PercentBad, WorstQuality, WorstQuality2
Statistics StandardDeviationSample, VarianceSample, StandardDeviationPopulation, VariancePopulation

The exact per-aggregate semantics (type, bounding behaviour, timestamp, status-code rules, special cases) follow Part 13 §5.4.3.4–§5.4.3.40.

Architecture

HistoryRead (ReadProcessedDetails)
      ↓
AsyncCustomNodeManager / CustomNodeManager2  → HistorianDispatcher.DispatchProcessedReadAsync
      ↓                                              ├─ provider implements IHistorianProcessedProvider → native push-down
      ↓                                              └─ otherwise: framework fallback
      ↓                                                    ├─ AnnotationCount → IHistorianAnnotationProvider (counts Annotations)
      ↓                                                    └─ other aggregates → AggregateManager.CreateCalculator
      ↓                                                          → stream raw values through IAggregateCalculator
  • AggregateManager (Opc.Ua.Server) owns the registered aggregate factories, the server default AggregateConfiguration, and the MinimumProcessingInterval. The standard functions are registered via Aggregators and advertised into Server.ServerCapabilities.AggregateFunctions and HistoryServerCapabilities.AggregateFunctions.
  • IAggregateCalculator implementations (AggregateCalculator and the specialized Average/MinMax/Count/StartEnd/Status/StdDev calculators) compute the aggregates from a stream of raw DataValues.
  • A historian provider may compute aggregates itself by implementing IHistorianProcessedProvider (native push-down). When it does not, the framework streams raw values through the calculator.

AnnotationCount

AnnotationCount (Part 13 §5.4.3.20) counts Annotations in each interval, not raw data values. It is therefore computed from the node's annotation history via IHistorianAnnotationProvider, not from the raw-value calculator. If the resolved provider does not expose annotation history, an AnnotationCount request returns Bad_AggregateNotSupported.

Server configuration

AggregateConfiguration controls how non-Good data affects the result:

Property Default Meaning
TreatUncertainAsBad true Whether Uncertain samples are treated as Bad when computing the aggregate StatusCode (Part 13 §4.2.1.2).
PercentDataBad 100 Minimum % of Bad data in an interval for the interval StatusCode to be Bad.
PercentDataGood 100 Minimum % of Good data in an interval for the interval StatusCode to be Good.
UseSlopedExtrapolation false Stepped (hold-last) vs sloped extrapolation past the last value. Ignored for Simple Bounds.

The server's default configuration is returned by AggregateManager.GetDefaultConfiguration(...) and is used whenever a request sets AggregateConfiguration.UseServerCapabilitiesDefaults = true. Per Part 13 §4.2.1.2 the default TreatUncertainAsBad value is true.

Migration note: in earlier builds the server default used TreatUncertainAsBad = false. Clients that require the old behaviour should send an explicit AggregateConfiguration with TreatUncertainAsBad = false rather than relying on UseServerCapabilitiesDefaults. See the Migration Guide.

AggregateManager.MinimumProcessingInterval bounds the smallest interval the server will accept.

Client usage

Use the HistoryClient (session.Historian()) ReadProcessedAsync helper, which streams one DataValue per processing interval as an IAsyncEnumerable<DataValue>:

// Average of a historizing variable over the last hour, in 1-minute buckets.
DateTime end = DateTime.UtcNow;
DateTime start = end.AddHours(-1);

await foreach (DataValue value in session.Historian().ReadProcessedAsync(
    nodeId,
    aggregateFunctionId: ObjectIds.AggregateFunction_Average,
    startTime: start,
    endTime: end,
    processingInterval: 60_000 /* ms */))
{
    Console.WriteLine($"{value.SourceTimestamp:O}: {value.Value} ({value.StatusCode})");
}

Pass an explicit AggregateConfiguration to override the server defaults:

await foreach (DataValue value in session.Historian().ReadProcessedAsync(
    nodeId,
    ObjectIds.AggregateFunction_TimeAverage,
    start,
    end,
    processingInterval: 60_000,
    configuration: new AggregateConfiguration
    {
        TreatUncertainAsBad = false,
        PercentDataBad = 100,
        PercentDataGood = 100,
        UseSlopedExtrapolation = false
    }))
{
    // ...
}

Discover the aggregates a server supports with session.Historian().GetServerCapabilitiesAsync(...) or by browsing Server.ServerCapabilities.AggregateFunctions.

Notes and limitations

  • AnnotationCount requires a provider with annotation history; otherwise it returns Bad_AggregateNotSupported.
  • The bundled in-memory historian (InMemoryHistorianProvider) supports all aggregates through the framework fallback and supports annotation history, so AnnotationCount works out of the box.
  • Custom providers can override aggregate computation by implementing IHistorianProcessedProvider.

References