Skip to content

Commit 6468d99

Browse files
Infrastructure added in User managment api project to acquire db lock by connection to master db for executing migrations
1 parent 7897b81 commit 6468d99

File tree

2 files changed

+94
-0
lines changed

2 files changed

+94
-0
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using Microsoft.Data.SqlClient;
2+
using Microsoft.EntityFrameworkCore;
3+
4+
namespace UserManagementApi.Infrastructure
5+
{
6+
public static class MigrationWithLockExtensions
7+
{
8+
/// <summary>
9+
/// Acquires a SQL app-lock, then runs EF Migrate() and your seed delegate.
10+
/// Use the SAME lock name across ALL services hitting the same SQL Server.
11+
/// </summary>
12+
public static async Task MigrateAndSeedWithSqlLockAsync<TContext>(
13+
this IHost app,
14+
string connectionStringName, // e.g. "Default"
15+
string globalLockName, // e.g. "IMIS_GLOBAL_MIGRATE_SEED"
16+
Func<IServiceProvider, CancellationToken, Task> seedAsync,
17+
CancellationToken ct = default)
18+
where TContext : DbContext
19+
{
20+
using var scope = app.Services.CreateScope();
21+
var services = scope.ServiceProvider;
22+
23+
var cfg = services.GetRequiredService<IConfiguration>();
24+
var logger = services.GetRequiredService<ILogger<TContext>>();
25+
var db = services.GetRequiredService<TContext>();
26+
27+
// Build a **master** connection string on the same server
28+
var csb = new SqlConnectionStringBuilder(cfg.GetConnectionString(connectionStringName));
29+
var master = new SqlConnectionStringBuilder(csb.ConnectionString) { InitialCatalog = "master"}.ToString();
30+
31+
await SqlAppLock.WithExclusiveLockAsync(master, globalLockName, async token =>
32+
{
33+
logger.LogInformation("Applying EF migrations for DB '{Db}'...", csb.InitialCatalog);
34+
await db.Database.MigrateAsync(token); // creates DB if missing, applies migrations
35+
logger.LogInformation("Migrations OK for '{Db}'. Seeding...", csb.InitialCatalog);
36+
37+
await seedAsync(services, token); // call your existing seeder here
38+
39+
logger.LogInformation("Seed completed for '{Db}'.", csb.InitialCatalog);
40+
}, ct: ct);
41+
}
42+
}
43+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using Microsoft.Data.SqlClient;
2+
using System.Data;
3+
4+
namespace UserManagementApi.Infrastructure
5+
{
6+
public static class SqlAppLock
7+
{
8+
public static async Task WithExclusiveLockAsync(
9+
string masterConnectionString,
10+
string resourceName,
11+
Func<CancellationToken, Task> action,
12+
int timeoutMs = 60000,
13+
CancellationToken ct = default)
14+
{
15+
await using var conn = new SqlConnection(masterConnectionString);
16+
await conn.OpenAsync(ct);
17+
18+
// Acquire lock
19+
await using (var cmd = new SqlCommand("sp_getapplock", conn) { CommandType = CommandType.StoredProcedure })
20+
{
21+
cmd.Parameters.AddWithValue("@Resource", resourceName);
22+
cmd.Parameters.AddWithValue("@LockMode", "Exclusive");
23+
cmd.Parameters.AddWithValue("@LockOwner", "Session");
24+
cmd.Parameters.AddWithValue("@LockTimeout", timeoutMs); // <-- correct name
25+
26+
var ret = cmd.Parameters.Add("@RETURN_VALUE", SqlDbType.Int);
27+
ret.Direction = ParameterDirection.ReturnValue;
28+
29+
await cmd.ExecuteNonQueryAsync(ct);
30+
31+
var code = (int)(ret.Value ?? -999);
32+
// 0 = success, 1 = already held by caller; negatives are failure
33+
if (code < 0)
34+
throw new TimeoutException($"sp_getapplock failed ({code}) for '{resourceName}'.");
35+
}
36+
37+
try
38+
{
39+
await action(ct);
40+
}
41+
finally
42+
{
43+
// Release lock
44+
await using var rel = new SqlCommand("sp_releaseapplock", conn) { CommandType = CommandType.StoredProcedure };
45+
rel.Parameters.AddWithValue("@Resource", resourceName);
46+
rel.Parameters.AddWithValue("@LockOwner", "Session");
47+
await rel.ExecuteNonQueryAsync(ct);
48+
}
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)