11# Pandatech.EFCore.PostgresExtensions
22
3- Pandatech.EFCore.PostgresExtensions is an advanced NuGet package designed to enhance PostgreSQL functionalities within
4- Entity Framework Core, leveraging specific features not covered by the official Npgsql.EntityFrameworkCore.PostgreSQL
5- package. This package introduces optimized row-level locking mechanisms and PostgreSQL sequence random incrementing
6- features.
7-
8- ## Features
9-
10- 1 . ** Row-Level Locking** : Implements the PostgreSQL ` FOR UPDATE ` feature, providing three lock
11- behaviors - ` Wait ` , ` Skip ` , and
12- ` NoWait ` , to facilitate advanced transaction control and concurrency management.
13- 2 . ** Random Incrementing Sequence Generation:** Provides a secure way to generate sequential IDs with random increments
14- to prevent predictability and potential data exposure. This ensures IDs are non-sequential and non-predictable,
15- enhancing security and balancing database load.
16- 3 . ** Natural Sorting** : Provides way to calculate natural sort compliant order for string, which can be used
17- in ` ORDER BY ` clause. This is useful for sorting strings that contain numbers in a human-friendly way.
18- 4 . ** Schema Rollback Helpers** : Extension methods ` DropRandomIdSequence ` and ` DropNaturalSortKeyFunction ` simplify
19- cleanup in ` Down ` migrations.
3+ PostgreSQL-specific extensions for Entity Framework Core that fill the gaps left by the official Npgsql provider.
204
21- ## Installation
5+ | Feature | What it does |
6+ | -----------------------------| ----------------------------------------------------------------|
7+ | ** Row-level locking** | ` FOR UPDATE ` with ` Wait ` , ` SkipLocked ` , and ` NoWait ` behaviors |
8+ | ** Random-increment IDs** | Non-predictable sequential IDs backed by a PostgreSQL sequence |
9+ | ** Natural sort keys** | Human-friendly ordering for strings containing numbers |
10+ | ** Schema rollback helpers** | Clean ` Down() ` migration methods for all of the above |
11+
12+ Targets ** net8.0** , ** net9.0** , and ** net10.0** .
2213
23- To install Pandatech.EFCore.PostgresExtensions, use the following NuGet command:
14+ ## Installation
2415
2516``` bash
26- Install-Package Pandatech.EFCore.PostgresExtensions
17+ dotnet add package Pandatech.EFCore.PostgresExtensions
2718```
2819
29- ## Usage
20+ ## Row-level locking
21+
22+ PostgreSQL's ` FOR UPDATE ` clause lets you lock selected rows for the duration of a transaction. This package exposes it
23+ as a LINQ extension method with three lock behaviors.
3024
31- ### Row-Level Locking
25+ ### Setup
3226
33- Configure your DbContext to use Npgsql and enable query locks :
27+ Register the query interceptor on your ` DbContext ` :
3428
3529``` csharp
36- services .AddDbContext <MyDbContext >(options =>
37- {
38- options .UseNpgsql (Configuration .GetConnectionString (" MyDatabaseConnection" ))
39- .UseQueryLocks ();
40- });
30+ services .AddDbContextPool <AppDbContext >(options =>
31+ options .UseNpgsql (connectionString )
32+ .UseQueryLocks ());
4133```
4234
43- Within a transaction scope, apply the desired lock behavior using the ` ForUpdate ` extension method:
35+ ### Usage
36+
37+ The ` ForUpdate ` method must be called inside a transaction:
4438
4539``` csharp
46- using var transaction = _dbContext .Database .BeginTransaction ();
47- try
48- {
49- var entityToUpdate = _dbContext .Entities
50- .Where (e => e .Id == id )
51- .ForUpdate (LockBehavior .NoWait ) // Or use LockBehavior.Default (Wait)/ LockBehavior.SkipLocked
52- .FirstOrDefault ();
53-
54- // Perform updates on entityToUpdate
55- await _dbContext .SaveChangesAsync ();
56- transaction .Commit ();
57- }
58- catch (Exception ex )
59- {
60- transaction .Rollback ();
61- // Handle exception
62- }
40+ await using var transaction = await dbContext .Database .BeginTransactionAsync ();
41+
42+ var order = await dbContext .Orders
43+ .Where (o => o .Id == orderId )
44+ .ForUpdate (LockBehavior .SkipLocked )
45+ .FirstOrDefaultAsync ();
46+
47+ // Modify the locked row
48+ order .Status = OrderStatus .Processing ;
49+ await dbContext .SaveChangesAsync ();
50+ await transaction .CommitAsync ();
6351```
6452
65- ### Random Incrementing Sequence Generation
53+ ### Lock behaviors
54+
55+ | Behavior | SQL generated | When to use |
56+ | ------------------| --------------------------| -----------------------------------------------------------------|
57+ | ` Default ` (Wait) | ` FOR UPDATE ` | You need the row and can wait for it |
58+ | ` SkipLocked ` | ` FOR UPDATE SKIP LOCKED ` | Queue-style processing — skip rows another worker already holds |
59+ | ` NoWait ` | ` FOR UPDATE NOWAIT ` | Fail immediately if the row is locked |
60+
61+ ## Random-increment sequence IDs
6662
67- To configure a model to use the random ID sequence, use the ` HasRandomIdSequence ` extension method in your entity
68- configuration:
63+ Generates ` bigint ` IDs that increment by a random amount within a configurable range. The IDs are unique and always
64+ increasing, but the gaps between them are unpredictable — preventing enumeration attacks while keeping an index-friendly
65+ insert order.
66+
67+ ### 1. Configure the entity
6968
7069``` csharp
7170public class Animal
@@ -74,7 +73,7 @@ public class Animal
7473 public string Name { get ; set ; }
7574}
7675
77- public class AnimalEntityConfiguration : IEntityTypeConfiguration <Animal >
76+ public class AnimalConfiguration : IEntityTypeConfiguration <Animal >
7877{
7978 public void Configure (EntityTypeBuilder <Animal > builder )
8079 {
@@ -85,86 +84,119 @@ public class AnimalEntityConfiguration : IEntityTypeConfiguration<Animal>
8584}
8685```
8786
88- After creating a migration, add the custom function ** above create table** script in your migration class:
87+ ### 2. Create the function in a migration
88+
89+ The function must be created ** before** the table that references it. Add it manually at the top of your ` Up() ` method:
8990
9091``` csharp
91- public partial class PgFunction : Migration
92+ protected override void Up ( MigrationBuilder migrationBuilder )
9293{
93- /// <inheritdoc />
94- protected override void Up (MigrationBuilder migrationBuilder )
95- {
96- migrationBuilder .CreateRandomIdSequence (" animal" , " id" , 5 , 5 , 10 ); // Add this line manually
97-
98- migrationBuilder .CreateTable (
99- name : " animal" ,
100- columns : table => new
101- {
102- id = table .Column <long >(type : " bigint" , nullable : false , defaultValueSql : " animal_random_id_generator()" ),
103- name = table .Column <string >(type : " text" , nullable : false )
104- },
105- constraints : table =>
106- {
107- table .PrimaryKey (" pk_animal" , x => x .id );
108- });
109- }
94+ // Create the sequence + function first
95+ migrationBuilder .CreateRandomIdSequence (
96+ tableName : " animal" ,
97+ pkName : " id" ,
98+ startValue : 5 ,
99+ minRandIncrementValue : 5 ,
100+ maxRandIncrementValue : 10 );
101+
102+ // Then create the table (the default value references the function)
103+ migrationBuilder .CreateTable (.. .);
104+ }
110105
111- /// <inheritdoc />
112- protected override void Down (MigrationBuilder migrationBuilder )
113- {
114- migrationBuilder .DropRandomIdSequence (" animal" , " id" );
115-
116- migrationBuilder .DropTable (
117- name : " animal" );
118- }
106+ protected override void Down (MigrationBuilder migrationBuilder )
107+ {
108+ migrationBuilder .DropRandomIdSequence (" animal" , " id" );
109+ migrationBuilder .DropTable (" animal" );
119110}
120111```
121112
122- #### Additional notes
113+ Parameters: ` startValue ` is the first ID returned, ` minRandIncrementValue ` and ` maxRandIncrementValue ` define the random
114+ gap range between consecutive IDs.
123115
124- - The random incrementing sequence feature ensures the generated IDs are unique, non-sequential, and non-predictable,
125- enhancing security.
126- - The feature supports only ` long ` data type (` bigint ` in PostgreSQL).
116+ ## Natural sort keys
127117
128- ### Natural Sort Key
118+ Sorts strings that contain numbers the way a human would expect: ` "Item 2" ` before ` "Item 10" ` , not after it. The
119+ package creates a PostgreSQL function that zero-pads numeric substrings, producing a sortable text key.
129120
130- This package can generate a natural sort key for your text columns—especially useful when sorting addresses or other
131- fields that contain embedded numbers. It avoids plain lexicographic ordering (e.g. ` "10" ` < ` "2" ` ) by treating numeric
132- substrings numerically.
121+ ### 1. Create the function (once per database)
133122
134- #### How to Use
123+ ``` csharp
124+ protected override void Up (MigrationBuilder migrationBuilder )
125+ {
126+ migrationBuilder .CreateNaturalSortKeyFunction ();
127+ }
135128
136- 1 . Create the function in your migration (once per database). Call the helper method in ` Up() ` :
137- ``` csharp
138- public partial class AddNaturalSortKeyToBuildings : Migration
129+ protected override void Down (MigrationBuilder migrationBuilder )
130+ {
131+ migrationBuilder .DropNaturalSortKeyFunction ();
132+ }
133+ ```
134+
135+ ### 2. Add a computed column
136+
137+ ``` csharp
138+ public class Building
139+ {
140+ public long Id { get ; set ; }
141+ public string Address { get ; set ; }
142+ public string AddressNaturalSortKey { get ; set ; }
143+ }
144+
145+ public class BuildingConfiguration : IEntityTypeConfiguration <Building >
146+ {
147+ public void Configure (EntityTypeBuilder <Building > builder )
139148 {
140- protected override void Up (MigrationBuilder migrationBuilder )
141- {
142- // Create the natural sort key function in PostgreSQL
143- migrationBuilder .CreateNaturalSortKeyFunction ();
144-
145- protected override void Down (MigrationBuilder migrationBuilder )
146- {
147- migrationBuilder .DropNaturalSortKeyFunction ();
148- }
149- }
149+ builder .Property (x => x .AddressNaturalSortKey )
150+ .HasNaturalSortKey (" address" );
150151 }
151- ```
152- 2 . Configure your entity to use the natural sort key . In your `IEntityTypeConfiguration ` for the table:
153- ```csharp
154- public class BuildingConfiguration : IEntityTypeConfiguration<Building>
155- {
156- public void Configure(EntityTypeBuilder <Building > builder )
157- {
158- // Create a computed column in EF (like "address_natural_sort_key")
159- builder
160- .Property (x => x .AddressNaturalSortKey )
161- .HasNaturalSortKey (" address" ); // Points to the column storing your original address
162- }
163- }
164- ```
165-
166- When you query the entity , simply `ORDER BY AddressNaturalSortKey ` to get true “natural ” ordering in PostgreSQL .
152+ }
153+ ```
154+
155+ Then order by the computed column:
156+
157+ ``` csharp
158+ var sorted = await dbContext .Buildings
159+ .OrderBy (b => b .AddressNaturalSortKey )
160+ .ToListAsync ();
161+ ```
162+
163+ ## Encrypted-column unique indexes
164+
165+ For columns that store encrypted data where only the first 64 characters are deterministic (e.g., hash prefix), you can
166+ create a unique index on that prefix:
167+
168+ ``` csharp
169+ // In migration Up()
170+ migrationBuilder .CreateUniqueIndexOnEncryptedColumn (" users" , " email_encrypted" );
171+
172+ // With an optional WHERE condition
173+ migrationBuilder .CreateUniqueIndexOnEncryptedColumn (" users" , " email_encrypted" , " is_active = true" );
174+
175+ // In migration Down()
176+ migrationBuilder .DropUniqueIndexOnEncryptedColumn (" users" , " email_encrypted" );
177+ ```
178+
179+ ## API reference
180+
181+ ### Extension methods
182+
183+ | Method | Target | Description |
184+ | -------------------------------------------| ---------------------------| -------------------------------------------------------------------------------------|
185+ | ` UseQueryLocks() ` | ` DbContextOptionsBuilder ` | Registers the interceptor that rewrites tagged queries into ` FOR UPDATE ` SQL |
186+ | ` ForUpdate() ` | ` IQueryable<T> ` | Tags a query for row-level locking (must be inside a transaction) |
187+ | ` HasRandomIdSequence() ` | ` PropertyBuilder ` | Configures the property to use a random-increment sequence as its default value |
188+ | ` HasNaturalSortKey(column) ` | ` PropertyBuilder ` | Configures the property as a stored computed column using the natural sort function |
189+ | ` CreateRandomIdSequence(...) ` | ` MigrationBuilder ` | Creates the PostgreSQL sequence and generator function |
190+ | ` DropRandomIdSequence(...) ` | ` MigrationBuilder ` | Drops the sequence and generator function |
191+ | ` CreateNaturalSortKeyFunction() ` | ` MigrationBuilder ` | Creates the natural sort key function |
192+ | ` DropNaturalSortKeyFunction() ` | ` MigrationBuilder ` | Drops the natural sort key function |
193+ | ` CreateUniqueIndexOnEncryptedColumn(...) ` | ` MigrationBuilder ` | Creates a unique index on the first 64 chars of a column |
194+ | ` DropUniqueIndexOnEncryptedColumn(...) ` | ` MigrationBuilder ` | Drops the encrypted-column unique index |
195+
196+ ### Enums
197+
198+ ` LockBehavior ` : ` Default ` (wait), ` SkipLocked ` , ` NoWait ` .
167199
168200## License
169201
170- Pandatech . EFCore . PostgresExtensions is licensed under the MIT License .
202+ MIT
0 commit comments