perf(work-orders): batch attachment inserts and collapse photo updates

SaveAttachmentsAndPhotos committed once per attachment and re-fetched and
re-saved the work order once per photo field. Add AddWorkOrderAttachmentsAsync
for one commit, and load and save the work order once for all photo fields.
File storage stays per-file, since it is I/O rather than a database call.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
npal 2026-10-05 20:14:50 -05:00
parent 6c4de10349
commit 1a1d9d8dcb
3 changed files with 39 additions and 0 deletions

View file

@ -87,6 +87,34 @@ public class WorkOrderDataServiceBatchingTests
context.WorkOrderAuditLogs.Should().HaveCount(2); context.WorkOrderAuditLogs.Should().HaveCount(2);
} }
[Fact]
public async Task AddWorkOrderAttachmentsAsync_PersistsAllRowsInSingleSave()
{
await using var context = NewContext();
await SeedAsync(context);
var service = new WorkOrderDataService(context);
await service.AddWorkOrderAttachmentsAsync(new[]
{
new WorkOrderAttachments { WorkorderId = 1, Attachments = "https://files/a.pdf" },
new WorkOrderAttachments { WorkorderId = 1, Attachments = "https://files/b.pdf" }
});
context.workOrderAttachments.Should().HaveCount(2);
}
[Fact]
public async Task AddWorkOrderAttachmentsAsync_WithEmptyCollection_DoesNothing()
{
await using var context = NewContext();
await SeedAsync(context);
var service = new WorkOrderDataService(context);
await service.AddWorkOrderAttachmentsAsync(Array.Empty<WorkOrderAttachments>());
context.workOrderAttachments.Should().BeEmpty();
}
[Fact] [Fact]
public async Task GetByIdsOrderedAsync_PreservesInputOrderDeduplicatesAndSkipsMissing() public async Task GetByIdsOrderedAsync_PreservesInputOrderDeduplicatesAndSkipsMissing()
{ {

View file

@ -288,6 +288,15 @@ namespace SeaHaven.DataServices.Implementation
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
} }
public async Task AddWorkOrderAttachmentsAsync(IReadOnlyCollection<WorkOrderAttachments> attachments)
{
if (attachments.Count == 0)
return;
await _context.workOrderAttachments.AddRangeAsync(attachments);
await _context.SaveChangesAsync();
}
public async Task AddAuditLogAsync(WorkOrderAuditLog auditLog) public async Task AddAuditLogAsync(WorkOrderAuditLog auditLog)
{ {
await _context.WorkOrderAuditLogs.AddAsync(auditLog); await _context.WorkOrderAuditLogs.AddAsync(auditLog);

View file

@ -57,6 +57,8 @@ namespace SeaHaven.DataServices.Interfaces
Task AddWorkOrderContactsAsync(int workOrderId, IEnumerable<int> contactIds); Task AddWorkOrderContactsAsync(int workOrderId, IEnumerable<int> contactIds);
Task AddWorkOrderCategoriesAsync(int workOrderId, IEnumerable<int> categoryIds); Task AddWorkOrderCategoriesAsync(int workOrderId, IEnumerable<int> categoryIds);
Task AddWorkOrderAttachmentAsync(WorkOrderAttachments attachment); Task AddWorkOrderAttachmentAsync(WorkOrderAttachments attachment);
/// <summary>Adds every attachment in one commit (no per-row round trip).</summary>
Task AddWorkOrderAttachmentsAsync(IReadOnlyCollection<WorkOrderAttachments> attachments);
Task AddAuditLogAsync(WorkOrderAuditLog auditLog); Task AddAuditLogAsync(WorkOrderAuditLog auditLog);
Task AddAuditLogsAsync(IReadOnlyCollection<WorkOrderAuditLog> auditLogs); Task AddAuditLogsAsync(IReadOnlyCollection<WorkOrderAuditLog> auditLogs);