using ZB.MOM.WW.OtOpcUa.Configuration.Validation; namespace ZB.MOM.WW.OtOpcUa.Configuration.Apply; public sealed class GenerationApplier(ApplyCallbacks callbacks) : IGenerationApplier { public async Task ApplyAsync(DraftSnapshot? from, DraftSnapshot to, CancellationToken ct) { var diff = GenerationDiffer.Compute(from, to); var errors = new List(); // Removed first, then Added/Modified — prevents FK dangling while cascades settle. await ApplyPass(diff.Tags, ChangeKind.Removed, callbacks.OnTag, errors, ct); await ApplyPass(diff.PollGroups, ChangeKind.Removed, callbacks.OnPollGroup, errors, ct); await ApplyPass(diff.Equipment, ChangeKind.Removed, callbacks.OnEquipment, errors, ct); await ApplyPass(diff.Devices, ChangeKind.Removed, callbacks.OnDevice, errors, ct); await ApplyPass(diff.Drivers, ChangeKind.Removed, callbacks.OnDriver, errors, ct); await ApplyPass(diff.Namespaces, ChangeKind.Removed, callbacks.OnNamespace, errors, ct); foreach (var kind in new[] { ChangeKind.Added, ChangeKind.Modified }) { // Honour cancellation between passes — a caller can abort the apply between Removed // and Added phases even if individual callbacks don't observe the token themselves // (Configuration-007). ct.ThrowIfCancellationRequested(); await ApplyPass(diff.Namespaces, kind, callbacks.OnNamespace, errors, ct); await ApplyPass(diff.Drivers, kind, callbacks.OnDriver, errors, ct); await ApplyPass(diff.Devices, kind, callbacks.OnDevice, errors, ct); await ApplyPass(diff.Equipment, kind, callbacks.OnEquipment, errors, ct); await ApplyPass(diff.PollGroups, kind, callbacks.OnPollGroup, errors, ct); await ApplyPass(diff.Tags, kind, callbacks.OnTag, errors, ct); } return errors.Count == 0 ? ApplyResult.Ok(diff) : ApplyResult.Fail(diff, errors); } private static async Task ApplyPass( IReadOnlyList> changes, ChangeKind kind, Func, CancellationToken, Task>? callback, List errors, CancellationToken ct) { if (callback is null) return; foreach (var change in changes.Where(c => c.Kind == kind)) { try { await callback(change, ct); } // Configuration-007: cancellation must propagate, not be silently recorded as an // entity error. Distinguish caller cancellation (token signalled) from any // OperationCanceledException raised independently of the caller's token, which we // still want to surface as an entity error so a single misbehaving callback does // not crash the entire apply. catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; } catch (Exception ex) { errors.Add($"{typeof(T).Name} {change.Kind} '{change.LogicalId}': {ex.Message}"); } } } }