From 486133e05e3f53ab816e2bd2d985a7c735fe3237 Mon Sep 17 00:00:00 2001 From: mnbuhl Date: Sat, 25 Apr 2026 17:59:58 +0200 Subject: [PATCH] refactor(distributed-locks): move extensions from DbContext to DatabaseFacade Renames DbContextDistributedLockExtensions to DatabaseFacadeDistributedLockExtensions, targeting DbContext.Database (DatabaseFacade) instead of DbContext directly. Updates README, samples, tests, and CLAUDE.md to reflect the new `ctx.Database.Acquire...` call site. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 2 +- README.md | 16 ++--- samples/InventoryApi/Program.cs | 4 +- samples/QueueProcessor/Program.cs | 2 +- samples/README.md | 2 +- ...atabaseFacadeDistributedLockExtensions.cs} | 69 +++++++++++-------- .../DistributedLockIntegrationTests.cs | 3 +- .../DistributedLockIntegrationTests.cs | 15 ++-- .../DistributedLockIntegrationTests.cs | 5 +- .../DistributedLockIntegrationTestsBase.cs | 27 ++++---- .../DistributedLockUnitTests.cs | 28 ++++---- 11 files changed, 90 insertions(+), 83 deletions(-) rename src/EntityFrameworkCore.Locking/Extensions/{DbContextDistributedLockExtensions.cs => DatabaseFacadeDistributedLockExtensions.cs} (70%) diff --git a/CLAUDE.md b/CLAUDE.md index 36d4e5b..cdf23e9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,7 +45,7 @@ The library is split into a provider-agnostic core and three database provider p ### Distributed advisory locks flow -1. `DbContextDistributedLockExtensions.AcquireDistributedLockAsync()` resolves `IAdvisoryLockProvider` from `ILockingProvider.AdvisoryLockProvider`, opens the connection if needed, calls `DistributedLockRegistry.RegisterOrThrow` to prevent double-acquisition on the same context+connection, then delegates to the provider. +1. `DatabaseFacadeDistributedLockExtensions.AcquireDistributedLockAsync()` (called as `ctx.Database.AcquireDistributedLockAsync(...)`) resolves `IAdvisoryLockProvider` from `ILockingProvider.AdvisoryLockProvider`, opens the connection if needed, calls `DistributedLockRegistry.RegisterOrThrow` to prevent double-acquisition on the same context+connection, then delegates to the provider. 2. Each provider sends native advisory lock SQL (`pg_advisory_lock`, MySQL `GET_LOCK`, SQL Server `sp_getapplock`). 3. The returned `IDistributedLockHandle` releases the lock on dispose via a captured callback; `DistributedLockCleanupInterceptor` performs best-effort cleanup if handles are not disposed before connection close. diff --git a/README.md b/README.md index 77163c1..94ec782 100644 --- a/README.md +++ b/README.md @@ -135,30 +135,30 @@ No transaction is required. ```csharp // Acquire — blocks until available (optional timeout) -await using var handle = await ctx.AcquireDistributedLockAsync("invoice:generate"); +await using var handle = await ctx.Database.AcquireDistributedLockAsync("invoice:generate"); // ... critical section ... // lock released automatically on dispose // With a timeout — throws LockTimeoutException if not acquired within 5 s -await using var handle = await ctx.AcquireDistributedLockAsync( +await using var handle = await ctx.Database.AcquireDistributedLockAsync( "report:daily", TimeSpan.FromSeconds(5)); // With cancellation token -await using var handle = await ctx.AcquireDistributedLockAsync( +await using var handle = await ctx.Database.AcquireDistributedLockAsync( "report:daily", timeout: null, cancellationToken: ct); // TryAcquire — returns null immediately if already held -var handle = await ctx.TryAcquireDistributedLockAsync("invoice:generate"); +var handle = await ctx.Database.TryAcquireDistributedLockAsync("invoice:generate"); if (handle is null) return Results.Conflict("Another process is generating the invoice."); await using (handle) { /* critical section */ } // Synchronous variants are also available -using var handle = ctx.AcquireDistributedLock("report:daily"); -var handle = ctx.TryAcquireDistributedLock("report:daily"); +using var handle = ctx.Database.AcquireDistributedLock("report:daily"); +var handle = ctx.Database.TryAcquireDistributedLock("report:daily"); // Check support at runtime -if (ctx.SupportsDistributedLocks()) { ... } +if (ctx.Database.SupportsDistributedLocks()) { ... } ``` ### Lock keys @@ -186,7 +186,7 @@ Keys are plain strings, up to **255 characters**. The library handles provider-s ```csharp try { - await using var handle = await ctx.AcquireDistributedLockAsync( + await using var handle = await ctx.Database.AcquireDistributedLockAsync( "report:daily", TimeSpan.FromSeconds(5)); } catch (LockTimeoutException) diff --git a/samples/InventoryApi/Program.cs b/samples/InventoryApi/Program.cs index 48a407e..03aaa9c 100644 --- a/samples/InventoryApi/Program.cs +++ b/samples/InventoryApi/Program.cs @@ -203,7 +203,7 @@ "/inventory/snapshot", async (InventoryDbContext db) => { - await using var handle = await db.TryAcquireDistributedLockAsync("inventory:snapshot"); + await using var handle = await db.Database.TryAcquireDistributedLockAsync("inventory:snapshot"); if (handle is null) return Results.Conflict(new { error = "A snapshot is already in progress." }); @@ -234,7 +234,7 @@ { try { - await using var handle = await db.AcquireDistributedLockAsync( + await using var handle = await db.Database.AcquireDistributedLockAsync( "products:price-sync", timeout: TimeSpan.FromSeconds(3) ); diff --git a/samples/QueueProcessor/Program.cs b/samples/QueueProcessor/Program.cs index 7dd9757..7900c84 100644 --- a/samples/QueueProcessor/Program.cs +++ b/samples/QueueProcessor/Program.cs @@ -42,7 +42,7 @@ Console.WriteLine("\nRunning maintenance sweep..."); await using (var db = new JobDbContext(optionsBuilder.Options)) { - await using var sweepHandle = await db.TryAcquireDistributedLockAsync("jobs:maintenance-sweep"); + await using var sweepHandle = await db.Database.TryAcquireDistributedLockAsync("jobs:maintenance-sweep"); if (sweepHandle is null) { Console.WriteLine("Another process is already running the sweep — skipping."); diff --git a/samples/README.md b/samples/README.md index 05569e6..f450f18 100644 --- a/samples/README.md +++ b/samples/README.md @@ -46,7 +46,7 @@ var job = await db.Jobs After all workers finish, a maintenance sweep requeues any jobs stuck in `Processing` for more than 5 minutes: ```csharp -await using var sweepHandle = await db.TryAcquireDistributedLockAsync("jobs:maintenance-sweep"); +await using var sweepHandle = await db.Database.TryAcquireDistributedLockAsync("jobs:maintenance-sweep"); if (sweepHandle is null) { Console.WriteLine("Another process is already running the sweep — skipping."); diff --git a/src/EntityFrameworkCore.Locking/Extensions/DbContextDistributedLockExtensions.cs b/src/EntityFrameworkCore.Locking/Extensions/DatabaseFacadeDistributedLockExtensions.cs similarity index 70% rename from src/EntityFrameworkCore.Locking/Extensions/DbContextDistributedLockExtensions.cs rename to src/EntityFrameworkCore.Locking/Extensions/DatabaseFacadeDistributedLockExtensions.cs index e73523e..2a95753 100644 --- a/src/EntityFrameworkCore.Locking/Extensions/DbContextDistributedLockExtensions.cs +++ b/src/EntityFrameworkCore.Locking/Extensions/DatabaseFacadeDistributedLockExtensions.cs @@ -5,31 +5,32 @@ using EntityFrameworkCore.Locking.Internal; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; namespace EntityFrameworkCore.Locking; /// -/// Extension methods on for acquiring distributed (advisory) locks. -/// No active transaction is required — locks are session-scoped. +/// Extension methods on (accessed via DbContext.Database) for +/// acquiring distributed (advisory) locks. No active transaction is required — locks are session-scoped. /// -public static class DbContextDistributedLockExtensions +public static class DatabaseFacadeDistributedLockExtensions { /// /// Acquires a distributed lock with the given key, blocking until it is available. /// - /// The DbContext whose connection will hold the lock. + /// The whose connection will hold the lock. /// Lock key (1–255 characters). /// Maximum time to wait. Throws if exceeded. Null = wait indefinitely. /// Cancellation token. Cancellation is best-effort (driver-dependent). public static async Task AcquireDistributedLockAsync( - this DbContext ctx, + this DatabaseFacade database, string key, TimeSpan? timeout = null, CancellationToken ct = default ) { - var (provider, connection, openedByMe) = await PrepareAsync(ctx, key, ct).ConfigureAwait(false); + var (ctx, provider, connection, openedByMe) = await PrepareAsync(database, key, ct).ConfigureAwait(false); try { DistributedLockRegistry.RegisterOrThrow(ctx, connection, key); @@ -56,12 +57,12 @@ public static async Task AcquireDistributedLockAsync( /// Returns null immediately if the lock is held by another connection. /// public static async Task TryAcquireDistributedLockAsync( - this DbContext ctx, + this DatabaseFacade database, string key, CancellationToken ct = default ) { - var (provider, connection, openedByMe) = await PrepareAsync(ctx, key, ct).ConfigureAwait(false); + var (ctx, provider, connection, openedByMe) = await PrepareAsync(database, key, ct).ConfigureAwait(false); try { DistributedLockRegistry.RegisterOrThrow(ctx, connection, key); @@ -93,12 +94,12 @@ public static async Task AcquireDistributedLockAsync( /// Acquires a distributed lock synchronously. public static IDistributedLockHandle AcquireDistributedLock( - this DbContext ctx, + this DatabaseFacade database, string key, TimeSpan? timeout = null ) { - var (provider, connection, openedByMe) = PrepareSync(ctx, key); + var (ctx, provider, connection, openedByMe) = PrepareSync(database, key); try { DistributedLockRegistry.RegisterOrThrow(ctx, connection, key); @@ -121,9 +122,9 @@ public static IDistributedLockHandle AcquireDistributedLock( } /// Attempts to acquire a distributed lock synchronously. Returns null if contested. - public static IDistributedLockHandle? TryAcquireDistributedLock(this DbContext ctx, string key) + public static IDistributedLockHandle? TryAcquireDistributedLock(this DatabaseFacade database, string key) { - var (provider, connection, openedByMe) = PrepareSync(ctx, key); + var (ctx, provider, connection, openedByMe) = PrepareSync(database, key); try { DistributedLockRegistry.RegisterOrThrow(ctx, connection, key); @@ -153,46 +154,51 @@ public static IDistributedLockHandle AcquireDistributedLock( } } - /// Returns true if the current DbContext's provider supports distributed locks. - public static bool SupportsDistributedLocks(this DbContext ctx) + /// Returns true if the configured EF Core provider supports distributed locks. + public static bool SupportsDistributedLocks(this DatabaseFacade database) { - var lp = ctx.GetInfrastructure().GetService(); + var lp = ((IInfrastructure)database).Instance.GetService(); return lp?.AdvisoryLockProvider is not null; } - private static async Task<(IAdvisoryLockProvider provider, DbConnection connection, bool openedByMe)> PrepareAsync( + private static async Task<( DbContext ctx, - string key, - CancellationToken ct - ) + IAdvisoryLockProvider provider, + DbConnection connection, + bool openedByMe + )> PrepareAsync(DatabaseFacade database, string key, CancellationToken ct) { ValidateKey(key); - var provider = ResolveProvider(ctx); - var connection = ctx.Database.GetDbConnection(); + var ctx = GetContext(database); + var provider = ResolveProvider(database); + var connection = database.GetDbConnection(); bool openedByMe = false; if (connection.State != ConnectionState.Open) { await connection.OpenAsync(ct).ConfigureAwait(false); openedByMe = true; } - return (provider, connection, openedByMe); + return (ctx, provider, connection, openedByMe); } - private static (IAdvisoryLockProvider provider, DbConnection connection, bool openedByMe) PrepareSync( + private static ( DbContext ctx, - string key - ) + IAdvisoryLockProvider provider, + DbConnection connection, + bool openedByMe + ) PrepareSync(DatabaseFacade database, string key) { ValidateKey(key); - var provider = ResolveProvider(ctx); - var connection = ctx.Database.GetDbConnection(); + var ctx = GetContext(database); + var provider = ResolveProvider(database); + var connection = database.GetDbConnection(); bool openedByMe = false; if (connection.State != ConnectionState.Open) { connection.Open(); openedByMe = true; } - return (provider, connection, openedByMe); + return (ctx, provider, connection, openedByMe); } private static void ValidateKey(string key) @@ -203,9 +209,12 @@ private static void ValidateKey(string key) throw new ArgumentException("Lock key must not exceed 255 characters.", nameof(key)); } - private static IAdvisoryLockProvider ResolveProvider(DbContext ctx) + private static DbContext GetContext(DatabaseFacade database) => + ((IDatabaseFacadeDependenciesAccessor)database).Context; + + private static IAdvisoryLockProvider ResolveProvider(DatabaseFacade database) { - var lp = ctx.GetInfrastructure().GetService(); + var lp = ((IInfrastructure)database).Instance.GetService(); if (lp is null) throw new LockingConfigurationException( "No ILockingProvider is registered. Call UseLocking() when configuring the DbContext." diff --git a/tests/EntityFrameworkCore.Locking.MySql.Tests/DistributedLockIntegrationTests.cs b/tests/EntityFrameworkCore.Locking.MySql.Tests/DistributedLockIntegrationTests.cs index 9d6ef83..15d3809 100644 --- a/tests/EntityFrameworkCore.Locking.MySql.Tests/DistributedLockIntegrationTests.cs +++ b/tests/EntityFrameworkCore.Locking.MySql.Tests/DistributedLockIntegrationTests.cs @@ -1,5 +1,4 @@ using AwesomeAssertions; -using EntityFrameworkCore.Locking; using EntityFrameworkCore.Locking.MySql.Tests.Fixtures; using EntityFrameworkCore.Locking.Tests.Infrastructure; using Microsoft.EntityFrameworkCore; @@ -29,7 +28,7 @@ public async Task LongKey_ExceededMysqlLimit_HashesCorrectly() // Key > 64 chars is hashed to lock: (64 chars total) var longKey = new string('x', 100); await using var ctx = CreateContext(); - await using var handle = await ctx.AcquireDistributedLockAsync(longKey); + await using var handle = await ctx.Database.AcquireDistributedLockAsync(longKey); handle.Should().NotBeNull(); handle.Key.Should().Be(longKey); // public Key is the original, not encoded } diff --git a/tests/EntityFrameworkCore.Locking.PostgreSQL.Tests/DistributedLockIntegrationTests.cs b/tests/EntityFrameworkCore.Locking.PostgreSQL.Tests/DistributedLockIntegrationTests.cs index aa760b7..0218059 100644 --- a/tests/EntityFrameworkCore.Locking.PostgreSQL.Tests/DistributedLockIntegrationTests.cs +++ b/tests/EntityFrameworkCore.Locking.PostgreSQL.Tests/DistributedLockIntegrationTests.cs @@ -1,5 +1,4 @@ using AwesomeAssertions; -using EntityFrameworkCore.Locking; using EntityFrameworkCore.Locking.PostgreSQL.Tests.Fixtures; using EntityFrameworkCore.Locking.Tests.Infrastructure; using Microsoft.EntityFrameworkCore; @@ -21,12 +20,12 @@ public async Task Acquire_Contested_BlocksUntilReleased() const string key = "pg-block-key"; await using var ctxA = CreateContext(); - var handleA = await ctxA.AcquireDistributedLockAsync(key); + var handleA = await ctxA.Database.AcquireDistributedLockAsync(key); var acquireTask = Task.Run(async () => { await using var ctxB = CreateContext(); - await using var h = await ctxB.AcquireDistributedLockAsync(key); + await using var h = await ctxB.Database.AcquireDistributedLockAsync(key); }); var completed = await Task.WhenAny(acquireTask, Task.Delay(300)); @@ -42,12 +41,12 @@ public async Task TwoContexts_DifferentConnections_CanBothHoldSameKey() const string key = "pg-registry-scope"; await using var ctxA = CreateContext(); - await using var hA = await ctxA.AcquireDistributedLockAsync(key); + await using var hA = await ctxA.Database.AcquireDistributedLockAsync(key); await using var ctxB = CreateContext(); - var hB = await ctxB.TryAcquireDistributedLockAsync(key); + var hB = await ctxB.Database.TryAcquireDistributedLockAsync(key); await hA.DisposeAsync(); - hB = await ctxB.TryAcquireDistributedLockAsync(key); + hB = await ctxB.Database.TryAcquireDistributedLockAsync(key); hB.Should().NotBeNull("after ctxA releases, ctxB should acquire"); await hB!.DisposeAsync(); } @@ -58,14 +57,14 @@ public async Task Acquire_Cancelled_WithTimeout_ThrowsOperationCanceled() const string key = "pg-cancel-with-timeout"; await using var ctxA = CreateContext(); - await using var handleA = await ctxA.AcquireDistributedLockAsync(key); + await using var handleA = await ctxA.Database.AcquireDistributedLockAsync(key); await using var ctxB = CreateContext(); using var cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromMilliseconds(200)); var sw = System.Diagnostics.Stopwatch.StartNew(); - Func act = () => ctxB.AcquireDistributedLockAsync(key, TimeSpan.FromSeconds(10), cts.Token); + Func act = () => ctxB.Database.AcquireDistributedLockAsync(key, TimeSpan.FromSeconds(10), cts.Token); await act.Should().ThrowAsync(); sw.Stop(); sw.Elapsed.Should().BeLessThan(TimeSpan.FromSeconds(15)); diff --git a/tests/EntityFrameworkCore.Locking.SqlServer.Tests/DistributedLockIntegrationTests.cs b/tests/EntityFrameworkCore.Locking.SqlServer.Tests/DistributedLockIntegrationTests.cs index d3dfdba..d8d2e30 100644 --- a/tests/EntityFrameworkCore.Locking.SqlServer.Tests/DistributedLockIntegrationTests.cs +++ b/tests/EntityFrameworkCore.Locking.SqlServer.Tests/DistributedLockIntegrationTests.cs @@ -1,5 +1,4 @@ using AwesomeAssertions; -using EntityFrameworkCore.Locking; using EntityFrameworkCore.Locking.SqlServer.Tests.Fixtures; using EntityFrameworkCore.Locking.Tests.Infrastructure; using Microsoft.EntityFrameworkCore; @@ -20,14 +19,14 @@ public async Task Acquire_Cancelled_WithTimeout_Throws() { const string key = "ss-cancel-timeout"; await using var ctxA = CreateContext(); - await using var handleA = await ctxA.AcquireDistributedLockAsync(key); + await using var handleA = await ctxA.Database.AcquireDistributedLockAsync(key); await using var ctxB = CreateContext(); using var cts = new CancellationTokenSource(); cts.CancelAfter(200); var sw = System.Diagnostics.Stopwatch.StartNew(); - Func act = () => ctxB.AcquireDistributedLockAsync(key, TimeSpan.FromSeconds(10), cts.Token); + Func act = () => ctxB.Database.AcquireDistributedLockAsync(key, TimeSpan.FromSeconds(10), cts.Token); await act.Should().ThrowAsync(); sw.Stop(); sw.Elapsed.Should().BeLessThan(TimeSpan.FromSeconds(15)); diff --git a/tests/EntityFrameworkCore.Locking.Tests.Infrastructure/DistributedLockIntegrationTestsBase.cs b/tests/EntityFrameworkCore.Locking.Tests.Infrastructure/DistributedLockIntegrationTestsBase.cs index 6cccb6d..3d42091 100644 --- a/tests/EntityFrameworkCore.Locking.Tests.Infrastructure/DistributedLockIntegrationTestsBase.cs +++ b/tests/EntityFrameworkCore.Locking.Tests.Infrastructure/DistributedLockIntegrationTestsBase.cs @@ -1,5 +1,4 @@ using AwesomeAssertions; -using EntityFrameworkCore.Locking; using EntityFrameworkCore.Locking.Exceptions; using Xunit; @@ -27,7 +26,7 @@ public async Task InitializeAsync() public async Task Acquire_Free_Succeeds() { await using var ctx = CreateContext(); - await using var handle = await ctx.AcquireDistributedLockAsync("free-key"); + await using var handle = await ctx.Database.AcquireDistributedLockAsync("free-key"); handle.Should().NotBeNull(); handle.Key.Should().Be("free-key"); } @@ -36,7 +35,7 @@ public async Task Acquire_Free_Succeeds() public void SupportsDistributedLocks_ReturnsTrue() { using var ctx = CreateContext(); - ctx.SupportsDistributedLocks().Should().BeTrue(); + ctx.Database.SupportsDistributedLocks().Should().BeTrue(); } [Fact] @@ -45,12 +44,12 @@ public async Task Dispose_ReleasesLock_VerifiedByOtherConnection() const string key = "release-key"; await using var ctxA = CreateContext(); - var handle = await ctxA.AcquireDistributedLockAsync(key); + var handle = await ctxA.Database.AcquireDistributedLockAsync(key); await handle.DisposeAsync(); await using var ctxB = CreateContext(); - await using var handleB = await ctxB.TryAcquireDistributedLockAsync(key); + await using var handleB = await ctxB.Database.TryAcquireDistributedLockAsync(key); handleB.Should().NotBeNull(); } @@ -58,7 +57,7 @@ public async Task Dispose_ReleasesLock_VerifiedByOtherConnection() public async Task TryAcquire_Free_ReturnsHandle() { await using var ctx = CreateContext(); - await using var handle = await ctx.TryAcquireDistributedLockAsync("try-free"); + await using var handle = await ctx.Database.TryAcquireDistributedLockAsync("try-free"); handle.Should().NotBeNull(); } @@ -68,10 +67,10 @@ public async Task TryAcquire_Contested_ReturnsNull() const string key = "try-contested"; await using var ctxA = CreateContext(); - await using var handleA = await ctxA.AcquireDistributedLockAsync(key); + await using var handleA = await ctxA.Database.AcquireDistributedLockAsync(key); await using var ctxB = CreateContext(); - var handleB = await ctxB.TryAcquireDistributedLockAsync(key); + var handleB = await ctxB.Database.TryAcquireDistributedLockAsync(key); handleB.Should().BeNull(); } @@ -81,11 +80,11 @@ public async Task Acquire_Timeout_ThrowsLockTimeout() const string key = "timeout-key"; await using var ctxA = CreateContext(); - await using var handleA = await ctxA.AcquireDistributedLockAsync(key); + await using var handleA = await ctxA.Database.AcquireDistributedLockAsync(key); await using var ctxB = CreateContext(); var sw = System.Diagnostics.Stopwatch.StartNew(); - Func act = () => ctxB.AcquireDistributedLockAsync(key, DistributedLockAcquireTimeout); + Func act = () => ctxB.Database.AcquireDistributedLockAsync(key, DistributedLockAcquireTimeout); await act.Should().ThrowAsync(); sw.Stop(); sw.Elapsed.Should().BeLessThan(TimeSpan.FromSeconds(10)); @@ -95,7 +94,7 @@ public async Task Acquire_Timeout_ThrowsLockTimeout() public async Task ReleaseAsync_Idempotent() { await using var ctx = CreateContext(); - var handle = await ctx.AcquireDistributedLockAsync("idempotent"); + var handle = await ctx.Database.AcquireDistributedLockAsync("idempotent"); await handle.ReleaseAsync(); await handle.ReleaseAsync(); // must not throw } @@ -104,9 +103,11 @@ public async Task ReleaseAsync_Idempotent() public async Task DoubleAcquire_SameContext_ThrowsLockAlreadyHeld() { await using var ctx = CreateContext(); - await using var h1 = await ctx.AcquireDistributedLockAsync("double"); + await using var h1 = await ctx.Database.AcquireDistributedLockAsync("double"); - var ex = await Assert.ThrowsAsync(() => ctx.AcquireDistributedLockAsync("double")); + var ex = await Assert.ThrowsAsync(() => + ctx.Database.AcquireDistributedLockAsync("double") + ); ex.Key.Should().Be("double"); } } diff --git a/tests/EntityFrameworkCore.Locking.Tests/DistributedLockUnitTests.cs b/tests/EntityFrameworkCore.Locking.Tests/DistributedLockUnitTests.cs index a8794d4..551127f 100644 --- a/tests/EntityFrameworkCore.Locking.Tests/DistributedLockUnitTests.cs +++ b/tests/EntityFrameworkCore.Locking.Tests/DistributedLockUnitTests.cs @@ -1,12 +1,10 @@ using System.Data; using System.Data.Common; using AwesomeAssertions; -using EntityFrameworkCore.Locking; using EntityFrameworkCore.Locking.Abstractions; using EntityFrameworkCore.Locking.Exceptions; using EntityFrameworkCore.Locking.Internal; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; using Xunit; namespace EntityFrameworkCore.Locking.Tests; @@ -36,14 +34,14 @@ public void LockAlreadyHeldException_InheritsLockingException() public async Task AcquireDistributedLockAsync_NullKey_ThrowsArgumentException() { await using var ctx = CreateContext(); - await Assert.ThrowsAsync(() => ctx.AcquireDistributedLockAsync(null!)); + await Assert.ThrowsAsync(() => ctx.Database.AcquireDistributedLockAsync(null!)); } [Fact] public async Task AcquireDistributedLockAsync_EmptyKey_ThrowsArgumentException() { await using var ctx = CreateContext(); - await Assert.ThrowsAsync(() => ctx.AcquireDistributedLockAsync("")); + await Assert.ThrowsAsync(() => ctx.Database.AcquireDistributedLockAsync("")); } [Fact] @@ -51,7 +49,7 @@ public async Task AcquireDistributedLockAsync_KeyTooLong_ThrowsArgumentException { await using var ctx = CreateContext(); var longKey = new string('a', 256); - await Assert.ThrowsAsync(() => ctx.AcquireDistributedLockAsync(longKey)); + await Assert.ThrowsAsync(() => ctx.Database.AcquireDistributedLockAsync(longKey)); } [Fact] @@ -59,7 +57,7 @@ public async Task AcquireDistributedLockAsync_MaxKey255_Accepted() { await using var ctx = CreateContext(); var key = new string('a', 255); - await using var handle = await ctx.AcquireDistributedLockAsync(key); + await using var handle = await ctx.Database.AcquireDistributedLockAsync(key); handle.Should().NotBeNull(); handle.Key.Should().Be(key); } @@ -70,7 +68,7 @@ public async Task AcquireDistributedLockAsync_MaxKey255_Accepted() public void SupportsDistributedLocks_WithFakeProvider_ReturnsTrue() { using var ctx = CreateContext(); - ctx.SupportsDistributedLocks().Should().BeTrue(); + ctx.Database.SupportsDistributedLocks().Should().BeTrue(); } // --- Handle idempotence --- @@ -79,7 +77,7 @@ public void SupportsDistributedLocks_WithFakeProvider_ReturnsTrue() public async Task ReleaseAsync_CalledTwice_IsIdempotent() { await using var ctx = CreateContext(); - var handle = await ctx.AcquireDistributedLockAsync("key"); + var handle = await ctx.Database.AcquireDistributedLockAsync("key"); await handle.ReleaseAsync(); // Second release should not throw await handle.ReleaseAsync(); @@ -89,7 +87,7 @@ public async Task ReleaseAsync_CalledTwice_IsIdempotent() public async Task DisposeAfterRelease_IsIdempotent() { await using var ctx = CreateContext(); - var handle = await ctx.AcquireDistributedLockAsync("key"); + var handle = await ctx.Database.AcquireDistributedLockAsync("key"); await handle.ReleaseAsync(); await handle.DisposeAsync(); // no throw } @@ -100,9 +98,11 @@ public async Task DisposeAfterRelease_IsIdempotent() public async Task DoubleAcquire_SameKey_ThrowsLockAlreadyHeld() { await using var ctx = CreateContext(); - await using var h1 = await ctx.AcquireDistributedLockAsync("dup-key"); + await using var h1 = await ctx.Database.AcquireDistributedLockAsync("dup-key"); - var ex = await Assert.ThrowsAsync(() => ctx.AcquireDistributedLockAsync("dup-key")); + var ex = await Assert.ThrowsAsync(() => + ctx.Database.AcquireDistributedLockAsync("dup-key") + ); ex.Key.Should().Be("dup-key"); } @@ -110,10 +110,10 @@ public async Task DoubleAcquire_SameKey_ThrowsLockAlreadyHeld() public async Task AfterRelease_SameKey_CanBeAcquiredAgain() { await using var ctx = CreateContext(); - var h1 = await ctx.AcquireDistributedLockAsync("reuse-key"); + var h1 = await ctx.Database.AcquireDistributedLockAsync("reuse-key"); await h1.ReleaseAsync(); - await using var h2 = await ctx.AcquireDistributedLockAsync("reuse-key"); + await using var h2 = await ctx.Database.AcquireDistributedLockAsync("reuse-key"); h2.Should().NotBeNull(); } @@ -123,7 +123,7 @@ public async Task AfterRelease_SameKey_CanBeAcquiredAgain() public async Task TryAcquireDistributedLockAsync_FreeKey_ReturnsHandle() { await using var ctx = CreateContext(); - var handle = await ctx.TryAcquireDistributedLockAsync("free"); + var handle = await ctx.Database.TryAcquireDistributedLockAsync("free"); handle.Should().NotBeNull(); await handle!.DisposeAsync(); }