using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.MediaCollections.Mapper; namespace ErsatzTV.Application.MediaCollections; public class CreateMultiCollectionHandler : IRequestHandler> { private readonly IDbContextFactory _dbContextFactory; private readonly ISearchTargets _searchTargets; public CreateMultiCollectionHandler(IDbContextFactory dbContextFactory, ISearchTargets searchTargets) { _dbContextFactory = dbContextFactory; _searchTargets = searchTargets; } public async Task> Handle( CreateMultiCollection request, CancellationToken cancellationToken) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request); return await validation.Apply(c => PersistCollection(dbContext, c)); } private async Task PersistCollection( TvContext dbContext, MultiCollection multiCollection) { await dbContext.MultiCollections.AddAsync(multiCollection); await dbContext.SaveChangesAsync(); _searchTargets.SearchTargetsChanged(); await dbContext.Entry(multiCollection) .Collection(c => c.MultiCollectionItems) .Query() .Include(i => i.Collection) .LoadAsync(); await dbContext.Entry(multiCollection) .Collection(c => c.MultiCollectionSmartItems) .Query() .Include(i => i.SmartCollection) .LoadAsync(); return ProjectToViewModel(multiCollection); } private static Task> Validate( TvContext dbContext, CreateMultiCollection request) => ValidateName(dbContext, request) .BindT(name => ValidateWeights(request).Map(_ => name)) .MapT(name => new MultiCollection { Name = name, MultiCollectionItems = request.Items.Bind(i => { if (i.CollectionId.HasValue) { return Some( new MultiCollectionItem { CollectionId = i.CollectionId.Value, ScheduleAsGroup = i.ScheduleAsGroup, PlaybackOrder = i.PlaybackOrder, Weight = i.Weight }); } return Option.None; }) .ToList(), MultiCollectionSmartItems = request.Items.Bind(i => { if (i.SmartCollectionId.HasValue) { return Some( new MultiCollectionSmartItem { SmartCollectionId = i.SmartCollectionId.Value, ScheduleAsGroup = i.ScheduleAsGroup, PlaybackOrder = i.PlaybackOrder, Weight = i.Weight }); } return Option.None; }) .ToList() }); // Bounds are shared with the update path so the two cannot drift -- they silently disagreed before #402: // EF's HasDefaultValue substitutes 1 for a 0 on INSERT (0 reads as "not set") while an UPDATE writes the 0 // through, so the same input landed differently depending on the verb. The enumerator clamps out-of-range // weights, so neither a 0 nor a huge value can reach the rotation; this gate refuses input that has no // meaning on a share-of-airtime scale, and keeps create and update honest with each other. See #70. private static Validation ValidateWeights(CreateMultiCollection request) => request.Items.All(i => MultiCollectionItemWeight.IsValid(i.Weight)) ? Unit.Default : BaseError.New(MultiCollectionItemWeight.ValidationMessage); private static async Task> ValidateName( TvContext dbContext, CreateMultiCollection createMultiCollection) { Validation result1 = createMultiCollection.NotEmpty(c => c.Name) .Bind(_ => createMultiCollection.NotLongerThan(50)(c => c.Name)); bool duplicateName = await dbContext.MultiCollections .AnyAsync(c => c.Name == createMultiCollection.Name); Validation result2 = duplicateName ? Fail("MultiCollection name must be unique") : Success(Unit.Default); return (result1, result2).Apply((_, _) => createMultiCollection.Name); } }