// Example: Converting DeleteItem from Sync to Async // File: Jellyfin.Server.Implementations\Item\BaseItemRepository.cs // ========================================== // BEFORE (Current Synchronous Version) // ========================================== public void DeleteItem(params IReadOnlyList 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 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 GetRelatedItemsAsync( IReadOnlyList ids, JellyfinDbContext context, CancellationToken cancellationToken) { var relatedItems = new HashSet(); 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 ids); // AFTER: Task DeleteItemAsync(IReadOnlyList 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 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( async () => await _repository.DeleteItemAsync(new[] { itemId }, cts.Token) ); }