Files
pgsql-jellyfin/docs/ASYNC_CONVERSION_EXAMPLE.cs
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

263 lines
9.0 KiB
C#

// Example: Converting DeleteItem from Sync to Async
// File: Jellyfin.Server.Implementations\Item\BaseItemRepository.cs
// ==========================================
// BEFORE (Current Synchronous Version)
// ==========================================
public void DeleteItem(params IReadOnlyList<Guid> ids)
{
if (ids is null || ids.Count == 0 || ids.Any(f => f.Equals(PlaceholderId)))
{
throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(ids));
}
using var context = _dbProvider.CreateDbContext();
using var transaction = context.Database.BeginTransaction();
var date = (DateTime?)DateTime.UtcNow;
var relatedItems = ids.SelectMany(f => TraverseHirachyDown(f, context)).ToArray();
// Remove conflicting UserData
context.UserData
.Join(
context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId),
placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
userData => new { userData.UserId, userData.CustomDataKey },
(placeholder, userData) => placeholder)
.Where(e => e.ItemId == PlaceholderId)
.ExecuteDelete();
// Detach user data
context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId)
.ExecuteUpdate(e => e
.SetProperty(f => f.RetentionDate, date)
.SetProperty(f => f.ItemId, PlaceholderId));
// Delete related entities
context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.BaseItems.WhereOneOrMany(relatedItems, e => e.Id).ExecuteDelete();
var peopleIds = context.PeopleBaseItemMap
.WhereOneOrMany(relatedItems, e => e.ItemId)
.Select(f => f.PeopleId)
.Distinct()
.ToArray(); // 🔴 SYNC: ToArray()
context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.Peoples.WhereOneOrMany(peopleIds, e => e.Id)
.Where(e => e.BaseItems!.Count == 0)
.ExecuteDelete();
context.SaveChanges(); // 🔴 SYNC: SaveChanges()
transaction.Commit(); // 🔴 SYNC: Commit()
}
// ==========================================
// AFTER (Async Version)
// ==========================================
public async Task DeleteItemAsync(IReadOnlyList<Guid> ids, CancellationToken cancellationToken = default)
{
if (ids is null || ids.Count == 0 || ids.Any(f => f.Equals(PlaceholderId)))
{
throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(ids));
}
await using var context = _dbProvider.CreateDbContext(); // ✅ await using
await using var transaction = await context.Database.BeginTransactionAsync(cancellationToken); // ✅ BeginTransactionAsync
var date = (DateTime?)DateTime.UtcNow;
// ✅ Convert TraverseHirachyDown to async
var relatedItems = await GetRelatedItemsAsync(ids, context, cancellationToken);
// Remove conflicting UserData
await context.UserData
.Join(
context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId),
placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
userData => new { userData.UserId, userData.CustomDataKey },
(placeholder, userData) => placeholder)
.Where(e => e.ItemId == PlaceholderId)
.ExecuteDeleteAsync(cancellationToken); // ✅ ExecuteDeleteAsync
// Detach user data
await context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId)
.ExecuteUpdateAsync(e => e
.SetProperty(f => f.RetentionDate, date)
.SetProperty(f => f.ItemId, PlaceholderId),
cancellationToken); // ✅ ExecuteUpdateAsync
// Delete related entities (can be done in parallel if no dependencies)
await Task.WhenAll(
context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId)
.ExecuteDeleteAsync(cancellationToken),
context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId)
.ExecuteDeleteAsync(cancellationToken)
);
// Delete base items
await context.BaseItems.WhereOneOrMany(relatedItems, e => e.Id)
.ExecuteDeleteAsync(cancellationToken);
// Get people IDs to check for orphans
var peopleIds = await context.PeopleBaseItemMap
.WhereOneOrMany(relatedItems, e => e.ItemId)
.Select(f => f.PeopleId)
.Distinct()
.ToArrayAsync(cancellationToken); // ✅ ToArrayAsync
await context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId)
.ExecuteDeleteAsync(cancellationToken);
await context.Peoples.WhereOneOrMany(peopleIds, e => e.Id)
.Where(e => e.BaseItems!.Count == 0)
.ExecuteDeleteAsync(cancellationToken);
await context.SaveChangesAsync(cancellationToken); // ✅ SaveChangesAsync
await transaction.CommitAsync(cancellationToken); // ✅ CommitAsync
}
// Helper method - also converted to async
private async Task<Guid[]> GetRelatedItemsAsync(
IReadOnlyList<Guid> ids,
JellyfinDbContext context,
CancellationToken cancellationToken)
{
var relatedItems = new HashSet<Guid>();
foreach (var id in ids)
{
var related = await TraverseHirachyDownAsync(id, context, cancellationToken);
foreach (var item in related)
{
relatedItems.Add(item);
}
}
return relatedItems.ToArray();
}
// ==========================================
// Interface Change Required
// ==========================================
// In IItemRepository.cs:
// BEFORE:
void DeleteItem(params IReadOnlyList<Guid> ids);
// AFTER:
Task DeleteItemAsync(IReadOnlyList<Guid> ids, CancellationToken cancellationToken = default);
// ==========================================
// Calling Code Changes
// ==========================================
// In LibraryManager.cs or similar:
// BEFORE:
public void DeleteItem(BaseItem item)
{
_itemRepository.DeleteItem(new[] { item.Id });
}
// AFTER:
public async Task DeleteItemAsync(BaseItem item, CancellationToken cancellationToken = default)
{
await _itemRepository.DeleteItemAsync(new[] { item.Id }, cancellationToken);
}
// ==========================================
// API Controller Changes
// ==========================================
// In ItemsController.cs:
// BEFORE:
[HttpDelete("{id}")]
public ActionResult DeleteItem([FromRoute] Guid id)
{
var item = _libraryManager.GetItem(id);
if (item is null)
{
return NotFound();
}
_libraryManager.DeleteItem(item);
return NoContent();
}
// AFTER:
[HttpDelete("{id}")]
public async Task<ActionResult> DeleteItem(
[FromRoute] Guid id,
CancellationToken cancellationToken)
{
var item = await _libraryManager.GetItemAsync(id, cancellationToken);
if (item is null)
{
return NotFound();
}
await _libraryManager.DeleteItemAsync(item, cancellationToken);
return NoContent();
}
// ==========================================
// Performance Optimization: Parallel Deletes
// ==========================================
// When deleting multiple independent entities, use Task.WhenAll:
// BEFORE (Sequential):
context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.BaseItemImageInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.Chapters.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
// AFTER (Parallel):
await Task.WhenAll(
context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId)
.ExecuteDeleteAsync(cancellationToken),
context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId)
.ExecuteDeleteAsync(cancellationToken),
context.BaseItemImageInfos.WhereOneOrMany(relatedItems, e => e.ItemId)
.ExecuteDeleteAsync(cancellationToken),
context.Chapters.WhereOneOrMany(relatedItems, e => e.ItemId)
.ExecuteDeleteAsync(cancellationToken)
);
// ⚡ This can significantly improve performance for operations with many independent deletes!
// ==========================================
// Testing the Async Version
// ==========================================
[Fact]
public async Task DeleteItemAsync_ValidId_DeletesSuccessfully()
{
// Arrange
var itemId = Guid.NewGuid();
await _repository.SaveItemAsync(new BaseItem { Id = itemId }, CancellationToken.None);
// Act
await _repository.DeleteItemAsync(new[] { itemId }, CancellationToken.None);
// Assert
var item = await _repository.RetrieveItemAsync(itemId, CancellationToken.None);
Assert.Null(item);
}
[Fact]
public async Task DeleteItemAsync_Cancelled_ThrowsOperationCanceledException()
{
// Arrange
var itemId = Guid.NewGuid();
var cts = new CancellationTokenSource();
cts.Cancel();
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(
async () => await _repository.DeleteItemAsync(new[] { itemId }, cts.Token)
);
}