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

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 CancellationToken parameter to all methods
  • Change return types:
    • voidTask
    • TTask<T>
    • IEnumerable<T>IAsyncEnumerable<T> (for streaming) or Task<List<T>>
  • Rename methods to include Async suffix
  • 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 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
    // 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
    // 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:

[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>
    // 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:

[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:

// 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