diff --git a/DataLayer/Migrations/20191119144803_Fresh.Designer.cs b/DataLayer/Migrations/20191125182309_Fresh.Designer.cs similarity index 97% rename from DataLayer/Migrations/20191119144803_Fresh.Designer.cs rename to DataLayer/Migrations/20191125182309_Fresh.Designer.cs index b3d720fe..3d5a695c 100644 --- a/DataLayer/Migrations/20191119144803_Fresh.Designer.cs +++ b/DataLayer/Migrations/20191125182309_Fresh.Designer.cs @@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace DataLayer.Migrations { [DbContext(typeof(LibationContext))] - [Migration("20191119144803_Fresh")] + [Migration("20191125182309_Fresh")] partial class Fresh { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -129,6 +129,13 @@ namespace DataLayer.Migrations b.HasIndex("Name"); b.ToTable("Contributors"); + + b.HasData( + new + { + ContributorId = -1, + Name = "" + }); }); modelBuilder.Entity("DataLayer.LibraryBook", b => diff --git a/DataLayer/Migrations/20191119144803_Fresh.cs b/DataLayer/Migrations/20191125182309_Fresh.cs similarity index 98% rename from DataLayer/Migrations/20191119144803_Fresh.cs rename to DataLayer/Migrations/20191125182309_Fresh.cs index 78fe47a7..fe659837 100644 --- a/DataLayer/Migrations/20191119144803_Fresh.cs +++ b/DataLayer/Migrations/20191125182309_Fresh.cs @@ -200,6 +200,11 @@ namespace DataLayer.Migrations columns: new[] { "CategoryId", "AudibleCategoryId", "Name", "ParentCategoryCategoryId" }, values: new object[] { -1, "", "", null }); + migrationBuilder.InsertData( + table: "Contributors", + columns: new[] { "ContributorId", "AudibleContributorId", "Name" }, + values: new object[] { -1, null, "" }); + migrationBuilder.CreateIndex( name: "IX_BookContributor_BookId", table: "BookContributor", diff --git a/DataLayer/Migrations/LibationContextModelSnapshot.cs b/DataLayer/Migrations/LibationContextModelSnapshot.cs index 262933d6..9260d181 100644 --- a/DataLayer/Migrations/LibationContextModelSnapshot.cs +++ b/DataLayer/Migrations/LibationContextModelSnapshot.cs @@ -127,6 +127,13 @@ namespace DataLayer.Migrations b.HasIndex("Name"); b.ToTable("Contributors"); + + b.HasData( + new + { + ContributorId = -1, + Name = "" + }); }); modelBuilder.Entity("DataLayer.LibraryBook", b => diff --git a/DataLayer/UNTESTED/EfClasses/Book.cs b/DataLayer/UNTESTED/EfClasses/Book.cs index 312eda51..cc695409 100644 --- a/DataLayer/UNTESTED/EfClasses/Book.cs +++ b/DataLayer/UNTESTED/EfClasses/Book.cs @@ -62,7 +62,8 @@ namespace DataLayer string description, int lengthInMinutes, IEnumerable authors, - IEnumerable narrators) + IEnumerable narrators, + Category category) { // validate ArgumentValidator.EnsureNotNull(audibleProductId, nameof(audibleProductId)); @@ -80,8 +81,7 @@ namespace DataLayer _seriesLink = new HashSet(); _supplements = new HashSet(); - // since category/id is never null, nullity means it hasn't been loaded - CategoryId = Category.GetEmpty().CategoryId; + Category = category; // simple assigns Title = title; @@ -91,7 +91,7 @@ namespace DataLayer // assigns with biz logic ReplaceAuthors(authors); ReplaceNarrators(narrators); - } + } #region contributors, authors, narrators // use uninitialised backing fields - this means we can detect if the collection was loaded @@ -233,8 +233,8 @@ namespace DataLayer public void UpdateCategory(Category category, DbContext context = null) { - // since category is never null, nullity means it hasn't been loaded - if (Category != null || CategoryId == Category.GetEmpty().CategoryId) + // since category is never null, nullity means it hasn't been loaded. non null means we're correctly loaded. just overwrite + if (Category != null) { Category = category; return; diff --git a/DataLayer/UNTESTED/EfClasses/Category.cs b/DataLayer/UNTESTED/EfClasses/Category.cs index 2ff336fc..b97b4e5b 100644 --- a/DataLayer/UNTESTED/EfClasses/Category.cs +++ b/DataLayer/UNTESTED/EfClasses/Category.cs @@ -18,8 +18,7 @@ namespace DataLayer public class Category { // Empty is a special case. use private ctor w/o validation - public static Category GetEmpty() => new Category { CategoryId = -1, AudibleCategoryId = "", Name = "", ParentCategory = null }; - public bool IsEmpty() => string.IsNullOrWhiteSpace(AudibleCategoryId) || string.IsNullOrWhiteSpace(Name) || ParentCategory == null; + public static Category GetEmpty() => new Category { CategoryId = -1, AudibleCategoryId = "", Name = "" }; internal int CategoryId { get; private set; } public string AudibleCategoryId { get; private set; } diff --git a/DataLayer/UNTESTED/EfClasses/Contributor.cs b/DataLayer/UNTESTED/EfClasses/Contributor.cs index 0c14554a..99351e38 100644 --- a/DataLayer/UNTESTED/EfClasses/Contributor.cs +++ b/DataLayer/UNTESTED/EfClasses/Contributor.cs @@ -5,21 +5,24 @@ using Dinah.Core; namespace DataLayer { public class Contributor - { - // contributors search links are just name with url-encoding. space can be + or %20 - // author search link: /search?searchAuthor=Robert+Bevan - // narrator search link: /search?searchNarrator=Robert+Bevan - // can also search multiples. concat with comma before url encode + { + // Empty is a special case. use private ctor w/o validation + public static Contributor GetEmpty() => new Contributor { ContributorId = -1, Name = "" }; - // id.s - // ---- - // https://www.audible.com/author/Neil-Gaiman/B000AQ01G2 == https://www.audible.com/author/B000AQ01G2 - // goes to summary page - // at bottom "See all titles by Neil Gaiman" goes to https://www.audible.com/search?searchAuthor=Neil+Gaiman - // some authors have no id. simply goes to https://www.audible.com/search?searchAuthor=Rufus+Fears - // all narrators have no id: https://www.audible.com/search?searchNarrator=Neil+Gaiman + // contributors search links are just name with url-encoding. space can be + or %20 + // author search link: /search?searchAuthor=Robert+Bevan + // narrator search link: /search?searchNarrator=Robert+Bevan + // can also search multiples. concat with comma before url encode - internal int ContributorId { get; private set; } + // id.s + // ---- + // https://www.audible.com/author/Neil-Gaiman/B000AQ01G2 == https://www.audible.com/author/B000AQ01G2 + // goes to summary page + // at bottom "See all titles by Neil Gaiman" goes to https://www.audible.com/search?searchAuthor=Neil+Gaiman + // some authors have no id. simply goes to https://www.audible.com/search?searchAuthor=Rufus+Fears + // all narrators have no id: https://www.audible.com/search?searchNarrator=Neil+Gaiman + + internal int ContributorId { get; private set; } public string Name { get; private set; } private HashSet _booksLink; diff --git a/DataLayer/UNTESTED/LibationContext.cs b/DataLayer/UNTESTED/LibationContext.cs index 8448ed7b..986d9e6d 100644 --- a/DataLayer/UNTESTED/LibationContext.cs +++ b/DataLayer/UNTESTED/LibationContext.cs @@ -56,10 +56,13 @@ namespace DataLayer modelBuilder.ApplyConfiguration(new SeriesBookConfig()); modelBuilder.ApplyConfiguration(new CategoryConfig()); - // seeds go here. examples in scratch pad - modelBuilder - .Entity() - .HasData(Category.GetEmpty()); + // seeds go here. examples in scratch pad + modelBuilder + .Entity() + .HasData(Category.GetEmpty()); + modelBuilder + .Entity() + .HasData(Contributor.GetEmpty()); // views are now supported via "query types" (instead of "entity types"): https://docs.microsoft.com/en-us/ef/core/modeling/query-types } diff --git a/DtoImporterService/BookImporter.cs b/DtoImporterService/BookImporter.cs index 5874e213..2dec4d2c 100644 --- a/DtoImporterService/BookImporter.cs +++ b/DtoImporterService/BookImporter.cs @@ -63,20 +63,30 @@ namespace DtoImporterService private static Book createNewBook(Item item, LibationContext context) { + // absence of authors is very rare, but possible + if (!item.Authors?.Any() ?? true) + item.Authors = new[] { new Person { Name = "", Asin = null } }; + // nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db var authors = item .Authors .Select(a => context.Contributors.Local.Single(c => a.Name == c.Name)) .ToList(); - // if no narrators listed, author is the narrator - if (item.Narrators is null || !item.Narrators.Any()) - item.Narrators = item.Authors; - // nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db - var narrators = item - .Narrators - .Select(n => context.Contributors.Local.Single(c => n.Name == c.Name)) - .ToList(); + var narrators + = item.Narrators is null || !item.Narrators.Any() + // if no narrators listed, author is the narrator + ? authors + // nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db + : item + .Narrators + .Select(n => context.Contributors.Local.Single(c => n.Name == c.Name)) + .ToList(); + + // categories are laid out for a breadcrumb. category is 1st, subcategory is 2nd + // absence of categories is very rare, but possible + var lastCategory = item.Categories.LastOrDefault()?.CategoryId ?? ""; + var category = context.Categories.Local.SingleOrDefault(c => c.AudibleCategoryId == lastCategory); var book = context.Books.Add(new Book( new AudibleProductId(item.ProductId), @@ -84,7 +94,8 @@ namespace DtoImporterService item.Description, item.LengthInMinutes, authors, - narrators) + narrators, + category) ).Entity; var publisherName = item.Publisher; @@ -94,11 +105,6 @@ namespace DtoImporterService book.ReplacePublisher(publisher); } - // categories are laid out for a breadcrumb. category is 1st, subcategory is 2nd - var category = context.Categories.Local.SingleOrDefault(c => c.AudibleCategoryId == item.Categories.LastOrDefault().CategoryId); - if (category != null) - book.UpdateCategory(category, context); - book.UpdateBookDetails(item.IsAbridged, item.DatePublished); if (!string.IsNullOrWhiteSpace(item.SupplementUrl)) diff --git a/DtoImporterService/CategoryImporter.cs b/DtoImporterService/CategoryImporter.cs index 4936ed02..505310f3 100644 --- a/DtoImporterService/CategoryImporter.cs +++ b/DtoImporterService/CategoryImporter.cs @@ -33,8 +33,11 @@ namespace DtoImporterService .Except(localIds) .ToList(); + // load existing => local + // remember to include default/empty/missing + var emptyName = Contributor.GetEmpty().Name; if (remainingCategoryIds.Any()) - context.Categories.Where(c => remainingCategoryIds.Contains(c.AudibleCategoryId)).ToList(); + context.Categories.Where(c => remainingCategoryIds.Contains(c.AudibleCategoryId) || c.Name == emptyName).ToList(); } // only use after loading contributors => local diff --git a/DtoImporterService/ContributorImporter.cs b/DtoImporterService/ContributorImporter.cs index 77126008..1617a831 100644 --- a/DtoImporterService/ContributorImporter.cs +++ b/DtoImporterService/ContributorImporter.cs @@ -47,11 +47,10 @@ namespace DtoImporterService .ToList(); // load existing => local + // remember to include default/empty/missing + var emptyName = Contributor.GetEmpty().Name; if (remainingContribNames.Any()) - context.Contributors.Where(c => remainingContribNames.Contains(c.Name)).ToList(); - // _________________________________^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - // i tried to extract this pattern, but this part prohibits doing so - // wouldn't work anyway for Books.GetBooks() + context.Contributors.Where(c => remainingContribNames.Contains(c.Name) || c.Name == emptyName).ToList(); } // only use after loading contributors => local diff --git a/InternalUtilities/UNTESTED/AudibleApiValidators.cs b/InternalUtilities/UNTESTED/AudibleApiValidators.cs index 0769a62e..7d492bf3 100644 --- a/InternalUtilities/UNTESTED/AudibleApiValidators.cs +++ b/InternalUtilities/UNTESTED/AudibleApiValidators.cs @@ -29,12 +29,12 @@ namespace InternalUtilities { var exceptions = new List(); + // a book having no authors is rare but allowed + if (items.Any(i => string.IsNullOrWhiteSpace(i.ProductId))) exceptions.Add(new ArgumentException($"Collection contains item(s) with blank {nameof(Item.ProductId)}", nameof(items))); if (items.Any(i => string.IsNullOrWhiteSpace(i.Title))) exceptions.Add(new ArgumentException($"Collection contains item(s) with blank {nameof(Item.Title)}", nameof(items))); - if (items.Any(i => i.Authors is null)) - exceptions.Add(new ArgumentException($"Collection contains item(s) with null {nameof(Item.Authors)}", nameof(items))); return exceptions; }