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.7 KiB
9.7 KiB
Async/Await Quick Reference for Jellyfin Database Conversion
🔄 Common Conversions
| Synchronous | Asynchronous | Notes |
|---|---|---|
context.SaveChanges() |
await context.SaveChangesAsync(cancellationToken) |
Always pass cancellation token |
query.ToList() |
await query.ToListAsync(cancellationToken) |
Materializes full list |
query.ToArray() |
await query.ToArrayAsync(cancellationToken) |
Materializes full array |
query.FirstOrDefault() |
await query.FirstOrDefaultAsync(cancellationToken) |
Returns null if not found |
query.First() |
await query.FirstAsync(cancellationToken) |
Throws if not found |
query.SingleOrDefault() |
await query.SingleOrDefaultAsync(cancellationToken) |
Throws if multiple found |
query.Any() |
await query.AnyAsync(cancellationToken) |
Existence check |
query.Count() |
await query.CountAsync(cancellationToken) |
Count results |
query.Sum(x => x.Value) |
await query.SumAsync(x => x.Value, cancellationToken) |
Aggregate |
query.ExecuteDelete() |
await query.ExecuteDeleteAsync(cancellationToken) |
Bulk delete |
query.ExecuteUpdate(...) |
await query.ExecuteUpdateAsync(..., cancellationToken) |
Bulk update |
context.Database.BeginTransaction() |
await context.Database.BeginTransactionAsync(cancellationToken) |
Start transaction |
transaction.Commit() |
await transaction.CommitAsync(cancellationToken) |
Commit transaction |
transaction.Rollback() |
await transaction.RollbackAsync(cancellationToken) |
Rollback transaction |
using var context = ... |
await using var context = ... |
Async disposal |
foreach (var item in items) |
await foreach (var item in items) |
Async enumeration |
📝 Method Signature Changes
// BEFORE: Synchronous
public void SaveUser(User user)
{
using var context = _dbProvider.CreateDbContext();
context.Users.Add(user);
context.SaveChanges();
}
// AFTER: Asynchronous
public async Task SaveUserAsync(User user, CancellationToken cancellationToken = default)
{
await using var context = _dbProvider.CreateDbContext();
context.Users.Add(user);
await context.SaveChangesAsync(cancellationToken);
}
🎯 Interface Changes
// BEFORE
public interface IUserRepository
{
void SaveUser(User user);
User? GetUser(Guid id);
List<User> GetAllUsers();
void DeleteUser(Guid id);
int GetUserCount();
}
// AFTER
public interface IUserRepository
{
Task SaveUserAsync(User user, CancellationToken cancellationToken = default);
Task<User?> GetUserAsync(Guid id, CancellationToken cancellationToken = default);
Task<List<User>> GetAllUsersAsync(CancellationToken cancellationToken = default);
Task DeleteUserAsync(Guid id, CancellationToken cancellationToken = default);
Task<int> GetUserCountAsync(CancellationToken cancellationToken = default);
}
🔧 Controller Changes
// BEFORE: Synchronous controller
[HttpGet("{id}")]
public ActionResult<UserDto> GetUser([FromRoute] Guid id)
{
var user = _userRepository.GetUser(id);
return user is null ? NotFound() : Ok(user);
}
[HttpPost]
public ActionResult<UserDto> CreateUser([FromBody] CreateUserRequest request)
{
var user = new User { ... };
_userRepository.SaveUser(user);
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}
// AFTER: Asynchronous controller
[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(user);
}
[HttpPost]
public async Task<ActionResult<UserDto>> CreateUser(
[FromBody] CreateUserRequest request,
CancellationToken cancellationToken)
{
var user = new User { ... };
await _userRepository.SaveUserAsync(user, cancellationToken);
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}
🧪 Test Changes
// BEFORE: Synchronous test
[Fact]
public void GetUser_ValidId_ReturnsUser()
{
var user = _repository.GetUser(_userId);
Assert.NotNull(user);
}
// AFTER: Asynchronous test
[Fact]
public async Task GetUserAsync_ValidId_ReturnsUser()
{
var user = await _repository.GetUserAsync(_userId, CancellationToken.None);
Assert.NotNull(user);
}
// Test cancellation
[Fact]
public async Task GetUserAsync_Cancelled_ThrowsOperationCanceledException()
{
var cts = new CancellationTokenSource();
cts.Cancel();
await Assert.ThrowsAsync<OperationCanceledException>(
() => _repository.GetUserAsync(_userId, cts.Token)
);
}
🔍 Mock Setup Changes
// BEFORE: Synchronous mock
_mockRepository
.Setup(x => x.GetUser(It.IsAny<Guid>()))
.Returns(user);
// AFTER: Asynchronous mock
_mockRepository
.Setup(x => x.GetUserAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(user);
// For void methods
_mockRepository
.Setup(x => x.SaveUserAsync(It.IsAny<User>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
// For exceptions
_mockRepository
.Setup(x => x.GetUserAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException());
⚡ Parallel Operations
// Sequential (slow)
await DeleteUserDataAsync(userId, cancellationToken);
await DeleteUserSessionsAsync(userId, cancellationToken);
await DeleteUserDevicesAsync(userId, cancellationToken);
// Parallel (fast) - only when operations are independent!
await Task.WhenAll(
DeleteUserDataAsync(userId, cancellationToken),
DeleteUserSessionsAsync(userId, cancellationToken),
DeleteUserDevicesAsync(userId, cancellationToken)
);
📊 Streaming Large Results
// Old way - loads everything into memory
public async Task<List<BaseItem>> GetAllItemsAsync(CancellationToken cancellationToken)
{
await using var context = _dbProvider.CreateDbContext();
return await context.BaseItems.ToListAsync(cancellationToken);
}
// New way - streams results
public async IAsyncEnumerable<BaseItem> GetAllItemsAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await using var context = _dbProvider.CreateDbContext();
await foreach (var item in context.BaseItems
.AsAsyncEnumerable()
.WithCancellation(cancellationToken))
{
yield return item;
}
}
// Consumer code
await foreach (var item in repository.GetAllItemsAsync(cancellationToken))
{
// Process item without loading entire collection into memory
ProcessItem(item);
}
🚫 Anti-Patterns to Avoid
❌ Blocking on async code
// WRONG - causes deadlocks
var user = GetUserAsync(id).Result;
var user = GetUserAsync(id).GetAwaiter().GetResult();
GetUserAsync(id).Wait();
❌ Async void
// WRONG - exceptions can't be caught
public async void SaveUserAsync(User user) { ... }
❌ Not passing cancellation token
// WRONG - can't cancel
public async Task<User> GetUserAsync(Guid id)
{
return await context.Users.FirstOrDefaultAsync(x => x.Id == id);
// Should be: FirstOrDefaultAsync(x => x.Id == id, cancellationToken)
}
❌ Using Task.Run for database operations
// WRONG - creates extra threads unnecessarily
public async Task<User> GetUserAsync(Guid id)
{
return await Task.Run(() => _repository.GetUser(id));
}
✅ Best Practices
1. Always add CancellationToken parameter
public async Task DoSomethingAsync(
CancellationToken cancellationToken = default)
2. Use await using for IAsyncDisposable
await using var context = _dbProvider.CreateDbContext();
await using var transaction = await context.Database.BeginTransactionAsync();
3. Propagate cancellation tokens
public async Task SaveUserAsync(User user, CancellationToken cancellationToken)
{
await using var context = _dbProvider.CreateDbContext();
context.Users.Add(user);
await context.SaveChangesAsync(cancellationToken); // ✅ Pass it through
}
4. ConfigureAwait(false) in library code (optional)
// Library code that doesn't need synchronization context
var user = await context.Users
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken)
.ConfigureAwait(false);
// Note: ASP.NET Core doesn't need ConfigureAwait(false)
5. Use ValueTask for hot paths
// For frequently called methods that often complete synchronously
public ValueTask<User?> GetCachedUserAsync(Guid id)
{
if (_cache.TryGetValue(id, out var user))
{
return new ValueTask<User?>(user);
}
return new ValueTask<User?>(_repository.GetUserAsync(id));
}
📏 Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Async methods | End with Async suffix |
GetUserAsync |
| Sync methods (old) | No suffix | GetUser |
| Interface methods | Include Async suffix |
Task<User> GetUserAsync(...) |
| CancellationToken param | Always last parameter | GetUserAsync(Guid id, CancellationToken token) |
| CancellationToken default | = default |
CancellationToken token = default |
🔗 Useful Links
- Async/Await Best Practices
- EF Core Async Queries
- Task-based Asynchronous Pattern
- Cancellation Tokens
Quick Tip: Use Ctrl+F to search this document for specific conversions!