From 18f03fd9d1bfdc97668295b9f2f6e84bf0f0f287 Mon Sep 17 00:00:00 2001 From: Barry S Stahl Date: Sun, 23 Aug 2026 19:20:41 -0700 Subject: [PATCH 1/5] refactor: Extract data project, reorganize domain model structure - Move test project from src/ to tst/ folder - Move exception types into Exceptions/ folder - Move ITalkCatalogRepository into Interfaces/ folder - Move repository and service into Services/ folder - Rename PresentationFamilyReference -> PresentationFamily in Entities/ - Move ProposalCopyItem, RelatedContentItem into Entities/ - Rename TalkRecord -> Talk in Entities/ - Move TalkCatalog into Entities/ - Move PublicPresentationReference into Entities/ - Extract FileSystemTalkCatalogRepository and YAML DTOs into new TalkFolio.Data.YamlFile project - Extract repository tests into new TalkFolio.Data.YamlFile.Tests project - Add TalkCatalogService thin client wrapper in Services/ - Remove YamlDotNet and Extensions.Options from domain project (no longer needed) - Update solution file with new project structure - Update all namespaces to match new folder layout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .vscode/settings.json | 3 +- TalkFolio.slnx | 9 ++++-- src/TalkFolio.Api/Program.cs | 5 +++- src/TalkFolio.Api/TalkFolio.Api.csproj | 3 +- ...FileSystemTalkCatalogRepository.Logging.cs | 4 +-- .../FileSystemTalkCatalogRepository.cs | 16 ++++++----- .../TalkCatalogRepositoryOptions.cs | 2 +- .../TalkFolio.Data.YamlFile.csproj | 19 +++++++++++++ .../YamlPresentationFamilyReference.cs | 2 +- .../YamlProposalCopyItem.cs | 2 +- .../YamlPublicPresentationReference.cs | 2 +- .../YamlRelatedContentItem.cs | 2 +- .../YamlTalkRecord.cs | 2 +- .../PresentationFamily.cs} | 6 ++-- .../{ => Entities}/ProposalCopyItem.cs | 2 +- .../PublicPresentationReference.cs | 4 +-- .../{ => Entities}/RelatedContentItem.cs | 2 +- .../{TalkRecord.cs => Entities/Talk.cs} | 10 +++---- src/TalkFolio/{ => Entities}/TalkCatalog.cs | 4 +-- .../DuplicateTalkIdException.cs | 0 .../DuplicateTalkTitleVariantException.cs | 0 .../MalformedTalkYamlException.cs | 0 .../TalkCatalogLoadException.cs | 0 .../ITalkCatalogRepository.cs | 8 ++++-- src/TalkFolio/Services/TalkCatalogService.cs | 18 ++++++++++++ src/TalkFolio/TalkFolio.csproj | 6 ++-- ...mTalkCatalogRepository_LoadAsync_Should.cs | 3 +- .../TalkFolio.Data.YamlFile.Tests.csproj | 28 +++++++++++++++++++ .../TalkFolio.Tests/TalkFolio.Tests.csproj | 4 +-- .../TalksEndpoint_GetTalks_Should.cs | 3 +- 30 files changed, 124 insertions(+), 45 deletions(-) rename src/{TalkFolio => TalkFolio.Data.YamlFile}/FileSystemTalkCatalogRepository.Logging.cs (98%) rename src/{TalkFolio => TalkFolio.Data.YamlFile}/FileSystemTalkCatalogRepository.cs (95%) rename src/{TalkFolio => TalkFolio.Data.YamlFile}/TalkCatalogRepositoryOptions.cs (90%) create mode 100644 src/TalkFolio.Data.YamlFile/TalkFolio.Data.YamlFile.csproj rename src/{TalkFolio => TalkFolio.Data.YamlFile}/YamlPresentationFamilyReference.cs (92%) rename src/{TalkFolio => TalkFolio.Data.YamlFile}/YamlProposalCopyItem.cs (91%) rename src/{TalkFolio => TalkFolio.Data.YamlFile}/YamlPublicPresentationReference.cs (92%) rename src/{TalkFolio => TalkFolio.Data.YamlFile}/YamlRelatedContentItem.cs (93%) rename src/{TalkFolio => TalkFolio.Data.YamlFile}/YamlTalkRecord.cs (97%) rename src/TalkFolio/{PresentationFamilyReference.cs => Entities/PresentationFamily.cs} (53%) rename src/TalkFolio/{ => Entities}/ProposalCopyItem.cs (90%) rename src/TalkFolio/{ => Entities}/PublicPresentationReference.cs (86%) rename src/TalkFolio/{ => Entities}/RelatedContentItem.cs (94%) rename src/TalkFolio/{TalkRecord.cs => Entities/Talk.cs} (89%) rename src/TalkFolio/{ => Entities}/TalkCatalog.cs (65%) rename src/TalkFolio/{ => Exceptions}/DuplicateTalkIdException.cs (100%) rename src/TalkFolio/{ => Exceptions}/DuplicateTalkTitleVariantException.cs (100%) rename src/TalkFolio/{ => Exceptions}/MalformedTalkYamlException.cs (100%) rename src/TalkFolio/{ => Exceptions}/TalkCatalogLoadException.cs (100%) rename src/TalkFolio/{ => Interfaces}/ITalkCatalogRepository.cs (86%) create mode 100644 src/TalkFolio/Services/TalkCatalogService.cs rename {src/TalkFolio.Tests => tst/TalkFolio.Data.YamlFile.Tests}/FileSystemTalkCatalogRepository_LoadAsync_Should.cs (99%) create mode 100644 tst/TalkFolio.Data.YamlFile.Tests/TalkFolio.Data.YamlFile.Tests.csproj rename {src => tst}/TalkFolio.Tests/TalkFolio.Tests.csproj (84%) rename {src => tst}/TalkFolio.Tests/TalksEndpoint_GetTalks_Should.cs (98%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 0a9bde3..a843abf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "/^dotnet --version$/": { "approve": true, "matchCommandLine": true - } + }, + "dotnet build": true } } \ No newline at end of file diff --git a/TalkFolio.slnx b/TalkFolio.slnx index 0c6ce4a..a4f16e1 100644 --- a/TalkFolio.slnx +++ b/TalkFolio.slnx @@ -1,6 +1,11 @@ - + + - + + + + + \ No newline at end of file diff --git a/src/TalkFolio.Api/Program.cs b/src/TalkFolio.Api/Program.cs index 2f42917..c81d720 100644 --- a/src/TalkFolio.Api/Program.cs +++ b/src/TalkFolio.Api/Program.cs @@ -1,5 +1,8 @@ namespace TalkFolio.Api; +using TalkFolio.Data.YamlFile; +using TalkFolio.Interfaces; + #pragma warning disable CA1052, CA1515 public partial class Program { @@ -46,4 +49,4 @@ public static void Main(string[] args) BuildApp(args).Run(); } } -#pragma warning restore CA1052, CA1515 +#pragma warning restore CA1052, CA1515 \ No newline at end of file diff --git a/src/TalkFolio.Api/TalkFolio.Api.csproj b/src/TalkFolio.Api/TalkFolio.Api.csproj index ed7050d..28f6b55 100644 --- a/src/TalkFolio.Api/TalkFolio.Api.csproj +++ b/src/TalkFolio.Api/TalkFolio.Api.csproj @@ -8,6 +8,7 @@ + - + \ No newline at end of file diff --git a/src/TalkFolio/FileSystemTalkCatalogRepository.Logging.cs b/src/TalkFolio.Data.YamlFile/FileSystemTalkCatalogRepository.Logging.cs similarity index 98% rename from src/TalkFolio/FileSystemTalkCatalogRepository.Logging.cs rename to src/TalkFolio.Data.YamlFile/FileSystemTalkCatalogRepository.Logging.cs index 94ab9d8..d30fa9a 100644 --- a/src/TalkFolio/FileSystemTalkCatalogRepository.Logging.cs +++ b/src/TalkFolio.Data.YamlFile/FileSystemTalkCatalogRepository.Logging.cs @@ -1,4 +1,4 @@ -namespace TalkFolio; +namespace TalkFolio.Data.YamlFile; using Microsoft.Extensions.Logging; @@ -42,4 +42,4 @@ internal static partial class FileSystemTalkCatalogRepositoryLog [LoggerMessage(EventId = 13, Level = LogLevel.Error, Message = "Catalog load failed because duplicate talk title and variant were found for Title '{Title}' and Variant '{Variant}' in {DuplicateFilePath}. First seen in {FirstFilePath}.")] public static partial void DuplicateTalkTitleVariant(ILogger logger, Exception exception, string title, string variant, string duplicateFilePath, string firstFilePath); -} +} \ No newline at end of file diff --git a/src/TalkFolio/FileSystemTalkCatalogRepository.cs b/src/TalkFolio.Data.YamlFile/FileSystemTalkCatalogRepository.cs similarity index 95% rename from src/TalkFolio/FileSystemTalkCatalogRepository.cs rename to src/TalkFolio.Data.YamlFile/FileSystemTalkCatalogRepository.cs index 5aa393b..98ebd8d 100644 --- a/src/TalkFolio/FileSystemTalkCatalogRepository.cs +++ b/src/TalkFolio.Data.YamlFile/FileSystemTalkCatalogRepository.cs @@ -1,8 +1,10 @@ -namespace TalkFolio; +namespace TalkFolio.Data.YamlFile; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using TalkFolio.Entities; +using TalkFolio.Interfaces; using YamlDotNet.Core; using YamlDotNet.Serialization; @@ -46,7 +48,7 @@ public async Task LoadAsync(CancellationToken cancellationToken = d return new TalkCatalog(talks); } - private async Task> LoadTalksAsync(string talksDirectory, CancellationToken cancellationToken) + private async Task> LoadTalksAsync(string talksDirectory, CancellationToken cancellationToken) { if (!Directory.Exists(talksDirectory)) { @@ -54,7 +56,7 @@ private async Task> LoadTalksAsync(string talksDirecto return []; } - var talks = new List(); + var talks = new List(); var seenTalkIds = new Dictionary(); var seenTitleVariants = new Dictionary(); var files = Directory.EnumerateFiles(talksDirectory, "*.*", SearchOption.TopDirectoryOnly) @@ -110,9 +112,9 @@ private async Task> LoadTalksAsync(string talksDirecto return talks.AsReadOnly(); } - private static TalkRecord MapTalk(YamlTalkRecord source) + private static Talk MapTalk(YamlTalkRecord source) { - return new TalkRecord( + return new Talk( Id: source.Id, Title: source.Title, AlternateTitles: source.AlternateTitles ?? [], @@ -120,7 +122,7 @@ private static TalkRecord MapTalk(YamlTalkRecord source) Tags: source.Tags ?? [], LifecycleStatus: source.LifecycleStatus ?? string.Empty, TargetAudience: source.TargetAudience ?? [], - PresentationFamily: source.PresentationFamily is null ? null : new PresentationFamilyReference( + PresentationFamily: source.PresentationFamily is null ? null : new PresentationFamily( source.PresentationFamily.Name ?? string.Empty, source.PresentationFamily.Variant ?? string.Empty), SlideDeckIds: source.SlideDeckIds ?? [], @@ -177,4 +179,4 @@ private YamlTalkRecord DeserializeTalk(string yaml, string filePath) } private readonly record struct TalkTitleVariantKey(string Title, string Variant); -} +} \ No newline at end of file diff --git a/src/TalkFolio/TalkCatalogRepositoryOptions.cs b/src/TalkFolio.Data.YamlFile/TalkCatalogRepositoryOptions.cs similarity index 90% rename from src/TalkFolio/TalkCatalogRepositoryOptions.cs rename to src/TalkFolio.Data.YamlFile/TalkCatalogRepositoryOptions.cs index 76a19c4..5a98a73 100644 --- a/src/TalkFolio/TalkCatalogRepositoryOptions.cs +++ b/src/TalkFolio.Data.YamlFile/TalkCatalogRepositoryOptions.cs @@ -1,4 +1,4 @@ -namespace TalkFolio; +namespace TalkFolio.Data.YamlFile; /// /// Configures the file-backed repository used to read TalkFolio data. diff --git a/src/TalkFolio.Data.YamlFile/TalkFolio.Data.YamlFile.csproj b/src/TalkFolio.Data.YamlFile/TalkFolio.Data.YamlFile.csproj new file mode 100644 index 0000000..b4d38e4 --- /dev/null +++ b/src/TalkFolio.Data.YamlFile/TalkFolio.Data.YamlFile.csproj @@ -0,0 +1,19 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/TalkFolio/YamlPresentationFamilyReference.cs b/src/TalkFolio.Data.YamlFile/YamlPresentationFamilyReference.cs similarity index 92% rename from src/TalkFolio/YamlPresentationFamilyReference.cs rename to src/TalkFolio.Data.YamlFile/YamlPresentationFamilyReference.cs index 79c91bc..cd4a7c5 100644 --- a/src/TalkFolio/YamlPresentationFamilyReference.cs +++ b/src/TalkFolio.Data.YamlFile/YamlPresentationFamilyReference.cs @@ -1,4 +1,4 @@ -namespace TalkFolio; +namespace TalkFolio.Data.YamlFile; using System.Diagnostics.CodeAnalysis; diff --git a/src/TalkFolio/YamlProposalCopyItem.cs b/src/TalkFolio.Data.YamlFile/YamlProposalCopyItem.cs similarity index 91% rename from src/TalkFolio/YamlProposalCopyItem.cs rename to src/TalkFolio.Data.YamlFile/YamlProposalCopyItem.cs index e2c1dad..ea3e730 100644 --- a/src/TalkFolio/YamlProposalCopyItem.cs +++ b/src/TalkFolio.Data.YamlFile/YamlProposalCopyItem.cs @@ -1,4 +1,4 @@ -namespace TalkFolio; +namespace TalkFolio.Data.YamlFile; using System.Diagnostics.CodeAnalysis; diff --git a/src/TalkFolio/YamlPublicPresentationReference.cs b/src/TalkFolio.Data.YamlFile/YamlPublicPresentationReference.cs similarity index 92% rename from src/TalkFolio/YamlPublicPresentationReference.cs rename to src/TalkFolio.Data.YamlFile/YamlPublicPresentationReference.cs index 6fb94c4..41424cd 100644 --- a/src/TalkFolio/YamlPublicPresentationReference.cs +++ b/src/TalkFolio.Data.YamlFile/YamlPublicPresentationReference.cs @@ -1,4 +1,4 @@ -namespace TalkFolio; +namespace TalkFolio.Data.YamlFile; using System.Diagnostics.CodeAnalysis; diff --git a/src/TalkFolio/YamlRelatedContentItem.cs b/src/TalkFolio.Data.YamlFile/YamlRelatedContentItem.cs similarity index 93% rename from src/TalkFolio/YamlRelatedContentItem.cs rename to src/TalkFolio.Data.YamlFile/YamlRelatedContentItem.cs index ed4f280..6a00a9e 100644 --- a/src/TalkFolio/YamlRelatedContentItem.cs +++ b/src/TalkFolio.Data.YamlFile/YamlRelatedContentItem.cs @@ -1,4 +1,4 @@ -namespace TalkFolio; +namespace TalkFolio.Data.YamlFile; using System.Diagnostics.CodeAnalysis; diff --git a/src/TalkFolio/YamlTalkRecord.cs b/src/TalkFolio.Data.YamlFile/YamlTalkRecord.cs similarity index 97% rename from src/TalkFolio/YamlTalkRecord.cs rename to src/TalkFolio.Data.YamlFile/YamlTalkRecord.cs index 9a626cb..668a7a9 100644 --- a/src/TalkFolio/YamlTalkRecord.cs +++ b/src/TalkFolio.Data.YamlFile/YamlTalkRecord.cs @@ -1,4 +1,4 @@ -namespace TalkFolio; +namespace TalkFolio.Data.YamlFile; using System.Diagnostics.CodeAnalysis; diff --git a/src/TalkFolio/PresentationFamilyReference.cs b/src/TalkFolio/Entities/PresentationFamily.cs similarity index 53% rename from src/TalkFolio/PresentationFamilyReference.cs rename to src/TalkFolio/Entities/PresentationFamily.cs index 2953592..b204fac 100644 --- a/src/TalkFolio/PresentationFamilyReference.cs +++ b/src/TalkFolio/Entities/PresentationFamily.cs @@ -1,8 +1,8 @@ -namespace TalkFolio; +namespace TalkFolio.Entities; /// -/// Represents the family relationship for a talk within the canonical model. +/// Represents the presentation family for a talk within the canonical model. /// /// The stable family name the talk belongs to. /// The talk's variant within the family. -public sealed record PresentationFamilyReference(string Name, string Variant); +public sealed record PresentationFamily(string Name, string Variant); diff --git a/src/TalkFolio/ProposalCopyItem.cs b/src/TalkFolio/Entities/ProposalCopyItem.cs similarity index 90% rename from src/TalkFolio/ProposalCopyItem.cs rename to src/TalkFolio/Entities/ProposalCopyItem.cs index 2b85c2b..14b84ab 100644 --- a/src/TalkFolio/ProposalCopyItem.cs +++ b/src/TalkFolio/Entities/ProposalCopyItem.cs @@ -1,4 +1,4 @@ -namespace TalkFolio; +namespace TalkFolio.Entities; /// /// Represents typed proposal copy attached to a talk. diff --git a/src/TalkFolio/PublicPresentationReference.cs b/src/TalkFolio/Entities/PublicPresentationReference.cs similarity index 86% rename from src/TalkFolio/PublicPresentationReference.cs rename to src/TalkFolio/Entities/PublicPresentationReference.cs index 6ef3276..611202b 100644 --- a/src/TalkFolio/PublicPresentationReference.cs +++ b/src/TalkFolio/Entities/PublicPresentationReference.cs @@ -1,4 +1,4 @@ -namespace TalkFolio; +namespace TalkFolio.Entities; /// /// Represents a public presentation reference for a talk. @@ -8,4 +8,4 @@ namespace TalkFolio; /// The public identifier used by the source. #pragma warning disable CA1054, CA1056 public sealed record PublicPresentationReference(string Source, string? Url, string? PublicId); -#pragma warning restore CA1054, CA1056 +#pragma warning restore CA1054, CA1056 \ No newline at end of file diff --git a/src/TalkFolio/RelatedContentItem.cs b/src/TalkFolio/Entities/RelatedContentItem.cs similarity index 94% rename from src/TalkFolio/RelatedContentItem.cs rename to src/TalkFolio/Entities/RelatedContentItem.cs index bbfe09a..47b8cb9 100644 --- a/src/TalkFolio/RelatedContentItem.cs +++ b/src/TalkFolio/Entities/RelatedContentItem.cs @@ -1,4 +1,4 @@ -namespace TalkFolio; +namespace TalkFolio.Entities; /// /// Represents lightweight companion material related to a talk. diff --git a/src/TalkFolio/TalkRecord.cs b/src/TalkFolio/Entities/Talk.cs similarity index 89% rename from src/TalkFolio/TalkRecord.cs rename to src/TalkFolio/Entities/Talk.cs index 09d533d..b0cf66c 100644 --- a/src/TalkFolio/TalkRecord.cs +++ b/src/TalkFolio/Entities/Talk.cs @@ -1,7 +1,7 @@ -namespace TalkFolio; +namespace TalkFolio.Entities; /// -/// Represents a canonical Talk record in the TalkFolio read model. +/// Represents a canonical Talk in the TalkFolio read model. /// /// The unique identifier for the talk. /// The title of the talk. @@ -19,7 +19,7 @@ namespace TalkFolio; /// Optional ideation notes for the talk. /// The date the talk record was created. /// The date the talk record was last updated. -public sealed record TalkRecord( +public sealed record Talk( Guid Id, string Title, IReadOnlyList AlternateTitles, @@ -27,7 +27,7 @@ public sealed record TalkRecord( IReadOnlyList Tags, string LifecycleStatus, IReadOnlyList TargetAudience, - PresentationFamilyReference? PresentationFamily, + PresentationFamily? PresentationFamily, IReadOnlyList SlideDeckIds, IReadOnlyList ProposalCopyItems, IReadOnlyList PublicPresentationReferences, @@ -35,4 +35,4 @@ public sealed record TalkRecord( IReadOnlyDictionary? Flags, string? IdeationNotes, DateTimeOffset? CreatedAt, - DateTimeOffset? UpdatedAt); + DateTimeOffset? UpdatedAt); \ No newline at end of file diff --git a/src/TalkFolio/TalkCatalog.cs b/src/TalkFolio/Entities/TalkCatalog.cs similarity index 65% rename from src/TalkFolio/TalkCatalog.cs rename to src/TalkFolio/Entities/TalkCatalog.cs index 83c4e1f..9f15ffc 100644 --- a/src/TalkFolio/TalkCatalog.cs +++ b/src/TalkFolio/Entities/TalkCatalog.cs @@ -1,7 +1,7 @@ -namespace TalkFolio; +namespace TalkFolio.Entities; /// /// Represents the complete TalkFolio catalog returned by the repository. /// /// The talks managed by the catalog. -public sealed record TalkCatalog(IReadOnlyList Talks); +public sealed record TalkCatalog(IReadOnlyList Talks); \ No newline at end of file diff --git a/src/TalkFolio/DuplicateTalkIdException.cs b/src/TalkFolio/Exceptions/DuplicateTalkIdException.cs similarity index 100% rename from src/TalkFolio/DuplicateTalkIdException.cs rename to src/TalkFolio/Exceptions/DuplicateTalkIdException.cs diff --git a/src/TalkFolio/DuplicateTalkTitleVariantException.cs b/src/TalkFolio/Exceptions/DuplicateTalkTitleVariantException.cs similarity index 100% rename from src/TalkFolio/DuplicateTalkTitleVariantException.cs rename to src/TalkFolio/Exceptions/DuplicateTalkTitleVariantException.cs diff --git a/src/TalkFolio/MalformedTalkYamlException.cs b/src/TalkFolio/Exceptions/MalformedTalkYamlException.cs similarity index 100% rename from src/TalkFolio/MalformedTalkYamlException.cs rename to src/TalkFolio/Exceptions/MalformedTalkYamlException.cs diff --git a/src/TalkFolio/TalkCatalogLoadException.cs b/src/TalkFolio/Exceptions/TalkCatalogLoadException.cs similarity index 100% rename from src/TalkFolio/TalkCatalogLoadException.cs rename to src/TalkFolio/Exceptions/TalkCatalogLoadException.cs diff --git a/src/TalkFolio/ITalkCatalogRepository.cs b/src/TalkFolio/Interfaces/ITalkCatalogRepository.cs similarity index 86% rename from src/TalkFolio/ITalkCatalogRepository.cs rename to src/TalkFolio/Interfaces/ITalkCatalogRepository.cs index 469aa14..9d1d882 100644 --- a/src/TalkFolio/ITalkCatalogRepository.cs +++ b/src/TalkFolio/Interfaces/ITalkCatalogRepository.cs @@ -1,7 +1,9 @@ -namespace TalkFolio; +namespace TalkFolio.Interfaces; + +using TalkFolio.Entities; /// -/// Provides a repository that loads the TalkFolio catalog from YAML files on disk. +/// Provides a repository that loads the TalkFolio catalog from a data source. /// public interface ITalkCatalogRepository { @@ -11,4 +13,4 @@ public interface ITalkCatalogRepository /// A token that can be used to cancel the load operation. /// The loaded catalog. Task LoadAsync(CancellationToken cancellationToken = default); -} +} \ No newline at end of file diff --git a/src/TalkFolio/Services/TalkCatalogService.cs b/src/TalkFolio/Services/TalkCatalogService.cs new file mode 100644 index 0000000..b36ea5b --- /dev/null +++ b/src/TalkFolio/Services/TalkCatalogService.cs @@ -0,0 +1,18 @@ +namespace TalkFolio.Services; + +using TalkFolio.Entities; +using TalkFolio.Interfaces; + +/// +/// Provides catalog operations for use within the TalkFolio domain. +/// +public sealed class TalkCatalogService(ITalkCatalogRepository repository) +{ + /// + /// Loads the canonical TalkFolio catalog. + /// + /// A token that can be used to cancel the load operation. + /// The loaded catalog. + public Task LoadAsync(CancellationToken cancellationToken = default) + => repository.LoadAsync(cancellationToken); +} \ No newline at end of file diff --git a/src/TalkFolio/TalkFolio.csproj b/src/TalkFolio/TalkFolio.csproj index f564cd1..8d1234e 100644 --- a/src/TalkFolio/TalkFolio.csproj +++ b/src/TalkFolio/TalkFolio.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -8,8 +8,6 @@ - - - + \ No newline at end of file diff --git a/src/TalkFolio.Tests/FileSystemTalkCatalogRepository_LoadAsync_Should.cs b/tst/TalkFolio.Data.YamlFile.Tests/FileSystemTalkCatalogRepository_LoadAsync_Should.cs similarity index 99% rename from src/TalkFolio.Tests/FileSystemTalkCatalogRepository_LoadAsync_Should.cs rename to tst/TalkFolio.Data.YamlFile.Tests/FileSystemTalkCatalogRepository_LoadAsync_Should.cs index 77a4002..9e801f8 100644 --- a/src/TalkFolio.Tests/FileSystemTalkCatalogRepository_LoadAsync_Should.cs +++ b/tst/TalkFolio.Data.YamlFile.Tests/FileSystemTalkCatalogRepository_LoadAsync_Should.cs @@ -1,7 +1,8 @@ -namespace TalkFolio.Tests; +namespace TalkFolio.Data.YamlFile.Tests; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using TalkFolio.Data.YamlFile; using NSubstitute; public sealed class FileSystemTalkCatalogRepository_LoadAsync_Should : IDisposable diff --git a/tst/TalkFolio.Data.YamlFile.Tests/TalkFolio.Data.YamlFile.Tests.csproj b/tst/TalkFolio.Data.YamlFile.Tests/TalkFolio.Data.YamlFile.Tests.csproj new file mode 100644 index 0000000..8ae9e81 --- /dev/null +++ b/tst/TalkFolio.Data.YamlFile.Tests/TalkFolio.Data.YamlFile.Tests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + $(NoWarn);CA1707;CA2007 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/TalkFolio.Tests/TalkFolio.Tests.csproj b/tst/TalkFolio.Tests/TalkFolio.Tests.csproj similarity index 84% rename from src/TalkFolio.Tests/TalkFolio.Tests.csproj rename to tst/TalkFolio.Tests/TalkFolio.Tests.csproj index 4faca3b..1e5914a 100644 --- a/src/TalkFolio.Tests/TalkFolio.Tests.csproj +++ b/tst/TalkFolio.Tests/TalkFolio.Tests.csproj @@ -22,8 +22,8 @@ - - + + diff --git a/src/TalkFolio.Tests/TalksEndpoint_GetTalks_Should.cs b/tst/TalkFolio.Tests/TalksEndpoint_GetTalks_Should.cs similarity index 98% rename from src/TalkFolio.Tests/TalksEndpoint_GetTalks_Should.cs rename to tst/TalkFolio.Tests/TalksEndpoint_GetTalks_Should.cs index e14c0c4..49c37cb 100644 --- a/src/TalkFolio.Tests/TalksEndpoint_GetTalks_Should.cs +++ b/tst/TalkFolio.Tests/TalksEndpoint_GetTalks_Should.cs @@ -3,6 +3,7 @@ namespace TalkFolio.Tests; using System.Net.Http.Json; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; +using TalkFolio.Entities; public sealed class TalksEndpoint_GetTalks_Should : IDisposable { @@ -37,7 +38,7 @@ public async Task ReturnCanonicalTalks_WhenRepositoryContainsTalkData() // Assert response.EnsureSuccessStatusCode(); - var talks = await response.Content.ReadFromJsonAsync>(CancellationToken.None); + var talks = await response.Content.ReadFromJsonAsync>(CancellationToken.None); var talk = Assert.Single(talks!); Assert.Equal(Guid.Parse("6c8d4d27-9cc7-4c41-9bf8-19e55758e7cc"), talk.Id); Assert.Equal("Finding TP for Your People's Bungholes", talk.Title); From e5e7e2a56bba1a2a4dc49d35e5fc82ba99102bda Mon Sep 17 00:00:00 2001 From: Barry S Stahl Date: Sun, 23 Aug 2026 19:27:08 -0700 Subject: [PATCH 2/5] refactor: simplify data-layer naming - Rename TalkFolio.Data.YamlFile repository and options types - Remove FileSystem and Yaml prefixes from data-layer DTOs - Rename repository log helper to a simpler form - Update API configuration binding and tests for the new names - Keep domain and data project boundaries intact Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/TalkFolio.Api/Program.cs | 6 +- ...ence.cs => PresentationFamilyReference.cs} | 4 +- ...roposalCopyItem.cs => ProposalCopyItem.cs} | 4 +- ...ence.cs => PublicPresentationReference.cs} | 4 +- ...edContentItem.cs => RelatedContentItem.cs} | 4 +- ...sitoryOptions.cs => TalkCatalogOptions.cs} | 4 +- ...Repository.cs => TalkCatalogRepository.cs} | 69 ++++++++++--------- ...Logging.cs => TalkCatalogRepositoryLog.cs} | 6 +- .../{YamlTalkRecord.cs => TalkRecord.cs} | 12 ++-- ...TalkCatalogRepository_LoadAsync_Should.cs} | 29 ++++---- .../TalksEndpoint_GetTalks_Should.cs | 2 +- 11 files changed, 74 insertions(+), 70 deletions(-) rename src/TalkFolio.Data.YamlFile/{YamlPresentationFamilyReference.cs => PresentationFamilyReference.cs} (75%) rename src/TalkFolio.Data.YamlFile/{YamlProposalCopyItem.cs => ProposalCopyItem.cs} (77%) rename src/TalkFolio.Data.YamlFile/{YamlPublicPresentationReference.cs => PublicPresentationReference.cs} (76%) rename src/TalkFolio.Data.YamlFile/{YamlRelatedContentItem.cs => RelatedContentItem.cs} (80%) rename src/TalkFolio.Data.YamlFile/{TalkCatalogRepositoryOptions.cs => TalkCatalogOptions.cs} (66%) rename src/TalkFolio.Data.YamlFile/{FileSystemTalkCatalogRepository.cs => TalkCatalogRepository.cs} (66%) rename src/TalkFolio.Data.YamlFile/{FileSystemTalkCatalogRepository.Logging.cs => TalkCatalogRepositoryLog.cs} (93%) rename src/TalkFolio.Data.YamlFile/{YamlTalkRecord.cs => TalkRecord.cs} (68%) rename tst/TalkFolio.Data.YamlFile.Tests/{FileSystemTalkCatalogRepository_LoadAsync_Should.cs => TalkCatalogRepository_LoadAsync_Should.cs} (92%) diff --git a/src/TalkFolio.Api/Program.cs b/src/TalkFolio.Api/Program.cs index c81d720..eb0548a 100644 --- a/src/TalkFolio.Api/Program.cs +++ b/src/TalkFolio.Api/Program.cs @@ -11,10 +11,10 @@ public static WebApplication BuildApp(string[] args) var builder = WebApplication.CreateBuilder(args); builder.Services - .AddOptions() - .BindConfiguration("TalkCatalogRepository"); + .AddOptions() + .BindConfiguration("TalkCatalog"); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); var app = builder.Build(); diff --git a/src/TalkFolio.Data.YamlFile/YamlPresentationFamilyReference.cs b/src/TalkFolio.Data.YamlFile/PresentationFamilyReference.cs similarity index 75% rename from src/TalkFolio.Data.YamlFile/YamlPresentationFamilyReference.cs rename to src/TalkFolio.Data.YamlFile/PresentationFamilyReference.cs index cd4a7c5..4371e8e 100644 --- a/src/TalkFolio.Data.YamlFile/YamlPresentationFamilyReference.cs +++ b/src/TalkFolio.Data.YamlFile/PresentationFamilyReference.cs @@ -3,10 +3,10 @@ namespace TalkFolio.Data.YamlFile; using System.Diagnostics.CodeAnalysis; /// -/// Raw YAML projection of presentation family data. +/// Raw projection of presentation family data. /// [SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Used by YamlDotNet reflection deserialization.")] -internal sealed class YamlPresentationFamilyReference +internal sealed class PresentationFamilyReference { public string? Name { get; set; } diff --git a/src/TalkFolio.Data.YamlFile/YamlProposalCopyItem.cs b/src/TalkFolio.Data.YamlFile/ProposalCopyItem.cs similarity index 77% rename from src/TalkFolio.Data.YamlFile/YamlProposalCopyItem.cs rename to src/TalkFolio.Data.YamlFile/ProposalCopyItem.cs index ea3e730..1a7d46b 100644 --- a/src/TalkFolio.Data.YamlFile/YamlProposalCopyItem.cs +++ b/src/TalkFolio.Data.YamlFile/ProposalCopyItem.cs @@ -3,10 +3,10 @@ namespace TalkFolio.Data.YamlFile; using System.Diagnostics.CodeAnalysis; /// -/// Raw YAML projection of proposal copy item data. +/// Raw projection of proposal copy item data. /// [SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Used by YamlDotNet reflection deserialization.")] -internal sealed class YamlProposalCopyItem +internal sealed class ProposalCopyItem { public string? Type { get; set; } diff --git a/src/TalkFolio.Data.YamlFile/YamlPublicPresentationReference.cs b/src/TalkFolio.Data.YamlFile/PublicPresentationReference.cs similarity index 76% rename from src/TalkFolio.Data.YamlFile/YamlPublicPresentationReference.cs rename to src/TalkFolio.Data.YamlFile/PublicPresentationReference.cs index 41424cd..8bbc0c3 100644 --- a/src/TalkFolio.Data.YamlFile/YamlPublicPresentationReference.cs +++ b/src/TalkFolio.Data.YamlFile/PublicPresentationReference.cs @@ -3,10 +3,10 @@ namespace TalkFolio.Data.YamlFile; using System.Diagnostics.CodeAnalysis; /// -/// Raw YAML projection of a public presentation reference. +/// Raw projection of a public presentation reference. /// [SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Used by YamlDotNet reflection deserialization.")] -internal sealed class YamlPublicPresentationReference +internal sealed class PublicPresentationReference { public string? Source { get; set; } diff --git a/src/TalkFolio.Data.YamlFile/YamlRelatedContentItem.cs b/src/TalkFolio.Data.YamlFile/RelatedContentItem.cs similarity index 80% rename from src/TalkFolio.Data.YamlFile/YamlRelatedContentItem.cs rename to src/TalkFolio.Data.YamlFile/RelatedContentItem.cs index 6a00a9e..14e44c4 100644 --- a/src/TalkFolio.Data.YamlFile/YamlRelatedContentItem.cs +++ b/src/TalkFolio.Data.YamlFile/RelatedContentItem.cs @@ -3,10 +3,10 @@ namespace TalkFolio.Data.YamlFile; using System.Diagnostics.CodeAnalysis; /// -/// Raw YAML projection of related companion content. +/// Raw projection of related companion content. /// [SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Used by YamlDotNet reflection deserialization.")] -internal sealed class YamlRelatedContentItem +internal sealed class RelatedContentItem { public string? Type { get; set; } diff --git a/src/TalkFolio.Data.YamlFile/TalkCatalogRepositoryOptions.cs b/src/TalkFolio.Data.YamlFile/TalkCatalogOptions.cs similarity index 66% rename from src/TalkFolio.Data.YamlFile/TalkCatalogRepositoryOptions.cs rename to src/TalkFolio.Data.YamlFile/TalkCatalogOptions.cs index 5a98a73..96e9a41 100644 --- a/src/TalkFolio.Data.YamlFile/TalkCatalogRepositoryOptions.cs +++ b/src/TalkFolio.Data.YamlFile/TalkCatalogOptions.cs @@ -1,9 +1,9 @@ namespace TalkFolio.Data.YamlFile; /// -/// Configures the file-backed repository used to read TalkFolio data. +/// Configures the repository used to read TalkFolio data. /// -public sealed class TalkCatalogRepositoryOptions +public sealed class TalkCatalogOptions { /// /// Gets or sets the root directory that contains the repository data. diff --git a/src/TalkFolio.Data.YamlFile/FileSystemTalkCatalogRepository.cs b/src/TalkFolio.Data.YamlFile/TalkCatalogRepository.cs similarity index 66% rename from src/TalkFolio.Data.YamlFile/FileSystemTalkCatalogRepository.cs rename to src/TalkFolio.Data.YamlFile/TalkCatalogRepository.cs index 98ebd8d..682f2dd 100644 --- a/src/TalkFolio.Data.YamlFile/FileSystemTalkCatalogRepository.cs +++ b/src/TalkFolio.Data.YamlFile/TalkCatalogRepository.cs @@ -3,60 +3,65 @@ namespace TalkFolio.Data.YamlFile; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using TalkFolio.Entities; +using DomainPresentationFamily = TalkFolio.Entities.PresentationFamily; +using DomainProposalCopyItem = TalkFolio.Entities.ProposalCopyItem; +using DomainPublicPresentationReference = TalkFolio.Entities.PublicPresentationReference; +using DomainRelatedContentItem = TalkFolio.Entities.RelatedContentItem; +using DomainTalk = TalkFolio.Entities.Talk; +using DomainTalkCatalog = TalkFolio.Entities.TalkCatalog; using TalkFolio.Interfaces; using YamlDotNet.Core; using YamlDotNet.Serialization; /// -/// Reads the TalkFolio catalog from a file-based YAML data source. +/// Reads the TalkFolio catalog from a file-based data source. /// -public sealed class FileSystemTalkCatalogRepository( - IOptions options, - ILogger? logger = null) : ITalkCatalogRepository +public sealed class TalkCatalogRepository( + IOptions options, + ILogger? logger = null) : ITalkCatalogRepository { private static readonly IDeserializer Deserializer = new DeserializerBuilder() .IgnoreUnmatchedProperties() .Build(); - private readonly ILogger _logger = logger ?? NullLogger.Instance; - private readonly IOptions _options = options ?? throw new ArgumentNullException(nameof(options)); + private readonly ILogger _logger = logger ?? NullLogger.Instance; + private readonly IOptions _options = options ?? throw new ArgumentNullException(nameof(options)); /// - public async Task LoadAsync(CancellationToken cancellationToken = default) + public async Task LoadAsync(CancellationToken cancellationToken = default) { - FileSystemTalkCatalogRepositoryLog.LoadingCatalog(_logger); + TalkCatalogRepositoryLog.LoadingCatalog(_logger); var dataRoot = _options.Value.DataRoot; if (string.IsNullOrWhiteSpace(dataRoot)) { - FileSystemTalkCatalogRepositoryLog.LoadingCatalogFailedBecauseDataRootNotConfigured(_logger); + TalkCatalogRepositoryLog.LoadingCatalogFailedBecauseDataRootNotConfigured(_logger); throw new InvalidOperationException("The repository data root is not configured."); } if (!Directory.Exists(dataRoot)) { - FileSystemTalkCatalogRepositoryLog.LoadingCatalogFailedBecauseDataRootDoesNotExist(_logger, dataRoot); + TalkCatalogRepositoryLog.LoadingCatalogFailedBecauseDataRootDoesNotExist(_logger, dataRoot); throw new DirectoryNotFoundException($"The TalkFolio data root '{dataRoot}' does not exist."); } var talksDirectory = Path.Combine(dataRoot, "talks"); - FileSystemTalkCatalogRepositoryLog.LoadingTalksFrom(_logger, talksDirectory); + TalkCatalogRepositoryLog.LoadingTalksFrom(_logger, talksDirectory); var talks = await LoadTalksAsync(talksDirectory, cancellationToken).ConfigureAwait(false); - FileSystemTalkCatalogRepositoryLog.LoadedCatalog(_logger, talks.Count); + TalkCatalogRepositoryLog.LoadedCatalog(_logger, talks.Count); - return new TalkCatalog(talks); + return new DomainTalkCatalog(talks); } - private async Task> LoadTalksAsync(string talksDirectory, CancellationToken cancellationToken) + private async Task> LoadTalksAsync(string talksDirectory, CancellationToken cancellationToken) { if (!Directory.Exists(talksDirectory)) { - FileSystemTalkCatalogRepositoryLog.TalksDirectoryDoesNotExist(_logger, talksDirectory); + TalkCatalogRepositoryLog.TalksDirectoryDoesNotExist(_logger, talksDirectory); return []; } - var talks = new List(); + var talks = new List(); var seenTalkIds = new Dictionary(); var seenTitleVariants = new Dictionary(); var files = Directory.EnumerateFiles(talksDirectory, "*.*", SearchOption.TopDirectoryOnly) @@ -66,16 +71,16 @@ private async Task> LoadTalksAsync(string talksDirectory, Ca foreach (var file in files) { cancellationToken.ThrowIfCancellationRequested(); - FileSystemTalkCatalogRepositoryLog.ReadingTalkFile(_logger, file); + TalkCatalogRepositoryLog.ReadingTalkFile(_logger, file); var yaml = await File.ReadAllTextAsync(file, cancellationToken).ConfigureAwait(false); var payload = DeserializeTalk(yaml, file); - FileSystemTalkCatalogRepositoryLog.DeserializedTalkPayload(_logger, payload.Id, payload.Title, file); + TalkCatalogRepositoryLog.DeserializedTalkPayload(_logger, payload.Id, payload.Title, file); if (seenTalkIds.TryGetValue(payload.Id, out var firstTalkIdFilePath)) { var duplicateIdException = new DuplicateTalkIdException(payload.Id, firstTalkIdFilePath, file); - FileSystemTalkCatalogRepositoryLog.DuplicateTalkId( + TalkCatalogRepositoryLog.DuplicateTalkId( _logger, duplicateIdException, payload.Id, @@ -93,7 +98,7 @@ private async Task> LoadTalksAsync(string talksDirectory, Ca variant, firstTitleVariantFilePath, file); - FileSystemTalkCatalogRepositoryLog.DuplicateTalkTitleVariant( + TalkCatalogRepositoryLog.DuplicateTalkTitleVariant( _logger, duplicateTitleVariantException, payload.Title, @@ -106,15 +111,15 @@ private async Task> LoadTalksAsync(string talksDirectory, Ca seenTalkIds.Add(payload.Id, file); seenTitleVariants.Add(titleVariantKey, file); talks.Add(MapTalk(payload)); - FileSystemTalkCatalogRepositoryLog.MappedTalkRecord(_logger, payload.Id, file); + TalkCatalogRepositoryLog.MappedTalk(_logger, payload.Id, file); } return talks.AsReadOnly(); } - private static Talk MapTalk(YamlTalkRecord source) + private static DomainTalk MapTalk(TalkRecord source) { - return new Talk( + return new DomainTalk( Id: source.Id, Title: source.Title, AlternateTitles: source.AlternateTitles ?? [], @@ -122,20 +127,20 @@ private static Talk MapTalk(YamlTalkRecord source) Tags: source.Tags ?? [], LifecycleStatus: source.LifecycleStatus ?? string.Empty, TargetAudience: source.TargetAudience ?? [], - PresentationFamily: source.PresentationFamily is null ? null : new PresentationFamily( + PresentationFamily: source.PresentationFamily is null ? null : new DomainPresentationFamily( source.PresentationFamily.Name ?? string.Empty, source.PresentationFamily.Variant ?? string.Empty), SlideDeckIds: source.SlideDeckIds ?? [], ProposalCopyItems: source.ProposalCopyItems is null ? [] : source.ProposalCopyItems - .Select(static item => new ProposalCopyItem(item.Type ?? string.Empty, item.Copy ?? string.Empty)) + .Select(static item => new DomainProposalCopyItem(item.Type ?? string.Empty, item.Copy ?? string.Empty)) .ToList() .AsReadOnly(), PublicPresentationReferences: source.PublicPresentationReferences is null ? [] : source.PublicPresentationReferences - .Select(static item => new PublicPresentationReference( + .Select(static item => new DomainPublicPresentationReference( item.Source ?? string.Empty, item.Url, item.PublicId)) @@ -144,7 +149,7 @@ private static Talk MapTalk(YamlTalkRecord source) RelatedContent: source.RelatedContent is null ? [] : source.RelatedContent - .Select(static item => new RelatedContentItem( + .Select(static item => new DomainRelatedContentItem( item.Type ?? string.Empty, item.Title ?? string.Empty, item.Url, @@ -157,23 +162,23 @@ private static Talk MapTalk(YamlTalkRecord source) UpdatedAt: source.UpdatedAt); } - private YamlTalkRecord DeserializeTalk(string yaml, string filePath) + private TalkRecord DeserializeTalk(string yaml, string filePath) { try { - return Deserializer.Deserialize(yaml) + return Deserializer.Deserialize(yaml) ?? throw new InvalidOperationException($"Talk YAML file '{filePath}' did not produce a talk record."); } catch (YamlException ex) { var malformedTalkYamlException = MalformedTalkYamlException.ForFilePath(filePath, ex); - FileSystemTalkCatalogRepositoryLog.TalkFileMalformed(_logger, malformedTalkYamlException, filePath); + TalkCatalogRepositoryLog.TalkFileMalformed(_logger, malformedTalkYamlException, filePath); throw malformedTalkYamlException; } catch (InvalidOperationException ex) { var malformedTalkYamlException = MalformedTalkYamlException.ForFilePath(filePath, ex); - FileSystemTalkCatalogRepositoryLog.TalkFileCouldNotBeDeserialized(_logger, malformedTalkYamlException, filePath); + TalkCatalogRepositoryLog.TalkFileCouldNotBeDeserialized(_logger, malformedTalkYamlException, filePath); throw malformedTalkYamlException; } } diff --git a/src/TalkFolio.Data.YamlFile/FileSystemTalkCatalogRepository.Logging.cs b/src/TalkFolio.Data.YamlFile/TalkCatalogRepositoryLog.cs similarity index 93% rename from src/TalkFolio.Data.YamlFile/FileSystemTalkCatalogRepository.Logging.cs rename to src/TalkFolio.Data.YamlFile/TalkCatalogRepositoryLog.cs index d30fa9a..f68c4a5 100644 --- a/src/TalkFolio.Data.YamlFile/FileSystemTalkCatalogRepository.Logging.cs +++ b/src/TalkFolio.Data.YamlFile/TalkCatalogRepositoryLog.cs @@ -2,7 +2,7 @@ namespace TalkFolio.Data.YamlFile; using Microsoft.Extensions.Logging; -internal static partial class FileSystemTalkCatalogRepositoryLog +internal static partial class TalkCatalogRepositoryLog { [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Loading TalkFolio catalog.")] public static partial void LoadingCatalog(ILogger logger); @@ -28,8 +28,8 @@ internal static partial class FileSystemTalkCatalogRepositoryLog [LoggerMessage(EventId = 8, Level = LogLevel.Trace, Message = "Deserialized talk payload {TalkId} ({TalkTitle}) from {FilePath}.")] public static partial void DeserializedTalkPayload(ILogger logger, Guid talkId, string talkTitle, string filePath); - [LoggerMessage(EventId = 9, Level = LogLevel.Trace, Message = "Mapped talk record {TalkId} from {FilePath}.")] - public static partial void MappedTalkRecord(ILogger logger, Guid talkId, string filePath); + [LoggerMessage(EventId = 9, Level = LogLevel.Trace, Message = "Mapped talk {TalkId} from {FilePath}.")] + public static partial void MappedTalk(ILogger logger, Guid talkId, string filePath); [LoggerMessage(EventId = 10, Level = LogLevel.Error, Message = "Catalog load failed because talk file {FilePath} contains malformed YAML.")] public static partial void TalkFileMalformed(ILogger logger, Exception exception, string filePath); diff --git a/src/TalkFolio.Data.YamlFile/YamlTalkRecord.cs b/src/TalkFolio.Data.YamlFile/TalkRecord.cs similarity index 68% rename from src/TalkFolio.Data.YamlFile/YamlTalkRecord.cs rename to src/TalkFolio.Data.YamlFile/TalkRecord.cs index 668a7a9..676293e 100644 --- a/src/TalkFolio.Data.YamlFile/YamlTalkRecord.cs +++ b/src/TalkFolio.Data.YamlFile/TalkRecord.cs @@ -3,10 +3,10 @@ namespace TalkFolio.Data.YamlFile; using System.Diagnostics.CodeAnalysis; /// -/// Raw YAML projection for a talk record. +/// Raw projection for a talk record. /// [SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Used by YamlDotNet reflection deserialization.")] -internal sealed class YamlTalkRecord +internal sealed class TalkRecord { public Guid Id { get; set; } @@ -18,7 +18,7 @@ internal sealed class YamlTalkRecord public List? Tags { get; set; } - public YamlPresentationFamilyReference? PresentationFamily { get; set; } + public PresentationFamilyReference? PresentationFamily { get; set; } public string? LifecycleStatus { get; set; } @@ -28,11 +28,11 @@ internal sealed class YamlTalkRecord public List? SlideDeckIds { get; set; } - public List? ProposalCopyItems { get; set; } + public List? ProposalCopyItems { get; set; } - public List? PublicPresentationReferences { get; set; } + public List? PublicPresentationReferences { get; set; } - public List? RelatedContent { get; set; } + public List? RelatedContent { get; set; } public string? IdeationNotes { get; set; } diff --git a/tst/TalkFolio.Data.YamlFile.Tests/FileSystemTalkCatalogRepository_LoadAsync_Should.cs b/tst/TalkFolio.Data.YamlFile.Tests/TalkCatalogRepository_LoadAsync_Should.cs similarity index 92% rename from tst/TalkFolio.Data.YamlFile.Tests/FileSystemTalkCatalogRepository_LoadAsync_Should.cs rename to tst/TalkFolio.Data.YamlFile.Tests/TalkCatalogRepository_LoadAsync_Should.cs index 9e801f8..01653cc 100644 --- a/tst/TalkFolio.Data.YamlFile.Tests/FileSystemTalkCatalogRepository_LoadAsync_Should.cs +++ b/tst/TalkFolio.Data.YamlFile.Tests/TalkCatalogRepository_LoadAsync_Should.cs @@ -5,11 +5,11 @@ namespace TalkFolio.Data.YamlFile.Tests; using TalkFolio.Data.YamlFile; using NSubstitute; -public sealed class FileSystemTalkCatalogRepository_LoadAsync_Should : IDisposable +public sealed class TalkCatalogRepository_LoadAsync_Should : IDisposable { private readonly string _dataRoot; - public FileSystemTalkCatalogRepository_LoadAsync_Should() + public TalkCatalogRepository_LoadAsync_Should() { _dataRoot = Path.Combine(Path.GetTempPath(), $"talkfolio-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(_dataRoot); @@ -20,8 +20,8 @@ public async Task ReturnCanonicalCatalog_WhenYamlFilesExist() { // Arrange var repositoryRoot = await CreateRepositoryRoot(); - var target = new FileSystemTalkCatalogRepository( - Options.Create(new TalkCatalogRepositoryOptions + var target = new TalkCatalogRepository( + Options.Create(new TalkCatalogOptions { DataRoot = repositoryRoot, })); @@ -101,9 +101,9 @@ await File.WriteAllTextAsync( try { - var logger = Substitute.For>(); - var target = new FileSystemTalkCatalogRepository( - Options.Create(new TalkCatalogRepositoryOptions + var logger = Substitute.For>(); + var target = new TalkCatalogRepository( + Options.Create(new TalkCatalogOptions { DataRoot = repositoryRoot, }), @@ -167,8 +167,8 @@ await File.WriteAllTextAsync( try { - var target = new FileSystemTalkCatalogRepository( - Options.Create(new TalkCatalogRepositoryOptions + var target = new TalkCatalogRepository( + Options.Create(new TalkCatalogOptions { DataRoot = repositoryRoot, })); @@ -219,8 +219,8 @@ await File.WriteAllTextAsync( try { - var target = new FileSystemTalkCatalogRepository( - Options.Create(new TalkCatalogRepositoryOptions + var target = new TalkCatalogRepository( + Options.Create(new TalkCatalogOptions { DataRoot = repositoryRoot, })); @@ -247,9 +247,9 @@ public async Task EmitBoundaryLogs_WhenLoadingCatalog() { // Arrange var repositoryRoot = await CreateRepositoryRoot(); - var logger = new CollectingLogger(); - var target = new FileSystemTalkCatalogRepository( - Options.Create(new TalkCatalogRepositoryOptions + var logger = new CollectingLogger(); + var target = new TalkCatalogRepository( + Options.Create(new TalkCatalogOptions { DataRoot = repositoryRoot, }), @@ -363,4 +363,3 @@ Consider a sequel on identifying sources of caffeine. } } - diff --git a/tst/TalkFolio.Tests/TalksEndpoint_GetTalks_Should.cs b/tst/TalkFolio.Tests/TalksEndpoint_GetTalks_Should.cs index 49c37cb..567c060 100644 --- a/tst/TalkFolio.Tests/TalksEndpoint_GetTalks_Should.cs +++ b/tst/TalkFolio.Tests/TalksEndpoint_GetTalks_Should.cs @@ -27,7 +27,7 @@ public async Task ReturnCanonicalTalks_WhenRepositoryContainsTalkData() { configBuilder.AddInMemoryCollection(new Dictionary { - ["TalkCatalogRepository:DataRoot"] = repositoryRoot, + ["TalkCatalog:DataRoot"] = repositoryRoot, }); }); }); From c44e8aedea32b5ba6ecf08f89fa4c66b92c6c8a6 Mon Sep 17 00:00:00 2001 From: Barry S Stahl Date: Sun, 23 Aug 2026 19:31:36 -0700 Subject: [PATCH 3/5] refactor: separate serialization DTOs - Move YAML serialization models into a Serialization folder - Keep repository implementation and options alongside functional data-layer code - Update repository to use the new serialization namespace Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../{ => Serialization}/PresentationFamilyReference.cs | 2 +- .../{ => Serialization}/ProposalCopyItem.cs | 2 +- .../{ => Serialization}/PublicPresentationReference.cs | 2 +- .../{ => Serialization}/RelatedContentItem.cs | 2 +- src/TalkFolio.Data.YamlFile/{ => Serialization}/TalkRecord.cs | 2 +- src/TalkFolio.Data.YamlFile/TalkCatalogRepository.cs | 1 + 6 files changed, 6 insertions(+), 5 deletions(-) rename src/TalkFolio.Data.YamlFile/{ => Serialization}/PresentationFamilyReference.cs (89%) rename src/TalkFolio.Data.YamlFile/{ => Serialization}/ProposalCopyItem.cs (88%) rename src/TalkFolio.Data.YamlFile/{ => Serialization}/PublicPresentationReference.cs (90%) rename src/TalkFolio.Data.YamlFile/{ => Serialization}/RelatedContentItem.cs (90%) rename src/TalkFolio.Data.YamlFile/{ => Serialization}/TalkRecord.cs (96%) diff --git a/src/TalkFolio.Data.YamlFile/PresentationFamilyReference.cs b/src/TalkFolio.Data.YamlFile/Serialization/PresentationFamilyReference.cs similarity index 89% rename from src/TalkFolio.Data.YamlFile/PresentationFamilyReference.cs rename to src/TalkFolio.Data.YamlFile/Serialization/PresentationFamilyReference.cs index 4371e8e..c93dad5 100644 --- a/src/TalkFolio.Data.YamlFile/PresentationFamilyReference.cs +++ b/src/TalkFolio.Data.YamlFile/Serialization/PresentationFamilyReference.cs @@ -1,4 +1,4 @@ -namespace TalkFolio.Data.YamlFile; +namespace TalkFolio.Data.YamlFile.Serialization; using System.Diagnostics.CodeAnalysis; diff --git a/src/TalkFolio.Data.YamlFile/ProposalCopyItem.cs b/src/TalkFolio.Data.YamlFile/Serialization/ProposalCopyItem.cs similarity index 88% rename from src/TalkFolio.Data.YamlFile/ProposalCopyItem.cs rename to src/TalkFolio.Data.YamlFile/Serialization/ProposalCopyItem.cs index 1a7d46b..4aa9d1e 100644 --- a/src/TalkFolio.Data.YamlFile/ProposalCopyItem.cs +++ b/src/TalkFolio.Data.YamlFile/Serialization/ProposalCopyItem.cs @@ -1,4 +1,4 @@ -namespace TalkFolio.Data.YamlFile; +namespace TalkFolio.Data.YamlFile.Serialization; using System.Diagnostics.CodeAnalysis; diff --git a/src/TalkFolio.Data.YamlFile/PublicPresentationReference.cs b/src/TalkFolio.Data.YamlFile/Serialization/PublicPresentationReference.cs similarity index 90% rename from src/TalkFolio.Data.YamlFile/PublicPresentationReference.cs rename to src/TalkFolio.Data.YamlFile/Serialization/PublicPresentationReference.cs index 8bbc0c3..11f33ed 100644 --- a/src/TalkFolio.Data.YamlFile/PublicPresentationReference.cs +++ b/src/TalkFolio.Data.YamlFile/Serialization/PublicPresentationReference.cs @@ -1,4 +1,4 @@ -namespace TalkFolio.Data.YamlFile; +namespace TalkFolio.Data.YamlFile.Serialization; using System.Diagnostics.CodeAnalysis; diff --git a/src/TalkFolio.Data.YamlFile/RelatedContentItem.cs b/src/TalkFolio.Data.YamlFile/Serialization/RelatedContentItem.cs similarity index 90% rename from src/TalkFolio.Data.YamlFile/RelatedContentItem.cs rename to src/TalkFolio.Data.YamlFile/Serialization/RelatedContentItem.cs index 14e44c4..7942d08 100644 --- a/src/TalkFolio.Data.YamlFile/RelatedContentItem.cs +++ b/src/TalkFolio.Data.YamlFile/Serialization/RelatedContentItem.cs @@ -1,4 +1,4 @@ -namespace TalkFolio.Data.YamlFile; +namespace TalkFolio.Data.YamlFile.Serialization; using System.Diagnostics.CodeAnalysis; diff --git a/src/TalkFolio.Data.YamlFile/TalkRecord.cs b/src/TalkFolio.Data.YamlFile/Serialization/TalkRecord.cs similarity index 96% rename from src/TalkFolio.Data.YamlFile/TalkRecord.cs rename to src/TalkFolio.Data.YamlFile/Serialization/TalkRecord.cs index 676293e..c79ba81 100644 --- a/src/TalkFolio.Data.YamlFile/TalkRecord.cs +++ b/src/TalkFolio.Data.YamlFile/Serialization/TalkRecord.cs @@ -1,4 +1,4 @@ -namespace TalkFolio.Data.YamlFile; +namespace TalkFolio.Data.YamlFile.Serialization; using System.Diagnostics.CodeAnalysis; diff --git a/src/TalkFolio.Data.YamlFile/TalkCatalogRepository.cs b/src/TalkFolio.Data.YamlFile/TalkCatalogRepository.cs index 682f2dd..2b2ef0e 100644 --- a/src/TalkFolio.Data.YamlFile/TalkCatalogRepository.cs +++ b/src/TalkFolio.Data.YamlFile/TalkCatalogRepository.cs @@ -3,6 +3,7 @@ namespace TalkFolio.Data.YamlFile; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using TalkFolio.Data.YamlFile.Serialization; using DomainPresentationFamily = TalkFolio.Entities.PresentationFamily; using DomainProposalCopyItem = TalkFolio.Entities.ProposalCopyItem; using DomainPublicPresentationReference = TalkFolio.Entities.PublicPresentationReference; From 57deb35358c87b304ea29bb1b1f642ccf2c7ba2e Mon Sep 17 00:00:00 2001 From: Barry S Stahl Date: Sun, 23 Aug 2026 20:07:04 -0700 Subject: [PATCH 4/5] Wire domain service composition and required talk-id validation - Register TalkCatalogService in API DI and route /talks through the domain service - Fail fast when a talk file omits Id by throwing MissingTalkIdException - Add repository log event and regression test for missing Id - Update schema and ADR docs to clarify duplicate key is Title + PresentationFamily.Variant only Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/instructions/csharp.instructions.md | 2 - docs/ADRs.md | 1 + docs/TalkSchema.md | 8 +- src/TalkFolio.Api/Program.cs | 6 +- .../TalkCatalogRepository.cs | 33 +++---- .../TalkCatalogRepositoryLog.cs | 3 + .../Exceptions/MissingTalkIdException.cs | 50 +++++++++++ .../TalkCatalogRepository_LoadAsync_Should.cs | 90 ++++++++++++++++++- .../TalkCatalogService_Should.cs | 22 +++++ 9 files changed, 191 insertions(+), 24 deletions(-) create mode 100644 src/TalkFolio/Exceptions/MissingTalkIdException.cs create mode 100644 tst/TalkFolio.Tests/TalkCatalogService_Should.cs diff --git a/.github/instructions/csharp.instructions.md b/.github/instructions/csharp.instructions.md index a3d29f1..669b98f 100644 --- a/.github/instructions/csharp.instructions.md +++ b/.github/instructions/csharp.instructions.md @@ -288,8 +288,6 @@ Demonstrates naming, structure, generics, primary constructors, nullable annotat ```csharp namespace Company.Project.Widgets; -using ItemCache = Dictionary; - /// Defines folding behavior for widgets. public interface IWidget { diff --git a/docs/ADRs.md b/docs/ADRs.md index fc73b6e..510dfb7 100644 --- a/docs/ADRs.md +++ b/docs/ADRs.md @@ -204,6 +204,7 @@ This document consolidates the design decisions reached for TalkFolio. Each entr * Malformed YAML throws `MalformedTalkYamlException`. * Duplicate talk IDs throw `DuplicateTalkIdException`. * Duplicate `(Title, PresentationFamily.Variant)` pairs throw `DuplicateTalkTitleVariantException`. +* `PresentationFamily.Name` does not participate in the duplicate-talk uniqueness key. * These exceptions are logged as load failures and then rethrown so upstream callers can handle each failure type distinctly. * Catalog loads only succeed when all talk files satisfy the repository invariants. diff --git a/docs/TalkSchema.md b/docs/TalkSchema.md index f9ce677..c5063a6 100644 --- a/docs/TalkSchema.md +++ b/docs/TalkSchema.md @@ -42,7 +42,7 @@ Tags: - embeddings - knowledge-graph PresentationFamily: - Id: 8ccdf8b8-fd2c-4d41-9fe0-32fade0f41dc + Name: RAG Deep Dive Variant: Canonical LifecycleStatus: Active ProposalCopyItems: @@ -103,7 +103,7 @@ The current best-fit set of core fields is: * AlternateTitles: list of marketing or branding variants * Category: coarse top-level selection bucket from a controlled list that can expand over time * Tags: topic labels for overlap and CFP matching; free-form strings constrained to alphanumerics and `-` -* PresentationFamily: family membership object with `Id` and `Variant` (for example `Canonical`, `ExecutiveOverview`, `Lightning`, `Workshop`) +* PresentationFamily: family membership object with `Name` and `Variant` (for example `Canonical`, `ExecutiveOverview`, `Lightning`, `Workshop`) * LifecycleStatus: concept-level state * ProposalCopyItems: typed array of inline proposal copy blocks, each with `Type` and `Copy` (`|-` literal block) * TargetAudience: list of audience descriptors drawn from a controlled list that can expand over time @@ -189,6 +189,7 @@ PresentationFamily: * PresentationFamily is not a taxonomy node. * It is a grouping concept, not a category hierarchy. * Family names are treated as stable identifiers for the grouping, not display-only text. +* Catalog duplicate detection keys on Talk `Title` + `PresentationFamily.Variant` only; `PresentationFamily.Name` does not participate in that uniqueness check. * Two talks in the same PresentationFamily should not be co-submitted to the same conference. * TalkCircuit enforces this rule at submission time. * TalkCircuit can find a talk's family members by querying Talks that share its `PresentationFamily.Name`. @@ -329,12 +330,13 @@ The working baseline for the first TalkFolio implementation is: * Talk entity with GUID `Id` and canonical field set * controlled-but-extensible Category list * Tags as free-form, hyphen-safe strings -* presentation family with the Talk owning membership via a nested `PresentationFamily` object (`Id` + `Variant`) +* presentation family with the Talk owning membership via a nested `PresentationFamily` object (`Name` + `Variant`) * concept lifecycle of Ideation | Active | Retired * references to SlideDeckIds and optional public publication references * proposal copy stored inline as `ProposalCopyItems` (typed items with `|-` literal-block copy) * companion material referenced via `RelatedContent` (typed, talk-level, lightweight references) * flexible talk-level flags via `Flags` * unstructured prose limited to `ProposalCopyItems[].Copy`, `IdeationNotes`, `PresentationFamily.Notes`, and `RelatedContent[].Notes` +* duplicate-talk validation keys on `Title` + `PresentationFamily.Variant` only; `PresentationFamily.Name` is not part of that uniqueness rule This gives a clean, minimal schema that matches the domain boundary without pulling in deck-building or submission-state concerns. diff --git a/src/TalkFolio.Api/Program.cs b/src/TalkFolio.Api/Program.cs index eb0548a..1543215 100644 --- a/src/TalkFolio.Api/Program.cs +++ b/src/TalkFolio.Api/Program.cs @@ -2,6 +2,7 @@ namespace TalkFolio.Api; using TalkFolio.Data.YamlFile; using TalkFolio.Interfaces; +using TalkFolio.Services; #pragma warning disable CA1052, CA1515 public partial class Program @@ -15,6 +16,7 @@ public static WebApplication BuildApp(string[] args) .BindConfiguration("TalkCatalog"); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); var app = builder.Build(); @@ -23,13 +25,13 @@ public static WebApplication BuildApp(string[] args) app.MapGet( "/talks", async Task ( - ITalkCatalogRepository repository, + TalkCatalogService service, ILoggerFactory loggerFactory, CancellationToken cancellationToken) => { var logger = loggerFactory.CreateLogger("TalkFolio.Api.TalksEndpoint"); ProgramLog.HandlingGetTalksRequest(logger); - var catalog = await repository.LoadAsync(cancellationToken).ConfigureAwait(false); + var catalog = await service.LoadAsync(cancellationToken).ConfigureAwait(false); ProgramLog.ReturningTalksFromGetTalks(logger, catalog.Talks.Count); if (logger.IsEnabled(LogLevel.Trace)) diff --git a/src/TalkFolio.Data.YamlFile/TalkCatalogRepository.cs b/src/TalkFolio.Data.YamlFile/TalkCatalogRepository.cs index 2b2ef0e..facf02f 100644 --- a/src/TalkFolio.Data.YamlFile/TalkCatalogRepository.cs +++ b/src/TalkFolio.Data.YamlFile/TalkCatalogRepository.cs @@ -4,12 +4,6 @@ namespace TalkFolio.Data.YamlFile; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using TalkFolio.Data.YamlFile.Serialization; -using DomainPresentationFamily = TalkFolio.Entities.PresentationFamily; -using DomainProposalCopyItem = TalkFolio.Entities.ProposalCopyItem; -using DomainPublicPresentationReference = TalkFolio.Entities.PublicPresentationReference; -using DomainRelatedContentItem = TalkFolio.Entities.RelatedContentItem; -using DomainTalk = TalkFolio.Entities.Talk; -using DomainTalkCatalog = TalkFolio.Entities.TalkCatalog; using TalkFolio.Interfaces; using YamlDotNet.Core; using YamlDotNet.Serialization; @@ -28,7 +22,7 @@ public sealed class TalkCatalogRepository( private readonly IOptions _options = options ?? throw new ArgumentNullException(nameof(options)); /// - public async Task LoadAsync(CancellationToken cancellationToken = default) + public async Task LoadAsync(CancellationToken cancellationToken = default) { TalkCatalogRepositoryLog.LoadingCatalog(_logger); @@ -51,10 +45,10 @@ public async Task LoadAsync(CancellationToken cancellationTok var talks = await LoadTalksAsync(talksDirectory, cancellationToken).ConfigureAwait(false); TalkCatalogRepositoryLog.LoadedCatalog(_logger, talks.Count); - return new DomainTalkCatalog(talks); + return new TalkFolio.Entities.TalkCatalog(talks); } - private async Task> LoadTalksAsync(string talksDirectory, CancellationToken cancellationToken) + private async Task> LoadTalksAsync(string talksDirectory, CancellationToken cancellationToken) { if (!Directory.Exists(talksDirectory)) { @@ -62,7 +56,7 @@ private async Task> LoadTalksAsync(string talksDirecto return []; } - var talks = new List(); + var talks = new List(); var seenTalkIds = new Dictionary(); var seenTitleVariants = new Dictionary(); var files = Directory.EnumerateFiles(talksDirectory, "*.*", SearchOption.TopDirectoryOnly) @@ -78,6 +72,13 @@ private async Task> LoadTalksAsync(string talksDirecto TalkCatalogRepositoryLog.DeserializedTalkPayload(_logger, payload.Id, payload.Title, file); + if (payload.Id == Guid.Empty) + { + var missingTalkIdException = MissingTalkIdException.ForFilePath(file); + TalkCatalogRepositoryLog.TalkFileMissingRequiredId(_logger, missingTalkIdException, file); + throw missingTalkIdException; + } + if (seenTalkIds.TryGetValue(payload.Id, out var firstTalkIdFilePath)) { var duplicateIdException = new DuplicateTalkIdException(payload.Id, firstTalkIdFilePath, file); @@ -118,9 +119,9 @@ private async Task> LoadTalksAsync(string talksDirecto return talks.AsReadOnly(); } - private static DomainTalk MapTalk(TalkRecord source) + private static TalkFolio.Entities.Talk MapTalk(TalkRecord source) { - return new DomainTalk( + return new TalkFolio.Entities.Talk( Id: source.Id, Title: source.Title, AlternateTitles: source.AlternateTitles ?? [], @@ -128,20 +129,20 @@ private static DomainTalk MapTalk(TalkRecord source) Tags: source.Tags ?? [], LifecycleStatus: source.LifecycleStatus ?? string.Empty, TargetAudience: source.TargetAudience ?? [], - PresentationFamily: source.PresentationFamily is null ? null : new DomainPresentationFamily( + PresentationFamily: source.PresentationFamily is null ? null : new TalkFolio.Entities.PresentationFamily( source.PresentationFamily.Name ?? string.Empty, source.PresentationFamily.Variant ?? string.Empty), SlideDeckIds: source.SlideDeckIds ?? [], ProposalCopyItems: source.ProposalCopyItems is null ? [] : source.ProposalCopyItems - .Select(static item => new DomainProposalCopyItem(item.Type ?? string.Empty, item.Copy ?? string.Empty)) + .Select(static item => new TalkFolio.Entities.ProposalCopyItem(item.Type ?? string.Empty, item.Copy ?? string.Empty)) .ToList() .AsReadOnly(), PublicPresentationReferences: source.PublicPresentationReferences is null ? [] : source.PublicPresentationReferences - .Select(static item => new DomainPublicPresentationReference( + .Select(static item => new TalkFolio.Entities.PublicPresentationReference( item.Source ?? string.Empty, item.Url, item.PublicId)) @@ -150,7 +151,7 @@ private static DomainTalk MapTalk(TalkRecord source) RelatedContent: source.RelatedContent is null ? [] : source.RelatedContent - .Select(static item => new DomainRelatedContentItem( + .Select(static item => new TalkFolio.Entities.RelatedContentItem( item.Type ?? string.Empty, item.Title ?? string.Empty, item.Url, diff --git a/src/TalkFolio.Data.YamlFile/TalkCatalogRepositoryLog.cs b/src/TalkFolio.Data.YamlFile/TalkCatalogRepositoryLog.cs index f68c4a5..fbc5bf0 100644 --- a/src/TalkFolio.Data.YamlFile/TalkCatalogRepositoryLog.cs +++ b/src/TalkFolio.Data.YamlFile/TalkCatalogRepositoryLog.cs @@ -42,4 +42,7 @@ internal static partial class TalkCatalogRepositoryLog [LoggerMessage(EventId = 13, Level = LogLevel.Error, Message = "Catalog load failed because duplicate talk title and variant were found for Title '{Title}' and Variant '{Variant}' in {DuplicateFilePath}. First seen in {FirstFilePath}.")] public static partial void DuplicateTalkTitleVariant(ILogger logger, Exception exception, string title, string variant, string duplicateFilePath, string firstFilePath); + + [LoggerMessage(EventId = 14, Level = LogLevel.Error, Message = "Catalog load failed because talk file {FilePath} is missing required Id.")] + public static partial void TalkFileMissingRequiredId(ILogger logger, Exception exception, string filePath); } \ No newline at end of file diff --git a/src/TalkFolio/Exceptions/MissingTalkIdException.cs b/src/TalkFolio/Exceptions/MissingTalkIdException.cs new file mode 100644 index 0000000..2eed49e --- /dev/null +++ b/src/TalkFolio/Exceptions/MissingTalkIdException.cs @@ -0,0 +1,50 @@ +namespace TalkFolio; + +/// +/// Represents a talk record that did not supply a required identifier. +/// +public sealed class MissingTalkIdException : TalkCatalogLoadException +{ + /// + /// Initializes a new instance of the class. + /// + public MissingTalkIdException() + : this("Talk file is missing a required Id value.") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message. + public MissingTalkIdException(string message) + : this(message, new InvalidOperationException(message)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message. + /// The inner exception. + public MissingTalkIdException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Creates a missing talk identifier exception for a specific file path. + /// + /// The talk file path that is missing an identifier. + /// The created exception. + public static MissingTalkIdException ForFilePath(string filePath) + => new($"Talk file '{filePath}' is missing a required Id value.") + { + FilePath = filePath, + }; + + /// + /// Gets the talk file path that is missing the required identifier. + /// + public string FilePath { get; private set; } = string.Empty; +} diff --git a/tst/TalkFolio.Data.YamlFile.Tests/TalkCatalogRepository_LoadAsync_Should.cs b/tst/TalkFolio.Data.YamlFile.Tests/TalkCatalogRepository_LoadAsync_Should.cs index 01653cc..77dff48 100644 --- a/tst/TalkFolio.Data.YamlFile.Tests/TalkCatalogRepository_LoadAsync_Should.cs +++ b/tst/TalkFolio.Data.YamlFile.Tests/TalkCatalogRepository_LoadAsync_Should.cs @@ -60,6 +60,49 @@ public async Task ReturnCanonicalCatalog_WhenYamlFilesExist() Assert.Equal("great-cornholio-tp", publicPresentation.PublicId); } + [Fact] + public async Task ThrowInvalidOperationException_WhenDataRootIsNotConfigured() + { + var target = new TalkCatalogRepository(Options.Create(new TalkCatalogOptions())); + + var actual = await Assert.ThrowsAsync( + () => target.LoadAsync(CancellationToken.None)); + + Assert.Equal("The repository data root is not configured.", actual.Message); + } + + [Fact] + public async Task ReturnEmptyCatalog_WhenTalksDirectoryIsMissing() + { + var repositoryRoot = Path.Combine(_dataRoot, "no-talks-directory"); + Directory.CreateDirectory(repositoryRoot); + var target = new TalkCatalogRepository( + Options.Create(new TalkCatalogOptions + { + DataRoot = repositoryRoot, + })); + + var actual = await target.LoadAsync(CancellationToken.None); + + Assert.Empty(actual.Talks); + } + + [Fact] + public async Task ThrowDirectoryNotFoundException_WhenDataRootDoesNotExist() + { + var repositoryRoot = Path.Combine(_dataRoot, "missing-root"); + var target = new TalkCatalogRepository( + Options.Create(new TalkCatalogOptions + { + DataRoot = repositoryRoot, + })); + + var actual = await Assert.ThrowsAsync( + () => target.LoadAsync(CancellationToken.None)); + + Assert.Contains(repositoryRoot, actual.Message, StringComparison.Ordinal); + } + [Fact] public async Task ThrowDuplicateTalkIdException_WhenLaterFilesResolveToSameId() { @@ -242,6 +285,52 @@ await File.WriteAllTextAsync( } } + [Fact] + public async Task ThrowMissingTalkIdException_WhenTalkRecordDoesNotSupplyId() + { + // Arrange + var repositoryRoot = Path.Combine(Path.GetTempPath(), $"talkfolio-missing-id-{Guid.NewGuid():N}"); + Directory.CreateDirectory(repositoryRoot); + var talksDirectory = Directory.CreateDirectory(Path.Combine(repositoryRoot, "talks")); + var talkFilePath = Path.Combine(talksDirectory.FullName, "missing-id.yaml"); + + await File.WriteAllTextAsync( + talkFilePath, + """ + Title: Missing Identifier Talk + Category: Leadership & Community + Tags: + - missing-id + PresentationFamily: + Name: Missing Identifier Family + Variant: Canonical + LifecycleStatus: Active + """); + + try + { + var target = new TalkCatalogRepository( + Options.Create(new TalkCatalogOptions + { + DataRoot = repositoryRoot, + })); + + // Act + var actual = await Assert.ThrowsAsync( + () => target.LoadAsync(CancellationToken.None)); + + // Assert + Assert.Equal(talkFilePath, actual.FilePath); + } + finally + { + if (Directory.Exists(repositoryRoot)) + { + Directory.Delete(repositoryRoot, recursive: true); + } + } + } + [Fact] public async Task EmitBoundaryLogs_WhenLoadingCatalog() { @@ -362,4 +451,3 @@ Consider a sequel on identifying sources of caffeine. return repositoryRoot; } } - diff --git a/tst/TalkFolio.Tests/TalkCatalogService_Should.cs b/tst/TalkFolio.Tests/TalkCatalogService_Should.cs new file mode 100644 index 0000000..bc9f7e7 --- /dev/null +++ b/tst/TalkFolio.Tests/TalkCatalogService_Should.cs @@ -0,0 +1,22 @@ +namespace TalkFolio.Tests; + +using NSubstitute; +using TalkFolio.Entities; +using TalkFolio.Interfaces; +using TalkFolio.Services; + +public sealed class TalkCatalogService_Should +{ + [Fact] + public async Task ReturnRepositoryCatalog_WhenLoadingAsync() + { + var repository = Substitute.For(); + var expected = new TalkCatalog([]); + repository.LoadAsync(Arg.Any()).Returns(expected); + var target = new TalkCatalogService(repository); + + var actual = await target.LoadAsync(CancellationToken.None); + + Assert.Same(expected, actual); + } +} From 27d0a322c38d1ea87f5c4586eb7b39a2783ea8a1 Mon Sep 17 00:00:00 2001 From: Barry Stahl Date: Sun, 23 Aug 2026 20:28:43 -0700 Subject: [PATCH 5/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tst/TalkFolio.Tests/TalkCatalogService_Should.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tst/TalkFolio.Tests/TalkCatalogService_Should.cs b/tst/TalkFolio.Tests/TalkCatalogService_Should.cs index bc9f7e7..7a87b17 100644 --- a/tst/TalkFolio.Tests/TalkCatalogService_Should.cs +++ b/tst/TalkFolio.Tests/TalkCatalogService_Should.cs @@ -5,10 +5,10 @@ namespace TalkFolio.Tests; using TalkFolio.Interfaces; using TalkFolio.Services; -public sealed class TalkCatalogService_Should +public sealed class TalkCatalogService_LoadAsync_Should { [Fact] - public async Task ReturnRepositoryCatalog_WhenLoadingAsync() + public async Task ReturnRepositoryCatalog_WhenLoading() { var repository = Substitute.For(); var expected = new TalkCatalog([]);