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.
9.5 KiB
9.5 KiB
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
CancellationTokenparameter to all methods - Change return types:
void→TaskT→Task<T>IEnumerable<T>→IAsyncEnumerable<T>(for streaming) orTask<List<T>>
- Rename methods to include
Asyncsuffix - Add XML documentation for cancellation token
Example:
// 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
asyncmodifier to method declarations - Change return type to
TaskorTask<T> - Add
CancellationTokenparameter
2.2 Database Context Usage
- Change
usingtoawait usingfor DbContext// 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
usingtoawait usingfor 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 asyncRemove()- no async version, but SaveChanges must be asyncUpdate()- 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 foreachfor IAsyncEnumerable// 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
Taskreturn types - Update service implementations to use
await - Add
CancellationTokenparameters
3.2 API Controllers
- Add
async Task<ActionResult>to controller methods - Add
CancellationTokenparameter (ASP.NET Core will inject it) - Use
awaitwhen calling services
Example:
[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
ExecuteAsyncproperly 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
awaitbefore repository calls
4.2 Mock Setup
- Update mocks to return
TaskorTask<T>// 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
awaitfor async operations in assertions - Test cancellation scenarios
Example Test:
[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
CancellationTokenpassed through entire call chain- No
Task.Resultor.Wait()calls (causes deadlocks) - No
async voidmethods (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:
// 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>:
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:
// 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()
// WRONG - causes deadlocks
var user = GetUserAsync(id).Result;
❌ DON'T: Forget cancellation token
// 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
// WRONG - exceptions can't be caught
public async void SaveUser(User user) { }
✅ DO: Proper async pattern
// 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