diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ICentralUiRepository.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ICentralUiRepository.cs
index 5c73de68..3d1d0c27 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ICentralUiRepository.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ICentralUiRepository.cs
@@ -32,6 +32,18 @@ public interface ICentralUiRepository
/// Cancellation token.
/// A task that resolves to a read-only list of matching instances.
Task> GetInstancesFilteredAsync(int? siteId = null, int? templateId = null, string? searchTerm = null, CancellationToken cancellationToken = default);
+ ///
+ /// Returns one page of instances with SQL-side filtering, ordered by unique name, plus the total
+ /// number of rows matching the filter (before paging).
+ ///
+ /// Optional site IDs to restrict to; null means all sites, an empty collection matches nothing.
+ /// Optional template ID to filter by.
+ /// Optional unique-name prefix (StartsWith).
+ /// Rows to skip.
+ /// Maximum rows to return.
+ /// Cancellation token.
+ /// A task that resolves to the page of rows and the total matching count.
+ Task<(IReadOnlyList Rows, int Total)> GetInstancesPagedAsync(IReadOnlyCollection? siteIds, int? templateId, string? search, int skip, int take, CancellationToken cancellationToken = default);
/// Returns the most recent deployment records up to the specified count.
/// Maximum number of records to return.
/// Cancellation token.
diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/CentralUiRepository.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/CentralUiRepository.cs
index 02ddd0ed..2ddff48c 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/CentralUiRepository.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/CentralUiRepository.cs
@@ -86,6 +86,38 @@ public class CentralUiRepository : ICentralUiRepository
.ToListAsync(cancellationToken);
}
+ ///
+ public async Task<(IReadOnlyList Rows, int Total)> GetInstancesPagedAsync(
+ IReadOnlyCollection? siteIds,
+ int? templateId,
+ string? search,
+ int skip,
+ int take,
+ CancellationToken cancellationToken = default)
+ {
+ var query = _context.Instances.AsNoTracking().AsQueryable();
+
+ if (siteIds is not null)
+ {
+ var ids = siteIds.ToList();
+ query = query.Where(i => ids.Contains(i.SiteId));
+ }
+
+ if (templateId.HasValue)
+ query = query.Where(i => i.TemplateId == templateId.Value);
+
+ if (!string.IsNullOrWhiteSpace(search))
+ query = query.Where(i => i.UniqueName.StartsWith(search));
+
+ var total = await query.CountAsync(cancellationToken);
+ var rows = await query
+ .OrderBy(i => i.UniqueName)
+ .Skip(Math.Max(0, skip))
+ .Take(Math.Max(0, take))
+ .ToListAsync(cancellationToken);
+ return (rows, total);
+ }
+
///
public async Task> GetRecentDeploymentsAsync(int count, CancellationToken cancellationToken = default)
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/RepositoryTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/RepositoryTests.cs
index 9939f08c..44888f79 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/RepositoryTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/RepositoryTests.cs
@@ -280,6 +280,38 @@ public class CentralUiRepositoryTests : IDisposable
Assert.Single(instances);
}
+ /// Verifies paged instances filter in SQL, order by name, page and report the total.
+ /// A task that represents the asynchronous operation.
+ [Fact]
+ public async Task GetInstancesPaged_FiltersOrdersAndPages()
+ {
+ var site1 = new Site("Site1", "S-001");
+ var site2 = new Site("Site2", "S-002");
+ var template = new Template("T1");
+ _context.Sites.AddRange(site1, site2);
+ _context.Templates.Add(template);
+ await _context.SaveChangesAsync();
+
+ _context.Instances.AddRange(
+ new Instance("Pump3") { SiteId = site1.Id, TemplateId = template.Id },
+ new Instance("Pump1") { SiteId = site1.Id, TemplateId = template.Id },
+ new Instance("Pump2") { SiteId = site2.Id, TemplateId = template.Id },
+ new Instance("XPump") { SiteId = site1.Id, TemplateId = template.Id });
+ await _context.SaveChangesAsync();
+
+ var (rows, total) = await _repository.GetInstancesPagedAsync(null, null, "Pump", 1, 2);
+ Assert.Equal(3, total);
+ Assert.Equal(new[] { "Pump2", "Pump3" }, rows.Select(r => r.UniqueName));
+
+ var (scoped, scopedTotal) = await _repository.GetInstancesPagedAsync(new[] { site1.Id }, template.Id, "Pump", 0, 10);
+ Assert.Equal(2, scopedTotal);
+ Assert.Equal(new[] { "Pump1", "Pump3" }, scoped.Select(r => r.UniqueName));
+
+ var (none, noneTotal) = await _repository.GetInstancesPagedAsync(Array.Empty(), null, null, 0, 10);
+ Assert.Empty(none);
+ Assert.Equal(0, noneTotal);
+ }
+
/// Verifies that recent deployments are returned in reverse chronological order.
/// A task that represents the asynchronous operation.
[Fact]