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

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