perf(adminui): pooled DbContext factory; role-grants reads per-call, not per-circuit
This commit is contained in:
+47
-14
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
@@ -8,21 +9,9 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Services;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class LdapGroupRoleMappingServiceTests : IDisposable
|
||||
public sealed class LdapGroupRoleMappingServiceTests
|
||||
{
|
||||
private readonly OtOpcUaConfigDbContext _db;
|
||||
|
||||
/// <summary>Initializes a new instance of the LdapGroupRoleMappingServiceTests class.</summary>
|
||||
public LdapGroupRoleMappingServiceTests()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
|
||||
.UseInMemoryDatabase($"ldap-grm-{Guid.NewGuid():N}")
|
||||
.Options;
|
||||
_db = new OtOpcUaConfigDbContext(options);
|
||||
}
|
||||
|
||||
/// <summary>Disposes the database context.</summary>
|
||||
public void Dispose() => _db.Dispose();
|
||||
private readonly RecordingContextFactory _db = new();
|
||||
|
||||
private LdapGroupRoleMapping Make(string group, AdminRole role, string? clusterId = null, bool? isSystemWide = null) =>
|
||||
new()
|
||||
@@ -165,4 +154,48 @@ public sealed class LdapGroupRoleMappingServiceTests : IDisposable
|
||||
var byGroup = await svc.GetByGroupsAsync(["cn=sysadmins,dc=x"], CancellationToken.None);
|
||||
byGroup.ShouldContain(r => r.Id == saved.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The service must take its context from the factory PER CALL and dispose it before returning —
|
||||
/// it must never pin one context (and therefore one pooled connection) for its own lifetime. That
|
||||
/// is what makes it safe on the RoleGrants Blazor page, where the DI scope is the circuit and two
|
||||
/// renders can overlap: a pinned context would throw
|
||||
/// "A second operation was started on this context instance" the first time they did.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Every_call_creates_and_disposes_its_own_context()
|
||||
{
|
||||
var svc = new LdapGroupRoleMappingService(_db);
|
||||
|
||||
await svc.CreateAsync(Make("cn=fleet,dc=x", AdminRole.Administrator), CancellationToken.None);
|
||||
await svc.ListAllAsync(CancellationToken.None);
|
||||
await svc.GetByGroupsAsync(["cn=fleet,dc=x"], CancellationToken.None);
|
||||
|
||||
// One context per call, and no leftovers: a service holding its own context would show 0.
|
||||
_db.Created.Count.ShouldBe(3);
|
||||
foreach (var ctx in _db.Created)
|
||||
Should.Throw<ObjectDisposedException>(() => ctx.LdapGroupRoleMappings.ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hands out a fresh <see cref="OtOpcUaConfigDbContext"/> per call over one shared in-memory store
|
||||
/// (an explicit <see cref="InMemoryDatabaseRoot"/>, so every context genuinely sees the same rows),
|
||||
/// and records what it created so a test can assert the per-call create/dispose discipline.
|
||||
/// </summary>
|
||||
private sealed class RecordingContextFactory : IDbContextFactory<OtOpcUaConfigDbContext>
|
||||
{
|
||||
private readonly DbContextOptions<OtOpcUaConfigDbContext> _options =
|
||||
new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
|
||||
.UseInMemoryDatabase($"ldap-grm-{Guid.NewGuid():N}", new InMemoryDatabaseRoot())
|
||||
.Options;
|
||||
|
||||
public List<OtOpcUaConfigDbContext> Created { get; } = [];
|
||||
|
||||
public OtOpcUaConfigDbContext CreateDbContext()
|
||||
{
|
||||
var ctx = new OtOpcUaConfigDbContext(_options);
|
||||
Created.Add(ctx);
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user