Summary
The native historian (IHistorianProvider, HistorianDispatcher, UseHistorian(), Historize()) covers the history read/update services, but four pieces of Part 11 / Part 13 behaviour that an archive-backed server needs still have to be implemented as AsyncCustomNodeManager overrides, and one fluent helper actively fights the provider. The Workshop/HistoricalAccess sample in UA-.NETStandard-Samples already registers its provider through Server.UseHistorian().UseProvider(...).RegisterForNamespace(...), yet 8 of its 15 hand-written members remain because of these gaps. The historian subsystem is unchanged between 2.0.0-preview.2 and master, so nothing on master closes them.
The four gaps
1. ReviseAggregateFilterAsync ignores the resolved historian
AsyncCustomNodeManager.ReviseAggregateFilterAsync (~6883) clamps ProcessingInterval to Server.AggregateManager.MinimumProcessingInterval and fills AggregateConfiguration from the server defaults. It never consults the provider, even though IHistorianProvider.GetCapabilitiesAsync(nodeId) already returns HistorianNodeCapabilities.Stepped, MinTimeInterval, MaxTimeInterval and DefaultAggregateConfiguration (src/Opc.Ua.Server/Historian/HistorianNodeCapabilities.cs ~127-177). The sample overrides it to clamp to the archive item's sampling interval, align StartTime to the queue size and set Stepped from the item.
Proposal: when a provider resolves for the node, the base implementation should derive the revised filter from GetCapabilitiesAsync (or expose a fluent OnReviseAggregateFilter hook per node / per historian registration).
2. No aggregate-support gate in front of processed reads
HistorianDispatcher.DispatchProcessedReadAsync (~363) forwards every requested aggregate to IHistorianProcessedProvider; there is no check against Server.AggregateManager.IsSupported(aggregateId), and provider reads have no per-operation status channel to answer BadAggregateNotSupported themselves. The sample therefore overrides HistoryReadProcessedAsync (~4703) to filter nodesToProcess first. OnHistoryRead on the builder replaces the whole read for a node, so it cannot act as a pre-filter.
Proposal: the dispatcher should answer BadAggregateNotSupported for aggregates the server does not support before calling the provider, or provider reads should get a status channel per operation.
3. No initial-value priming from history for aggregate-filtered monitored items
For a data-change item with an AggregateFilter whose StartTime lies in the past, the correct initial delivery is the raw history from StartTime to now, not the current value. ReadInitialValue (~6561) reads the attribute, and it runs before OnMonitoredItemCreated (~6537 vs ~6550), so a fluent handler that queues history afterwards delivers values out of time order behind the already-queued current value. The sample overrides ReadInitialValue and calls into its provider's raw-window read.
Proposal: when the node resolves an IHistorianDataProvider and the item carries an AggregateFilter with a past StartTime, the base should pre-queue ReadRawAsync(StartTime, now) instead of the current value; alternatively expose an OnReadInitialValue hook that runs before the default.
4. Historize() forces Historizing = true
HistorianFluentExtensions.Historize(...) calls HistorianBuilder.Historize(..., setHistorizing: true) (HistorianFluentExtensions.cs ~303, HistorianBuilder.cs ~146-161), which sets variable.Historizing = true unconditionally. A server whose archive decides per item whether it is currently archiving (the sample's static "Sample" items report false, the "Dynamic" ones true, and the value changes when the archive configuration is reloaded) cannot use Historize() at all and has to fall back to namespace registration plus hand-set access bits. The sample tests assert both values.
Proposal: add a historizing: parameter (or a Func<bool> / leave the attribute untouched when the provider's IsHistorizingAsync is authoritative) so that Historize() sets the HistoryRead/HistoryWrite access bits and registers the node without overriding the Historizing attribute.
Optional, same area
- A simulation/poll loop that runs only while a monitored item exists (tracked with the monitored-item lifecycle hooks) would remove the sample's remaining
OnMonitoredItemCreated / OnMonitoredItemDeletedAsync timer management.
docs/HistoricalAccess.md should state which of these the fluent surface covers once they land; today the sample's overrides are the only documentation of the gaps.
Related: #4397 (on-demand nodes), #4398 (monitored-item lifecycle hooks).
Summary
The native historian (
IHistorianProvider,HistorianDispatcher,UseHistorian(),Historize()) covers the history read/update services, but four pieces of Part 11 / Part 13 behaviour that an archive-backed server needs still have to be implemented asAsyncCustomNodeManageroverrides, and one fluent helper actively fights the provider. TheWorkshop/HistoricalAccesssample in UA-.NETStandard-Samples already registers its provider throughServer.UseHistorian().UseProvider(...).RegisterForNamespace(...), yet 8 of its 15 hand-written members remain because of these gaps. The historian subsystem is unchanged between2.0.0-preview.2and master, so nothing on master closes them.The four gaps
1.
ReviseAggregateFilterAsyncignores the resolved historianAsyncCustomNodeManager.ReviseAggregateFilterAsync(~6883) clampsProcessingIntervaltoServer.AggregateManager.MinimumProcessingIntervaland fillsAggregateConfigurationfrom the server defaults. It never consults the provider, even thoughIHistorianProvider.GetCapabilitiesAsync(nodeId)already returnsHistorianNodeCapabilities.Stepped,MinTimeInterval,MaxTimeIntervalandDefaultAggregateConfiguration(src/Opc.Ua.Server/Historian/HistorianNodeCapabilities.cs~127-177). The sample overrides it to clamp to the archive item's sampling interval, alignStartTimeto the queue size and setSteppedfrom the item.Proposal: when a provider resolves for the node, the base implementation should derive the revised filter from
GetCapabilitiesAsync(or expose a fluentOnReviseAggregateFilterhook per node / per historian registration).2. No aggregate-support gate in front of processed reads
HistorianDispatcher.DispatchProcessedReadAsync(~363) forwards every requested aggregate toIHistorianProcessedProvider; there is no check againstServer.AggregateManager.IsSupported(aggregateId), and provider reads have no per-operation status channel to answerBadAggregateNotSupportedthemselves. The sample therefore overridesHistoryReadProcessedAsync(~4703) to filternodesToProcessfirst.OnHistoryReadon the builder replaces the whole read for a node, so it cannot act as a pre-filter.Proposal: the dispatcher should answer
BadAggregateNotSupportedfor aggregates the server does not support before calling the provider, or provider reads should get a status channel per operation.3. No initial-value priming from history for aggregate-filtered monitored items
For a data-change item with an
AggregateFilterwhoseStartTimelies in the past, the correct initial delivery is the raw history fromStartTimeto now, not the current value.ReadInitialValue(~6561) reads the attribute, and it runs beforeOnMonitoredItemCreated(~6537 vs ~6550), so a fluent handler that queues history afterwards delivers values out of time order behind the already-queued current value. The sample overridesReadInitialValueand calls into its provider's raw-window read.Proposal: when the node resolves an
IHistorianDataProviderand the item carries anAggregateFilterwith a pastStartTime, the base should pre-queueReadRawAsync(StartTime, now)instead of the current value; alternatively expose anOnReadInitialValuehook that runs before the default.4.
Historize()forcesHistorizing = trueHistorianFluentExtensions.Historize(...)callsHistorianBuilder.Historize(..., setHistorizing: true)(HistorianFluentExtensions.cs~303,HistorianBuilder.cs~146-161), which setsvariable.Historizing = trueunconditionally. A server whose archive decides per item whether it is currently archiving (the sample's static "Sample" items reportfalse, the "Dynamic" onestrue, and the value changes when the archive configuration is reloaded) cannot useHistorize()at all and has to fall back to namespace registration plus hand-set access bits. The sample tests assert both values.Proposal: add a
historizing:parameter (or aFunc<bool>/ leave the attribute untouched when the provider'sIsHistorizingAsyncis authoritative) so thatHistorize()sets the HistoryRead/HistoryWrite access bits and registers the node without overriding theHistorizingattribute.Optional, same area
OnMonitoredItemCreated/OnMonitoredItemDeletedAsynctimer management.docs/HistoricalAccess.mdshould state which of these the fluent surface covers once they land; today the sample's overrides are the only documentation of the gaps.Related: #4397 (on-demand nodes), #4398 (monitored-item lifecycle hooks).