Files
pgsql-jellyfin/docs/ASYNC_CONVERSION_CHECKLIST.md
T
wjones 534b0cde91 Ensure initial user exists before startup wizard completes
Add logic to initialize at least one user if the startup wizard is not completed, both during application startup and in the startup user API. After user creation, reload the user entity with all related navigation properties to ensure the in-memory user object is fully populated. Also update build and publish metadata files.
2026-02-23 15:42:19 -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