From 2fa71e94eac38b0a4a35b89634c599d5da503c17 Mon Sep 17 00:00:00 2001 From: Dave Walker Date: Sat, 18 Nov 2023 09:43:56 +0000 Subject: [PATCH] Added report export to the UI --- docker/api/Dockerfile | 4 +- docker/ui/Dockerfile | 4 +- .../Controllers/ExportController.cs | 51 ++++++++++- .../ArtistStatisticsExportWorkItem.cs | 8 ++ .../Entities/GenreStatisticsExportWorkItem.cs | 8 ++ .../Entities/MonthlySpendExportWorkItem.cs | 7 ++ .../MusicCatalogue.Api.csproj | 6 +- src/MusicCatalogue.Api/Program.cs | 12 +++ .../Services/ArtistStatisticsExportService.cs | 47 ++++++++++ .../Services/BackgroundQueueProcessor.cs | 4 +- .../Services/CatalogueExportService.cs | 7 +- .../Services/GenreStatisticsExportService.cs | 45 ++++++++++ .../Services/MonthlySpendExportService.cs | 45 ++++++++++ src/MusicCatalogue.Api/appsettings.json | 1 + .../MusicCatalogue.Data.csproj | 4 +- .../Config/MusicApplicationSettings.cs | 1 + .../DataExchange/ExportEventArgs.cs | 11 +++ .../Interfaces/ICsvExporter.cs | 10 +++ .../Interfaces/IJobStatusManager.cs | 2 +- .../Interfaces/IMusicCatalogueFactory.cs | 4 +- .../MusicCatalogue.Entities.csproj | 4 +- .../Collection/AlbumLookupManager.cs | 2 +- .../CatalogueCsvExporter.cs} | 6 +- .../CatalogueCsvImporter.cs} | 8 +- .../CatalogueExporterBase.cs} | 8 +- .../CatalogueXlsxExporter.cs} | 6 +- .../{ => Catalogue}/DataExchangeBase.cs | 0 .../DataExchange/Generic/CsvExporter.cs | 85 +++++++++++++++++++ .../DataExchange/Generic/ExporterBase.cs | 31 +++++++ .../Factory/MusicCatalogueFactory.cs | 16 ++-- .../MusicCatalogue.Logic.csproj | 4 +- .../Logic/DataExport.cs | 2 +- .../MusicCatalogue.LookupTool.csproj | 6 +- .../AlbumLookupManagerTest.cs | 6 +- src/MusicCatalogue.Tests/AlbumManagerTest.cs | 6 +- ...geTest.cs => CatalogueDataExchangeTest.cs} | 8 +- .../JobStatusManagerTest.cs | 10 +-- .../RetailerManagerTest.cs | 4 +- 38 files changed, 425 insertions(+), 68 deletions(-) create mode 100644 src/MusicCatalogue.Api/Entities/ArtistStatisticsExportWorkItem.cs create mode 100644 src/MusicCatalogue.Api/Entities/GenreStatisticsExportWorkItem.cs create mode 100644 src/MusicCatalogue.Api/Entities/MonthlySpendExportWorkItem.cs create mode 100644 src/MusicCatalogue.Api/Services/ArtistStatisticsExportService.cs create mode 100644 src/MusicCatalogue.Api/Services/GenreStatisticsExportService.cs create mode 100644 src/MusicCatalogue.Api/Services/MonthlySpendExportService.cs create mode 100644 src/MusicCatalogue.Entities/DataExchange/ExportEventArgs.cs create mode 100644 src/MusicCatalogue.Entities/Interfaces/ICsvExporter.cs rename src/MusicCatalogue.Logic/DataExchange/{CsvExporter.cs => Catalogue/CatalogueCsvExporter.cs} (86%) rename src/MusicCatalogue.Logic/DataExchange/{CsvImporter.cs => Catalogue/CatalogueCsvImporter.cs} (93%) rename src/MusicCatalogue.Logic/DataExchange/{DataExportBase.cs => Catalogue/CatalogueExporterBase.cs} (93%) rename src/MusicCatalogue.Logic/DataExchange/{XlsxExporter.cs => Catalogue/CatalogueXlsxExporter.cs} (92%) rename src/MusicCatalogue.Logic/DataExchange/{ => Catalogue}/DataExchangeBase.cs (100%) create mode 100644 src/MusicCatalogue.Logic/DataExchange/Generic/CsvExporter.cs create mode 100644 src/MusicCatalogue.Logic/DataExchange/Generic/ExporterBase.cs rename src/MusicCatalogue.Tests/{DataExchangeTest.cs => CatalogueDataExchangeTest.cs} (93%) diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index 67582d1..5d7ba68 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -1,4 +1,4 @@ FROM mcr.microsoft.com/dotnet/core/aspnet:latest -COPY musiccatalogue.api-1.18.0.0 /opt/musiccatalogue.api-1.18.0.0 -WORKDIR /opt/musiccatalogue.api-1.18.0.0/bin +COPY musiccatalogue.api-1.19.0.0 /opt/musiccatalogue.api-1.19.0.0 +WORKDIR /opt/musiccatalogue.api-1.19.0.0/bin ENTRYPOINT [ "./MusicCatalogue.Api" ] diff --git a/docker/ui/Dockerfile b/docker/ui/Dockerfile index 1fcf5c6..22ab4bd 100644 --- a/docker/ui/Dockerfile +++ b/docker/ui/Dockerfile @@ -1,6 +1,6 @@ FROM node:20-alpine -COPY musiccatalogue.ui-1.18.0.0 /opt/musiccatalogue.ui-1.18.0.0 -WORKDIR /opt/musiccatalogue.ui-1.18.0.0 +COPY musiccatalogue.ui-1.19.0.0 /opt/musiccatalogue.ui-1.19.0.0 +WORKDIR /opt/musiccatalogue.ui-1.19.0.0 RUN npm install RUN npm run build ENTRYPOINT [ "npm", "start" ] diff --git a/src/MusicCatalogue.Api/Controllers/ExportController.cs b/src/MusicCatalogue.Api/Controllers/ExportController.cs index 18e76fd..dfbedc6 100644 --- a/src/MusicCatalogue.Api/Controllers/ExportController.cs +++ b/src/MusicCatalogue.Api/Controllers/ExportController.cs @@ -12,15 +12,26 @@ namespace MusicCatalogue.Api.Controllers public class ExportController : Controller { private readonly IBackgroundQueue _catalogueQueue; + private readonly IBackgroundQueue _artistStatisticsQueue; + private readonly IBackgroundQueue _genreStatisticsQueue; + private readonly IBackgroundQueue _monthlySpendQueue; - public ExportController(IBackgroundQueue catalogueQueue) + public ExportController( + IBackgroundQueue catalogueQueue, + IBackgroundQueue artistStatisticsQueue, + IBackgroundQueue genreStatisticsQueue, + IBackgroundQueue monthlySpendQueue + ) { _catalogueQueue = catalogueQueue; + _artistStatisticsQueue = artistStatisticsQueue; + _genreStatisticsQueue = genreStatisticsQueue; + _monthlySpendQueue = monthlySpendQueue; } [HttpPost] [Route("catalogue")] - public IActionResult Export([FromBody] CatalogueExportWorkItem item) + public IActionResult ExportCatalogue([FromBody] CatalogueExportWorkItem item) { // Set the job name used in the job status record item.JobName = "Catalogue Export"; @@ -29,5 +40,41 @@ public IActionResult Export([FromBody] CatalogueExportWorkItem item) _catalogueQueue.Enqueue(item); return Accepted(); } + + [HttpPost] + [Route("artiststatistics")] + public IActionResult ExportArtistStatisticsReport([FromBody] ArtistStatisticsExportWorkItem item) + { + // Set the job name used in the job status record + item.JobName = "Artist Statistics Export"; + + // Queue the work item + _artistStatisticsQueue.Enqueue(item); + return Accepted(); + } + + [HttpPost] + [Route("genrestatistics")] + public IActionResult ExportGenreStatisticsReport([FromBody] GenreStatisticsExportWorkItem item) + { + // Set the job name used in the job status record + item.JobName = "Genre Statistics Export"; + + // Queue the work item + _genreStatisticsQueue.Enqueue(item); + return Accepted(); + } + + [HttpPost] + [Route("monthlyspend")] + public IActionResult ExportMonthySpendReport([FromBody] MonthlySpendExportWorkItem item) + { + // Set the job name used in the job status record + item.JobName = "Monthly Spending Export"; + + // Queue the work item + _monthlySpendQueue.Enqueue(item); + return Accepted(); + } } } diff --git a/src/MusicCatalogue.Api/Entities/ArtistStatisticsExportWorkItem.cs b/src/MusicCatalogue.Api/Entities/ArtistStatisticsExportWorkItem.cs new file mode 100644 index 0000000..f6254b6 --- /dev/null +++ b/src/MusicCatalogue.Api/Entities/ArtistStatisticsExportWorkItem.cs @@ -0,0 +1,8 @@ +namespace MusicCatalogue.Api.Entities +{ + public class ArtistStatisticsExportWorkItem : BackgroundWorkItem + { + public string FileName { get; set; } = ""; + public bool WishList { get; set; } + } +} diff --git a/src/MusicCatalogue.Api/Entities/GenreStatisticsExportWorkItem.cs b/src/MusicCatalogue.Api/Entities/GenreStatisticsExportWorkItem.cs new file mode 100644 index 0000000..e3992bc --- /dev/null +++ b/src/MusicCatalogue.Api/Entities/GenreStatisticsExportWorkItem.cs @@ -0,0 +1,8 @@ +namespace MusicCatalogue.Api.Entities +{ + public class GenreStatisticsExportWorkItem : BackgroundWorkItem + { + public string FileName { get; set; } = ""; + public bool WishList { get; set; } + } +} diff --git a/src/MusicCatalogue.Api/Entities/MonthlySpendExportWorkItem.cs b/src/MusicCatalogue.Api/Entities/MonthlySpendExportWorkItem.cs new file mode 100644 index 0000000..64e9026 --- /dev/null +++ b/src/MusicCatalogue.Api/Entities/MonthlySpendExportWorkItem.cs @@ -0,0 +1,7 @@ +namespace MusicCatalogue.Api.Entities +{ + public class MonthlySpendExportWorkItem : BackgroundWorkItem + { + public string FileName { get; set; } = ""; + } +} diff --git a/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj b/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj index e63ac14..43c8b65 100644 --- a/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj +++ b/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj @@ -2,9 +2,9 @@ net7.0 - 1.18.0.0 - 1.18.0.0 - 1.18.0 + 1.19.0.0 + 1.19.0.0 + 1.19.0 enable enable diff --git a/src/MusicCatalogue.Api/Program.cs b/src/MusicCatalogue.Api/Program.cs index aaf0216..03fda86 100644 --- a/src/MusicCatalogue.Api/Program.cs +++ b/src/MusicCatalogue.Api/Program.cs @@ -103,6 +103,18 @@ public static void Main(string[] args) builder.Services.AddSingleton, BackgroundQueue>(); builder.Services.AddHostedService(); + // Add the artist statistics exporter hosted service + builder.Services.AddSingleton, BackgroundQueue>(); + builder.Services.AddHostedService(); + + // Add the genre statistics exporter hosted service + builder.Services.AddSingleton, BackgroundQueue>(); + builder.Services.AddHostedService(); + + // Add the monthly spend report exporter hosted service + builder.Services.AddSingleton, BackgroundQueue>(); + builder.Services.AddHostedService(); + // Configure JWT byte[] key = Encoding.ASCII.GetBytes(settings!.Secret); builder.Services.AddAuthentication(x => diff --git a/src/MusicCatalogue.Api/Services/ArtistStatisticsExportService.cs b/src/MusicCatalogue.Api/Services/ArtistStatisticsExportService.cs new file mode 100644 index 0000000..be2800e --- /dev/null +++ b/src/MusicCatalogue.Api/Services/ArtistStatisticsExportService.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.Options; +using MusicCatalogue.Api.Entities; +using MusicCatalogue.Api.Interfaces; +using MusicCatalogue.Entities.Config; +using MusicCatalogue.Entities.Interfaces; +using MusicCatalogue.Entities.Reporting; +using MusicCatalogue.Logic.DataExchange.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MusicCatalogue.Api.Services +{ + [ExcludeFromCodeCoverage] + public class ArtistStatisticsExportService : BackgroundQueueProcessor + { + private readonly MusicApplicationSettings _settings; + public ArtistStatisticsExportService( + ILogger> logger, + IBackgroundQueue queue, + IServiceScopeFactory serviceScopeFactory, + IOptions settings) + : base(logger, queue, serviceScopeFactory) + { + _settings = settings.Value; + } + + /// + /// Export the artist statistics report + /// + /// + /// + /// + protected override async Task ProcessWorkItem(ArtistStatisticsExportWorkItem item, IMusicCatalogueFactory factory) + { + // Get the report data + MessageLogger.LogInformation("Retrieving the artist statistics report for export"); + var records = await factory.ArtistStatistics.GenerateReportAsync(item.WishList, 1, int.MaxValue); + + // Construct the full path to the export file + var filePath = Path.Combine(_settings.ReportsExportPath, item.FileName); + + // Export the report + var exporter = new CsvExporter(); + exporter.Export(records, filePath, ','); + MessageLogger.LogInformation("Artist statistics report export completed"); + } + } +} \ No newline at end of file diff --git a/src/MusicCatalogue.Api/Services/BackgroundQueueProcessor.cs b/src/MusicCatalogue.Api/Services/BackgroundQueueProcessor.cs index 29462e8..4f6996d 100644 --- a/src/MusicCatalogue.Api/Services/BackgroundQueueProcessor.cs +++ b/src/MusicCatalogue.Api/Services/BackgroundQueueProcessor.cs @@ -53,7 +53,7 @@ protected override async Task ExecuteAsync(CancellationToken token) var factory = scope.ServiceProvider.GetService(); // Create the job status record - var status = await factory.JobStatuses.AddAsync(item.JobName, item.ToString()); + var status = await factory!.JobStatuses.AddAsync(item.JobName, item.ToString()); try { @@ -61,7 +61,9 @@ protected override async Task ExecuteAsync(CancellationToken token) // no error MessageLogger.LogInformation($"Processing work item {item.ToString()}"); await ProcessWorkItem(item, factory); +#pragma warning disable CS8625 await factory.JobStatuses.UpdateAsync(status.Id, null); +#pragma warning restore CS8625 MessageLogger.LogInformation($"Finished processing work item {item.ToString()}"); } catch (Exception ex) diff --git a/src/MusicCatalogue.Api/Services/CatalogueExportService.cs b/src/MusicCatalogue.Api/Services/CatalogueExportService.cs index bea68a3..bb4b529 100644 --- a/src/MusicCatalogue.Api/Services/CatalogueExportService.cs +++ b/src/MusicCatalogue.Api/Services/CatalogueExportService.cs @@ -22,24 +22,23 @@ public CatalogueExportService( } /// - /// Export all the sightings from the database + /// Export the catalogue /// /// /// /// protected override async Task ProcessWorkItem(CatalogueExportWorkItem item, IMusicCatalogueFactory factory) { - // Get the list of sightings to export MessageLogger.LogInformation("Retrieving tracks for export"); // Use the file extension to determine which exporter to use var extension = Path.GetExtension(item.FileName).ToLower(); - IExporter? exporter = extension == ".xlsx" ? factory.XlsxExporter : factory.CsvExporter; + IExporter? exporter = extension == ".xlsx" ? factory.CatalogueXlsxExporter : factory.CatalogueCsvExporter; // Construct the full path to the export file var filePath = Path.Combine(_settings.CatalogueExportPath, item.FileName); - // Export the file + // Export the catalogue await exporter.Export(filePath); MessageLogger.LogInformation("Catalogue export completed"); } diff --git a/src/MusicCatalogue.Api/Services/GenreStatisticsExportService.cs b/src/MusicCatalogue.Api/Services/GenreStatisticsExportService.cs new file mode 100644 index 0000000..9563109 --- /dev/null +++ b/src/MusicCatalogue.Api/Services/GenreStatisticsExportService.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Options; +using MusicCatalogue.Api.Entities; +using MusicCatalogue.Api.Interfaces; +using MusicCatalogue.Entities.Config; +using MusicCatalogue.Entities.Interfaces; +using MusicCatalogue.Entities.Reporting; +using MusicCatalogue.Logic.DataExchange.Generic; + +namespace MusicCatalogue.Api.Services +{ + public class GenreStatisticsExportService : BackgroundQueueProcessor + { + private readonly MusicApplicationSettings _settings; + public GenreStatisticsExportService( + ILogger> logger, + IBackgroundQueue queue, + IServiceScopeFactory serviceScopeFactory, + IOptions settings) + : base(logger, queue, serviceScopeFactory) + { + _settings = settings.Value; + } + + /// + /// Export the genre statistics report + /// + /// + /// + /// + protected override async Task ProcessWorkItem(GenreStatisticsExportWorkItem item, IMusicCatalogueFactory factory) + { + // Get the report data + MessageLogger.LogInformation("Retrieving the genre statistics report for export"); + var records = await factory.GenreStatistics.GenerateReportAsync(item.WishList, 1, int.MaxValue); + + // Construct the full path to the export file + var filePath = Path.Combine(_settings.ReportsExportPath, item.FileName); + + // Export the report + var exporter = new CsvExporter(); + exporter.Export(records, filePath, ','); + MessageLogger.LogInformation("Genre statistics report export completed"); + } + } +} \ No newline at end of file diff --git a/src/MusicCatalogue.Api/Services/MonthlySpendExportService.cs b/src/MusicCatalogue.Api/Services/MonthlySpendExportService.cs new file mode 100644 index 0000000..4b5fcfc --- /dev/null +++ b/src/MusicCatalogue.Api/Services/MonthlySpendExportService.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Options; +using MusicCatalogue.Api.Entities; +using MusicCatalogue.Api.Interfaces; +using MusicCatalogue.Entities.Config; +using MusicCatalogue.Entities.Interfaces; +using MusicCatalogue.Entities.Reporting; +using MusicCatalogue.Logic.DataExchange.Generic; + +namespace MusicCatalogue.Api.Services +{ + public class MonthlySpendExportService : BackgroundQueueProcessor + { + private readonly MusicApplicationSettings _settings; + public MonthlySpendExportService( + ILogger> logger, + IBackgroundQueue queue, + IServiceScopeFactory serviceScopeFactory, + IOptions settings) + : base(logger, queue, serviceScopeFactory) + { + _settings = settings.Value; + } + + /// + /// Export the monthly spend report + /// + /// + /// + /// + protected override async Task ProcessWorkItem(MonthlySpendExportWorkItem item, IMusicCatalogueFactory factory) + { + // Get the report data + MessageLogger.LogInformation("Retrieving the monthly spending report for export"); + var records = await factory.MonthlySpend.GenerateReportAsync(false, 1, int.MaxValue); + + // Construct the full path to the export file + var filePath = Path.Combine(_settings.ReportsExportPath, item.FileName); + + // Export the report + var exporter = new CsvExporter(); + exporter.Export(records, filePath, ','); + MessageLogger.LogInformation("Monthly spending report export completed"); + } + } +} \ No newline at end of file diff --git a/src/MusicCatalogue.Api/appsettings.json b/src/MusicCatalogue.Api/appsettings.json index 9becc2c..d7cb500 100644 --- a/src/MusicCatalogue.Api/appsettings.json +++ b/src/MusicCatalogue.Api/appsettings.json @@ -6,6 +6,7 @@ "MinimumLogLevel": "Info", "Environment": "Development", "CatalogueExportPath": "C:\\MyApps\\MusicCatalogue\\Export", + "ReportsExportPath": "C:\\MyApps\\MusicCatalogue\\Export\\Reports", "ApiEndpoints": [ { "EndpointType": "Albums", diff --git a/src/MusicCatalogue.Data/MusicCatalogue.Data.csproj b/src/MusicCatalogue.Data/MusicCatalogue.Data.csproj index bc01c2f..2d9e264 100644 --- a/src/MusicCatalogue.Data/MusicCatalogue.Data.csproj +++ b/src/MusicCatalogue.Data/MusicCatalogue.Data.csproj @@ -5,7 +5,7 @@ enable enable MusicCatalogue.Data - 1.17.0.0 + 1.18.0.0 Dave Walker Copyright (c) Dave Walker 2023 Dave Walker @@ -17,7 +17,7 @@ https://github.com/davewalker5/MusicCatalogue MIT false - 1.17.0.0 + 1.18.0.0 diff --git a/src/MusicCatalogue.Entities/Config/MusicApplicationSettings.cs b/src/MusicCatalogue.Entities/Config/MusicApplicationSettings.cs index 73ea003..e2da087 100644 --- a/src/MusicCatalogue.Entities/Config/MusicApplicationSettings.cs +++ b/src/MusicCatalogue.Entities/Config/MusicApplicationSettings.cs @@ -12,6 +12,7 @@ public class MusicApplicationSettings public string LogFile { get; set; } = ""; public MusicCatalogueEnvironment Environment { get; set; } public string CatalogueExportPath { get; set; } = ""; + public string ReportsExportPath { get; set; } = ""; public List ApiEndpoints { get; set; } = new List(); public List ApiServiceKeys { get; set; } = new List(); diff --git a/src/MusicCatalogue.Entities/DataExchange/ExportEventArgs.cs b/src/MusicCatalogue.Entities/DataExchange/ExportEventArgs.cs new file mode 100644 index 0000000..4be3cd8 --- /dev/null +++ b/src/MusicCatalogue.Entities/DataExchange/ExportEventArgs.cs @@ -0,0 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + +namespace MusicCatalogue.Entities.DataExchange +{ + [ExcludeFromCodeCoverage] + public class ExportEventArgs : EventArgs where T : class + { + public long RecordCount { get; set; } + public T? RecordSource { get; set; } + } +} diff --git a/src/MusicCatalogue.Entities/Interfaces/ICsvExporter.cs b/src/MusicCatalogue.Entities/Interfaces/ICsvExporter.cs new file mode 100644 index 0000000..d9d8110 --- /dev/null +++ b/src/MusicCatalogue.Entities/Interfaces/ICsvExporter.cs @@ -0,0 +1,10 @@ +using MusicCatalogue.Entities.DataExchange; + +namespace MusicCatalogue.Entities.Interfaces +{ + public interface ICsvExporter where T : class + { + event EventHandler> RecordExport; + void Export(IEnumerable entities, string fileName, char separator); + } +} diff --git a/src/MusicCatalogue.Entities/Interfaces/IJobStatusManager.cs b/src/MusicCatalogue.Entities/Interfaces/IJobStatusManager.cs index 303be43..4711a12 100644 --- a/src/MusicCatalogue.Entities/Interfaces/IJobStatusManager.cs +++ b/src/MusicCatalogue.Entities/Interfaces/IJobStatusManager.cs @@ -8,6 +8,6 @@ public interface IJobStatusManager Task AddAsync(string name, string parameters); Task GetAsync(Expression> predicate); IAsyncEnumerable ListAsync(Expression> predicate, int pageNumber, int pageSize); - Task UpdateAsync(long id, string error); + Task UpdateAsync(long id, string error); } } \ No newline at end of file diff --git a/src/MusicCatalogue.Entities/Interfaces/IMusicCatalogueFactory.cs b/src/MusicCatalogue.Entities/Interfaces/IMusicCatalogueFactory.cs index 56f7c0e..d7a2dcc 100644 --- a/src/MusicCatalogue.Entities/Interfaces/IMusicCatalogueFactory.cs +++ b/src/MusicCatalogue.Entities/Interfaces/IMusicCatalogueFactory.cs @@ -13,8 +13,8 @@ public interface IMusicCatalogueFactory IRetailerManager Retailers { get; } IUserManager Users { get; } IImporter Importer { get; } - IExporter CsvExporter { get; } - IExporter XlsxExporter { get; } + IExporter CatalogueCsvExporter { get; } + IExporter CatalogueXlsxExporter { get; } IJobStatusManager JobStatuses { get; } ISearchManager Search { get; } IWishListBasedReport GenreStatistics { get; } diff --git a/src/MusicCatalogue.Entities/MusicCatalogue.Entities.csproj b/src/MusicCatalogue.Entities/MusicCatalogue.Entities.csproj index 2e91ad9..8c8642d 100644 --- a/src/MusicCatalogue.Entities/MusicCatalogue.Entities.csproj +++ b/src/MusicCatalogue.Entities/MusicCatalogue.Entities.csproj @@ -5,7 +5,7 @@ enable enable MusicCatalogue.Entities - 1.17.0.0 + 1.18.0.0 Dave Walker Copyright (c) Dave Walker 2023 Dave Walker @@ -17,7 +17,7 @@ https://github.com/davewalker5/MusicCatalogue MIT false - 1.17.0.0 + 1.18.0.0 diff --git a/src/MusicCatalogue.Logic/Collection/AlbumLookupManager.cs b/src/MusicCatalogue.Logic/Collection/AlbumLookupManager.cs index 86529a8..a366997 100644 --- a/src/MusicCatalogue.Logic/Collection/AlbumLookupManager.cs +++ b/src/MusicCatalogue.Logic/Collection/AlbumLookupManager.cs @@ -79,7 +79,7 @@ private async Task StoreAlbumLocally(string artistName, Album template, b // Save the artist details, first. As with all the database calls in this method, the // logic to prevent duplication of artistsand genres is in the management class var artist = await _factory.Artists.AddAsync(artistName); - var genre = await _factory.Genres.AddAsync(template.Genre.Name); + var genre = await _factory.Genres.AddAsync(template.Genre!.Name); // Save the album details var album = await _factory.Albums.AddAsync( diff --git a/src/MusicCatalogue.Logic/DataExchange/CsvExporter.cs b/src/MusicCatalogue.Logic/DataExchange/Catalogue/CatalogueCsvExporter.cs similarity index 86% rename from src/MusicCatalogue.Logic/DataExchange/CsvExporter.cs rename to src/MusicCatalogue.Logic/DataExchange/Catalogue/CatalogueCsvExporter.cs index abd1689..cd9fe8d 100644 --- a/src/MusicCatalogue.Logic/DataExchange/CsvExporter.cs +++ b/src/MusicCatalogue.Logic/DataExchange/Catalogue/CatalogueCsvExporter.cs @@ -2,14 +2,14 @@ using MusicCatalogue.Entities.Interfaces; using System.Text; -namespace MusicCatalogue.Logic.DataExchange +namespace MusicCatalogue.Logic.DataExchange.Catalogue { - public class CsvExporter : DataExportBase, IExporter + public class CatalogueCsvExporter : CatalogueExporterBase, IExporter { private StreamWriter? _writer = null; #pragma warning disable CS8618 - internal CsvExporter(IMusicCatalogueFactory factory) : base(factory) + internal CatalogueCsvExporter(IMusicCatalogueFactory factory) : base(factory) { } #pragma warning restore CS8618 diff --git a/src/MusicCatalogue.Logic/DataExchange/CsvImporter.cs b/src/MusicCatalogue.Logic/DataExchange/Catalogue/CatalogueCsvImporter.cs similarity index 93% rename from src/MusicCatalogue.Logic/DataExchange/CsvImporter.cs rename to src/MusicCatalogue.Logic/DataExchange/Catalogue/CatalogueCsvImporter.cs index d58be23..50d0142 100644 --- a/src/MusicCatalogue.Logic/DataExchange/CsvImporter.cs +++ b/src/MusicCatalogue.Logic/DataExchange/Catalogue/CatalogueCsvImporter.cs @@ -5,15 +5,15 @@ using System.Diagnostics.CodeAnalysis; using System.Text; -namespace MusicCatalogue.Logic.DataExchange +namespace MusicCatalogue.Logic.DataExchange.Catalogue { [ExcludeFromCodeCoverage] - public partial class CsvImporter : DataExchangeBase, IImporter + public partial class CatalogueCsvImporter : DataExchangeBase, IImporter { public event EventHandler? TrackImport; #pragma warning disable CS8618 - internal CsvImporter(IMusicCatalogueFactory factory) : base(factory) + internal CatalogueCsvImporter(IMusicCatalogueFactory factory) : base(factory) { } #pragma warning restore CS8618 @@ -47,7 +47,7 @@ public async Task Import(string file) // Inflate the CSV record to a track and save the artist and genre var track = FlattenedTrack.FromCsv(fields!); var artist = await _factory.Artists.AddAsync(track.ArtistName); - var genre = await _factory.Genres.AddAsync(track.Genre); + var genre = await _factory.Genres.AddAsync(track.Genre!); // Add the retailer int? retailerId = null; diff --git a/src/MusicCatalogue.Logic/DataExchange/DataExportBase.cs b/src/MusicCatalogue.Logic/DataExchange/Catalogue/CatalogueExporterBase.cs similarity index 93% rename from src/MusicCatalogue.Logic/DataExchange/DataExportBase.cs rename to src/MusicCatalogue.Logic/DataExchange/Catalogue/CatalogueExporterBase.cs index 9f7b33e..c37ffd0 100644 --- a/src/MusicCatalogue.Logic/DataExchange/DataExportBase.cs +++ b/src/MusicCatalogue.Logic/DataExchange/Catalogue/CatalogueExporterBase.cs @@ -1,10 +1,9 @@ using MusicCatalogue.Entities.DataExchange; using MusicCatalogue.Entities.Interfaces; -using System.Diagnostics.CodeAnalysis; -namespace MusicCatalogue.Logic.DataExchange +namespace MusicCatalogue.Logic.DataExchange.Catalogue { - public abstract class DataExportBase : DataExchangeBase + public abstract class CatalogueExporterBase : DataExchangeBase { private readonly string[] ColumnHeaders = { @@ -24,9 +23,8 @@ public abstract class DataExportBase : DataExchangeBase public event EventHandler? TrackExport; - protected DataExportBase(IMusicCatalogueFactory factory) : base(factory) + protected CatalogueExporterBase(IMusicCatalogueFactory factory) : base(factory) { - } /// diff --git a/src/MusicCatalogue.Logic/DataExchange/XlsxExporter.cs b/src/MusicCatalogue.Logic/DataExchange/Catalogue/CatalogueXlsxExporter.cs similarity index 92% rename from src/MusicCatalogue.Logic/DataExchange/XlsxExporter.cs rename to src/MusicCatalogue.Logic/DataExchange/Catalogue/CatalogueXlsxExporter.cs index f021790..0b0bff3 100644 --- a/src/MusicCatalogue.Logic/DataExchange/XlsxExporter.cs +++ b/src/MusicCatalogue.Logic/DataExchange/Catalogue/CatalogueXlsxExporter.cs @@ -2,16 +2,16 @@ using MusicCatalogue.Entities.DataExchange; using MusicCatalogue.Entities.Interfaces; -namespace MusicCatalogue.Logic.DataExchange +namespace MusicCatalogue.Logic.DataExchange.Catalogue { - public class XlsxExporter : DataExportBase, IExporter + public class CatalogueXlsxExporter : CatalogueExporterBase, IExporter { private const string WorksheetName = "Music"; private IXLWorksheet? _worksheet = null; #pragma warning disable CS8618 - internal XlsxExporter(IMusicCatalogueFactory factory) : base(factory) + internal CatalogueXlsxExporter(IMusicCatalogueFactory factory) : base(factory) { } #pragma warning restore CS8618 diff --git a/src/MusicCatalogue.Logic/DataExchange/DataExchangeBase.cs b/src/MusicCatalogue.Logic/DataExchange/Catalogue/DataExchangeBase.cs similarity index 100% rename from src/MusicCatalogue.Logic/DataExchange/DataExchangeBase.cs rename to src/MusicCatalogue.Logic/DataExchange/Catalogue/DataExchangeBase.cs diff --git a/src/MusicCatalogue.Logic/DataExchange/Generic/CsvExporter.cs b/src/MusicCatalogue.Logic/DataExchange/Generic/CsvExporter.cs new file mode 100644 index 0000000..1fd0c13 --- /dev/null +++ b/src/MusicCatalogue.Logic/DataExchange/Generic/CsvExporter.cs @@ -0,0 +1,85 @@ +using MusicCatalogue.Entities.DataExchange; +using MusicCatalogue.Entities.Interfaces; +using System.Text; + +namespace MusicCatalogue.Logic.DataExchange.Generic +{ + public class CsvExporter : ExporterBase, ICsvExporter where T : class + { + private const string DateTimeFormat = "dd/MM/yyyy"; + +#pragma warning disable CS8618 + public event EventHandler> RecordExport; +#pragma warning restore CS8618 + + /// + /// Export a collection of entities as a CSV file + /// + /// + /// + /// + public virtual void Export(IEnumerable entities, string fileName, char separator) + { + long count = 0; + + using (var writer = new StreamWriter(fileName, false, Encoding.UTF8)) + { + // Construct and write the column headers + var header = string.Join(",", Properties.Keys); + writer.WriteLine(header); + + // Iterate over the entities and construct an output line for each one + foreach (var e in entities) + { + var builder = new StringBuilder(); + bool first = true; + + // Iterate over the properties, extracting the value for each one and appending it + // to the current line with a column separator, as needed + foreach (var property in Properties.Values) + { + // Get the value for this property + var value = property.GetValue(e, null); + + // Add the separator and opening quote + if (!first) builder.Append(separator); + builder.Append('"'); + + // Don't try to append null values + if (value != null) + { + // Date values are formatted in a specific manner, so see if this property is a non-null + // date + if (property.PropertyType == typeof(DateTime)) + { + // It is a date, so format it and append the formatted string + var valueAsFormattedfDate = ((DateTime)value).ToString(DateTimeFormat); + builder.Append(valueAsFormattedfDate); + } + else + { + // Not a date so append it directly + builder.Append(value); + } + } + + // Add the closing quote + builder.Append('"'); + first = false; + } + + // Write the current line + writer.WriteLine(builder.ToString()); + + // Notify subscribers + count++; + RecordExport?.Invoke(this, new ExportEventArgs + { + RecordCount = count, + RecordSource = e + }); + } + } + } + } +} diff --git a/src/MusicCatalogue.Logic/DataExchange/Generic/ExporterBase.cs b/src/MusicCatalogue.Logic/DataExchange/Generic/ExporterBase.cs new file mode 100644 index 0000000..d9238d6 --- /dev/null +++ b/src/MusicCatalogue.Logic/DataExchange/Generic/ExporterBase.cs @@ -0,0 +1,31 @@ +using MusicCatalogue.Entities.Attributes; +using System.Reflection; + +namespace MusicCatalogue.Logic.DataExchange.Generic +{ + public abstract class ExporterBase where T : class + { + protected Dictionary Properties { get; private set; } = new(); + + protected ExporterBase() + { + // Get a list of entity properties marked as exportable, ordering by the column + // order defined on the export attribute + IEnumerable properties = typeof(T) + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(x => Attribute.IsDefined(x, typeof(ExportAttribute))) + .OrderBy(x => x.GetCustomAttribute()!.Order); + + // Build a dictionary of properties where the key is the column name and the value is + // the corresponding property information instance + foreach (PropertyInfo property in properties) + { + var attribute = property.GetCustomAttribute(); + if (attribute != null) + { + Properties.Add(attribute.Name, property); + } + } + } + } +} \ No newline at end of file diff --git a/src/MusicCatalogue.Logic/Factory/MusicCatalogueFactory.cs b/src/MusicCatalogue.Logic/Factory/MusicCatalogueFactory.cs index 4408f4d..df8fc11 100644 --- a/src/MusicCatalogue.Logic/Factory/MusicCatalogueFactory.cs +++ b/src/MusicCatalogue.Logic/Factory/MusicCatalogueFactory.cs @@ -3,7 +3,7 @@ using MusicCatalogue.Entities.Interfaces; using MusicCatalogue.Entities.Reporting; using MusicCatalogue.Logic.Database; -using MusicCatalogue.Logic.DataExchange; +using MusicCatalogue.Logic.DataExchange.Catalogue; using MusicCatalogue.Logic.Reporting; using System.Diagnostics.CodeAnalysis; @@ -18,8 +18,8 @@ public class MusicCatalogueFactory : IMusicCatalogueFactory private readonly Lazy _retailers; private readonly Lazy _users; private readonly Lazy _importer; - private readonly Lazy _csvExporter; - private readonly Lazy _xlsxExporter; + private readonly Lazy _catalogueCsvExporter; + private readonly Lazy _catalogueXlsxExporter; private readonly Lazy _jobStatuses; private readonly Lazy _searchManager; private readonly Lazy> _genreStatistics; @@ -36,8 +36,8 @@ public class MusicCatalogueFactory : IMusicCatalogueFactory public ISearchManager Search { get { return _searchManager.Value; } } public IUserManager Users { get { return _users.Value; } } public IImporter Importer { get { return _importer.Value; } } - public IExporter CsvExporter { get { return _csvExporter.Value; } } - public IExporter XlsxExporter { get { return _xlsxExporter.Value; } } + public IExporter CatalogueCsvExporter { get { return _catalogueCsvExporter.Value; } } + public IExporter CatalogueXlsxExporter { get { return _catalogueXlsxExporter.Value; } } [ExcludeFromCodeCoverage] public IWishListBasedReport GenreStatistics { get { return _genreStatistics.Value; } } @@ -59,9 +59,9 @@ public MusicCatalogueFactory(MusicCatalogueDbContext context) _jobStatuses = new Lazy(() => new JobStatusManager(this)); _searchManager = new Lazy(() => new SearchManager(this)); _users = new Lazy(() => new UserManager(this)); - _importer = new Lazy(() => new CsvImporter(this)); - _csvExporter = new Lazy(() => new CsvExporter(this)); - _xlsxExporter = new Lazy(() => new XlsxExporter(this)); + _importer = new Lazy(() => new CatalogueCsvImporter(this)); + _catalogueCsvExporter = new Lazy(() => new CatalogueCsvExporter(this)); + _catalogueXlsxExporter = new Lazy(() => new CatalogueXlsxExporter(this)); _genreStatistics = new Lazy>(() => new WishListBasedReport(context)); _artistStatistics = new Lazy>(() => new WishListBasedReport(context)); _monthlySpend = new Lazy>(() => new WishListBasedReport(context)); diff --git a/src/MusicCatalogue.Logic/MusicCatalogue.Logic.csproj b/src/MusicCatalogue.Logic/MusicCatalogue.Logic.csproj index fe415bd..42bdf90 100644 --- a/src/MusicCatalogue.Logic/MusicCatalogue.Logic.csproj +++ b/src/MusicCatalogue.Logic/MusicCatalogue.Logic.csproj @@ -5,7 +5,7 @@ enable enable MusicCatalogue.Logic - 1.17.0.0 + 1.18.0.0 Dave Walker Copyright (c) Dave Walker 2023 Dave Walker @@ -17,7 +17,7 @@ https://github.com/davewalker5/MusicCatalogue MIT false - 1.17.0.0 + 1.18.0.0 diff --git a/src/MusicCatalogue.LookupTool/Logic/DataExport.cs b/src/MusicCatalogue.LookupTool/Logic/DataExport.cs index 14289fd..85e1b31 100644 --- a/src/MusicCatalogue.LookupTool/Logic/DataExport.cs +++ b/src/MusicCatalogue.LookupTool/Logic/DataExport.cs @@ -25,7 +25,7 @@ public void Export(string file) // Use the file extension to decide which exporter to use var extension = Path.GetExtension(file).ToLower(); - IExporter? exporter = extension == ".xlsx" ? _factory.XlsxExporter : _factory.CsvExporter; + IExporter? exporter = extension == ".xlsx" ? _factory.CatalogueXlsxExporter : _factory.CatalogueCsvExporter; try { diff --git a/src/MusicCatalogue.LookupTool/MusicCatalogue.LookupTool.csproj b/src/MusicCatalogue.LookupTool/MusicCatalogue.LookupTool.csproj index aa44384..e02e9e9 100644 --- a/src/MusicCatalogue.LookupTool/MusicCatalogue.LookupTool.csproj +++ b/src/MusicCatalogue.LookupTool/MusicCatalogue.LookupTool.csproj @@ -3,9 +3,9 @@ Exe net7.0 - 1.17.0.0 - 1.17.0.0 - 1.17.0 + 1.18.0.0 + 1.18.0.0 + 1.18.0 enable enable false diff --git a/src/MusicCatalogue.Tests/AlbumLookupManagerTest.cs b/src/MusicCatalogue.Tests/AlbumLookupManagerTest.cs index a210741..085ce10 100644 --- a/src/MusicCatalogue.Tests/AlbumLookupManagerTest.cs +++ b/src/MusicCatalogue.Tests/AlbumLookupManagerTest.cs @@ -94,7 +94,7 @@ public void AlbumWithTracksTest() Assert.IsNotNull(album); Assert.AreEqual(AlbumTitle, album.Title); Assert.AreEqual(Released, album.Released); - Assert.AreEqual(Genre, album.Genre.Name); + Assert.AreEqual(Genre, album.Genre!.Name); Assert.AreEqual(CoverUrl, album.CoverUrl); Assert.IsNotNull(album.Tracks); @@ -117,7 +117,7 @@ public void ArtistInDbButAlbumNotInDbTest() Assert.IsNotNull(album); Assert.AreEqual(AlbumTitle, album.Title); Assert.AreEqual(Released, album.Released); - Assert.AreEqual(Genre, album.Genre.Name); + Assert.AreEqual(Genre, album.Genre!.Name); Assert.AreEqual(CoverUrl, album.CoverUrl); Assert.IsNotNull(album.Tracks); @@ -139,7 +139,7 @@ public void ArtistAndAlbumInDbTest() Assert.IsNotNull(album); Assert.AreEqual(AlbumTitle, album.Title); Assert.AreEqual(Released, album.Released); - Assert.AreEqual(Genre, album.Genre.Name); + Assert.AreEqual(Genre, album.Genre!.Name); Assert.AreEqual(CoverUrl, album.CoverUrl); Assert.IsNotNull(album.Tracks); diff --git a/src/MusicCatalogue.Tests/AlbumManagerTest.cs b/src/MusicCatalogue.Tests/AlbumManagerTest.cs index 572c9ca..c8409da 100644 --- a/src/MusicCatalogue.Tests/AlbumManagerTest.cs +++ b/src/MusicCatalogue.Tests/AlbumManagerTest.cs @@ -56,7 +56,7 @@ public async Task AddAndGetTest() Assert.AreEqual(_artistId, album.ArtistId); Assert.AreEqual(AlbumTitle, album.Title); Assert.AreEqual(Released, album.Released); - Assert.AreEqual(Genre, album.Genre.Name); + Assert.AreEqual(Genre, album.Genre!.Name); Assert.AreEqual(CoverUrl, album.CoverUrl); Assert.IsFalse(album.IsWishListItem); Assert.IsNull(album.Purchased); @@ -73,7 +73,7 @@ public async Task UpdateTest() Assert.AreEqual(_artistId, album.ArtistId); Assert.AreEqual(AlbumTitle, album.Title); Assert.AreEqual(Released, album.Released); - Assert.AreEqual(Genre, album.Genre.Name); + Assert.AreEqual(Genre, album.Genre!.Name); Assert.AreEqual(CoverUrl, album.CoverUrl); Assert.IsTrue(album.IsWishListItem); Assert.AreEqual(Purchased, album.Purchased); @@ -116,7 +116,7 @@ public async Task DeleteTest() { var album = await _factory!.Albums.GetAsync(a => a.Title == AlbumTitle); Assert.IsNotNull(album); - Assert.AreEqual(1, album.Tracks.Count); + Assert.AreEqual(1, album.Tracks!.Count); await _factory!.Albums.DeleteAsync(_albumId); album = await _factory.Albums!.GetAsync(a => a.Title == AlbumTitle); diff --git a/src/MusicCatalogue.Tests/DataExchangeTest.cs b/src/MusicCatalogue.Tests/CatalogueDataExchangeTest.cs similarity index 93% rename from src/MusicCatalogue.Tests/DataExchangeTest.cs rename to src/MusicCatalogue.Tests/CatalogueDataExchangeTest.cs index dfab012..577d077 100644 --- a/src/MusicCatalogue.Tests/DataExchangeTest.cs +++ b/src/MusicCatalogue.Tests/CatalogueDataExchangeTest.cs @@ -6,7 +6,7 @@ namespace MusicCatalogue.Tests { [TestClass] - public class DataExchangeTest + public class CatalogueDataExchangeTest { private const string ArtistName = "Nat, King Cole\r\n"; private const string AlbumName = "After Midnight"; @@ -42,7 +42,7 @@ public void Initialise() public void ExportCsvTest() { var filepath = Path.ChangeExtension(Path.GetTempFileName(), "csv"); - Task.Run(() => _factory!.CsvExporter.Export(filepath)).Wait(); + Task.Run(() => _factory!.CatalogueCsvExporter.Export(filepath)).Wait(); var info = new FileInfo(filepath); Assert.AreEqual(info.FullName, filepath); @@ -55,7 +55,7 @@ public void ExportCsvTest() public void ExportXlsxTest() { var filepath = Path.ChangeExtension(Path.GetTempFileName(), "xlsx"); - Task.Run(() => _factory!.XlsxExporter.Export(filepath)).Wait(); + Task.Run(() => _factory!.CatalogueXlsxExporter.Export(filepath)).Wait(); var info = new FileInfo(filepath); Assert.AreEqual(info.FullName, filepath); @@ -69,7 +69,7 @@ public void ImportCsvTest() { // Export the data var filepath = Path.ChangeExtension(Path.GetTempFileName(), "csv"); - Task.Run(() => _factory!.CsvExporter.Export(filepath)).Wait(); + Task.Run(() => _factory!.CatalogueCsvExporter.Export(filepath)).Wait(); // Create a new instance of the factory with a new in-memory database MusicCatalogueDbContext context = MusicCatalogueDbContextFactory.CreateInMemoryDbContext(); diff --git a/src/MusicCatalogue.Tests/JobStatusManagerTest.cs b/src/MusicCatalogue.Tests/JobStatusManagerTest.cs index 4aaabee..4b4a7f2 100644 --- a/src/MusicCatalogue.Tests/JobStatusManagerTest.cs +++ b/src/MusicCatalogue.Tests/JobStatusManagerTest.cs @@ -11,7 +11,7 @@ public class JobStatusManagerTest private const string Parameters = "2023-10-28 Export.csv"; private const string Error = "Some error message"; - private IMusicCatalogueFactory _factory; + private IMusicCatalogueFactory? _factory; private long _statusId; [TestInitialize] @@ -25,7 +25,7 @@ public void TestInitialize() [TestMethod] public async Task AddAndGetAsyncTest() { - var status = await _factory.JobStatuses.GetAsync(x => x.Id == _statusId); + var status = await _factory!.JobStatuses.GetAsync(x => x.Id == _statusId); Assert.IsNotNull(status); Assert.AreEqual(_statusId, status.Id); @@ -39,7 +39,7 @@ public async Task AddAndGetAsyncTest() [TestMethod] public async Task ListAsyncTest() { - var statuses = await _factory.JobStatuses.ListAsync(x => true, 1, 10).ToListAsync(); + var statuses = await _factory!.JobStatuses.ListAsync(x => true, 1, 10).ToListAsync(); Assert.IsNotNull(statuses); Assert.AreEqual(1, statuses.Count); @@ -54,7 +54,7 @@ public async Task ListAsyncTest() [TestMethod] public async Task ListAllAsyncTest() { - var statuses = await _factory.JobStatuses.ListAsync(null, 1, 10).ToListAsync(); + var statuses = await _factory!.JobStatuses.ListAsync(x => true, 1, 10).ToListAsync(); Assert.IsNotNull(statuses); Assert.AreEqual(1, statuses.Count); @@ -69,7 +69,7 @@ public async Task ListAllAsyncTest() [TestMethod] public async Task UpdateAsyncTest() { - var status = await _factory.JobStatuses.UpdateAsync(_statusId, Error); + var status = await _factory!.JobStatuses.UpdateAsync(_statusId, Error); Assert.IsNotNull(status); Assert.AreEqual(_statusId, status.Id); diff --git a/src/MusicCatalogue.Tests/RetailerManagerTest.cs b/src/MusicCatalogue.Tests/RetailerManagerTest.cs index 96b7596..1d91a09 100644 --- a/src/MusicCatalogue.Tests/RetailerManagerTest.cs +++ b/src/MusicCatalogue.Tests/RetailerManagerTest.cs @@ -16,7 +16,7 @@ public class RetailerManagerTest private const string Name = "Dig Vinyl"; private const string UpdatedName = "Truck Store"; - private IMusicCatalogueFactory _factory = null; + private IMusicCatalogueFactory? _factory = null; private int _retailerId; [TestInitialize] @@ -105,7 +105,7 @@ public async Task DeleteMissingTest() public async Task DeleteInUseTest() { // Add an album that uses the retailer - var artist = await _factory.Artists.AddAsync(ArtistName); + var artist = await _factory!.Artists.AddAsync(ArtistName); var genre = await _factory.Genres.AddAsync(Genre); await _factory.Albums.AddAsync(artist.Id, genre.Id, AlbumTitle, Released, CoverUrl, false, null, null, _retailerId);