diff --git a/Api.SeaHavenIndustries.Tests/CommentDataServiceTests.cs b/Api.SeaHavenIndustries.Tests/CommentDataServiceTests.cs new file mode 100644 index 0000000..b569b8a --- /dev/null +++ b/Api.SeaHavenIndustries.Tests/CommentDataServiceTests.cs @@ -0,0 +1,47 @@ +using Data.SeaHavenIndustries; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using SeaHaven.DataServices.Implementation; +using Xunit; + +namespace Api.SeaHavenIndustries.Tests; + +public class CommentDataServiceTests +{ + private static ApplicationDbContext NewContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new ApplicationDbContext(options); + } + + [Fact] + public async Task DeleteAllForWorkOrderAsync_RemovesOnlyThatWorkOrdersCommentsInOneSave() + { + await using var context = NewContext(); + context.Comments.AddRange( + new Comments { Id = 1, WorkerOrderId = 5, Commenttext = "a" }, + new Comments { Id = 2, WorkerOrderId = 5, Commenttext = "b" }, + new Comments { Id = 3, WorkerOrderId = 9, Commenttext = "c" }); + await context.SaveChangesAsync(); + var service = new CommentDataService(context); + + await service.DeleteAllForWorkOrderAsync(5); + + context.Comments.Select(c => c.Id).Should().Equal(new[] { 3 }); + } + + [Fact] + public async Task DeleteAllForWorkOrderAsync_WithNoComments_DoesNothing() + { + await using var context = NewContext(); + context.Comments.Add(new Comments { Id = 1, WorkerOrderId = 9, Commenttext = "untouched" }); + await context.SaveChangesAsync(); + var service = new CommentDataService(context); + + await service.DeleteAllForWorkOrderAsync(5); + + context.Comments.Select(c => c.Id).Should().Equal(new[] { 1 }); + } +} diff --git a/Api.SeaHavenIndustries.Tests/ServiceBehaviorTests.cs b/Api.SeaHavenIndustries.Tests/ServiceBehaviorTests.cs index dccbbc6..3dc0845 100644 --- a/Api.SeaHavenIndustries.Tests/ServiceBehaviorTests.cs +++ b/Api.SeaHavenIndustries.Tests/ServiceBehaviorTests.cs @@ -254,23 +254,22 @@ public class WorkOrderServiceBehaviorTests } [Fact] - public async Task DeleteWorkOrderCascade_UnlinksQuotesAndDeletesComments() + public async Task DeleteWorkOrderCascade_UnlinksQuotesAndDeletesAllComments() { var woData = new Mock(); woData.Setup(d => d.ExistsAsync(5)).ReturnsAsync(true); var commentData = new Mock(); - commentData.Setup(c => c.GetByWorkOrderIdAsync(5)) - .ReturnsAsync(new List { new() { Id = 1 }, new() { Id = 2 } }); - var quotesData = new Mock(); var result = await CreateWorkOrderService(woData, commentData, quotesData).DeleteWorkOrderCascadeAsync(5, "user1"); Assert.True(result); quotesData.Verify(q => q.UnlinkFromWorkOrderAsync(5), Times.Once); - commentData.Verify(c => c.DeleteAsync(1), Times.Once); - commentData.Verify(c => c.DeleteAsync(2), Times.Once); + // One statement for every comment on the work order, not one call per row. + commentData.Verify(c => c.DeleteAllForWorkOrderAsync(5), Times.Once); + commentData.Verify(c => c.GetByWorkOrderIdAsync(It.IsAny()), Times.Never); + commentData.Verify(c => c.DeleteAsync(It.IsAny()), Times.Never); } private static WorkOrderService CreateWorkOrderService( diff --git a/SeaHaven.DataServices/Implementation/CommentDataService.cs b/SeaHaven.DataServices/Implementation/CommentDataService.cs index ed7e423..9613362 100644 --- a/SeaHaven.DataServices/Implementation/CommentDataService.cs +++ b/SeaHaven.DataServices/Implementation/CommentDataService.cs @@ -82,6 +82,18 @@ namespace SeaHaven.DataServices.Implementation } } + public async Task DeleteAllForWorkOrderAsync(int workOrderId) + { + var entities = await _context.Comments + .Where(c => c.WorkerOrderId == workOrderId) + .ToListAsync(); + if (entities.Count == 0) + return; + + _context.Comments.RemoveRange(entities); + await _context.SaveChangesAsync(); + } + public async Task ExistsAsync(int id) { return await _context.Comments.AnyAsync(c => c.Id == id); diff --git a/SeaHaven.DataServices/Interfaces/ICommentDataService.cs b/SeaHaven.DataServices/Interfaces/ICommentDataService.cs index f551f0c..8591b1d 100644 --- a/SeaHaven.DataServices/Interfaces/ICommentDataService.cs +++ b/SeaHaven.DataServices/Interfaces/ICommentDataService.cs @@ -16,6 +16,8 @@ namespace SeaHaven.DataServices.Interfaces Task AddAsync(Comments comment); Task UpdateAsync(Comments comment); Task DeleteAsync(int id); + /// Deletes every comment on a work order in one statement (no per-row round trip). + Task DeleteAllForWorkOrderAsync(int workOrderId); Task ExistsAsync(int id); Task StageAsync(Comments comment, CancellationToken cancellationToken); Task> GetVendorViewableForDispatchAsync(int dispatchId, CancellationToken cancellationToken); diff --git a/SeaHaven.Services/Implementation/WorkOrderService.cs b/SeaHaven.Services/Implementation/WorkOrderService.cs index 132cbe5..40dc0c7 100644 --- a/SeaHaven.Services/Implementation/WorkOrderService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderService.cs @@ -462,10 +462,7 @@ namespace SeaHaven.Services.Implementation if (!success) return false; await _quotesDataService.UnlinkFromWorkOrderAsync(id); - - var comments = await _commentDataService.GetByWorkOrderIdAsync(id); - foreach (var comment in comments) - await _commentDataService.DeleteAsync(comment.Id); + await _commentDataService.DeleteAllForWorkOrderAsync(id); return true; } @@ -587,48 +584,34 @@ namespace SeaHaven.Services.Implementation Microsoft.AspNetCore.Http.IFormFile? afterPhoto, Microsoft.AspNetCore.Http.IFormFile? signOffPhoto) { - if (attachments != null) + if (attachments != null && attachments.Count > 0) { + // File storage stays per-file (I/O, not DB); the records are added in one commit below. + var entities = new List(attachments.Count); foreach (var attachment in attachments) { var fileUrl = await _fileStorage.SaveFileAsync(attachment); - await _workOrderDataService.AddWorkOrderAttachmentAsync(new WorkOrderAttachments + entities.Add(new WorkOrderAttachments { WorkorderId = workOrderId, Attachments = fileUrl }); } + await _workOrderDataService.AddWorkOrderAttachmentsAsync(entities); } - if (beforPhoto != null) + if (beforPhoto != null || afterPhoto != null || signOffPhoto != null) { - var fileUrl = await _fileStorage.SaveFileAsync(beforPhoto); + var beforUrl = beforPhoto != null ? await _fileStorage.SaveFileAsync(beforPhoto) : null; + var afterUrl = afterPhoto != null ? await _fileStorage.SaveFileAsync(afterPhoto) : null; + var signOffUrl = signOffPhoto != null ? await _fileStorage.SaveFileAsync(signOffPhoto) : null; + var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId); if (workOrder != null) { - workOrder.BeforPhotoAttachment = fileUrl; - await _workOrderDataService.UpdateAsync(workOrder); - } - } - - if (afterPhoto != null) - { - var fileUrl = await _fileStorage.SaveFileAsync(afterPhoto); - var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId); - if (workOrder != null) - { - workOrder.AfterPhotoAttachment = fileUrl; - await _workOrderDataService.UpdateAsync(workOrder); - } - } - - if (signOffPhoto != null) - { - var fileUrl = await _fileStorage.SaveFileAsync(signOffPhoto); - var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId); - if (workOrder != null) - { - workOrder.SignOffAttachment = fileUrl; + if (beforUrl != null) workOrder.BeforPhotoAttachment = beforUrl; + if (afterUrl != null) workOrder.AfterPhotoAttachment = afterUrl; + if (signOffUrl != null) workOrder.SignOffAttachment = signOffUrl; await _workOrderDataService.UpdateAsync(workOrder); } }