From bcd2ea7db3ff581e887fa085c193893fb5a00616 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Fri, 2 Jan 2026 09:25:23 -0600 Subject: [PATCH] db optimizations around names and case-sensitivity (#2749) * generate (case-insensitive) unique names for fields that should be unique * move name case-insensitivity down to schema level * update changelog --- CHANGELOG.md | 1 + .../Commands/CreateFillerPresetHandler.cs | 96 +- .../Commands/UpdateFillerPresetHandler.cs | 48 +- .../Queries/GetPagedFillerPresetsHandler.cs | 12 +- .../Commands/CreateCollectionHandler.cs | 13 +- .../Commands/CreateMultiCollectionHandler.cs | 13 +- .../Commands/CreatePlaylistHandler.cs | 7 +- .../Commands/CreateRerunCollectionHandler.cs | 13 +- .../Commands/CreateSmartCollectionHandler.cs | 13 +- .../Commands/UpdateCollectionHandler.cs | 21 +- .../Commands/UpdateMultiCollectionHandler.cs | 14 +- .../Commands/UpdateRerunCollectionHandler.cs | 23 +- .../Commands/UpdateSmartCollectionHandler.cs | 14 +- .../Queries/GetPagedCollectionsHandler.cs | 6 +- .../GetPagedMultiCollectionsHandler.cs | 6 +- .../GetPagedRerunCollectionsHandler.cs | 6 +- .../GetPagedSmartCollectionsHandler.cs | 6 +- .../Queries/GetPagedTraktListsHandler.cs | 12 +- .../Queries/GetPagedPlayoutsHandler.cs | 4 +- .../Commands/CreateProgramScheduleHandler.cs | 10 +- .../Commands/UpdateProgramScheduleHandler.cs | 44 +- .../GetPagedProgramSchedulesHandler.cs | 6 +- .../Scheduling/Commands/CopyBlockHandler.cs | 14 +- .../Commands/CreateBlockGroupHandler.cs | 10 +- .../Scheduling/Commands/CreateBlockHandler.cs | 7 +- .../Commands/CreateDecoGroupHandler.cs | 10 +- .../Scheduling/Commands/CreateDecoHandler.cs | 7 +- .../CreateDecoTemplateGroupHandler.cs | 10 +- .../Commands/CreateDecoTemplateHandler.cs | 7 +- .../Commands/CreateTemplateGroupHandler.cs | 10 +- .../Commands/CreateTemplateHandler.cs | 7 +- .../Scheduling/Commands/UpdateDecoHandler.cs | 9 +- .../Search/Queries/SearchArtistsHandler.cs | 6 +- .../Queries/SearchCollectionsHandler.cs | 6 +- .../Search/Queries/SearchMoviesHandler.cs | 8 +- .../Queries/SearchMultiCollectionsHandler.cs | 6 +- .../Queries/SearchRerunCollectionsHandler.cs | 6 +- .../Queries/SearchSmartCollectionsHandler.cs | 6 +- .../Queries/SearchTelevisionSeasonsHandler.cs | 3 +- .../Queries/SearchTelevisionShowsHandler.cs | 6 +- .../Commands/CopyWatermarkHandler.cs | 19 +- .../Commands/CreateWatermarkHandler.cs | 25 +- .../Commands/UpdateWatermarkHandler.cs | 21 +- ...60102144855_Fix_DuplicateNames.Designer.cs | 6923 ++++++++++++++++ .../20260102144855_Fix_DuplicateNames.cs | 227 + ...2150305_Update_CaseSensitivity.Designer.cs | 6987 +++++++++++++++++ .../20260102150305_Update_CaseSensitivity.cs | 679 ++ .../Migrations/TvContextModelSnapshot.cs | 108 +- ...60102144811_Fix_DuplicateNames.Designer.cs | 6750 ++++++++++++++++ .../20260102144811_Fix_DuplicateNames.cs | 275 + ...2150221_Update_CaseSensitivity.Designer.cs | 6814 ++++++++++++++++ .../20260102150221_Update_CaseSensitivity.cs | 591 ++ .../Migrations/TvContextModelSnapshot.cs | 108 +- .../Configurations/ChannelConfiguration.cs | 4 + .../ChannelWatermarkConfiguration.cs | 12 +- .../Collection/CollectionConfiguration.cs | 7 + .../MultiCollectionConfiguration.cs | 7 + .../Collection/PlaylistConfiguration.cs | 6 +- .../RerunCollectionConfiguration.cs | 7 + .../SmartCollectionConfiguration.cs | 4 + .../Collection/TraktListConfiguration.cs | 2 + .../Filler/FillerPresetConfiguration.cs | 7 + .../Metadata/ArtistMetadataConfiguration.cs | 20 +- .../Metadata/MetadataGuidConfiguration.cs | 11 +- .../Metadata/MovieMetadataConfiguration.cs | 12 +- .../Metadata/ShowMetadataConfiguration.cs | 2 + .../ProgramScheduleConfiguration.cs | 4 + .../Scheduling/BlockConfiguration.cs | 4 + .../Scheduling/BlockGroupConfiguration.cs | 4 + .../Scheduling/DecoConfiguration.cs | 4 + .../Scheduling/DecoGroupConfiguration.cs | 4 + .../Scheduling/DecoTemplateConfiguration.cs | 4 + .../DecoTemplateGroupConfiguration.cs | 4 + .../Scheduling/TemplateConfiguration.cs | 4 + .../Scheduling/TemplateGroupConfiguration.cs | 4 + .../Data/Repositories/ChannelRepository.cs | 4 +- .../Repositories/MediaCollectionRepository.cs | 23 +- .../Data/Repositories/TelevisionRepository.cs | 9 +- ErsatzTV.Infrastructure/Data/TvContext.cs | 34 +- 79 files changed, 29899 insertions(+), 361 deletions(-) create mode 100644 ErsatzTV.Infrastructure.MySql/Migrations/20260102144855_Fix_DuplicateNames.Designer.cs create mode 100644 ErsatzTV.Infrastructure.MySql/Migrations/20260102144855_Fix_DuplicateNames.cs create mode 100644 ErsatzTV.Infrastructure.MySql/Migrations/20260102150305_Update_CaseSensitivity.Designer.cs create mode 100644 ErsatzTV.Infrastructure.MySql/Migrations/20260102150305_Update_CaseSensitivity.cs create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20260102144811_Fix_DuplicateNames.Designer.cs create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20260102144811_Fix_DuplicateNames.cs create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20260102150221_Update_CaseSensitivity.Designer.cs create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20260102150221_Update_CaseSensitivity.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index ae0177a18..7edb29811 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Sequential schedules: fix `count` instruction validation to accept integer (constant) or string (expression) - Fix multi-part episode grouping logic so that it does NOT require release date metadata for episodes within a single show - When **Treat Collections As Shows** is enabled (i.e. for crossover episodes) release date metadata is required for proper grouping +- Fix *many* cases of duplicate names; enforce case-insensitive unique names at the schema level ### Changed - No longer round framerate to nearest integer when normalizing framerate diff --git a/ErsatzTV.Application/Filler/Commands/CreateFillerPresetHandler.cs b/ErsatzTV.Application/Filler/Commands/CreateFillerPresetHandler.cs index 1849c6c83..5647f4bd1 100644 --- a/ErsatzTV.Application/Filler/Commands/CreateFillerPresetHandler.cs +++ b/ErsatzTV.Application/Filler/Commands/CreateFillerPresetHandler.cs @@ -1,57 +1,67 @@ -using Bugsnag; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.Filler; -public class CreateFillerPresetHandler : IRequestHandler> +public class CreateFillerPresetHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> { - private readonly IClient _client; - private readonly IDbContextFactory _dbContextFactory; - - public CreateFillerPresetHandler(IClient client, IDbContextFactory dbContextFactory) + public async Task> Handle(CreateFillerPreset request, CancellationToken cancellationToken) { - _client = client; - _dbContextFactory = dbContextFactory; + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(dbContext, request); + return await validation.Apply(fp => Persist(dbContext, fp, cancellationToken)); } - public async Task> Handle(CreateFillerPreset request, CancellationToken cancellationToken) + private static async Task Persist( + TvContext dbContext, + FillerPreset fillerPreset, + CancellationToken cancellationToken) { - try - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - var fillerPreset = new FillerPreset - { - Name = request.Name, - FillerKind = request.FillerKind, - FillerMode = request.FillerMode, - Duration = request.Duration, - Count = request.Count, - PadToNearestMinute = request.PadToNearestMinute, - AllowWatermarks = request.AllowWatermarks, - CollectionType = request.CollectionType, - CollectionId = request.CollectionId, - MediaItemId = request.MediaItemId, - MultiCollectionId = request.MultiCollectionId, - SmartCollectionId = request.SmartCollectionId, - PlaylistId = request.PlaylistId, - Expression = request.FillerKind is FillerKind.MidRoll ? request.Expression : null, - UseChaptersAsMediaItems = - request.FillerKind is not FillerKind.Fallback && request.UseChaptersAsMediaItems - }; - - await dbContext.FillerPresets.AddAsync(fillerPreset, cancellationToken); - await dbContext.SaveChangesAsync(cancellationToken); - - return Unit.Default; - } - catch (Exception ex) + await dbContext.FillerPresets.AddAsync(fillerPreset, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + return Unit.Default; + } + + private static Task> Validate( + TvContext dbContext, + CreateFillerPreset request) => + ValidateName(dbContext, request).MapT(name => new FillerPreset { - _client.Notify(ex); - return BaseError.New(ex.Message); - } + Name = name, + FillerKind = request.FillerKind, + FillerMode = request.FillerMode, + Duration = request.Duration, + Count = request.Count, + PadToNearestMinute = request.PadToNearestMinute, + AllowWatermarks = request.AllowWatermarks, + CollectionType = request.CollectionType, + CollectionId = request.CollectionId, + MediaItemId = request.MediaItemId, + MultiCollectionId = request.MultiCollectionId, + SmartCollectionId = request.SmartCollectionId, + PlaylistId = request.PlaylistId, + Expression = request.FillerKind is FillerKind.MidRoll ? request.Expression : null, + UseChaptersAsMediaItems = + request.FillerKind is not FillerKind.Fallback && request.UseChaptersAsMediaItems + }); + + private static async Task> ValidateName( + TvContext dbContext, + CreateFillerPreset request) + { + Validation result1 = request.NotEmpty(fp => fp.Name) + .Bind(_ => request.NotLongerThan(50)(fp => fp.Name)); + + bool duplicateName = await dbContext.FillerPresets + .AnyAsync(fp => fp.Name == request.Name); + + Validation result2 = duplicateName + ? Fail("Filler preset name must be unique") + : Success(Unit.Default); + + return (result1, result2).Apply((_, _) => request.Name); } } diff --git a/ErsatzTV.Application/Filler/Commands/UpdateFillerPresetHandler.cs b/ErsatzTV.Application/Filler/Commands/UpdateFillerPresetHandler.cs index 7c77d746b..33aa53de0 100644 --- a/ErsatzTV.Application/Filler/Commands/UpdateFillerPresetHandler.cs +++ b/ErsatzTV.Application/Filler/Commands/UpdateFillerPresetHandler.cs @@ -6,27 +6,21 @@ using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.Filler; -public class UpdateFillerPresetHandler : IRequestHandler> +public class UpdateFillerPresetHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> { - private readonly IDbContextFactory _dbContextFactory; - - public UpdateFillerPresetHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - public async Task> Handle(UpdateFillerPreset request, CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - Validation validation = await FillerPresetMustExist( - dbContext, - request, - cancellationToken); - return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request)); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(dbContext, request, cancellationToken); + return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request, cancellationToken)); } private static async Task ApplyUpdateRequest( TvContext dbContext, FillerPreset existing, - UpdateFillerPreset request) + UpdateFillerPreset request, + CancellationToken cancellationToken) { existing.Name = request.Name; existing.FillerKind = request.FillerKind; @@ -45,16 +39,40 @@ public class UpdateFillerPresetHandler : IRequestHandler> Validate( + TvContext dbContext, + UpdateFillerPreset request, + CancellationToken cancellationToken) => + (await FillerPresetMustExist(dbContext, request, cancellationToken), await ValidateName(dbContext, request)) + .Apply((collectionToUpdate, _) => collectionToUpdate); + private static Task> FillerPresetMustExist( TvContext dbContext, UpdateFillerPreset request, CancellationToken cancellationToken) => dbContext.FillerPresets .SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id, cancellationToken) - .Map(o => o.ToValidation("FillerPreset does not exist")); + .Map(o => o.ToValidation("Filler preset does not exist")); + + private static async Task> ValidateName( + TvContext dbContext, + UpdateFillerPreset request) + { + Validation result1 = request.NotEmpty(fp => fp.Name) + .Bind(_ => request.NotLongerThan(50)(fp => fp.Name)); + + bool duplicateName = await dbContext.FillerPresets + .AnyAsync(c => c.Id != request.Id && c.Name == request.Name); + + Validation result2 = duplicateName + ? Fail("Filler preset name must be unique") + : Success(Unit.Default); + + return (result1, result2).Apply((_, _) => request.Name); + } } diff --git a/ErsatzTV.Application/Filler/Queries/GetPagedFillerPresetsHandler.cs b/ErsatzTV.Application/Filler/Queries/GetPagedFillerPresetsHandler.cs index 444e133da..b668e6d06 100644 --- a/ErsatzTV.Application/Filler/Queries/GetPagedFillerPresetsHandler.cs +++ b/ErsatzTV.Application/Filler/Queries/GetPagedFillerPresetsHandler.cs @@ -4,22 +4,18 @@ using static ErsatzTV.Application.Filler.Mapper; namespace ErsatzTV.Application.Filler; -public class GetPagedFillerPresetsHandler : IRequestHandler +public class GetPagedFillerPresetsHandler(IDbContextFactory dbContextFactory) + : IRequestHandler { - private readonly IDbContextFactory _dbContextFactory; - - public GetPagedFillerPresetsHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - public async Task Handle( GetPagedFillerPresets request, CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); int count = await dbContext.FillerPresets.CountAsync(cancellationToken); List page = await dbContext.FillerPresets .AsNoTracking() - .OrderBy(f => EF.Functions.Collate(f.Name, TvContext.CaseInsensitiveCollation)) + .OrderBy(f => f.Name) .Skip(request.PageNum * request.PageSize) .Take(request.PageSize) .ToListAsync(cancellationToken) diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs index 0ab8b1ed2..8aac0b208 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs @@ -49,16 +49,15 @@ public class CreateCollectionHandler : TvContext dbContext, CreateCollection createCollection) { - List allNames = await dbContext.Collections - .Map(c => c.Name) - .ToListAsync(); - Validation result1 = createCollection.NotEmpty(c => c.Name) .Bind(_ => createCollection.NotLongerThan(50)(c => c.Name)); - var result2 = Optional(createCollection.Name) - .Where(name => !allNames.Contains(name)) - .ToValidation("Collection name must be unique"); + bool duplicateName = await dbContext.Collections + .AnyAsync(c => c.Name == createCollection.Name); + + Validation result2 = duplicateName + ? Fail("Collection name must be unique") + : Success(Unit.Default); return (result1, result2).Apply((_, _) => createCollection.Name); } diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs index 14e2e42af..9410ca8c2 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs @@ -92,16 +92,15 @@ public class CreateMultiCollectionHandler : TvContext dbContext, CreateMultiCollection createMultiCollection) { - List allNames = await dbContext.MultiCollections - .Map(c => c.Name) - .ToListAsync(); - Validation result1 = createMultiCollection.NotEmpty(c => c.Name) .Bind(_ => createMultiCollection.NotLongerThan(50)(c => c.Name)); - var result2 = Optional(createMultiCollection.Name) - .Where(name => !allNames.Contains(name)) - .ToValidation("MultiCollection name must be unique"); + 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); } diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreatePlaylistHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/CreatePlaylistHandler.cs index 36bf0e706..3239089df 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreatePlaylistHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreatePlaylistHandler.cs @@ -40,11 +40,10 @@ public class CreatePlaylistHandler(IDbContextFactory dbContextFactory return BaseError.New($"Playlist name \"{request.Name}\" is invalid"); } - Option maybeExisting = await dbContext.Playlists - .FirstOrDefaultAsync(r => r.PlaylistGroupId == request.PlaylistGroupId && r.Name == request.Name) - .Map(Optional); + bool duplicate = await dbContext.Playlists + .AnyAsync(r => r.PlaylistGroupId == request.PlaylistGroupId && r.Name == request.Name); - return maybeExisting.IsSome + return duplicate ? BaseError.New($"A playlist named \"{request.Name}\" already exists in that playlist group") : Success(request.Name); } diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateRerunCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateRerunCollectionHandler.cs index 7866da662..4e90dbc4b 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateRerunCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateRerunCollectionHandler.cs @@ -46,16 +46,15 @@ public class CreateRerunCollectionHandler(IDbContextFactory dbContext TvContext dbContext, CreateRerunCollection createCollection) { - List allNames = await dbContext.RerunCollections - .Map(c => c.Name) - .ToListAsync(); - Validation result1 = createCollection.NotEmpty(c => c.Name) .Bind(_ => createCollection.NotLongerThan(50)(c => c.Name)); - var result2 = Optional(createCollection.Name) - .Where(name => !allNames.Contains(name)) - .ToValidation("Rerun collection name must be unique"); + bool duplicateName = await dbContext.RerunCollections + .AnyAsync(c => c.Name == createCollection.Name); + + Validation result2 = duplicateName + ? Fail("Rerun collection name must be unique") + : Success(Unit.Default); return (result1, result2).Apply((_, _) => createCollection.Name); } diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollectionHandler.cs index 476b2bf96..5a9783c45 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollectionHandler.cs @@ -59,17 +59,16 @@ public class CreateSmartCollectionHandler : TvContext dbContext, CreateSmartCollection createSmartCollection) { - List allNames = await dbContext.SmartCollections - .Map(c => c.Name) - .ToListAsync(); + bool duplicateName = await dbContext.SmartCollections + .AnyAsync(c => c.Name == createSmartCollection.Name); + + Validation result2 = duplicateName + ? Fail("SmartCollection name must be unique") + : Success(Unit.Default); Validation result1 = createSmartCollection.NotEmpty(c => c.Name) .Bind(_ => createSmartCollection.NotLongerThan(50)(c => c.Name)); - var result2 = Optional(createSmartCollection.Name) - .Where(name => !allNames.Contains(name, StringComparer.OrdinalIgnoreCase)) - .ToValidation("SmartCollection name must be unique"); - return (result1, result2).Apply((_, _) => createSmartCollection.Name); } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs index 1ce264e2e..bc5c53554 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs @@ -70,7 +70,7 @@ public class UpdateCollectionHandler : IRequestHandler - (await CollectionMustExist(dbContext, request, cancellationToken), ValidateName(request)) + (await CollectionMustExist(dbContext, request, cancellationToken), await ValidateName(dbContext, request)) .Apply((collectionToUpdate, _) => collectionToUpdate); private static Task> CollectionMustExist( @@ -81,7 +81,20 @@ public class UpdateCollectionHandler : IRequestHandler c.Id, c => c.Id == updateCollection.CollectionId, cancellationToken) .Map(o => o.ToValidation("Collection does not exist.")); - private static Validation ValidateName(UpdateCollection updateSimpleMediaCollection) => - updateSimpleMediaCollection.NotEmpty(c => c.Name) - .Bind(_ => updateSimpleMediaCollection.NotLongerThan(50)(c => c.Name)); + private static async Task> ValidateName( + TvContext dbContext, + UpdateCollection updateCollection) + { + Validation result1 = updateCollection.NotEmpty(c => c.Name) + .Bind(_ => updateCollection.NotLongerThan(50)(c => c.Name)); + + bool duplicateName = await dbContext.Collections + .AnyAsync(c => c.Id != updateCollection.CollectionId && c.Name == updateCollection.Name); + + Validation result2 = duplicateName + ? Fail("Collection name must be unique") + : Success(Unit.Default); + + return (result1, result2).Apply((_, _) => updateCollection.Name); + } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs index 23b00f588..d29987398 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs @@ -155,17 +155,15 @@ public class UpdateMultiCollectionHandler : IRequestHandler allNames = await dbContext.MultiCollections - .Filter(mc => mc.Id != updateMultiCollection.MultiCollectionId) - .Map(c => c.Name) - .ToListAsync(); - Validation result1 = updateMultiCollection.NotEmpty(c => c.Name) .Bind(_ => updateMultiCollection.NotLongerThan(50)(c => c.Name)); - var result2 = Optional(updateMultiCollection.Name) - .Where(name => !allNames.Contains(name)) - .ToValidation("MultiCollection name must be unique"); + bool duplicateName = await dbContext.MultiCollections + .AnyAsync(c => c.Id != updateMultiCollection.MultiCollectionId && c.Name == updateMultiCollection.Name); + + Validation result2 = duplicateName + ? Fail("MultiCollection name must be unique") + : Success(Unit.Default); return (result1, result2).Apply((_, _) => updateMultiCollection.Name); } diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateRerunCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateRerunCollectionHandler.cs index c4f38efe9..5fde1033c 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateRerunCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateRerunCollectionHandler.cs @@ -54,10 +54,12 @@ public class UpdateRerunCollectionHandler( return Unit.Default; } - private static Task> Validate( + private static async Task> Validate( TvContext dbContext, UpdateRerunCollection request, - CancellationToken cancellationToken) => RerunCollectionMustExist(dbContext, request, cancellationToken); + CancellationToken cancellationToken) => + (await RerunCollectionMustExist(dbContext, request, cancellationToken), await ValidateName(dbContext, request)) + .Apply((collectionToUpdate, _) => collectionToUpdate); private static Task> RerunCollectionMustExist( TvContext dbContext, @@ -66,4 +68,21 @@ public class UpdateRerunCollectionHandler( dbContext.RerunCollections .SelectOneAsync(c => c.Id, c => c.Id == updateCollection.RerunCollectionId, cancellationToken) .Map(o => o.ToValidation("Rerun collection does not exist.")); + + private static async Task> ValidateName( + TvContext dbContext, + UpdateRerunCollection updateCollection) + { + Validation result1 = updateCollection.NotEmpty(c => c.Name) + .Bind(_ => updateCollection.NotLongerThan(50)(c => c.Name)); + + bool duplicateName = await dbContext.RerunCollections + .AnyAsync(c => c.Id != updateCollection.RerunCollectionId && c.Name == updateCollection.Name); + + Validation result2 = duplicateName + ? Fail("Rerun collection name must be unique") + : Success(Unit.Default); + + return (result1, result2).Apply((_, _) => updateCollection.Name); + } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs index 90cbb8760..a1e8e4609 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs @@ -88,17 +88,15 @@ public class TvContext dbContext, UpdateSmartCollection updateCollection) { - List allNames = await dbContext.SmartCollections - .Where(c => c.Id != updateCollection.Id) - .Map(c => c.Name) - .ToListAsync(); - Validation result1 = updateCollection.NotEmpty(c => c.Name) .Bind(_ => updateCollection.NotLongerThan(50)(c => c.Name)); - var result2 = Optional(updateCollection.Name) - .Where(name => !allNames.Contains(name, StringComparer.OrdinalIgnoreCase)) - .ToValidation("SmartCollection name must be unique"); + bool duplicateName = await dbContext.SmartCollections + .AnyAsync(c => c.Id != updateCollection.Id && c.Name == updateCollection.Name); + + Validation result2 = duplicateName + ? Fail("SmartCollection name must be unique") + : Success(Unit.Default); return (result1, result2).Apply((_, _) => updateCollection.Name); } diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs index 10ad04976..dbecd0789 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs @@ -19,13 +19,11 @@ public class GetPagedCollectionsHandler(IDbContextFactory dbContextFa if (!string.IsNullOrWhiteSpace(request.Query)) { - query = query.Where(s => EF.Functions.Like( - EF.Functions.Collate(s.Name, TvContext.CaseInsensitiveCollation), - $"%{request.Query}%")); + query = query.Where(c => EF.Functions.Like(c.Name, $"%{request.Query}%")); } List page = await query - .OrderBy(c => EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation)) + .OrderBy(c => c.Name) .Skip(request.PageNum * request.PageSize) .Take(request.PageSize) .ToListAsync(cancellationToken) diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollectionsHandler.cs index ab5d73e82..3de411f22 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollectionsHandler.cs @@ -19,13 +19,11 @@ public class GetPagedMultiCollectionsHandler(IDbContextFactory dbCont if (!string.IsNullOrWhiteSpace(request.Query)) { - query = query.Where(s => EF.Functions.Like( - EF.Functions.Collate(s.Name, TvContext.CaseInsensitiveCollation), - $"%{request.Query}%")); + query = query.Where(mc => EF.Functions.Like(mc.Name, $"%{request.Query}%")); } List page = await query - .OrderBy(f => EF.Functions.Collate(f.Name, TvContext.CaseInsensitiveCollation)) + .OrderBy(mc => mc.Name) .Skip(request.PageNum * request.PageSize) .Take(request.PageSize) .Include(mc => mc.MultiCollectionItems) diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollectionsHandler.cs index 5b63fab42..00b1df517 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedRerunCollectionsHandler.cs @@ -19,13 +19,11 @@ public class GetPagedRerunCollectionsHandler(IDbContextFactory dbCont if (!string.IsNullOrWhiteSpace(request.Query)) { - query = query.Where(s => EF.Functions.Like( - EF.Functions.Collate(s.Name, TvContext.CaseInsensitiveCollation), - $"%{request.Query}%")); + query = query.Where(rc => EF.Functions.Like(rc.Name, $"%{request.Query}%")); } List page = await query - .OrderBy(f => EF.Functions.Collate(f.Name, TvContext.CaseInsensitiveCollation)) + .OrderBy(rc => rc.Name) .Skip(request.PageNum * request.PageSize) .Take(request.PageSize) .ToListAsync(cancellationToken) diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs index cabed9b96..984481bb6 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs @@ -19,13 +19,11 @@ public class GetPagedSmartCollectionsHandler(IDbContextFactory dbCont if (!string.IsNullOrWhiteSpace(request.Query)) { - query = query.Where(s => EF.Functions.Like( - EF.Functions.Collate(s.Name, TvContext.CaseInsensitiveCollation), - $"%{request.Query}%")); + query = query.Where(sc => EF.Functions.Like(sc.Name, $"%{request.Query}%")); } List page = await query - .OrderBy(s => EF.Functions.Collate(s.Name, TvContext.CaseInsensitiveCollation)) + .OrderBy(s => s.Name) .Skip(request.PageNum * request.PageSize) .Take(request.PageSize) .ToListAsync(cancellationToken) diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedTraktListsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedTraktListsHandler.cs index e1de26911..2ae039daa 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedTraktListsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedTraktListsHandler.cs @@ -4,22 +4,18 @@ using static ErsatzTV.Application.MediaCollections.Mapper; namespace ErsatzTV.Application.MediaCollections; -public class GetPagedTraktListsHandler : IRequestHandler +public class GetPagedTraktListsHandler(IDbContextFactory dbContextFactory) + : IRequestHandler { - private readonly IDbContextFactory _dbContextFactory; - - public GetPagedTraktListsHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - public async Task Handle( GetPagedTraktLists request, CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); int count = await dbContext.TraktLists.CountAsync(cancellationToken); List page = await dbContext.TraktLists .AsNoTracking() - .OrderBy(f => EF.Functions.Collate(f.Name, TvContext.CaseInsensitiveCollation)) + .OrderBy(l => l.Name) .Skip(request.PageNum * request.PageSize) .Take(request.PageSize) .Include(l => l.Items) diff --git a/ErsatzTV.Application/Playouts/Queries/GetPagedPlayoutsHandler.cs b/ErsatzTV.Application/Playouts/Queries/GetPagedPlayoutsHandler.cs index 3e11aaffd..a45972cee 100644 --- a/ErsatzTV.Application/Playouts/Queries/GetPagedPlayoutsHandler.cs +++ b/ErsatzTV.Application/Playouts/Queries/GetPagedPlayoutsHandler.cs @@ -24,9 +24,7 @@ public class GetPagedPlayoutsHandler(IDbContextFactory dbContextFacto if (!string.IsNullOrWhiteSpace(request.Query)) { - query = query.Where(p => EF.Functions.Like( - EF.Functions.Collate(p.Channel.Name, TvContext.CaseInsensitiveCollation), - $"%{request.Query}%")); + query = query.Where(p => EF.Functions.Like(p.Channel.Name, $"%{request.Query}%")); } List page = await query diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleHandler.cs b/ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleHandler.cs index eee851374..c16dec56a 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleHandler.cs @@ -51,12 +51,12 @@ public class CreateProgramScheduleHandler(IDbContextFactory dbContext Validation result1 = createProgramSchedule.NotEmpty(c => c.Name) .Bind(_ => createProgramSchedule.NotLongerThan(50)(c => c.Name)); - int duplicateNameCount = await dbContext.ProgramSchedules - .CountAsync(ps => ps.Name == createProgramSchedule.Name); + bool duplicateName = await dbContext.ProgramSchedules + .AnyAsync(ps => ps.Name == createProgramSchedule.Name); - var result2 = Optional(duplicateNameCount) - .Where(count => count == 0) - .ToValidation("Schedule name must be unique"); + Validation result2 = duplicateName + ? Fail("Schedule name must be unique") + : Success(Unit.Default); return (result1, result2).Apply((_, _) => createProgramSchedule.Name); } diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs b/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs index 0aa7bf280..ba4c2ce95 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs @@ -9,25 +9,17 @@ using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.ProgramSchedules; -public class UpdateProgramScheduleHandler : - IRequestHandler> +public class UpdateProgramScheduleHandler( + IDbContextFactory dbContextFactory, + ChannelWriter channel) + : + IRequestHandler> { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - - public UpdateProgramScheduleHandler( - IDbContextFactory dbContextFactory, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _channel = channel; - } - public async Task> Handle( UpdateProgramSchedule request, CancellationToken cancellationToken) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request, cancellationToken); return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request)); } @@ -64,7 +56,7 @@ public class UpdateProgramScheduleHandler : foreach (int playoutId in playoutIds) { - await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); + await channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); } } @@ -75,7 +67,7 @@ public class UpdateProgramScheduleHandler : TvContext dbContext, UpdateProgramSchedule request, CancellationToken cancellationToken) => - (await ProgramScheduleMustExist(dbContext, request, cancellationToken), ValidateName(request)) + (await ProgramScheduleMustExist(dbContext, request, cancellationToken), await ValidateName(dbContext, request, cancellationToken)) .Apply((programSchedule, _) => programSchedule); private static Task> ProgramScheduleMustExist( @@ -84,9 +76,23 @@ public class UpdateProgramScheduleHandler : CancellationToken cancellationToken) => dbContext.ProgramSchedules .SelectOneAsync(ps => ps.Id, ps => ps.Id == request.ProgramScheduleId, cancellationToken) - .Map(o => o.ToValidation("ProgramSchedule does not exist")); + .Map(o => o.ToValidation("Schedule does not exist")); - private static Validation ValidateName(UpdateProgramSchedule request) => - request.NotEmpty(c => c.Name) + private static async Task> ValidateName( + TvContext dbContext, + UpdateProgramSchedule request, + CancellationToken cancellationToken) + { + Validation result1 = request.NotEmpty(c => c.Name) .Bind(_ => request.NotLongerThan(50)(c => c.Name)); + + bool duplicateName = await dbContext.ProgramSchedules + .AnyAsync(c => c.Id != request.ProgramScheduleId && c.Name == request.Name, cancellationToken); + + Validation result2 = duplicateName + ? Fail("Schedule name must be unique") + : Success(Unit.Default); + + return (result1, result2).Apply((_, _) => request.Name); + } } diff --git a/ErsatzTV.Application/ProgramSchedules/Queries/GetPagedProgramSchedulesHandler.cs b/ErsatzTV.Application/ProgramSchedules/Queries/GetPagedProgramSchedulesHandler.cs index 8a1f6e573..28d45f704 100644 --- a/ErsatzTV.Application/ProgramSchedules/Queries/GetPagedProgramSchedulesHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Queries/GetPagedProgramSchedulesHandler.cs @@ -19,13 +19,11 @@ public class GetPagedProgramSchedulesHandler(IDbContextFactory dbCont if (!string.IsNullOrWhiteSpace(request.Query)) { - query = query.Where(s => EF.Functions.Like( - EF.Functions.Collate(s.Name, TvContext.CaseInsensitiveCollation), - $"%{request.Query}%")); + query = query.Where(ps => EF.Functions.Like(ps.Name, $"%{request.Query}%")); } List page = await query - .OrderBy(s => EF.Functions.Collate(s.Name, TvContext.CaseInsensitiveCollation)) + .OrderBy(ps => ps.Name) .Skip(request.PageNum * request.PageSize) .Take(request.PageSize) .ToListAsync(cancellationToken) diff --git a/ErsatzTV.Application/Scheduling/Commands/CopyBlockHandler.cs b/ErsatzTV.Application/Scheduling/Commands/CopyBlockHandler.cs index 47746b1a1..5c3189484 100644 --- a/ErsatzTV.Application/Scheduling/Commands/CopyBlockHandler.cs +++ b/ErsatzTV.Application/Scheduling/Commands/CopyBlockHandler.cs @@ -92,17 +92,15 @@ public class CopyBlockHandler(IDbContextFactory dbContextFactory) private static async Task> ValidateName(TvContext dbContext, CopyBlock request) { - List allNames = await dbContext.Blocks - .Where(b => b.BlockGroupId == request.NewBlockGroupId) - .Map(ps => ps.Name) - .ToListAsync(); - Validation result1 = request.NotEmpty(c => c.NewBlockName) .Bind(_ => request.NotLongerThan(50)(c => c.NewBlockName)); - var result2 = Optional(request.NewBlockName) - .Where(name => !allNames.Contains(name)) - .ToValidation("Block name must be unique within the block group."); + bool duplicateName = await dbContext.Blocks + .AnyAsync(b => b.BlockGroupId == request.NewBlockGroupId && b.Name == request.NewBlockName); + + Validation result2 = duplicateName + ? Fail("Block name must be unique within the block group") + : Success(Unit.Default); return (result1, result2).Apply((_, _) => request.NewBlockName); } diff --git a/ErsatzTV.Application/Scheduling/Commands/CreateBlockGroupHandler.cs b/ErsatzTV.Application/Scheduling/Commands/CreateBlockGroupHandler.cs index 3972c2155..90618fd43 100644 --- a/ErsatzTV.Application/Scheduling/Commands/CreateBlockGroupHandler.cs +++ b/ErsatzTV.Application/Scheduling/Commands/CreateBlockGroupHandler.cs @@ -34,12 +34,12 @@ public class CreateBlockGroupHandler(IDbContextFactory dbContextFacto Validation result1 = createBlockGroup.NotEmpty(c => c.Name) .Bind(_ => createBlockGroup.NotLongerThan(50)(c => c.Name)); - int duplicateNameCount = await dbContext.BlockGroups - .CountAsync(ps => ps.Name == createBlockGroup.Name); + bool duplicateName = await dbContext.BlockGroups + .AnyAsync(bg => bg.Name == createBlockGroup.Name); - var result2 = Optional(duplicateNameCount) - .Where(count => count == 0) - .ToValidation("Block group name must be unique"); + Validation result2 = duplicateName + ? Fail("Block group name must be unique") + : Success(Unit.Default); return (result1, result2).Apply((_, _) => createBlockGroup.Name); } diff --git a/ErsatzTV.Application/Scheduling/Commands/CreateBlockHandler.cs b/ErsatzTV.Application/Scheduling/Commands/CreateBlockHandler.cs index 709a04bc9..9c0262148 100644 --- a/ErsatzTV.Application/Scheduling/Commands/CreateBlockHandler.cs +++ b/ErsatzTV.Application/Scheduling/Commands/CreateBlockHandler.cs @@ -42,11 +42,10 @@ public class CreateBlockHandler(IDbContextFactory dbContextFactory) return BaseError.New($"Block name \"{request.Name}\" is invalid"); } - Option maybeExisting = await dbContext.Blocks - .FirstOrDefaultAsync(r => r.BlockGroupId == request.BlockGroupId && r.Name == request.Name) - .Map(Optional); + bool duplicate = await dbContext.Blocks + .AnyAsync(r => r.BlockGroupId == request.BlockGroupId && r.Name == request.Name); - return maybeExisting.IsSome + return duplicate ? BaseError.New($"A block named \"{request.Name}\" already exists in that block group") : Success(request.Name); } diff --git a/ErsatzTV.Application/Scheduling/Commands/CreateDecoGroupHandler.cs b/ErsatzTV.Application/Scheduling/Commands/CreateDecoGroupHandler.cs index a065df63b..bf9318548 100644 --- a/ErsatzTV.Application/Scheduling/Commands/CreateDecoGroupHandler.cs +++ b/ErsatzTV.Application/Scheduling/Commands/CreateDecoGroupHandler.cs @@ -34,12 +34,12 @@ public class CreateDecoGroupHandler(IDbContextFactory dbContextFactor Validation result1 = createDecoGroup.NotEmpty(c => c.Name) .Bind(_ => createDecoGroup.NotLongerThan(50)(c => c.Name)); - int duplicateNameCount = await dbContext.DecoGroups - .CountAsync(ps => ps.Name == createDecoGroup.Name); + bool duplicateName = await dbContext.DecoGroups + .AnyAsync(ps => ps.Name == createDecoGroup.Name); - var result2 = Optional(duplicateNameCount) - .Where(count => count == 0) - .ToValidation("Deco group name must be unique"); + Validation result2 = duplicateName + ? Fail("Deco group name must be unique") + : Success(Unit.Default); return (result1, result2).Apply((_, _) => createDecoGroup.Name); } diff --git a/ErsatzTV.Application/Scheduling/Commands/CreateDecoHandler.cs b/ErsatzTV.Application/Scheduling/Commands/CreateDecoHandler.cs index 78d05fa8c..ad9ab33b4 100644 --- a/ErsatzTV.Application/Scheduling/Commands/CreateDecoHandler.cs +++ b/ErsatzTV.Application/Scheduling/Commands/CreateDecoHandler.cs @@ -44,11 +44,10 @@ public class CreateDecoHandler(IDbContextFactory dbContextFactory) return BaseError.New($"Deco name \"{request.Name}\" is invalid"); } - Option maybeExisting = await dbContext.Decos - .FirstOrDefaultAsync(r => r.DecoGroupId == request.DecoGroupId && r.Name == request.Name) - .Map(Optional); + bool duplicateName = await dbContext.Decos + .AnyAsync(r => r.DecoGroupId == request.DecoGroupId && r.Name == request.Name); - return maybeExisting.IsSome + return duplicateName ? BaseError.New($"A deco named \"{request.Name}\" already exists in that deco group") : Success(request.Name); } diff --git a/ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateGroupHandler.cs b/ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateGroupHandler.cs index dbcb7fe41..5da6b13ee 100644 --- a/ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateGroupHandler.cs +++ b/ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateGroupHandler.cs @@ -38,12 +38,12 @@ public class CreateDecoTemplateGroupHandler(IDbContextFactory dbConte Validation result1 = createDecoTemplateGroup.NotEmpty(c => c.Name) .Bind(_ => createDecoTemplateGroup.NotLongerThan(50)(c => c.Name)); - int duplicateNameCount = await dbContext.DecoTemplateGroups - .CountAsync(ps => ps.Name == createDecoTemplateGroup.Name); + bool duplicateName = await dbContext.DecoTemplateGroups + .AnyAsync(ps => ps.Name == createDecoTemplateGroup.Name); - var result2 = Optional(duplicateNameCount) - .Where(count => count == 0) - .ToValidation("Deco template group name must be unique"); + Validation result2 = duplicateName + ? Fail("Deco template group name must be unique") + : Success(Unit.Default); return (result1, result2).Apply((_, _) => createDecoTemplateGroup.Name); } diff --git a/ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateHandler.cs b/ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateHandler.cs index 410eadf96..9a684e3bb 100644 --- a/ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateHandler.cs +++ b/ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateHandler.cs @@ -43,11 +43,10 @@ public class CreateDecoTemplateHandler(IDbContextFactory dbContextFac return BaseError.New($"Deco template name \"{request.Name}\" is invalid"); } - Option maybeExisting = await dbContext.DecoTemplates - .FirstOrDefaultAsync(r => r.DecoTemplateGroupId == request.DecoTemplateGroupId && r.Name == request.Name) - .Map(Optional); + bool duplicateName = await dbContext.DecoTemplates + .AnyAsync(r => r.DecoTemplateGroupId == request.DecoTemplateGroupId && r.Name == request.Name); - return maybeExisting.IsSome + return duplicateName ? BaseError.New($"A deco template named \"{request.Name}\" already exists in that deco template group") : Success(request.Name); } diff --git a/ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroupHandler.cs b/ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroupHandler.cs index f0d0ed1b9..254f06746 100644 --- a/ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroupHandler.cs +++ b/ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroupHandler.cs @@ -38,12 +38,12 @@ public class CreateTemplateGroupHandler(IDbContextFactory dbContextFa Validation result1 = createTemplateGroup.NotEmpty(c => c.Name) .Bind(_ => createTemplateGroup.NotLongerThan(50)(c => c.Name)); - int duplicateNameCount = await dbContext.TemplateGroups - .CountAsync(ps => ps.Name == createTemplateGroup.Name); + bool duplicateName = await dbContext.TemplateGroups + .AnyAsync(ps => ps.Name == createTemplateGroup.Name); - var result2 = Optional(duplicateNameCount) - .Where(count => count == 0) - .ToValidation("Template group name must be unique"); + Validation result2 = duplicateName + ? Fail("Template group name must be unique") + : Success(Unit.Default); return (result1, result2).Apply((_, _) => createTemplateGroup.Name); } diff --git a/ErsatzTV.Application/Scheduling/Commands/CreateTemplateHandler.cs b/ErsatzTV.Application/Scheduling/Commands/CreateTemplateHandler.cs index 756ff599d..944972cbe 100644 --- a/ErsatzTV.Application/Scheduling/Commands/CreateTemplateHandler.cs +++ b/ErsatzTV.Application/Scheduling/Commands/CreateTemplateHandler.cs @@ -41,11 +41,10 @@ public class CreateTemplateHandler(IDbContextFactory dbContextFactory return BaseError.New($"Template name \"{request.Name}\" is invalid"); } - Option