Skip to content

Commit 28e40a1

Browse files
Inital Commit from private to public repository
1 parent f2f4283 commit 28e40a1

10 files changed

Lines changed: 1180 additions & 0 deletions
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
# ThreadSafeEFCore.SQLite
2+
3+
**Eliminate SQLite "database is locked" errors with simple, thread-safe database operations.**
4+
5+
## The Problem It Solves
6+
7+
If you've ever used SQLite with Entity Framework Core in a multi-threaded application, you've probably encountered the dreaded `Microsoft.Data.Sqlite.SqliteException: SQLite Error 5: 'database is locked'`. SQLite only allows one writer at a time, causing failures when multiple threads try to write simultaneously.
8+
9+
**ThreadSafeEFCore.SQLite** solves this completely. It automatically queues write operations while allowing unlimited parallel reads.
10+
11+
## Installation
12+
13+
```bash
14+
dotnet add package ThreadSafeEFCore.SQLite
15+
```
16+
17+
## Quick Start
18+
19+
### 1. Create Your DbContext Normally
20+
21+
```csharp
22+
public class BlogDbContext : DbContext
23+
{
24+
public DbSet<Post> Posts { get; set; }
25+
public DbSet<Comment> Comments { get; set; }
26+
27+
protected override void OnModelCreating(ModelBuilder modelBuilder)
28+
{
29+
// Your normal configuration
30+
modelBuilder.Entity<Post>()
31+
.HasIndex(p => p.Slug)
32+
.IsUnique();
33+
}
34+
}
35+
```
36+
37+
### 2. Configure with One Line of Code
38+
39+
In your `Program.cs` or startup configuration:
40+
41+
```csharp
42+
// Simple configuration
43+
builder.Services.AddDbContext<BlogDbContext>(options =>
44+
options.UseSqliteWithConcurrency("Data Source=blog.db"));
45+
```
46+
47+
Or with custom options:
48+
49+
```csharp
50+
builder.Services.AddDbContext<BlogDbContext>(options =>
51+
options.UseSqliteWithConcurrency(
52+
"Data Source=blog.db",
53+
sqliteOptions =>
54+
{
55+
sqliteOptions.UseWriteQueue = true; // Enable write serialization
56+
sqliteOptions.BusyTimeout = TimeSpan.FromSeconds(30);
57+
sqliteOptions.MaxRetryAttempts = 5;
58+
}));
59+
```
60+
61+
## Basic Usage Examples
62+
63+
### Writing Data (Automatically Thread-Safe)
64+
65+
```csharp
66+
public class PostService
67+
{
68+
private readonly BlogDbContext _context;
69+
70+
public PostService(BlogDbContext context)
71+
{
72+
_context = context;
73+
}
74+
75+
public async Task CreatePostAsync(string title, string content)
76+
{
77+
// Write operations are automatically serialized
78+
// No need to worry about locks or concurrency issues
79+
var post = new Post
80+
{
81+
Title = title,
82+
Content = content,
83+
Slug = GenerateSlug(title),
84+
CreatedAt = DateTime.UtcNow
85+
};
86+
87+
_context.Posts.Add(post);
88+
await _context.SaveChangesAsync(); // Thread-safe!
89+
}
90+
91+
public async Task AddCommentAsync(int postId, string author, string text)
92+
{
93+
var comment = new Comment
94+
{
95+
PostId = postId,
96+
Author = author,
97+
Text = text,
98+
PostedAt = DateTime.UtcNow
99+
};
100+
101+
// Multiple threads can call this simultaneously
102+
// Writes are queued automatically
103+
_context.Comments.Add(comment);
104+
await _context.SaveChangesAsync();
105+
}
106+
}
107+
```
108+
109+
### Reading Data (Fully Parallel)
110+
111+
```csharp
112+
public class PostService
113+
{
114+
// ... constructor and other methods ...
115+
116+
public async Task<PostSummary> GetPostSummaryAsync(int postId)
117+
{
118+
// Reads execute in parallel - no blocking!
119+
var postTask = _context.Posts
120+
.FirstOrDefaultAsync(p => p.Id == postId);
121+
122+
var commentsTask = _context.Comments
123+
.Where(c => c.PostId == postId)
124+
.OrderByDescending(c => c.PostedAt)
125+
.Take(10)
126+
.ToListAsync();
127+
128+
var countTask = _context.Comments
129+
.CountAsync(c => c.PostId == postId);
130+
131+
// All reads execute simultaneously
132+
await Task.WhenAll(postTask, commentsTask, countTask);
133+
134+
return new PostSummary
135+
{
136+
Post = await postTask,
137+
RecentComments = await commentsTask,
138+
TotalComments = await countTask
139+
};
140+
}
141+
}
142+
```
143+
144+
### Bulk Operations Made Easy
145+
146+
```csharp
147+
public class ImportService
148+
{
149+
private readonly BlogDbContext _context;
150+
151+
public async Task ImportPostsAsync(List<Post> posts)
152+
{
153+
// Bulk insert with automatic concurrency handling
154+
await _context.BulkInsertOptimizedAsync(posts);
155+
156+
// Or use the retry wrapper for extra safety
157+
await _context.ExecuteWithRetryAsync(async ctx =>
158+
{
159+
// Complex import logic
160+
await ProcessAndSavePostsAsync(ctx, posts);
161+
});
162+
}
163+
}
164+
```
165+
166+
## Real-World Scenario: Background Processing
167+
168+
Imagine a scenario where multiple background workers are processing tasks:
169+
170+
```csharp
171+
// WITHOUT ThreadSafeEFCore.SQLite - This would fail with "database is locked"
172+
public class TaskProcessor
173+
{
174+
public async Task ProcessTasksConcurrently()
175+
{
176+
var tasks = Enumerable.Range(1, 10)
177+
.Select(i => ProcessSingleTaskAsync(i));
178+
179+
await Task.WhenAll(tasks); // 💥 Database locked errors!
180+
}
181+
}
182+
183+
// WITH ThreadSafeEFCore.SQLite - Just works!
184+
public class TaskProcessor
185+
{
186+
private readonly AppDbContext _context;
187+
188+
public async Task ProcessTasksConcurrently()
189+
{
190+
var tasks = Enumerable.Range(1, 10)
191+
.Select(i => ProcessSingleTaskAsync(i));
192+
193+
await Task.WhenAll(tasks); // ✅ All tasks complete successfully
194+
}
195+
196+
private async Task ProcessSingleTaskAsync(int taskId)
197+
{
198+
// Each task writes to the database
199+
var result = await PerformWorkAsync(taskId);
200+
201+
// The package automatically queues these writes
202+
_context.TaskResults.Add(new TaskResult
203+
{
204+
TaskId = taskId,
205+
Result = result,
206+
CompletedAt = DateTime.UtcNow
207+
});
208+
209+
await _context.SaveChangesAsync();
210+
}
211+
}
212+
```
213+
214+
## Factory Pattern (When Not Using Dependency Injection)
215+
216+
```csharp
217+
// Create contexts manually when needed
218+
var dbContext = ThreadSafeFactory.CreateContext<BlogDbContext>(
219+
"Data Source=blog.db",
220+
options => options.UseWriteQueue = true);
221+
222+
// Use it
223+
await dbContext.Posts.AddAsync(new Post { Title = "Hello World" });
224+
await dbContext.SaveChangesAsync();
225+
```
226+
227+
## Error Handling and Retries
228+
229+
The package includes built-in retry logic, but you can add your own:
230+
231+
```csharp
232+
public async Task UpdatePostWithRetryAsync(int postId, string newContent)
233+
{
234+
try
235+
{
236+
await _context.ExecuteWithRetryAsync(async ctx =>
237+
{
238+
var post = await ctx.Posts.FindAsync(postId);
239+
post.Content = newContent;
240+
post.UpdatedAt = DateTime.UtcNow;
241+
await ctx.SaveChangesAsync();
242+
}, maxRetries: 5);
243+
}
244+
catch (Exception ex)
245+
{
246+
// Handle persistent failures
247+
_logger.LogError(ex, "Failed to update post {PostId}", postId);
248+
throw;
249+
}
250+
}
251+
```
252+
253+
## Configuration Options
254+
255+
| Option | Default | Description |
256+
|--------|---------|-------------|
257+
| `UseWriteQueue` | `true` | Automatically queue write operations |
258+
| `BusyTimeout` | 30 seconds | How long to wait if database is busy |
259+
| `MaxRetryAttempts` | 3 | Number of retries for busy errors |
260+
| `CommandTimeout` | 300 seconds | SQL command timeout |
261+
| `EnableWalCheckpointManagement` | `true` | Automatically manage WAL checkpoints |
262+
263+
## Best Practices
264+
265+
1. **Use Dependency Injection** when possible for automatic context management
266+
2. **Keep write transactions short** - queue your data and write quickly
267+
3. **Use `BulkInsertOptimizedAsync`** for importing large amounts of data
268+
4. **Enable WAL mode** (already done by default) for better concurrency
269+
5. **Monitor performance** with the built-in diagnostics when needed
270+
271+
## What Makes It Different
272+
273+
| Traditional EF Core + SQLite | ThreadSafeEFCore.SQLite |
274+
|------------------------------|-------------------------|
275+
|`database is locked` errors | ✅ Automatic write queuing |
276+
| ❌ Manual retry logic needed | ✅ Built-in exponential backoff |
277+
| ❌ Read blocking during writes | ✅ True parallel reads |
278+
| ❌ Complex synchronization code | ✅ Simple, intuitive API |
279+
280+
## Summary
281+
282+
**ThreadSafeEFCore.SQLite** lets you write multi-threaded applications as if SQLite had full concurrent write support. Just change your `UseSqlite()` call to `UseSqliteWithConcurrency()` and forget about database locks forever.
283+
284+
```csharp
285+
// Before: Constant locking issues
286+
options.UseSqlite("Data Source=app.db");
287+
288+
// After: Thread-safe by default
289+
options.UseSqliteWithConcurrency("Data Source=app.db");
290+
```
291+
292+
Write your application logic, not concurrency workarounds.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
#if INCLUDEMEMORYPACK
3+
using MemoryPack;
4+
5+
namespace EFCore.Sqlite.Concurrency
6+
{
7+
public static class MemoryPackExtensions
8+
{
9+
public static byte[] ToMemoryPack<T>(this T obj) => MemoryPackSerializer.Serialize(obj);
10+
public static T FromMemoryPack<T>(this byte[] data) => MemoryPackSerializer.Deserialize<T>(data);
11+
12+
public static async Task BulkInsertWithMemoryPack<T>(
13+
this DbContext context,
14+
IEnumerable<T> entities,
15+
CancellationToken ct = default) where T : class
16+
{
17+
// Store serialized entities in a separate table for fast reads
18+
var serialized = entities.Select(e => new SerializedEntity
19+
{
20+
Id = Guid.NewGuid(),
21+
TypeName = typeof(T).FullName!,
22+
Data = e.ToMemoryPack(),
23+
Created = DateTime.UtcNow
24+
}).ToList();
25+
26+
await context.BulkInsertOptimizedAsync(serialized, ct);
27+
}
28+
}
29+
30+
[MemoryPackable]
31+
public partial class SerializedEntity
32+
{
33+
public Guid Id { get; set; }
34+
public string TypeName { get; set; } = string.Empty;
35+
36+
[MemoryPackInclude]
37+
public byte[] Data { get; set; } = Array.Empty<byte>();
38+
39+
public DateTime Created { get; set; }
40+
}
41+
}
42+
#endif
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using EFCore.Sqlite.Concurrency.Models;
2+
using Microsoft.EntityFrameworkCore;
3+
using Microsoft.Extensions.DependencyInjection;
4+
5+
namespace EFCore.Sqlite.Concurrency.ExtensionMethods;
6+
7+
public static class SqliteConcurrencyServiceCollectionExtensions
8+
{
9+
public static IServiceCollection AddConcurrentSqliteDbContext<TContext>(
10+
this IServiceCollection services,
11+
string connectionString,
12+
Action<SqliteConcurrencyOptions>? configure = null,
13+
ServiceLifetime contextLifetime = ServiceLifetime.Scoped)
14+
where TContext : DbContext
15+
{
16+
services.AddDbContext<TContext>((provider, options) =>
17+
{
18+
options.UseSqliteWithConcurrency(connectionString, configure);
19+
}, contextLifetime);
20+
21+
return services;
22+
}
23+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace EFCore.Sqlite.Concurrency.Models;
2+
3+
public class SqliteConcurrencyOptions
4+
{
5+
public bool UseWriteQueue { get; set; } = true;
6+
public int MaxRetryAttempts { get; set; } = 3;
7+
public TimeSpan BusyTimeout { get; set; } = TimeSpan.FromSeconds(30);
8+
public bool EnableWalCheckpointManagement { get; set; } = true;
9+
public int CommandTimeout { get; set; } = 300; // 5 minutes
10+
public int WalAutoCheckpoint { get; set; } = 1000;
11+
public bool EnableMemoryPack { get; set; } = false;
12+
}

0 commit comments

Comments
 (0)