# 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.
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 defaultAggregateConfiguration, and theMinimumProcessingInterval. The standard functions are registered viaAggregatorsand advertised intoServer.ServerCapabilities.AggregateFunctionsandHistoryServerCapabilities.AggregateFunctions.IAggregateCalculatorimplementations (AggregateCalculatorand the specializedAverage/MinMax/Count/StartEnd/Status/StdDevcalculators) compute the aggregates from a stream of rawDataValues.- 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 explicitAggregateConfigurationwithTreatUncertainAsBad = falserather than relying onUseServerCapabilitiesDefaults. 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
AnnotationCountrequires a provider with annotation history; otherwise it returnsBad_AggregateNotSupported.- The bundled in-memory historian (
InMemoryHistorianProvider) supports all aggregates through the framework fallback and supports annotation history, soAnnotationCountworks out of the box. - Custom providers can override aggregate computation by implementing
IHistorianProcessedProvider.
References
- OPC 10000-13 (Aggregates) v1.05.07: https://reference.opcfoundation.org/Core/Part13/v105/docs/
- Historical Access (Part 11)
- Migration Guide