From c6e4c7820ad0cabcfb0537a5ee49a11a78c074f6 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 22 Oct 2026 08:32:45 -0400 Subject: [PATCH] fix(configdb): InstanceLocator single untracked projected join (review 2026-10-21 08/P7) --- .../Services/InstanceLocator.cs | 18 ++++++++---------- .../RepositoryCoverageTests.cs | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Services/InstanceLocator.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Services/InstanceLocator.cs index 18b1eaea..ed0dbcf6 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Services/InstanceLocator.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Services/InstanceLocator.cs @@ -22,15 +22,13 @@ public class InstanceLocator : IInstanceLocator string instanceUniqueName, CancellationToken cancellationToken = default) { - var instance = await _context.Set() - .FirstOrDefaultAsync(i => i.UniqueName == instanceUniqueName, cancellationToken); - - if (instance == null) - return null; - - var site = await _context.Set() - .FindAsync(new object[] { instance.SiteId }, cancellationToken); - - return site?.SiteIdentifier; + // One untracked projected join: this sits on the inbound-API Route.To hot path and + // only needs a single string, so it must not materialise (and track) two entities. + return await ( + from i in _context.Set().AsNoTracking() + join s in _context.Set().AsNoTracking() on i.SiteId equals s.Id + where i.UniqueName == instanceUniqueName + select s.SiteIdentifier) + .FirstOrDefaultAsync(cancellationToken); } } diff --git a/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/RepositoryCoverageTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/RepositoryCoverageTests.cs index 8efd568e..40915e3c 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/RepositoryCoverageTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/RepositoryCoverageTests.cs @@ -1486,6 +1486,24 @@ public class InstanceLocatorTests : IDisposable Assert.Equal("SITE-001", result); } + /// The lookup must not track entities (single untracked projection, review 08/P7). + /// A task that represents the asynchronous operation. + [Fact] + public async Task GetSiteIdForInstance_DoesNotTrackEntities() + { + var site = new Site("Site1", "SITE-001"); + var template = new Template("T1"); + _context.Sites.Add(site); + _context.Templates.Add(template); + await _context.SaveChangesAsync(); + _context.Instances.Add(new Instance("Pump1") { SiteId = site.Id, TemplateId = template.Id }); + await _context.SaveChangesAsync(); + _context.ChangeTracker.Clear(); + + Assert.Equal("SITE-001", await _locator.GetSiteIdForInstanceAsync("Pump1")); + Assert.Empty(_context.ChangeTracker.Entries()); + } + /// Verifies that getting the site id for a missing instance returns null. /// A task that represents the asynchronous operation. [Fact]