Files
pgsql-jellyfin/ASYNC_CONVERSION_CHECKLIST.md
T
wjones 86883cd5c6 Refactor PostgreSQL provider: multi-schema & async prep
- 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.
2026-02-23 09:38:22 -05:00

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