86883cd5c6
- Refactor migrations and provider to use multiple PostgreSQL schemas, each matching a legacy SQLite database (activitylog, authentication, displaypreferences, library, users). - All tables, foreign keys, and indexes are now schema-qualified; Down migration drops tables by schema. - Provider ensures schemas exist before migrations; entities are mapped to correct schemas in OnModelCreating. - Add support for max-pool-size, min-pool-size, and multiplexing connection options; update logging accordingly. - VACUUM ANALYZE now runs per schema during scheduled optimization. - TruncateAllTablesAsync now truncates tables with schema qualification. - README updated with schema structure, new options, and multiplexing warnings. - CacheDecorator now calls async repository methods using .GetAwaiter().GetResult(), with documentation. - Lays groundwork for full async/await and multiplexing support in the database layer.
325 lines
9.5 KiB
Markdown
325 lines
9.5 KiB
Markdown
# Repository Async Conversion Checklist
|
|
|
|
Use this checklist when converting a repository class from synchronous to asynchronous operations.
|
|
|
|
## 📋 Pre-Conversion Checklist
|
|
|
|
- [ ] **Identify all public methods** in the repository interface
|
|
- [ ] **Check for method dependencies** - what calls these methods?
|
|
- [ ] **Review current tests** - ensure you understand expected behavior
|
|
- [ ] **Create a feature branch** for the conversion
|
|
- [ ] **Document breaking changes** for consumers
|
|
|
|
## 🔄 Conversion Steps
|
|
|
|
### Step 1: Update Interface Definition
|
|
|
|
**File**: `MediaBrowser.Controller\Persistence\I[RepositoryName].cs`
|
|
|
|
- [ ] Add `using System.Threading.Tasks;`
|
|
- [ ] Add `CancellationToken` parameter to all methods
|
|
- [ ] Change return types:
|
|
- `void` → `Task`
|
|
- `T` → `Task<T>`
|
|
- `IEnumerable<T>` → `IAsyncEnumerable<T>` (for streaming) or `Task<List<T>>`
|
|
- [ ] Rename methods to include `Async` suffix
|
|
- [ ] Add XML documentation for cancellation token
|
|
|
|
**Example**:
|
|
```csharp
|
|
// BEFORE
|
|
void SaveUser(User user);
|
|
User? GetUser(Guid id);
|
|
IEnumerable<User> GetAllUsers();
|
|
|
|
// AFTER
|
|
Task SaveUserAsync(User user, CancellationToken cancellationToken = default);
|
|
Task<User?> GetUserAsync(Guid id, CancellationToken cancellationToken = default);
|
|
Task<List<User>> GetAllUsersAsync(CancellationToken cancellationToken = default);
|
|
// OR for streaming:
|
|
IAsyncEnumerable<User> GetAllUsersAsync(CancellationToken cancellationToken = default);
|
|
```
|
|
|
|
### Step 2: Update Implementation
|
|
|
|
**File**: `[Project]\[RepositoryName].cs`
|
|
|
|
#### 2.1 Method Signatures
|
|
- [ ] Add `async` modifier to method declarations
|
|
- [ ] Change return type to `Task` or `Task<T>`
|
|
- [ ] Add `CancellationToken` parameter
|
|
|
|
#### 2.2 Database Context Usage
|
|
- [ ] Change `using` to `await using` for DbContext
|
|
```csharp
|
|
// BEFORE: using var context = _dbProvider.CreateDbContext();
|
|
// AFTER: await using var context = _dbProvider.CreateDbContext();
|
|
```
|
|
|
|
#### 2.3 Transaction Handling
|
|
- [ ] `BeginTransaction()` → `BeginTransactionAsync(cancellationToken)`
|
|
- [ ] `Commit()` → `CommitAsync(cancellationToken)`
|
|
- [ ] `Rollback()` → `RollbackAsync(cancellationToken)`
|
|
- [ ] Change `using` to `await using` for transactions
|
|
|
|
#### 2.4 Query Operations
|
|
- [ ] `ToList()` → `ToListAsync(cancellationToken)`
|
|
- [ ] `ToArray()` → `ToArrayAsync(cancellationToken)`
|
|
- [ ] `ToDictionary()` → `ToDictionaryAsync(cancellationToken)`
|
|
- [ ] `FirstOrDefault()` → `FirstOrDefaultAsync(cancellationToken)`
|
|
- [ ] `First()` → `FirstAsync(cancellationToken)`
|
|
- [ ] `SingleOrDefault()` → `SingleOrDefaultAsync(cancellationToken)`
|
|
- [ ] `Single()` → `SingleAsync(cancellationToken)`
|
|
- [ ] `Any()` → `AnyAsync(cancellationToken)`
|
|
- [ ] `Count()` → `CountAsync(cancellationToken)`
|
|
- [ ] `LongCount()` → `LongCountAsync(cancellationToken)`
|
|
- [ ] `Sum()` → `SumAsync(cancellationToken)`
|
|
- [ ] `Average()` → `AverageAsync(cancellationToken)`
|
|
- [ ] `Min()` → `MinAsync(cancellationToken)`
|
|
- [ ] `Max()` → `MaxAsync(cancellationToken)`
|
|
|
|
#### 2.5 Command Operations
|
|
- [ ] `SaveChanges()` → `SaveChangesAsync(cancellationToken)`
|
|
- [ ] `ExecuteDelete()` → `ExecuteDeleteAsync(cancellationToken)`
|
|
- [ ] `ExecuteUpdate()` → `ExecuteUpdateAsync(cancellationToken)`
|
|
- [ ] `Add()` - no async version, but SaveChanges must be async
|
|
- [ ] `Remove()` - no async version, but SaveChanges must be async
|
|
- [ ] `Update()` - no async version, but SaveChanges must be async
|
|
|
|
#### 2.6 Navigation Properties
|
|
- [ ] `.Load()` → `.LoadAsync(cancellationToken)`
|
|
- [ ] `.Entry().Collection().Load()` → `.Entry().Collection().LoadAsync(cancellationToken)`
|
|
- [ ] `.Include()` - works with async, no change needed
|
|
|
|
#### 2.7 ForEach Loops
|
|
- [ ] Convert to `await foreach` for IAsyncEnumerable
|
|
```csharp
|
|
// BEFORE
|
|
foreach (var item in items)
|
|
{
|
|
// process
|
|
}
|
|
|
|
// AFTER
|
|
await foreach (var item in items.WithCancellation(cancellationToken))
|
|
{
|
|
// process
|
|
}
|
|
```
|
|
|
|
#### 2.8 Helper Methods
|
|
- [ ] Convert all private helper methods that use database to async
|
|
- [ ] Propagate async/await through call chain
|
|
|
|
### Step 3: Update Consumers
|
|
|
|
#### 3.1 Service Layer
|
|
- [ ] Update service interfaces to use `Task` return types
|
|
- [ ] Update service implementations to use `await`
|
|
- [ ] Add `CancellationToken` parameters
|
|
|
|
#### 3.2 API Controllers
|
|
- [ ] Add `async Task<ActionResult>` to controller methods
|
|
- [ ] Add `CancellationToken` parameter (ASP.NET Core will inject it)
|
|
- [ ] Use `await` when calling services
|
|
|
|
**Example**:
|
|
```csharp
|
|
[HttpGet("{id}")]
|
|
public async Task<ActionResult<UserDto>> GetUser(
|
|
[FromRoute] Guid id,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var user = await _userRepository.GetUserAsync(id, cancellationToken);
|
|
return user is null ? NotFound() : Ok(_mapper.Map<UserDto>(user));
|
|
}
|
|
```
|
|
|
|
#### 3.3 Background Services
|
|
- [ ] Ensure `ExecuteAsync` properly awaits all operations
|
|
- [ ] Pass cancellation token through the chain
|
|
|
|
### Step 4: Update Tests
|
|
|
|
#### 4.1 Test Method Signatures
|
|
- [ ] Change test methods to `async Task`
|
|
- [ ] Add `await` before repository calls
|
|
|
|
#### 4.2 Mock Setup
|
|
- [ ] Update mocks to return `Task` or `Task<T>`
|
|
```csharp
|
|
// BEFORE
|
|
_mockRepository.Setup(x => x.GetUser(It.IsAny<Guid>()))
|
|
.Returns(user);
|
|
|
|
// AFTER
|
|
_mockRepository.Setup(x => x.GetUserAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(user);
|
|
```
|
|
|
|
#### 4.3 Test Assertions
|
|
- [ ] Use `await` for async operations in assertions
|
|
- [ ] Test cancellation scenarios
|
|
|
|
**Example Test**:
|
|
```csharp
|
|
[Fact]
|
|
public async Task GetUserAsync_ValidId_ReturnsUser()
|
|
{
|
|
// Arrange
|
|
var userId = Guid.NewGuid();
|
|
var expectedUser = new User { Id = userId };
|
|
_mockRepository
|
|
.Setup(x => x.GetUserAsync(userId, It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(expectedUser);
|
|
|
|
// Act
|
|
var result = await _service.GetUserAsync(userId, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal(userId, result.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetUserAsync_Cancelled_ThrowsOperationCanceledException()
|
|
{
|
|
// Arrange
|
|
var cts = new CancellationTokenSource();
|
|
cts.Cancel();
|
|
|
|
// Act & Assert
|
|
await Assert.ThrowsAsync<OperationCanceledException>(
|
|
() => _service.GetUserAsync(Guid.NewGuid(), cts.Token)
|
|
);
|
|
}
|
|
```
|
|
|
|
## 🔍 Code Review Checklist
|
|
|
|
Before submitting PR:
|
|
|
|
- [ ] All synchronous database operations converted
|
|
- [ ] `CancellationToken` passed through entire call chain
|
|
- [ ] No `Task.Result` or `.Wait()` calls (causes deadlocks)
|
|
- [ ] No `async void` methods (except event handlers)
|
|
- [ ] Proper `ConfigureAwait(false)` usage in library code (optional in ASP.NET Core)
|
|
- [ ] All tests updated and passing
|
|
- [ ] No performance regressions
|
|
- [ ] Memory usage acceptable
|
|
- [ ] Documentation updated
|
|
|
|
## ⚡ Performance Optimization Opportunities
|
|
|
|
Consider these optimizations during conversion:
|
|
|
|
### 1. Parallel Operations
|
|
When operations are independent, use `Task.WhenAll`:
|
|
|
|
```csharp
|
|
// Sequential (slow)
|
|
await DeleteAncestorsAsync(id, cancellationToken);
|
|
await DeleteImagesAsync(id, cancellationToken);
|
|
await DeleteChaptersAsync(id, cancellationToken);
|
|
|
|
// Parallel (fast)
|
|
await Task.WhenAll(
|
|
DeleteAncestorsAsync(id, cancellationToken),
|
|
DeleteImagesAsync(id, cancellationToken),
|
|
DeleteChaptersAsync(id, cancellationToken)
|
|
);
|
|
```
|
|
|
|
### 2. Streaming Results
|
|
For large result sets, use `IAsyncEnumerable<T>`:
|
|
|
|
```csharp
|
|
public async IAsyncEnumerable<User> GetAllUsersAsync(
|
|
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
|
{
|
|
await using var context = _dbProvider.CreateDbContext();
|
|
|
|
await foreach (var user in context.Users.AsAsyncEnumerable()
|
|
.WithCancellation(cancellationToken))
|
|
{
|
|
yield return user;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Batch Operations
|
|
Use `ExecuteUpdateAsync` / `ExecuteDeleteAsync` instead of loading then saving:
|
|
|
|
```csharp
|
|
// Slow - loads all into memory
|
|
var items = await context.Items.Where(x => x.IsOld).ToListAsync();
|
|
context.Items.RemoveRange(items);
|
|
await context.SaveChangesAsync();
|
|
|
|
// Fast - executes in database
|
|
await context.Items
|
|
.Where(x => x.IsOld)
|
|
.ExecuteDeleteAsync(cancellationToken);
|
|
```
|
|
|
|
## 🐛 Common Pitfalls to Avoid
|
|
|
|
### ❌ DON'T: Use Task.Result or .Wait()
|
|
```csharp
|
|
// WRONG - causes deadlocks
|
|
var user = GetUserAsync(id).Result;
|
|
```
|
|
|
|
### ❌ DON'T: Forget cancellation token
|
|
```csharp
|
|
// WRONG - can't cancel long-running operations
|
|
public async Task<User> GetUserAsync(Guid id)
|
|
{
|
|
return await context.Users.FirstOrDefaultAsync(x => x.Id == id);
|
|
}
|
|
```
|
|
|
|
### ❌ DON'T: Use async void
|
|
```csharp
|
|
// WRONG - exceptions can't be caught
|
|
public async void SaveUser(User user) { }
|
|
```
|
|
|
|
### ✅ DO: Proper async pattern
|
|
```csharp
|
|
// CORRECT
|
|
public async Task<User?> GetUserAsync(Guid id, CancellationToken cancellationToken = default)
|
|
{
|
|
await using var context = _dbProvider.CreateDbContext();
|
|
return await context.Users
|
|
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
|
}
|
|
```
|
|
|
|
## 📝 Commit Message Template
|
|
|
|
```
|
|
feat(repository): Convert [RepositoryName] to async
|
|
|
|
- Updated I[RepositoryName] interface with async signatures
|
|
- Converted all database operations to async/await
|
|
- Added CancellationToken support throughout
|
|
- Updated all tests to async
|
|
- Updated consumers in service and controller layers
|
|
|
|
BREAKING CHANGE: All [RepositoryName] methods are now async and require await.
|
|
Migration guide: [link to documentation]
|
|
|
|
Closes #[issue-number]
|
|
```
|
|
|
|
## 🎯 Success Criteria
|
|
|
|
- [ ] All tests passing
|
|
- [ ] No compiler warnings
|
|
- [ ] Code coverage maintained or improved
|
|
- [ ] No performance regressions
|
|
- [ ] Documentation updated
|
|
- [ ] Migration guide provided
|
|
- [ ] Breaking changes documented
|