Skip to content

Commit ab2c176

Browse files
authored
Merge pull request #16 from PandaTechAM/development
dependency changes + documentation fix + multiplatform support
2 parents d691dc4 + 0ba0b05 commit ab2c176

File tree

7 files changed

+249
-159
lines changed

7 files changed

+249
-159
lines changed

.github/workflows/main.yml

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,67 @@
1-
name: Deploy NuGet Package
1+
name: Publish EFCore.PostgresExtensions To NuGet
22

33
env:
4-
PROJECT_PATH: './src/EFCore.PostgresExtensions/EFCore.PostgresExtensions.csproj'
5-
OUTPUT_DIR: 'nupkgs'
6-
NUGET_SOURCE: 'https://api.nuget.org/v3/index.json'
7-
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
4+
NUGET_SOURCE: https://api.nuget.org/v3/index.json
5+
OUTPUT_DIR: nupkgs
86

97
on:
108
push:
11-
branches:
12-
- main
9+
branches: [ main ]
10+
11+
permissions:
12+
contents: read
13+
1314
jobs:
14-
deploy:
15+
publish:
1516
runs-on: ubuntu-latest
17+
environment: Environment Settings
1618

1719
steps:
1820
- name: Checkout
1921
uses: actions/checkout@v6
2022

21-
- name: Setup .NET Core
23+
- name: Setup .NET
2224
uses: actions/setup-dotnet@v5
2325
with:
2426
global-json-file: global.json
2527

28+
- name: Restore
29+
run: dotnet restore
30+
2631
- name: Build
27-
run: dotnet build ${{ env.PROJECT_PATH }}
32+
run: dotnet build --no-restore --configuration Release
33+
34+
- name: Test
35+
run: dotnet test --no-build --configuration Release --verbosity normal
2836

2937
- name: Pack
30-
run: dotnet pack ${{ env.PROJECT_PATH }} --output ${{ env.OUTPUT_DIR }}
38+
run: dotnet pack src/EFCore.PostgresExtensions/EFCore.PostgresExtensions.csproj --no-build --configuration Release --output ${{ env.OUTPUT_DIR }}
39+
40+
- name: Publish packages
41+
env:
42+
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
43+
shell: bash
44+
run: |
45+
set -euo pipefail
46+
47+
if [ -z "${NUGET_API_KEY:-}" ]; then
48+
echo "NUGET_API_KEY is missing. If you stored it as an Environment secret, set jobs.publish.environment to that Environment name."
49+
exit 1
50+
fi
51+
52+
shopt -s nullglob
53+
54+
nupkgs=( "${{ env.OUTPUT_DIR }}"/*.nupkg )
55+
snupkgs=( "${{ env.OUTPUT_DIR }}"/*.snupkg )
56+
57+
if [ ${#nupkgs[@]} -eq 0 ]; then
58+
echo "No .nupkg files found in ${{ env.OUTPUT_DIR }}"
59+
ls -la "${{ env.OUTPUT_DIR }}" || true
60+
exit 1
61+
fi
62+
63+
dotnet nuget push "${nupkgs[@]}" --api-key "$NUGET_API_KEY" --source "${{ env.NUGET_SOURCE }}" --skip-duplicate
3164
32-
- name: Publish
33-
run: dotnet nuget push ${{ env.OUTPUT_DIR }}/*.nupkg -k ${{ env.NUGET_API_KEY }} -s ${{ env.NUGET_SOURCE }}
65+
if [ ${#snupkgs[@]} -gt 0 ]; then
66+
dotnet nuget push "${snupkgs[@]}" --api-key "$NUGET_API_KEY" --source "${{ env.NUGET_SOURCE }}" --skip-duplicate
67+
fi

Readme.md

Lines changed: 148 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,70 @@
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
7170
public 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 truenaturalordering 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

Comments
 (0)