using System.Data; using Dapper; using PlotLine.Models; namespace PlotLine.Data; public interface IProjectRepository { Task> ListAsync(); Task GetAsync(int projectId); Task SaveAsync(Project project); Task ArchiveAsync(int projectId); } public interface IBookRepository { Task> ListByProjectAsync(int projectId); Task GetAsync(int bookId); Task SaveAsync(Book book); Task ArchiveAsync(int bookId); } public interface IChapterRepository { Task> ListByBookAsync(int bookId); Task GetAsync(int chapterId); Task SaveAsync(Chapter chapter); Task ArchiveAsync(int chapterId); } public interface ISceneRepository { Task> ListByChapterAsync(int chapterId); Task GetAsync(int sceneId); Task SaveAsync(Scene scene); Task SavePurposesAsync(int sceneId, IEnumerable purposeIds); Task SaveMetricValuesAsync(int sceneId, IEnumerable metricValues); Task ArchiveAsync(int sceneId); Task MoveAsync(int sceneId, string direction); Task> ListChapterOptionsAsync(int projectId); Task GetMovePreviewAsync(int sceneId, int targetChapterId, string position); Task MoveToChapterAsync(int sceneId, int targetChapterId, string position, bool renumber); Task RenumberChapterAsync(int chapterId); } public interface ILookupRepository { Task GetAllAsync(); } public interface ITimelineRepository { Task GetByProjectAsync(int projectId, int? bookId); } public interface IPlotRepository { Task GetLookupsAsync(); Task> ListPlotLinesAsync(int projectId); Task GetPlotLineAsync(int plotLineId); Task SavePlotLineAsync(PlotLineItem plotLine); Task ArchivePlotLineAsync(int plotLineId); Task> ListPlotThreadsByProjectAsync(int projectId); Task> ListPlotThreadsByPlotLineAsync(int plotLineId); Task GetPlotThreadAsync(int plotThreadId); Task SavePlotThreadAsync(PlotThread plotThread); Task ArchivePlotThreadAsync(int plotThreadId); Task> ListThreadEventsBySceneAsync(int sceneId); Task> ListThreadEventsByPlotThreadAsync(int plotThreadId); Task SaveThreadEventAsync(ThreadEvent threadEvent); Task DeleteThreadEventAsync(int threadEventId); Task> ListSceneOptionsAsync(int projectId); } public interface IAssetRepository { Task GetLookupsAsync(); Task> ListAssetsAsync(int projectId); Task GetAssetAsync(int storyAssetId); Task SaveAssetAsync(StoryAsset asset); Task ArchiveAssetAsync(int storyAssetId); Task> ListEventsBySceneAsync(int sceneId); Task> ListEventsByAssetAsync(int storyAssetId); Task SaveEventAsync(AssetEvent assetEvent); Task DeleteEventAsync(int assetEventId); Task> ListDependenciesByAssetAsync(int storyAssetId); Task SaveDependencyAsync(AssetDependency dependency); Task DeleteDependencyAsync(int assetDependencyId); Task> ListCustodyBySceneAsync(int sceneId); Task> ListCustodyByAssetAsync(int storyAssetId); Task SaveCustodyEventAsync(AssetCustodyEvent custodyEvent, string custodianNames, IEnumerable custodianCharacterIds, int custodyRoleId); Task DeleteCustodyEventAsync(int assetCustodyEventId); Task<(IReadOnlyList Assets, IReadOnlyList Events)> GetTimelineAsync(int projectId, int? bookId); Task> CheckDependencyIssuesAsync(int storyAssetId, int sceneId, int? toStateId); } public interface ICharacterRepository { Task GetLookupsAsync(); Task> ListCharactersAsync(int projectId); Task GetCharacterAsync(int characterId); Task SaveCharacterAsync(Character character); Task ArchiveCharacterAsync(int characterId); Task> ListSceneCharactersAsync(int sceneId); Task SaveSceneCharacterAsync(SceneCharacter sceneCharacter); Task DeleteSceneCharacterAsync(int sceneCharacterId); Task> ListAttributeEventsByCharacterAsync(int characterId); Task SaveAttributeEventAsync(CharacterAttributeEvent attributeEvent); Task> ListKnowledgeBySceneAsync(int sceneId); Task> ListKnowledgeByCharacterAsync(int characterId); Task SaveKnowledgeAsync(CharacterKnowledgeItem knowledge); Task> ListRelationshipsByProjectAsync(int projectId); Task> ListRelationshipsByCharacterAsync(int characterId); Task SaveRelationshipAsync(CharacterRelationship relationship); Task ArchiveRelationshipAsync(int characterRelationshipId); Task> ListRelationshipEventsBySceneAsync(int sceneId); Task> ListRelationshipEventsByCharacterAsync(int characterId); Task SaveRelationshipEventAsync(RelationshipEvent relationshipEvent); Task<(IReadOnlyList Characters, IReadOnlyList Appearances)> GetTimelineAsync(int projectId, int? bookId); } public interface ILocationRepository { Task GetLookupsAsync(); Task> ListByProjectAsync(int projectId); Task GetAsync(int locationId); Task SaveAsync(LocationItem location); Task ArchiveAsync(int locationId); Task> ListRelationshipsByProjectAsync(int projectId); Task> ListRelationshipsByLocationAsync(int locationId); Task SaveRelationshipAsync(LocationRelationship relationship); Task ArchiveRelationshipAsync(int locationRelationshipId); Task> ListSceneAssetLocationsAsync(int sceneId); Task SaveSceneAssetLocationAsync(SceneAssetLocation sceneAssetLocation, bool updateCurrentLocation); Task DeleteSceneAssetLocationAsync(int sceneAssetLocationId); Task> ListScenesByLocationAsync(int locationId); Task> ListCharactersByLocationAsync(int locationId); Task> ListAssetsByLocationAsync(int locationId); } public interface IWarningRepository { Task GetLookupsAsync(); Task ValidateProjectAsync(int projectId); Task ValidateBookAsync(int bookId); Task ValidateSceneAsync(int sceneId); Task> ListAsync(int projectId, int? bookId, int? sceneId, int? warningTypeId, int? warningSeverityId, string? entityType, bool includeDismissed, bool includeIntentional); Task> ListBySceneAsync(int sceneId); Task> GetSceneCountsAsync(int projectId, int? bookId); Task DismissAsync(int continuityWarningId); Task MarkIntentionalAsync(int continuityWarningId); Task RestoreAsync(int continuityWarningId); Task FinaliseSceneDependencyWarningsAsync(int projectId, int? bookId); } public interface ISceneDependencyRepository { Task GetLookupsAsync(); Task> ListBySceneAsync(int sceneId); Task> ListByProjectAsync(int projectId, int? bookId); Task> GetCountsAsync(int projectId, int? bookId); Task SaveAsync(SceneDependency dependency); Task ArchiveAsync(int sceneDependencyId); } public interface IAnalyticsRepository { Task GetStoryHealthAsync(int projectId, int? bookId, decimal? startChapterNumber, decimal? endChapterNumber, decimal? startSceneNumber, decimal? endSceneNumber); } public sealed class TimelineData { public Project? Project { get; init; } public IReadOnlyList Books { get; init; } = []; public IReadOnlyList Chapters { get; init; } = []; public IReadOnlyList Scenes { get; init; } = []; public IReadOnlyList PurposeLabels { get; init; } = []; public IReadOnlyList MetricTypes { get; init; } = []; public IReadOnlyList MetricPoints { get; init; } = []; public IReadOnlyList PlotLines { get; init; } = []; public IReadOnlyList ThreadEvents { get; init; } = []; } public sealed class LocationRepository(ISqlConnectionFactory connectionFactory) : ILocationRepository { public async Task GetLookupsAsync() { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.LocationLookup_All", commandType: CommandType.StoredProcedure); return new LocationLookupData { LocationTypes = (await result.ReadAsync()).ToList(), RelationshipTypes = (await result.ReadAsync()).ToList() }; } public async Task> ListByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Location_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Location_Get", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); } public async Task SaveAsync(LocationItem location) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Location_Save", new { location.LocationID, location.ProjectID, location.ParentLocationID, location.LocationName, location.LocationTypeID, location.Description }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Location_Archive", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); } public async Task> ListRelationshipsByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.LocationRelationship_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListRelationshipsByLocationAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.LocationRelationship_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveRelationshipAsync(LocationRelationship relationship) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.LocationRelationship_Save", new { relationship.LocationRelationshipID, relationship.FromLocationID, relationship.ToLocationID, relationship.LocationRelationshipTypeID, relationship.IsBidirectional, relationship.Strength, relationship.CanHearNormalSpeech, relationship.CanHearLoudNoise, relationship.CanSeeMovement, relationship.CanSeeClearly, relationship.CanTravelDirectly, relationship.Notes }, commandType: CommandType.StoredProcedure); } public async Task ArchiveRelationshipAsync(int locationRelationshipId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.LocationRelationship_Archive", new { LocationRelationshipID = locationRelationshipId }, commandType: CommandType.StoredProcedure); } public async Task> ListSceneAssetLocationsAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneAssetLocation_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveSceneAssetLocationAsync(SceneAssetLocation sceneAssetLocation, bool updateCurrentLocation) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneAssetLocation_Save", new { sceneAssetLocation.SceneAssetLocationID, sceneAssetLocation.SceneID, sceneAssetLocation.StoryAssetID, sceneAssetLocation.LocationID, sceneAssetLocation.Description, UpdateCurrentLocation = updateCurrentLocation }, commandType: CommandType.StoredProcedure); } public async Task DeleteSceneAssetLocationAsync(int sceneAssetLocationId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneAssetLocation_Delete", new { SceneAssetLocationID = sceneAssetLocationId }, commandType: CommandType.StoredProcedure); } public async Task> ListScenesByLocationAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.LocationScene_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListCharactersByLocationAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.LocationCharacter_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListAssetsByLocationAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneAssetLocation_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } } public sealed class WarningRepository(ISqlConnectionFactory connectionFactory) : IWarningRepository { public async Task GetLookupsAsync() { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.WarningLookup_All", commandType: CommandType.StoredProcedure); return new WarningLookupData { WarningTypes = (await result.ReadAsync()).ToList(), Severities = (await result.ReadAsync()).ToList() }; } public async Task ValidateProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ContinuityValidation_ValidateProject", new { ProjectID = projectId, TriggeredBy = "User" }, commandType: CommandType.StoredProcedure); } public async Task ValidateBookAsync(int bookId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ContinuityValidation_ValidateBook", new { BookID = bookId, TriggeredBy = "User" }, commandType: CommandType.StoredProcedure); } public async Task ValidateSceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ContinuityValidation_ValidateScene", new { SceneID = sceneId, TriggeredBy = "User" }, commandType: CommandType.StoredProcedure); } public async Task> ListAsync(int projectId, int? bookId, int? sceneId, int? warningTypeId, int? warningSeverityId, string? entityType, bool includeDismissed, bool includeIntentional) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.ContinuityWarning_List", new { ProjectID = projectId, BookID = bookId, SceneID = sceneId, WarningTypeID = warningTypeId, WarningSeverityID = warningSeverityId, EntityType = string.IsNullOrWhiteSpace(entityType) ? null : entityType, IncludeDismissed = includeDismissed, IncludeIntentional = includeIntentional }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.ContinuityWarning_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> GetSceneCountsAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.ContinuityWarning_SceneCounts", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task DismissAsync(int continuityWarningId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.ContinuityWarning_Dismiss", new { ContinuityWarningID = continuityWarningId }, commandType: CommandType.StoredProcedure); } public async Task MarkIntentionalAsync(int continuityWarningId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.ContinuityWarning_MarkIntentional", new { ContinuityWarningID = continuityWarningId }, commandType: CommandType.StoredProcedure); } public async Task RestoreAsync(int continuityWarningId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.ContinuityWarning_Restore", new { ContinuityWarningID = continuityWarningId }, commandType: CommandType.StoredProcedure); } public async Task FinaliseSceneDependencyWarningsAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneDependency_FinaliseWarnings", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); } } public sealed class AnalyticsData { public IReadOnlyList MetricPoints { get; init; } = []; public IReadOnlyList RevisionSummary { get; init; } = []; public IReadOnlyList RevisionAttentionScenes { get; init; } = []; public IReadOnlyList PurposeSummary { get; init; } = []; public IReadOnlyList SceneGaps { get; init; } = []; public IReadOnlyList ThreadHealth { get; init; } = []; public IReadOnlyList AssetHealth { get; init; } = []; public IReadOnlyList WarningSeverityCounts { get; init; } = []; public IReadOnlyList WarningTypeCounts { get; init; } = []; public IReadOnlyList WarningHotspots { get; init; } = []; public IReadOnlyList PovSummary { get; init; } = []; public IReadOnlyList MultiPovChapters { get; init; } = []; public IReadOnlyList CharacterAppearances { get; init; } = []; public IReadOnlyList BusyScenes { get; init; } = []; public IReadOnlyList LocationUsage { get; init; } = []; } public sealed class AnalyticsRepository(ISqlConnectionFactory connectionFactory) : IAnalyticsRepository { public async Task GetStoryHealthAsync(int projectId, int? bookId, decimal? startChapterNumber, decimal? endChapterNumber, decimal? startSceneNumber, decimal? endSceneNumber) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync( "dbo.Analytics_StoryHealth", new { ProjectID = projectId, BookID = bookId, StartChapterNumber = startChapterNumber, EndChapterNumber = endChapterNumber, StartSceneNumber = startSceneNumber, EndSceneNumber = endSceneNumber }, commandType: CommandType.StoredProcedure); return new AnalyticsData { MetricPoints = (await result.ReadAsync()).ToList(), RevisionSummary = (await result.ReadAsync()).ToList(), RevisionAttentionScenes = (await result.ReadAsync()).ToList(), PurposeSummary = (await result.ReadAsync()).ToList(), SceneGaps = (await result.ReadAsync()).ToList(), ThreadHealth = (await result.ReadAsync()).ToList(), AssetHealth = (await result.ReadAsync()).ToList(), WarningSeverityCounts = (await result.ReadAsync()).ToList(), WarningTypeCounts = (await result.ReadAsync()).ToList(), WarningHotspots = (await result.ReadAsync()).ToList(), PovSummary = (await result.ReadAsync()).ToList(), MultiPovChapters = (await result.ReadAsync()).ToList(), CharacterAppearances = (await result.ReadAsync()).ToList(), BusyScenes = (await result.ReadAsync()).ToList(), LocationUsage = (await result.ReadAsync()).ToList() }; } } public sealed class SceneDependencyRepository(ISqlConnectionFactory connectionFactory) : ISceneDependencyRepository { public async Task GetLookupsAsync() { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneDependencyLookup_All", commandType: CommandType.StoredProcedure); return new SceneDependencyLookupData { DependencyTypes = rows.ToList() }; } public async Task> ListBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneDependency_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListByProjectAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneDependency_ListByProject", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> GetCountsAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneDependency_Counts", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveAsync(SceneDependency dependency) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneDependency_Save", new { dependency.SceneDependencyID, dependency.ProjectID, dependency.SourceSceneID, dependency.TargetSceneID, dependency.SceneDependencyTypeID, dependency.Description, dependency.IsHardDependency }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int sceneDependencyId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneDependency_Archive", new { SceneDependencyID = sceneDependencyId }, commandType: CommandType.StoredProcedure); } } public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) : IProjectRepository { public async Task> ListAsync() { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Project_List", commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Project_Get", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); } public async Task SaveAsync(Project project) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Project_Save", new { project.ProjectID, project.ProjectName, project.Description }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Project_Archive", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); } } public sealed class BookRepository(ISqlConnectionFactory connectionFactory) : IBookRepository { public async Task> ListByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Book_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int bookId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Book_Get", new { BookID = bookId }, commandType: CommandType.StoredProcedure); } public async Task SaveAsync(Book book) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Book_Save", new { book.BookID, book.ProjectID, book.BookTitle, book.BookNumber, book.Description }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int bookId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Book_Archive", new { BookID = bookId }, commandType: CommandType.StoredProcedure); } } public sealed class ChapterRepository(ISqlConnectionFactory connectionFactory) : IChapterRepository { public async Task> ListByBookAsync(int bookId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Chapter_ListByBook", new { BookID = bookId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Chapter_Get", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); } public async Task SaveAsync(Chapter chapter) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Chapter_Save", new { chapter.ChapterID, chapter.BookID, chapter.ChapterNumber, chapter.ChapterTitle, chapter.Summary, chapter.RevisionStatusID }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Chapter_Archive", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); } } public sealed class SceneRepository(ISqlConnectionFactory connectionFactory) : ISceneRepository { public async Task> ListByChapterAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Scene_ListByChapter", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Scene_Get", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); var scene = await result.ReadSingleOrDefaultAsync(); if (scene is null) { return null; } scene.SelectedPurposeTypeIds = (await result.ReadAsync()).ToList(); scene.Metrics = (await result.ReadAsync()).ToList(); return scene; } public async Task SaveAsync(Scene scene) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Scene_Save", new { scene.SceneID, scene.ChapterID, scene.SceneNumber, scene.SceneTitle, scene.Summary, scene.TimeModeID, scene.StartDateTime, scene.EndDateTime, scene.DurationAmount, scene.DurationUnitID, scene.RelativeTimeText, scene.TimeConfidenceID, scene.ScenePurposeNotes, scene.SceneOutcomeNotes, scene.RevisionStatusID, scene.PrimaryLocationID }, commandType: CommandType.StoredProcedure); } public async Task SavePurposesAsync(int sceneId, IEnumerable purposeIds) { using var connection = connectionFactory.CreateConnection(); var cleanPurposeIds = purposeIds .Where(id => id > 0) .Distinct() .ToList(); await connection.ExecuteAsync( "dbo.Scene_SavePurposes", new { SceneID = sceneId, PurposeIds = string.Join(',', cleanPurposeIds) }, commandType: CommandType.StoredProcedure); } public async Task SaveMetricValuesAsync(int sceneId, IEnumerable metricValues) { using var connection = connectionFactory.CreateConnection(); foreach (var metric in metricValues) { await connection.ExecuteAsync( "dbo.SceneMetric_SaveValue", new { SceneID = sceneId, metric.MetricTypeID, metric.Value, metric.Notes }, commandType: CommandType.StoredProcedure); } } public async Task ArchiveAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Scene_Archive", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); } public async Task MoveAsync(int sceneId, string direction) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Scene_Move", new { SceneID = sceneId, Direction = direction }, commandType: CommandType.StoredProcedure); } public async Task> ListChapterOptionsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.ChapterOption_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetMovePreviewAsync(int sceneId, int targetChapterId, string position) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.SceneMovePreview_Get", new { SceneID = sceneId, TargetChapterID = targetChapterId, Position = position }, commandType: CommandType.StoredProcedure); } public async Task MoveToChapterAsync(int sceneId, int targetChapterId, string position, bool renumber) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Scene_MoveToChapter", new { SceneID = sceneId, TargetChapterID = targetChapterId, Position = position, Renumber = renumber }, commandType: CommandType.StoredProcedure); } public async Task RenumberChapterAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Scene_RenumberChapter", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); } } public sealed class LookupRepository(ISqlConnectionFactory connectionFactory) : ILookupRepository { public async Task GetAllAsync() { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Lookup_All", commandType: CommandType.StoredProcedure); return new LookupData { RevisionStatuses = (await result.ReadAsync()).ToList(), TimeModes = (await result.ReadAsync()).ToList(), DurationUnits = (await result.ReadAsync()).ToList(), TimeConfidences = (await result.ReadAsync()).ToList(), ScenePurposeTypes = (await result.ReadAsync()).ToList() }; } } public sealed class TimelineRepository(ISqlConnectionFactory connectionFactory) : ITimelineRepository { public async Task GetByProjectAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync( "dbo.Timeline_GetByProject", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); var project = await result.ReadSingleOrDefaultAsync(); var books = (await result.ReadAsync()).ToList(); var chapters = (await result.ReadAsync()).ToList(); var scenes = (await result.ReadAsync()).ToList(); var purposeLabels = (await result.ReadAsync()).ToList(); var metricTypes = (await result.ReadAsync()).ToList(); var metricPoints = (await result.ReadAsync()).ToList(); var plotLines = (await result.ReadAsync()).ToList(); var threadEvents = (await result.ReadAsync()).ToList(); return new TimelineData { Project = project, Books = books, Chapters = chapters, Scenes = scenes, PurposeLabels = purposeLabels, MetricTypes = metricTypes, MetricPoints = metricPoints, PlotLines = plotLines, ThreadEvents = threadEvents }; } } public sealed class PlotRepository(ISqlConnectionFactory connectionFactory) : IPlotRepository { public async Task GetLookupsAsync() { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.PlotLookup_All", commandType: CommandType.StoredProcedure); return new PlotLookupData { PlotLineTypes = (await result.ReadAsync()).ToList(), ThreadTypes = (await result.ReadAsync()).ToList(), ThreadStatuses = (await result.ReadAsync()).ToList(), ThreadEventTypes = (await result.ReadAsync()).ToList() }; } public async Task> ListPlotLinesAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.PlotLine_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetPlotLineAsync(int plotLineId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.PlotLine_Get", new { PlotLineID = plotLineId }, commandType: CommandType.StoredProcedure); } public async Task SavePlotLineAsync(PlotLineItem plotLine) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.PlotLine_Save", new { plotLine.PlotLineID, plotLine.ProjectID, plotLine.BookID, plotLine.PlotLineName, plotLine.PlotLineTypeID, plotLine.Description, plotLine.ParentPlotLineID, plotLine.SortOrder, plotLine.Colour, plotLine.IsMainPlot, plotLine.IsVisibleOnTimeline }, commandType: CommandType.StoredProcedure); } public async Task ArchivePlotLineAsync(int plotLineId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.PlotLine_Archive", new { PlotLineID = plotLineId }, commandType: CommandType.StoredProcedure); } public async Task> ListPlotThreadsByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.PlotThread_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListPlotThreadsByPlotLineAsync(int plotLineId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.PlotThread_ListByPlotLine", new { PlotLineID = plotLineId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetPlotThreadAsync(int plotThreadId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.PlotThread_Get", new { PlotThreadID = plotThreadId }, commandType: CommandType.StoredProcedure); } public async Task SavePlotThreadAsync(PlotThread plotThread) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.PlotThread_Save", new { plotThread.PlotThreadID, plotThread.PlotLineID, plotThread.ThreadTitle, plotThread.ThreadTypeID, plotThread.ThreadStatusID, plotThread.Importance, plotThread.Summary, plotThread.IntroducedSceneID, plotThread.PlannedResolutionSceneID, plotThread.ActualResolutionSceneID }, commandType: CommandType.StoredProcedure); } public async Task ArchivePlotThreadAsync(int plotThreadId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.PlotThread_Archive", new { PlotThreadID = plotThreadId }, commandType: CommandType.StoredProcedure); } public async Task> ListThreadEventsBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.ThreadEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListThreadEventsByPlotThreadAsync(int plotThreadId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.ThreadEvent_ListByPlotThread", new { PlotThreadID = plotThreadId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveThreadEventAsync(ThreadEvent threadEvent) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ThreadEvent_Save", new { threadEvent.ThreadEventID, threadEvent.PlotThreadID, threadEvent.SceneID, threadEvent.EventTypeID, threadEvent.EventTitle, threadEvent.EventDescription }, commandType: CommandType.StoredProcedure); } public async Task DeleteThreadEventAsync(int threadEventId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.ThreadEvent_Delete", new { ThreadEventID = threadEventId }, commandType: CommandType.StoredProcedure); } public async Task> ListSceneOptionsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneOption_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } } public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : IAssetRepository { public async Task GetLookupsAsync() { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.AssetLookup_All", commandType: CommandType.StoredProcedure); return new AssetLookupData { AssetKinds = (await result.ReadAsync()).ToList(), AssetStates = (await result.ReadAsync()).ToList(), AssetEventTypes = (await result.ReadAsync()).ToList(), AssetDependencyTypes = (await result.ReadAsync()).ToList(), AssetCustodyEventTypes = (await result.ReadAsync()).ToList(), CustodyRoles = (await result.ReadAsync()).ToList() }; } public async Task> ListAssetsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.StoryAsset_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAssetAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.StoryAsset_Get", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure); } public async Task SaveAssetAsync(StoryAsset asset) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.StoryAsset_Save", new { asset.StoryAssetID, asset.ProjectID, asset.AssetName, asset.AssetKindID, asset.Description, asset.Importance, asset.CurrentStateID, asset.CurrentLocationID, asset.IsResolved }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAssetAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.StoryAsset_Archive", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure); } public async Task> ListEventsBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.AssetEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListEventsByAssetAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.AssetEvent_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveEventAsync(AssetEvent assetEvent) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.AssetEvent_Save", new { assetEvent.AssetEventID, assetEvent.StoryAssetID, assetEvent.SceneID, assetEvent.AssetEventTypeID, assetEvent.FromStateID, assetEvent.ToStateID, assetEvent.EventTitle, assetEvent.EventDescription }, commandType: CommandType.StoredProcedure); } public async Task DeleteEventAsync(int assetEventId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.AssetEvent_Delete", new { AssetEventID = assetEventId }, commandType: CommandType.StoredProcedure); } public async Task> ListDependenciesByAssetAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.AssetDependency_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveDependencyAsync(AssetDependency dependency) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.AssetDependency_Save", new { dependency.AssetDependencyID, dependency.SourceAssetID, dependency.TargetAssetID, dependency.AssetDependencyTypeID, dependency.SourceRequiredStateID, dependency.TargetBlockedStateID, dependency.Description }, commandType: CommandType.StoredProcedure); } public async Task DeleteDependencyAsync(int assetDependencyId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.AssetDependency_Delete", new { AssetDependencyID = assetDependencyId }, commandType: CommandType.StoredProcedure); } public async Task> ListCustodyBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.AssetCustodyEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListCustodyByAssetAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.AssetCustodyEvent_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveCustodyEventAsync(AssetCustodyEvent custodyEvent, string custodianNames, IEnumerable custodianCharacterIds, int custodyRoleId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.AssetCustodyEvent_Save", new { custodyEvent.AssetCustodyEventID, custodyEvent.StoryAssetID, custodyEvent.SceneID, custodyEvent.AssetCustodyEventTypeID, custodyEvent.Description, CustodianNames = custodianNames, CustodianCharacterIds = string.Join(',', custodianCharacterIds.Where(id => id > 0).Distinct()), CustodyRoleID = custodyRoleId }, commandType: CommandType.StoredProcedure); } public async Task DeleteCustodyEventAsync(int assetCustodyEventId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.AssetCustodyEvent_Delete", new { AssetCustodyEventID = assetCustodyEventId }, commandType: CommandType.StoredProcedure); } public async Task<(IReadOnlyList Assets, IReadOnlyList Events)> GetTimelineAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.AssetTimeline_GetByProject", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); var assets = (await result.ReadAsync()).ToList(); var events = (await result.ReadAsync()).ToList(); return (assets, events); } public async Task> CheckDependencyIssuesAsync(int storyAssetId, int sceneId, int? toStateId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.AssetDependency_CheckForEvent", new { StoryAssetID = storyAssetId, SceneID = sceneId, ToStateID = toStateId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } } public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory) : ICharacterRepository { public async Task GetLookupsAsync() { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.CharacterLookup_All", commandType: CommandType.StoredProcedure); return new CharacterLookupData { RoleTypes = (await result.ReadAsync()).ToList(), PresenceTypes = (await result.ReadAsync()).ToList(), AttributeTypes = (await result.ReadAsync()).ToList(), KnowledgeStates = (await result.ReadAsync()).ToList(), RelationshipTypes = (await result.ReadAsync()).ToList(), RelationshipStates = (await result.ReadAsync()).ToList() }; } public async Task> ListCharactersAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Character_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Character_Get", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); } public async Task SaveCharacterAsync(Character character) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Character_Save", new { character.CharacterID, character.ProjectID, character.CharacterName, character.ShortName, character.Sex, character.BirthDate, character.AgeAtSeriesStart, character.AgeReferenceSceneID, character.Height, character.EyeColour, character.DefaultDescription }, commandType: CommandType.StoredProcedure); } public async Task ArchiveCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Character_Archive", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); } public async Task> ListSceneCharactersAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneCharacter_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveSceneCharacterAsync(SceneCharacter sceneCharacter) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneCharacter_Save", new { sceneCharacter.SceneCharacterID, sceneCharacter.SceneID, sceneCharacter.CharacterID, sceneCharacter.RoleInSceneTypeID, sceneCharacter.PresenceTypeID, sceneCharacter.LocationID, sceneCharacter.EntryLocationID, sceneCharacter.ExitLocationID, sceneCharacter.AppearanceNotes, sceneCharacter.OutfitDescription, sceneCharacter.PhysicalCondition, sceneCharacter.EmotionalState, sceneCharacter.KnowledgeNotes }, commandType: CommandType.StoredProcedure); } public async Task DeleteSceneCharacterAsync(int sceneCharacterId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneCharacter_Delete", new { SceneCharacterID = sceneCharacterId }, commandType: CommandType.StoredProcedure); } public async Task> ListAttributeEventsByCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.CharacterAttributeEvent_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveAttributeEventAsync(CharacterAttributeEvent attributeEvent) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.CharacterAttributeEvent_Save", new { attributeEvent.CharacterAttributeEventID, attributeEvent.CharacterID, attributeEvent.CharacterAttributeTypeID, attributeEvent.SceneID, attributeEvent.AttributeValue, attributeEvent.Description }, commandType: CommandType.StoredProcedure); } public async Task> ListKnowledgeBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.CharacterKnowledge_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListKnowledgeByCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.CharacterKnowledge_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveKnowledgeAsync(CharacterKnowledgeItem knowledge) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.CharacterKnowledge_Save", new { knowledge.CharacterKnowledgeID, knowledge.CharacterID, knowledge.StoryAssetID, knowledge.PlotThreadID, knowledge.SceneID, knowledge.KnowledgeStateID, knowledge.SourceEventID, knowledge.Description }, commandType: CommandType.StoredProcedure); } public async Task> ListRelationshipsByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Relationship_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListRelationshipsByCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Relationship_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveRelationshipAsync(CharacterRelationship relationship) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Relationship_Save", new { relationship.CharacterRelationshipID, relationship.ProjectID, relationship.CharacterAID, relationship.CharacterBID, relationship.RelationshipTypeID, relationship.IsPermanent, relationship.StartSceneID, relationship.EndSceneID, relationship.IsKnownToReader, relationship.Notes }, commandType: CommandType.StoredProcedure); } public async Task ArchiveRelationshipAsync(int characterRelationshipId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Relationship_Archive", new { CharacterRelationshipID = characterRelationshipId }, commandType: CommandType.StoredProcedure); } public async Task> ListRelationshipEventsBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.RelationshipEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListRelationshipEventsByCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.RelationshipEvent_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveRelationshipEventAsync(RelationshipEvent relationshipEvent) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.RelationshipEvent_Save", new { relationshipEvent.RelationshipEventID, relationshipEvent.CharacterRelationshipID, relationshipEvent.SceneID, relationshipEvent.RelationshipStateID, relationshipEvent.Intensity, relationshipEvent.IsKnownToOtherCharacter, relationshipEvent.Description }, commandType: CommandType.StoredProcedure); } public async Task<(IReadOnlyList Characters, IReadOnlyList Appearances)> GetTimelineAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.CharacterTimeline_GetByProject", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); var characters = (await result.ReadAsync()).ToList(); var appearances = (await result.ReadAsync()).ToList(); return (characters, appearances); } }