534b0cde91
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.
263 lines
9.0 KiB
C#
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)
|
|
);
|
|
}
|