Skip to content

Commit e57c3c3

Browse files
v10.3.1 - README update and GitHub packages
1 parent 4062398 commit e57c3c3

2 files changed

Lines changed: 127 additions & 78 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,6 @@ jobs:
7171

7272
- name: Push to NuGet
7373
run: dotnet nuget push "out/*.nupkg" --source https://api.nuget.org/v3/index.json --skip-duplicate --api-key ${{ secrets.NUGET_API_KEY }}
74+
75+
- name: Push to GitHub Packages
76+
run: dotnet nuget push "out/*.nupkg" --source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json --skip-duplicate --api-key ${{ secrets.GITHUB_TOKEN }}

README.md

Lines changed: 124 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
[![License: MIT](https://img.shields.io/badge/License-MIT-5E2B97?style=flat-square)](https://opensource.org/licenses/MIT)
66
[![.NET 10](https://img.shields.io/badge/.NET-10-2A4F7B?style=flat-square&logo=dotnet)](https://dotnet.microsoft.com)
77

8-
## 🚀 Solve SQLite Concurrency & Performance
8+
## Solve SQLite Concurrency & Performance in EF Core
99

10-
Tired of `"database is locked"` (`SQLITE_BUSY`) errors in your multi-threaded .NET 10 app? Need to insert data faster than the standard `SaveChanges()` allows?
10+
Tired of `"database is locked"` (`SQLITE_BUSY`) errors in your multi-threaded .NET app? Need to insert data faster than the standard `SaveChanges()` allows?
1111

1212
**EntityFrameworkCore.Sqlite.Concurrency** is a high-performance add-on to `Microsoft.EntityFrameworkCore.Sqlite` that adds **automatic transaction upgrades**, **write serialization**, and **production-ready optimizations**, making SQLite robust and fast for enterprise applications.
1313

@@ -19,44 +19,45 @@ options.UseSqlite("Data Source=app.db");
1919
// With this:
2020
options.UseSqliteWithConcurrency("Data Source=app.db");
2121
```
22-
Guaranteed 100% write reliability and up to 10x faster bulk operations.
22+
Eliminates write contention errors and provides up to 10x faster bulk operations.
2323

2424
---
2525

2626
## Why Choose This Package?
2727

2828
| Problem with Standard EF Core SQLite | Our Solution & Benefit |
2929
| :--- | :--- |
30-
| **❌ Concurrency Errors:** `SQLITE_BUSY` / `database is locked` under load. | **✅ Automatic Write Serialization:** BEGIN IMMEDIATE transactions and optional app-level locking eliminate locking errors. |
30+
| **❌ Concurrency Errors:** `SQLITE_BUSY` / `database is locked` under load. | **✅ Automatic Write Serialization:** `BEGIN IMMEDIATE` transactions and app-level locking eliminate locking errors. |
3131
| **❌ Slow Bulk Inserts:** Linear `SaveChanges()` performance. | **✅ Intelligent Batching:** ~10x faster bulk inserts with optimized transactions and PRAGMAs. |
3232
| **❌ Read Contention:** Reads can block behind writes. | **✅ True Parallel Reads:** Automatic WAL mode + optimized connection pooling for non-blocking reads. |
33-
| **❌ Complex Retry Logic:** You need to build resilience yourself. | **✅ Built-In Resilience:** Exponential backoff retry and robust connection management out of the box. |
34-
| **High Memory Usage:** Large operations are inefficient. | **Optimized Performance:** Streamlined operations for speed and lower memory overhead. |
33+
| **❌ Complex Retry Logic:** You need to build resilience yourself. | **✅ Built-In Resilience:** Exponential backoff retry with full jitter, handling all `SQLITE_BUSY*` variants correctly. |
34+
| **EF DbContext not thread-safe:** Sharing one context across concurrent tasks throws. | **IDbContextFactory support:** `AddConcurrentSqliteDbContextFactory<T>` wires up the correct EF Core pattern for concurrent workloads. |
3535

3636
---
3737

3838
## Simple Installation
39+
3940
1. Install the package:
4041

4142
```bash
42-
# Package Manager
43-
Install-Package EntityFrameworkCore.Sqlite.Concurrency
44-
45-
OR
46-
4743
# .NET CLI
4844
dotnet add package EntityFrameworkCore.Sqlite.Concurrency
45+
46+
# Package Manager
47+
Install-Package EntityFrameworkCore.Sqlite.Concurrency
4948
```
5049

51-
2. Update your DbContext configuration (e.g., in Program.cs):
50+
2. Register in `Program.cs`:
5251

5352
```csharp
54-
builder.Services.AddDbContext<ApplicationDbContext>(options =>
55-
options.UseSqliteWithConcurrency("Data Source=app.db"));
53+
// For request-scoped use (controllers, Razor Pages, Blazor Server):
54+
builder.Services.AddConcurrentSqliteDbContext<AppDbContext>("Data Source=app.db");
55+
56+
// For concurrent workloads (background services, Task.WhenAll, channels):
57+
builder.Services.AddConcurrentSqliteDbContextFactory<AppDbContext>("Data Source=app.db");
5658
```
57-
Run your app. Concurrent writes are now serialized automatically, and reads are parallel. Your existing DbContext, models, and LINQ queries work unchanged.
5859

59-
Next, explore high-performance bulk inserts or fine-tune the configuration.
60+
Your existing `DbContext`, models, and LINQ queries work unchanged. Concurrent writes are serialized automatically. Reads execute in parallel.
6061

6162
---
6263

@@ -74,105 +75,150 @@ Next, explore high-performance bulk inserts or fine-tune the configuration.
7475

7576
---
7677

77-
## Advanced Usage & Performance
78-
High-Performance Bulk Operations
78+
## Usage Examples
79+
80+
### Bulk Operations
81+
7982
```csharp
80-
// Process massive datasets with speed and reliability
81-
public async Task PerformDataMigrationAsync(List<LegacyData> legacyRecords)
83+
public async Task ImportPostsAsync(List<Post> posts)
8284
{
83-
var modernRecords = legacyRecords.Select(ConvertToModernFormat).ToList();
84-
85-
// Optimized bulk insert with automatic transaction management and locking
86-
await _context.BulkInsertOptimizedAsync(modernRecords);
85+
// Bulk insert with automatic transaction management and write serialization
86+
await _context.BulkInsertOptimizedAsync(posts);
8787
}
8888
```
8989

90-
Optimized Concurrent Operations
90+
### Concurrent Workloads — use `IDbContextFactory`
91+
92+
A `DbContext` is not thread-safe and must not be shared across concurrent operations. Inject `IDbContextFactory<T>` and call `CreateDbContext()` to give each concurrent flow its own independent instance:
93+
9194
```csharp
92-
// Multiple threads writing simultaneously just work
93-
public async Task ProcessHighVolumeWorkload()
95+
public class ReportService
9496
{
95-
var writeTasks = new[]
97+
private readonly IDbContextFactory<AppDbContext> _factory;
98+
99+
public ReportService(IDbContextFactory<AppDbContext> factory)
100+
=> _factory = factory;
101+
102+
public async Task ProcessAllAsync(IEnumerable<int> ids, CancellationToken ct)
96103
{
97-
ProcessUserRegistrationAsync(newUser1),
98-
ProcessUserRegistrationAsync(newUser2),
99-
LogAuditEventsAsync(events)
100-
};
101-
102-
await Task.WhenAll(writeTasks); // All complete successfully without "database is locked"
104+
var tasks = ids.Select(async id =>
105+
{
106+
// Each task gets its own context — no EF thread-safety violation.
107+
// ThreadSafeEFCore.SQLite serializes writes at the SQLite level.
108+
await using var db = _factory.CreateDbContext();
109+
var item = await db.Items.FindAsync(id, ct);
110+
item.ProcessedAt = DateTime.UtcNow;
111+
await db.SaveChangesAsync(ct);
112+
});
113+
114+
await Task.WhenAll(tasks); // ✅ All complete without "database is locked"
115+
}
103116
}
104117
```
105-
Factory Pattern for Maximum Control
118+
119+
### Retry Wrapper
120+
106121
```csharp
107-
// Create performance-optimized contexts on demand
108-
public async Task<TResult> ExecuteHighPerformanceOperationAsync<TResult>(
109-
Func<DbContext, Task<TResult>> operation)
122+
public async Task UpdateWithRetryAsync(int postId, string newContent)
110123
{
111-
using var context = ThreadSafeFactory.CreateContext<AppDbContext>(
112-
"Data Source=app.db",
113-
options => options.MaxRetryAttempts = 5);
114-
115-
return await context.ExecuteWithRetryAsync(operation, maxRetries: 5);
124+
await _context.ExecuteWithRetryAsync(async ctx =>
125+
{
126+
var post = await ctx.Posts.FindAsync(postId);
127+
post.Content = newContent;
128+
post.UpdatedAt = DateTime.UtcNow;
129+
await ctx.SaveChangesAsync();
130+
}, maxRetries: 5);
116131
}
117132
```
118133

134+
### Factory Pattern (without Dependency Injection)
135+
136+
```csharp
137+
using var context = ThreadSafeFactory.CreateContext<AppDbContext>(
138+
"Data Source=app.db",
139+
options => options.MaxRetryAttempts = 5);
140+
141+
return await context.ExecuteWithRetryAsync(operation, maxRetries: 5);
142+
```
143+
119144
---
120145

121146
## Configuration
122-
Maximize your SQLite performance with these optimized settings:
123147

124148
```csharp
125-
services.AddDbContext<AppDbContext>(options =>
126-
options.UseSqliteWithConcurrency(
127-
"Data Source=app.db",
128-
concurrencyOptions =>
129-
{
130-
concurrencyOptions.MaxRetryAttempts = 3; // Automatic retry for SQLITE_BUSY
131-
concurrencyOptions.BusyTimeout = TimeSpan.FromSeconds(30);
132-
concurrencyOptions.CommandTimeout = 300; // 5-minute timeout for large operations
133-
concurrencyOptions.WalAutoCheckpoint = 1000; // Optimized WAL management
134-
}));
149+
builder.Services.AddConcurrentSqliteDbContext<AppDbContext>(
150+
"Data Source=app.db",
151+
options =>
152+
{
153+
options.BusyTimeout = TimeSpan.FromSeconds(30);
154+
options.MaxRetryAttempts = 3;
155+
options.CommandTimeout = 300;
156+
options.WalAutoCheckpoint = 1000;
157+
options.SynchronousMode = SqliteSynchronousMode.Normal;
158+
options.UpgradeTransactionsToImmediate = true;
159+
});
135160
```
136161

162+
| Option | Default | Description |
163+
|--------|---------|-------------|
164+
| `BusyTimeout` | 30 s | `PRAGMA busy_timeout` — SQLite retries lock acquisition internally for this duration before surfacing `SQLITE_BUSY`. |
165+
| `MaxRetryAttempts` | 3 | Application-level retry attempts after `SQLITE_BUSY*`, with exponential backoff and full jitter. |
166+
| `CommandTimeout` | 300 s | EF Core SQL command timeout. |
167+
| `WalAutoCheckpoint` | 1000 pages | `PRAGMA wal_autocheckpoint` — triggers a passive checkpoint after this many WAL frames (~4 MB). Set to `0` to disable. |
168+
| `SynchronousMode` | `Normal` | `PRAGMA synchronous` — durability vs. write-speed trade-off. `Normal` is recommended for WAL mode. |
169+
| `UpgradeTransactionsToImmediate` | `true` | Rewrites `BEGIN` to `BEGIN IMMEDIATE` to prevent `SQLITE_BUSY_SNAPSHOT` mid-transaction. |
170+
171+
> **Note:** `Cache=Shared` in the connection string is incompatible with WAL mode and will throw `ArgumentException` at startup. Connection pooling (`Pooling=true`) is enabled automatically and is the correct alternative.
172+
137173
---
138174

139175
## FAQ
140-
Q: How does it achieve 10x faster bulk inserts?
141-
A: Through intelligent batching, optimized transaction management, and reduced database round-trips. We process data in optimal chunks and minimize overhead at every layer.
142176

143-
Q: Will this work with my existing queries and LINQ code?
144-
A: Absolutely. Your existing query patterns, includes, and projections work unchanged while benefiting from improved read concurrency and reduced locking.
177+
**Q: How does it achieve 10x faster bulk inserts?**
178+
A: Through intelligent batching, optimized transaction management, and reduced database round-trips. Data is processed in optimal chunks with all PRAGMAs applied once per connection.
179+
180+
**Q: Will this work with my existing queries and LINQ code?**
181+
A: Yes. Existing DbContext types, models, and LINQ queries work unchanged.
145182

146-
Q: Is there a performance cost for the thread safety?
147-
A: Less than 1ms per write operationnegligible compared to the performance gains from optimized bulk operations and parallel reads.
183+
**Q: Is there a performance cost for the write serialization?**
184+
A: Under 1ms per write operation. The semaphore overhead is negligible compared to actual I/O, and the WAL-mode PRAGMA tuning more than compensates for it on read-heavy workloads.
148185

149-
Q: How does memory usage compare to standard EF Core?
150-
A: Our optimized operations use significantly less memory, especially for bulk inserts and large queries, thanks to streaming and intelligent caching strategies.
186+
**Q: Why do I need `IDbContextFactory` for concurrent workloads?**
187+
A: EF Core's `DbContext` is not thread-safe by design — it tracks state per instance. `IDbContextFactory<T>` creates an independent context per concurrent flow, which both satisfies EF Core's threading model and lets `ThreadSafeEFCore.SQLite` serialize the writes correctly at the SQLite level.
151188

152-
Q: Can I still use SQLite-specific features?
153-
A: Yes. All SQLite features remain accessible while gaining our performance and concurrency enhancements.
189+
**Q: Does this work on network filesystems?**
190+
A: No. SQLite WAL mode requires all connections to be on the same physical host. Do not use this library against a database on NFS, SMB, or any other network-mounted path. Use a client/server database for multi-host deployments.
154191

155-
## Migration: From Slow to Fast
156-
Upgrade path for existing applications:
192+
---
157193

158-
Add NuGet Package → Install-Package EntityFrameworkCore.Sqlite.Concurrency
194+
## Upgrade Guide
159195

160-
Update DbContext Configuration → Change UseSqlite() to UseSqliteWithConcurrency()
196+
```csharp
197+
// 1. Replace raw AddDbContext + UseSqlite:
198+
// Before:
199+
builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlite("Data Source=app.db"));
200+
// After (request-scoped):
201+
builder.Services.AddConcurrentSqliteDbContext<AppDbContext>("Data Source=app.db");
202+
// After (concurrent workloads):
203+
builder.Services.AddConcurrentSqliteDbContextFactory<AppDbContext>("Data Source=app.db");
161204

162-
Replace Bulk Operations → Change loops with SaveChanges() to BulkInsertOptimizedAsync()
205+
// 2. For concurrent workloads, inject IDbContextFactory<AppDbContext>
206+
// and call CreateDbContext() per concurrent operation — not a shared _context.
163207
164-
Remove Custom Retry Logic → Our built-in retry handles everything optimally
208+
// 3. Remove Cache=Shared from any connection string that contains it.
165209
166-
Monitor Performance Gains → Watch your operation times drop significantly
210+
// 4. Remove any custom retry or locking logic — the library handles it.
211+
```
167212

168-
## 🏗️ System Requirements
169-
.NET 10.0+
213+
---
170214

171-
Entity Framework Core 10.0+
215+
## System Requirements
172216

173-
SQLite 3.35.0+
217+
- .NET 10.0+
218+
- Entity Framework Core 10.0+
219+
- Microsoft.Data.Sqlite 10.0+
220+
- SQLite 3.35.0+
174221

175-
## 📄 License
176-
EntityFrameworkCore.Sqlite.Concurrency is licensed under the MIT License. Free for commercial use, open source projects, and enterprise applications.
222+
## License
177223

178-
Stop compromising on SQLite performance. Get enterprise-grade speed and 100% reliability with EntityFrameworkCore.Sqlite.Concurrency—the only EF Core extension that fixes SQLite's limitations while unlocking its full potential.
224+
EntityFrameworkCore.Sqlite.Concurrency is licensed under the MIT License. Free for commercial use, open-source projects, and enterprise applications.

0 commit comments

Comments
 (0)