Skip to content

Commit ee8d83d

Browse files
author
mnacmargaryan
committed
added implementation of 'for update' locking feature for ef core
1 parent 841afa7 commit ee8d83d

File tree

6 files changed

+160
-7
lines changed

6 files changed

+160
-7
lines changed

Readme.md

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,58 @@
11
# Pandatech.EFCore.PostgresExtensions
2-
2+
Pandatech.EFCore.PostgresExtensions is a NuGet package that enhances Entity Framework Core with support for PostgreSQL-specific syntax for update operations.
33

44
## Introduction
5-
6-
5+
You can install the Pandatech.EFCore.PostgresExtensions NuGet package via the NuGet Package Manager UI or the Package Manager Console using the following command:
6+
Install-Package Pandatech.EFCore.PostgresExtensions
77

88
## Features
9-
9+
Adds support for PostgreSQL-specific update syntax.
10+
Simplifies handling of update operations when working with PostgreSQL databases.
1011

1112
## Installation
13+
1. Install Pandatech.EFCore.PostgresExtensions Package
14+
Command: Install-Package Pandatech.EFCore.PostgresExtensions
15+
16+
2. Enable Query Locks
1217

18+
Inside the AddDbContext or AddDbContextPool method, after calling UseNpgsql(), call the UseQueryLocks() method on the DbContextOptionsBuilder to enable query locks.
1319

20+
services.AddDbContext<MyDbContext>(options =>
21+
{
22+
options.UseNpgsql(Configuration.GetConnectionString("MyDatabaseConnection"))
23+
.UseQueryLocks();
24+
});
1425

15-
## Usage
1626

27+
## Usage
28+
Use the provided ForUpdate extension method on IQueryable within your application to apply PostgreSQL-specific update syntax.
29+
30+
using Pandatech.EFCore.PostgresExtensions;
31+
using Microsoft.EntityFrameworkCore;
32+
33+
// Inside your service or repository method
34+
using (var transaction = _dbContext.Database.BeginTransaction())
35+
{
36+
try
37+
{
38+
// Use the ForUpdate extension method on IQueryable inside the transaction scope
39+
var entityToUpdate = _dbContext.Entities
40+
.Where(e => e.Id == id)
41+
.ForUpdate()
42+
.FirstOrDefault();
43+
44+
// Perform updates on entityToUpdate
45+
46+
await _dbContext.SaveChangesAsync();
47+
48+
transaction.Commit();
49+
}
50+
catch (Exception ex)
51+
{
52+
transaction.Rollback();
53+
// Handle exception
54+
}
55+
}
1756

1857
## License
1958

src/EFCore.PostgresExtensions/EFCore.PostgresExtensions.csproj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@
1818
</PropertyGroup>
1919

2020
<ItemGroup>
21-
<None Include="..\..\pandatech.png" Pack="true" PackagePath="\"/>
22-
<None Include="..\..\Readme.md" Pack="true" PackagePath="\"/>
21+
<None Include="..\..\pandatech.png" Pack="true" PackagePath="\" />
22+
<None Include="..\..\Readme.md" Pack="true" PackagePath="\" />
23+
</ItemGroup>
24+
25+
<ItemGroup>
26+
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.3" />
2327
</ItemGroup>
2428

2529
</Project>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
namespace EFCore.PostgresExtensions.Enums
2+
{
3+
public enum LockBehavior
4+
{
5+
/// <summary>
6+
/// Using this behavior forces transaction to wait until row is unlocked.
7+
/// </summary>
8+
Default = 0,
9+
10+
/// <summary>
11+
/// Using this behavior will skip rows that are locked by another transaction.
12+
/// </summary>
13+
SkipLocked = 1,
14+
15+
/// <summary>
16+
/// Using this behavior will throw an exception if requested rows are locked by another transaction.
17+
/// </summary>
18+
NoWait = 2,
19+
}
20+
21+
public static class LockBehaviorExtensions
22+
{
23+
public static string GetSqlKeyword(this LockBehavior lockBehavior) => lockBehavior switch
24+
{
25+
LockBehavior.Default => string.Empty,
26+
LockBehavior.SkipLocked => "skip locked",
27+
LockBehavior.NoWait => "nowait",
28+
_ => string.Empty,
29+
};
30+
}
31+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using EFCore.PostgresExtensions.Interceptors;
2+
using Microsoft.EntityFrameworkCore;
3+
4+
namespace EFCore.PostgresExtensions.Extensions
5+
{
6+
public static class OptionsBuilderExtensions
7+
{
8+
public static DbContextOptionsBuilder UseQueryLocks(this DbContextOptionsBuilder builder)
9+
{
10+
builder.AddInterceptors(new TaggedQueryCommandInterceptor());
11+
12+
return builder;
13+
}
14+
}
15+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using EFCore.PostgresExtensions.Enums;
2+
using Microsoft.EntityFrameworkCore;
3+
4+
namespace EFCore.PostgresExtensions.Extensions
5+
{
6+
public static class QueryableExtensions
7+
{
8+
internal const string ForUpdateKey = "for update ";
9+
/// <summary>
10+
/// Use this method for selecting data with locking.
11+
/// <para>Attention! Be aware that this method works only inside the transaction scope(dbContext.BeginTransaction) and you need to register it in startup.</para>
12+
/// </summary>
13+
/// <typeparam name="T"></typeparam>
14+
/// <param name="query">Query to lock.</param>
15+
/// <param name="lockBehavior">Behavior organizes the way data should be locked, for more information check enum values.</param>
16+
/// <returns>The same query with locking behavior added.</returns>
17+
public static IQueryable<T> ForUpdate<T>(this IQueryable<T> query, LockBehavior lockBehavior = LockBehavior.Default)
18+
{
19+
query = query.TagWith(ForUpdateKey + lockBehavior.GetSqlKeyword());
20+
21+
return query.AsQueryable();
22+
}
23+
}
24+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using EFCore.PostgresExtensions.Extensions;
2+
using Microsoft.EntityFrameworkCore.Diagnostics;
3+
using System.Data.Common;
4+
5+
namespace EFCore.PostgresExtensions.Interceptors
6+
{
7+
public class TaggedQueryCommandInterceptor : DbCommandInterceptor
8+
{
9+
public override InterceptionResult<DbDataReader> ReaderExecuting(
10+
DbCommand command,
11+
CommandEventData eventData,
12+
InterceptionResult<DbDataReader> result)
13+
{
14+
ManipulateCommand(command);
15+
16+
return result;
17+
}
18+
19+
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
20+
DbCommand command,
21+
CommandEventData eventData,
22+
InterceptionResult<DbDataReader> result,
23+
CancellationToken cancellationToken = default)
24+
{
25+
ManipulateCommand(command);
26+
27+
return new ValueTask<InterceptionResult<DbDataReader>>(result);
28+
}
29+
30+
private static void ManipulateCommand(DbCommand command)
31+
{
32+
if (command.CommandText.StartsWith($"-- {QueryableExtensions.ForUpdateKey}", StringComparison.Ordinal))
33+
{
34+
var tagEndIndex = command.CommandText.IndexOf('\n');
35+
36+
command.CommandText += command.CommandText[2..tagEndIndex];
37+
}
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)