Three of the four findings standing on the 2026-09-05 16:24 review verdict, which the branch had not answered. The count cap is the blocking one. The three id-list validators took whatever the request carried, so the only bound on `graphicsElementIds`/`watermarkIds` was the Kestrel body cap -- a transport limit, not a collection limit. The earlier disposition deferred it to #917 on the grounds that `ApplyUpdateRequest` reconciles the same list uncapped anyway; that is true and does not answer the ask, because the reconcile is downstream of a validator that can refuse the request outright. One shared `Validators.IdsMustExist` now carries the cap for all three, counted on the RAW list before `Distinct` (a million copies of one id costs the same to parse and materialize whatever the distinct count is) and before any database work. The same helper is where the field name and the diagnostic cap now live. The 422 said "Graphics element(s) do not exist: 999" without naming which request field carried the 999, and echoed every rejected id -- an oversized request answered with an oversized response. Both fixed once, in the shared place, so the three sites cannot drift. `Kind` moves into `GraphicsElementDefaults.IsOnNowNext`. The seeder required `Kind == Text` and the API's `builtIn` did not, so an Image row at the exact seeded path was `builtIn:true` on the wire while `GetBuiltInElementId` refused to treat it as the built-in element -- two sites disagreeing about one row, which is the shape #568 exists to close. Identity is now one predicate applied whole at both sites; the seeder's SQL `Kind` filter is gone rather than kept as a duplicate, since a duplicate guard would mask the predicate's own clause. Also the fourth finding, the check-then-write race: `RefreshGraphicsElements` can delete a validated element between `Validate` and `SaveChangesAsync`, handing the join insert the FK violation the validator exists to prevent. A transaction does not close it -- neither provider locks rows the validator merely read -- so both handlers catch `DbUpdateException`, re-ask the existence question on a fresh context, and return the validator's own 422 when an id has since gone; anything else keeps its own exception. Foreign keys are off in `InMemoryTvContext`, so the trigger is simulated by an armed save-failure interceptor while the recovery itself runs against real post-delete state. Refs #568 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
33 lines
1.3 KiB
C#
33 lines
1.3 KiB
C#
using ErsatzTV.Core.Api.Graphics;
|
|
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Core.Graphics;
|
|
using ErsatzTV.Infrastructure.Data;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using static ErsatzTV.Application.Graphics.Mapper;
|
|
|
|
namespace ErsatzTV.Application.Graphics;
|
|
|
|
public class GetAllGraphicsElementsForApiHandler(IDbContextFactory<TvContext> dbContextFactory)
|
|
: IRequestHandler<GetAllGraphicsElementsForApi, List<GraphicsElementResponseModel>>
|
|
{
|
|
public async Task<List<GraphicsElementResponseModel>> Handle(
|
|
GetAllGraphicsElementsForApi request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
|
List<GraphicsElement> graphicsElements = await dbContext.GraphicsElements
|
|
.AsNoTracking()
|
|
.ToListAsync(cancellationToken);
|
|
return graphicsElements
|
|
.Select(e => new
|
|
{
|
|
Vm = ProjectToViewModel(e),
|
|
BuiltIn = GraphicsElementDefaults.IsOnNowNext(e.Path, e.Kind)
|
|
})
|
|
.OrderBy(x => x.Vm.Name == x.Vm.FileName)
|
|
.ThenBy(x => x.Vm.Name)
|
|
.Select(x => new GraphicsElementResponseModel(x.Vm.Id, x.Vm.Name, x.BuiltIn))
|
|
.ToList();
|
|
}
|
|
}
|