534b0cde91
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.
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