perf(adminui): pooled DbContext factory; role-grants reads per-call, not per-circuit

This commit is contained in:
Joseph Doherty
2026-08-16 04:05:34 -04:00
parent 34f836e03c
commit 140bf0c131
11 changed files with 346 additions and 50 deletions
@@ -8,7 +8,18 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Services;
/// "exactly one of (ClusterId, IsSystemWide)" invariant at the write surface so a
/// malformed row can't land in the DB.
/// </summary>
public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILdapGroupRoleMappingService
/// <remarks>
/// Takes the <see cref="IDbContextFactory{TContext}"/> and creates + disposes one context PER CALL
/// rather than holding an injected context for its lifetime. Its consumers are the RoleGrants Blazor
/// page (InteractiveServer, so its DI scope is the whole circuit) and <c>AuthEndpoints.LoginAsync</c>;
/// an injected context would pin one context — and one pooled connection — for the life of a circuit,
/// and would fault outright ("a second operation was started on this context instance") the moment two
/// of that page's renders overlapped. The factory is pooled (see
/// <c>ServiceCollectionExtensions.AddOtOpcUaConfigDb</c>), so the per-call create is a pool rent, not
/// a fresh context build.
/// </remarks>
public sealed class LdapGroupRoleMappingService(IDbContextFactory<OtOpcUaConfigDbContext> dbFactory)
: ILdapGroupRoleMappingService
{
/// <inheritdoc />
public async Task<IReadOnlyList<LdapGroupRoleMapping>> GetByGroupsAsync(
@@ -18,6 +29,7 @@ public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILd
var groupSet = ldapGroups.ToList();
if (groupSet.Count == 0) return [];
await using var db = await dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
return await db.LdapGroupRoleMappings
.AsNoTracking()
.Where(m => groupSet.Contains(m.LdapGroup))
@@ -27,12 +39,15 @@ public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILd
/// <inheritdoc />
public async Task<IReadOnlyList<LdapGroupRoleMapping>> ListAllAsync(CancellationToken cancellationToken)
=> await db.LdapGroupRoleMappings
{
await using var db = await dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
return await db.LdapGroupRoleMappings
.AsNoTracking()
.OrderBy(m => m.LdapGroup)
.ThenBy(m => m.ClusterId)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<LdapGroupRoleMapping> CreateAsync(LdapGroupRoleMapping row, CancellationToken cancellationToken)
@@ -43,6 +58,7 @@ public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILd
if (row.Id == Guid.Empty) row.Id = Guid.NewGuid();
if (row.CreatedAtUtc == default) row.CreatedAtUtc = DateTime.UtcNow;
await using var db = await dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
db.LdapGroupRoleMappings.Add(row);
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return row;
@@ -51,6 +67,7 @@ public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILd
/// <inheritdoc />
public async Task DeleteAsync(Guid id, CancellationToken cancellationToken)
{
await using var db = await dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
var existing = await db.LdapGroupRoleMappings.FindAsync([id], cancellationToken).ConfigureAwait(false);
if (existing is null) return;
db.LdapGroupRoleMappings.Remove(existing);