using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
public class TemplateEngineRepository : ITemplateEngineRepository
{
private readonly ScadaBridgeDbContext _context;
private readonly ITemplateGraphWatermark _watermark;
///
/// Initializes a new instance of the TemplateEngineRepository class.
///
/// The database context used to access template and instance data.
///
/// Process-wide template/instance version watermark. This repository is the
/// single unit-of-work through which EVERY template-graph mutation commits —
/// TemplateService, the ManagementActor native-alarm-source
/// handlers, and the Transport bundle importer all funnel through the same
/// — so bumping here (rather than in each
/// caller) is the only placement that cannot be bypassed.
///
public TemplateEngineRepository(ScadaBridgeDbContext context, ITemplateGraphWatermark watermark)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_watermark = watermark ?? throw new ArgumentNullException(nameof(watermark));
}
// Template
///
public async Task GetTemplateByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await _context.Templates
.Include(t => t.Attributes)
.Include(t => t.Alarms)
.Include(t => t.Scripts)
.Include(t => t.Compositions)
.Include(t => t.NativeAlarmSources)
.AsSplitQuery()
.FirstOrDefaultAsync(t => t.Id == id, cancellationToken);
}
///
public async Task GetTemplateWithChildrenAsync(int id, CancellationToken cancellationToken = default)
{
return await GetTemplateByIdAsync(id, cancellationToken);
}
///
public async Task> GetTemplatesWithChildrenAsync(
IEnumerable names, CancellationToken cancellationToken = default)
{
// Bulk lookup replaces the per-name N+1 in
// BundleImporter.PreviewAsync. Filter out null / empty / duplicate
// names before the query so EF emits a clean, deduplicated IN clause.
if (names is null) return Array.Empty();
var distinct = names
.Where(n => !string.IsNullOrEmpty(n))
.Distinct(StringComparer.Ordinal)
.ToArray();
if (distinct.Length == 0) return Array.Empty();
return await _context.Templates
.Where(t => distinct.Contains(t.Name))
.Include(t => t.Attributes)
.Include(t => t.Alarms)
.Include(t => t.Scripts)
.Include(t => t.Compositions)
.Include(t => t.NativeAlarmSources)
.AsSplitQuery()
.ToListAsync(cancellationToken);
}
///
public async Task> GetAllTemplatesAsync(CancellationToken cancellationToken = default)
{
return await _context.Templates
.Include(t => t.Attributes)
.Include(t => t.Alarms)
.Include(t => t.Scripts)
.Include(t => t.Compositions)
.Include(t => t.NativeAlarmSources)
.AsSplitQuery()
.ToListAsync(cancellationToken);
}
///
///
/// Two deliberate departures from :
///
/// -
/// AsNoTracking — the analysis walks (cycle detection, collision
/// detection, canonical-name resolution) never write through the loaded
/// graph, so paying for a change-tracker snapshot of every attribute,
/// alarm, script and composition in the database is pure waste.
///
/// -
/// TemplateScript.Code is projected away (left empty). Script
/// bodies are by far the largest column in the graph and NONE of the
/// analysis consumers read them — CycleDetector reads only ids and
/// edges, CollisionDetector and TemplateResolver read only
/// member names and lock flags.
///
///
/// Callers that DO need bodies or tracked entities — the inheritance
/// reconciler and the flattener — must keep using
/// / .
///
public async Task> GetAllTemplatesForAnalysisAsync(CancellationToken cancellationToken = default)
{
return await _context.Templates
.AsNoTracking()
.Select(t => new Template(t.Name)
{
Id = t.Id,
Description = t.Description,
ParentTemplateId = t.ParentTemplateId,
FolderId = t.FolderId,
IsDerived = t.IsDerived,
OwnerCompositionId = t.OwnerCompositionId,
Attributes = t.Attributes.ToList(),
Alarms = t.Alarms.ToList(),
NativeAlarmSources = t.NativeAlarmSources.ToList(),
Compositions = t.Compositions.ToList(),
Scripts = t.Scripts
.Select(s => new TemplateScript(s.Name, string.Empty)
{
Id = s.Id,
TemplateId = s.TemplateId,
IsLocked = s.IsLocked,
TriggerType = s.TriggerType,
TriggerConfiguration = s.TriggerConfiguration,
ParameterDefinitions = s.ParameterDefinitions,
ReturnDefinition = s.ReturnDefinition,
MinTimeBetweenRuns = s.MinTimeBetweenRuns,
ExecutionTimeoutSeconds = s.ExecutionTimeoutSeconds,
IsInherited = s.IsInherited,
LockedInDerived = s.LockedInDerived
})
.ToList()
})
.ToListAsync(cancellationToken);
}
///
public async Task> GetTemplateSummariesAsync(
int skip,
int? take,
CancellationToken cancellationToken = default)
{
var query = _context.Templates
.AsNoTracking()
.OrderBy(t => t.Id)
.Skip(Math.Max(0, skip));
if (take is > 0)
query = query.Take(Math.Min(take.Value, TemplateSummary.MaxPageSize));
return await query
.Select(t => new TemplateSummary(
t.Id,
t.Name,
t.Description,
t.ParentTemplateId,
t.FolderId,
t.IsDerived,
t.OwnerCompositionId,
t.Attributes.Count,
t.Alarms.Count,
t.Scripts.Count,
t.Compositions.Count,
t.NativeAlarmSources.Count))
.ToListAsync(cancellationToken);
}
///
public async Task> GetTemplatesComposingAsync(int composedTemplateId, CancellationToken cancellationToken = default)
{
return await _context.Templates
.Where(t => t.Compositions.Any(c => c.ComposedTemplateId == composedTemplateId))
.Include(t => t.Attributes)
.Include(t => t.Scripts)
.Include(t => t.Compositions)
.AsSplitQuery()
.ToListAsync(cancellationToken);
}
///
public async Task AddTemplateAsync(Template template, CancellationToken cancellationToken = default)
{
await _context.Templates.AddAsync(template, cancellationToken);
}
///
public Task UpdateTemplateAsync(Template template, CancellationToken cancellationToken = default)
{
_context.Templates.Update(template);
return Task.CompletedTask;
}
///
public async Task DeleteTemplateAsync(int id, CancellationToken cancellationToken = default)
{
var template = await _context.Templates.FindAsync(new object[] { id }, cancellationToken);
if (template != null)
{
_context.Templates.Remove(template);
}
}
// TemplateAttribute
///
public async Task GetTemplateAttributeByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await _context.TemplateAttributes.FindAsync(new object[] { id }, cancellationToken);
}
///
public async Task> GetAttributesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default)
{
return await _context.TemplateAttributes
.Where(a => a.TemplateId == templateId)
.ToListAsync(cancellationToken);
}
///
public async Task AddTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default)
{
await _context.TemplateAttributes.AddAsync(attribute, cancellationToken);
}
///
public Task UpdateTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default)
{
_context.TemplateAttributes.Update(attribute);
return Task.CompletedTask;
}
///
public async Task DeleteTemplateAttributeAsync(int id, CancellationToken cancellationToken = default)
{
var attribute = await _context.TemplateAttributes.FindAsync(new object[] { id }, cancellationToken);
if (attribute != null)
{
_context.TemplateAttributes.Remove(attribute);
}
}
// TemplateAlarm
///
public async Task GetTemplateAlarmByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await _context.TemplateAlarms.FindAsync(new object[] { id }, cancellationToken);
}
///
public async Task> GetAlarmsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default)
{
return await _context.TemplateAlarms
.Where(a => a.TemplateId == templateId)
.ToListAsync(cancellationToken);
}
///
public async Task AddTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default)
{
await _context.TemplateAlarms.AddAsync(alarm, cancellationToken);
}
///
public Task UpdateTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default)
{
_context.TemplateAlarms.Update(alarm);
return Task.CompletedTask;
}
///
public async Task DeleteTemplateAlarmAsync(int id, CancellationToken cancellationToken = default)
{
var alarm = await _context.TemplateAlarms.FindAsync(new object[] { id }, cancellationToken);
if (alarm != null)
{
_context.TemplateAlarms.Remove(alarm);
}
}
// TemplateNativeAlarmSource
///
public async Task GetTemplateNativeAlarmSourceByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await _context.TemplateNativeAlarmSources.FindAsync(new object[] { id }, cancellationToken);
}
///
public async Task> GetNativeAlarmSourcesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default)
{
return await _context.TemplateNativeAlarmSources
.Where(s => s.TemplateId == templateId)
.ToListAsync(cancellationToken);
}
///
public async Task AddTemplateNativeAlarmSourceAsync(TemplateNativeAlarmSource source, CancellationToken cancellationToken = default)
{
await _context.TemplateNativeAlarmSources.AddAsync(source, cancellationToken);
}
///
public Task UpdateTemplateNativeAlarmSourceAsync(TemplateNativeAlarmSource source, CancellationToken cancellationToken = default)
{
_context.TemplateNativeAlarmSources.Update(source);
return Task.CompletedTask;
}
///
public async Task DeleteTemplateNativeAlarmSourceAsync(int id, CancellationToken cancellationToken = default)
{
var source = await _context.TemplateNativeAlarmSources.FindAsync(new object[] { id }, cancellationToken);
if (source != null)
{
_context.TemplateNativeAlarmSources.Remove(source);
}
}
// TemplateScript
///
public async Task GetTemplateScriptByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await _context.TemplateScripts.FindAsync(new object[] { id }, cancellationToken);
}
///
public async Task> GetScriptsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default)
{
return await _context.TemplateScripts
.Where(s => s.TemplateId == templateId)
.ToListAsync(cancellationToken);
}
///
public async Task AddTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default)
{
await _context.TemplateScripts.AddAsync(script, cancellationToken);
}
///
public Task UpdateTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default)
{
_context.TemplateScripts.Update(script);
return Task.CompletedTask;
}
///
public async Task DeleteTemplateScriptAsync(int id, CancellationToken cancellationToken = default)
{
var script = await _context.TemplateScripts.FindAsync(new object[] { id }, cancellationToken);
if (script != null)
{
_context.TemplateScripts.Remove(script);
}
}
// TemplateComposition
///
public async Task GetTemplateCompositionByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await _context.TemplateCompositions.FindAsync(new object[] { id }, cancellationToken);
}
///
public async Task> GetCompositionsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default)
{
return await _context.TemplateCompositions
.Where(c => c.TemplateId == templateId)
.ToListAsync(cancellationToken);
}
///
public async Task AddTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default)
{
await _context.TemplateCompositions.AddAsync(composition, cancellationToken);
}
///
public Task UpdateTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default)
{
_context.TemplateCompositions.Update(composition);
return Task.CompletedTask;
}
///
public async Task DeleteTemplateCompositionAsync(int id, CancellationToken cancellationToken = default)
{
var composition = await _context.TemplateCompositions.FindAsync(new object[] { id }, cancellationToken);
if (composition != null)
{
_context.TemplateCompositions.Remove(composition);
}
}
// Instance
///
public async Task GetInstanceByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await _context.Instances
.Include(i => i.AttributeOverrides)
.Include(i => i.AlarmOverrides)
.Include(i => i.ConnectionBindings)
.Include(i => i.NativeAlarmSourceOverrides)
.AsSplitQuery()
.FirstOrDefaultAsync(i => i.Id == id, cancellationToken);
}
///
public async Task> GetAllInstancesAsync(CancellationToken cancellationToken = default)
{
return await _context.Instances
.Include(i => i.AttributeOverrides)
.Include(i => i.AlarmOverrides)
.Include(i => i.ConnectionBindings)
.Include(i => i.NativeAlarmSourceOverrides)
.AsSplitQuery()
.ToListAsync(cancellationToken);
}
///
public async Task> GetInstancesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default)
{
return await _context.Instances
.Where(i => i.TemplateId == templateId)
.ToListAsync(cancellationToken);
}
///
public async Task> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
{
return await _context.Instances
.Where(i => i.SiteId == siteId)
.Include(i => i.AttributeOverrides)
.Include(i => i.AlarmOverrides)
.Include(i => i.ConnectionBindings)
.Include(i => i.NativeAlarmSourceOverrides)
.AsSplitQuery()
.ToListAsync(cancellationToken);
}
///
public async Task GetInstanceByUniqueNameAsync(string uniqueName, CancellationToken cancellationToken = default)
{
return await _context.Instances
.Include(i => i.AttributeOverrides)
.Include(i => i.AlarmOverrides)
.Include(i => i.ConnectionBindings)
.Include(i => i.NativeAlarmSourceOverrides)
.AsSplitQuery()
.FirstOrDefaultAsync(i => i.UniqueName == uniqueName, cancellationToken);
}
///
public async Task AddInstanceAsync(Instance instance, CancellationToken cancellationToken = default)
{
await _context.Instances.AddAsync(instance, cancellationToken);
}
///
public Task UpdateInstanceAsync(Instance instance, CancellationToken cancellationToken = default)
{
_context.Instances.Update(instance);
return Task.CompletedTask;
}
///
public async Task DeleteInstanceAsync(int id, CancellationToken cancellationToken = default)
{
var instance = await _context.Instances.FindAsync(new object[] { id }, cancellationToken);
if (instance != null)
{
_context.Instances.Remove(instance);
}
}
// InstanceAttributeOverride
///
public async Task> GetOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default)
{
return await _context.InstanceAttributeOverrides
.Where(o => o.InstanceId == instanceId)
.ToListAsync(cancellationToken);
}
///
public async Task AddInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default)
{
await _context.InstanceAttributeOverrides.AddAsync(attributeOverride, cancellationToken);
}
///
public Task UpdateInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default)
{
_context.InstanceAttributeOverrides.Update(attributeOverride);
return Task.CompletedTask;
}
///
public async Task DeleteInstanceAttributeOverrideAsync(int id, CancellationToken cancellationToken = default)
{
var attributeOverride = await _context.InstanceAttributeOverrides.FindAsync(new object[] { id }, cancellationToken);
if (attributeOverride != null)
{
_context.InstanceAttributeOverrides.Remove(attributeOverride);
}
}
// InstanceAlarmOverride
///
public async Task> GetAlarmOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default)
{
return await _context.InstanceAlarmOverrides
.Where(o => o.InstanceId == instanceId)
.ToListAsync(cancellationToken);
}
///
public async Task GetAlarmOverrideAsync(int instanceId, string alarmCanonicalName, CancellationToken cancellationToken = default)
{
return await _context.InstanceAlarmOverrides
.FirstOrDefaultAsync(
o => o.InstanceId == instanceId && o.AlarmCanonicalName == alarmCanonicalName,
cancellationToken);
}
///
public async Task AddInstanceAlarmOverrideAsync(InstanceAlarmOverride alarmOverride, CancellationToken cancellationToken = default)
{
await _context.InstanceAlarmOverrides.AddAsync(alarmOverride, cancellationToken);
}
///
public Task UpdateInstanceAlarmOverrideAsync(InstanceAlarmOverride alarmOverride, CancellationToken cancellationToken = default)
{
_context.InstanceAlarmOverrides.Update(alarmOverride);
return Task.CompletedTask;
}
///
public async Task DeleteInstanceAlarmOverrideAsync(int id, CancellationToken cancellationToken = default)
{
var alarmOverride = await _context.InstanceAlarmOverrides.FindAsync(new object[] { id }, cancellationToken);
if (alarmOverride != null)
{
_context.InstanceAlarmOverrides.Remove(alarmOverride);
}
}
// InstanceNativeAlarmSourceOverride
///
public async Task> GetNativeAlarmSourceOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default)
{
return await _context.InstanceNativeAlarmSourceOverrides
.Where(o => o.InstanceId == instanceId)
.ToListAsync(cancellationToken);
}
///
public async Task GetNativeAlarmSourceOverrideAsync(int instanceId, string sourceCanonicalName, CancellationToken cancellationToken = default)
{
return await _context.InstanceNativeAlarmSourceOverrides
.FirstOrDefaultAsync(o => o.InstanceId == instanceId && o.SourceCanonicalName == sourceCanonicalName, cancellationToken);
}
///
public async Task AddInstanceNativeAlarmSourceOverrideAsync(InstanceNativeAlarmSourceOverride ovr, CancellationToken cancellationToken = default)
{
await _context.InstanceNativeAlarmSourceOverrides.AddAsync(ovr, cancellationToken);
}
///
public Task UpdateInstanceNativeAlarmSourceOverrideAsync(InstanceNativeAlarmSourceOverride ovr, CancellationToken cancellationToken = default)
{
_context.InstanceNativeAlarmSourceOverrides.Update(ovr);
return Task.CompletedTask;
}
///
public async Task DeleteInstanceNativeAlarmSourceOverrideAsync(int id, CancellationToken cancellationToken = default)
{
var ovr = await _context.InstanceNativeAlarmSourceOverrides.FindAsync(new object[] { id }, cancellationToken);
if (ovr != null)
{
_context.InstanceNativeAlarmSourceOverrides.Remove(ovr);
}
}
// InstanceConnectionBinding
///
public async Task> GetBindingsByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default)
{
return await _context.InstanceConnectionBindings
.Where(b => b.InstanceId == instanceId)
.ToListAsync(cancellationToken);
}
///
public async Task AddInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default)
{
await _context.InstanceConnectionBindings.AddAsync(binding, cancellationToken);
}
///
public Task UpdateInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default)
{
_context.InstanceConnectionBindings.Update(binding);
return Task.CompletedTask;
}
///
public async Task DeleteInstanceConnectionBindingAsync(int id, CancellationToken cancellationToken = default)
{
var binding = await _context.InstanceConnectionBindings.FindAsync(new object[] { id }, cancellationToken);
if (binding != null)
{
_context.InstanceConnectionBindings.Remove(binding);
}
}
// Area
///
public async Task GetAreaByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await _context.Areas
.Include(a => a.Children)
.FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
}
///
public async Task> GetAreasBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
{
return await _context.Areas
.Where(a => a.SiteId == siteId)
.Include(a => a.Children)
.ToListAsync(cancellationToken);
}
///
public async Task AddAreaAsync(Area area, CancellationToken cancellationToken = default)
{
await _context.Areas.AddAsync(area, cancellationToken);
}
///
public Task UpdateAreaAsync(Area area, CancellationToken cancellationToken = default)
{
_context.Areas.Update(area);
return Task.CompletedTask;
}
///
public async Task DeleteAreaAsync(int id, CancellationToken cancellationToken = default)
{
var area = await _context.Areas.FindAsync(new object[] { id }, cancellationToken);
if (area != null)
{
_context.Areas.Remove(area);
}
}
// SharedScript
///
public async Task GetSharedScriptByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await _context.SharedScripts.FindAsync(new object[] { id }, cancellationToken);
}
///
public async Task GetSharedScriptByNameAsync(string name, CancellationToken cancellationToken = default)
{
return await _context.SharedScripts
.FirstOrDefaultAsync(s => s.Name == name, cancellationToken);
}
///
public async Task> GetAllSharedScriptsAsync(CancellationToken cancellationToken = default)
{
return await _context.SharedScripts.ToListAsync(cancellationToken);
}
///
public async Task AddSharedScriptAsync(SharedScript sharedScript, CancellationToken cancellationToken = default)
{
await _context.SharedScripts.AddAsync(sharedScript, cancellationToken);
}
///
public Task UpdateSharedScriptAsync(SharedScript sharedScript, CancellationToken cancellationToken = default)
{
_context.SharedScripts.Update(sharedScript);
return Task.CompletedTask;
}
///
public async Task DeleteSharedScriptAsync(int id, CancellationToken cancellationToken = default)
{
var sharedScript = await _context.SharedScripts.FindAsync(new object[] { id }, cancellationToken);
if (sharedScript != null)
{
_context.SharedScripts.Remove(sharedScript);
}
}
// TemplateFolder
///
public async Task GetFolderByIdAsync(int id, CancellationToken cancellationToken = default)
=> await _context.TemplateFolders.FindAsync(new object[] { id }, cancellationToken);
///
public async Task> GetAllFoldersAsync(CancellationToken cancellationToken = default)
=> await _context.TemplateFolders
.OrderBy(f => f.SortOrder)
.ThenBy(f => f.Name)
.ToListAsync(cancellationToken);
///
public async Task AddFolderAsync(TemplateFolder folder, CancellationToken cancellationToken = default)
=> await _context.TemplateFolders.AddAsync(folder, cancellationToken);
///
public Task UpdateFolderAsync(TemplateFolder folder, CancellationToken cancellationToken = default)
{
_context.TemplateFolders.Update(folder);
return Task.CompletedTask;
}
///
public async Task DeleteFolderAsync(int id, CancellationToken cancellationToken = default)
{
var folder = await _context.TemplateFolders.FindAsync(new object[] { id }, cancellationToken);
if (folder != null)
{
_context.TemplateFolders.Remove(folder);
}
}
///
///
/// Inspects the change tracker BEFORE committing (afterwards every entry is
/// and the attribution is lost) and bumps
/// the for each affected template /
/// instance. The bump is applied only AFTER a successful commit, so a failed
/// save does not invalidate caches for a change that never landed.
///
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
var pending = CollectWatermarkBumps();
var written = await _context.SaveChangesAsync(cancellationToken);
ApplyWatermarkBumps(pending);
return written;
}
///
/// Snapshot of the watermark bumps implied by the current change-tracker
/// contents. Captured pre-commit, applied post-commit.
///
private readonly record struct WatermarkBumps(
HashSet Templates,
HashSet StructuralTemplates,
HashSet Instances,
bool Unattributed);
///
/// Walks the change tracker and maps every added/modified/deleted
/// template-graph or instance-graph entry back to the template (or instance)
/// whose flattened output it can affect.
///
///
/// A change is treated as STRUCTURAL — invalidating cached chain membership,
/// not just chain contents — when it adds or removes a
/// row, changes a , or touches a
/// row. Those are exactly the edges the
/// flattener walks to build a chain.
///
///
///
/// An entry whose owning id cannot be resolved (a child row deleted before
/// its FK was materialised, or an entity type added later that this switch
/// does not know) sets Unattributed, which conservatively invalidates
/// every cached chain rather than silently letting a stale entry survive.
///
///
private WatermarkBumps CollectWatermarkBumps()
{
var templates = new HashSet();
var structural = new HashSet();
var instances = new HashSet();
var unattributed = false;
foreach (var entry in _context.ChangeTracker.Entries())
{
if (entry.State is not (EntityState.Added or EntityState.Modified or EntityState.Deleted))
continue;
switch (entry.Entity)
{
case Template t:
templates.Add(t.Id);
// Add/Delete changes chain membership; a re-parent changes it too.
if (entry.State != EntityState.Modified
|| entry.Property(nameof(Template.ParentTemplateId)).IsModified)
{
structural.Add(t.Id);
}
break;
case TemplateAttribute a:
templates.Add(a.TemplateId);
break;
case TemplateAlarm al:
templates.Add(al.TemplateId);
break;
case TemplateScript s:
templates.Add(s.TemplateId);
break;
case TemplateNativeAlarmSource ns:
templates.Add(ns.TemplateId);
break;
case TemplateComposition c:
templates.Add(c.TemplateId);
structural.Add(c.TemplateId);
break;
case Instance i:
instances.Add(i.Id);
break;
case InstanceAttributeOverride ao:
instances.Add(ao.InstanceId);
break;
case InstanceAlarmOverride alo:
instances.Add(alo.InstanceId);
break;
case InstanceNativeAlarmSourceOverride nso:
instances.Add(nso.InstanceId);
break;
case InstanceConnectionBinding cb:
instances.Add(cb.InstanceId);
break;
case SharedScript:
// Shared scripts are a session-global input to validation, not
// owned by any one template — invalidate everything.
unattributed = true;
break;
default:
// Anything this switch does not recognise is invalidated
// conservatively, as the doc comment above promises. The
// previous "areas, folders and anything else do not feed the
// flattener" no-op was wrong in at least one shipped case:
// DataConnection rows land here, and their Protocol /
// Primary+Backup configuration / FailoverRetryCount ARE hash
// inputs (FlatteningService packages them as ConnectionConfig,
// RevisionHashService hashes them as HashableConnection). A
// silent no-op on an unrecognised entity is the one failure
// mode this watermark must not have: it produces STALE work,
// whereas an over-broad bump only ever produces extra work.
unattributed = true;
break;
}
}
return new WatermarkBumps(templates, structural, instances, unattributed);
}
/// Applies a previously collected snapshot.
private void ApplyWatermarkBumps(WatermarkBumps bumps)
{
if (bumps.Unattributed)
_watermark.BumpAll();
foreach (var templateId in bumps.Templates)
_watermark.BumpTemplate(templateId, bumps.StructuralTemplates.Contains(templateId));
foreach (var instanceId in bumps.Instances)
_watermark.BumpInstance(instanceId);
}
}