Initial check-in

This commit is contained in:
Robert McRackan 2019-10-04 16:14:04 -04:00
parent c080c9e51d
commit 2cc93078d2
282 changed files with 24387 additions and 0 deletions

View file

@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.0;netstandard2.1</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<ApplicationIcon />
<OutputType>Library</OutputType>
<StartupObject />
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.2.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Dinah.Core\Dinah.EntityFrameworkCore\Dinah.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\FileManager\FileManager.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
namespace DataLayer
{
public static class RemoveOrphansCommand
{
public static int RemoveOrphans(this LibationContext context)
=> context.Database.ExecuteSqlCommand(@"
delete c
from Contributors c
left join BookContributor bc on c.ContributorId = bc.ContributorId
left join Books b on bc.BookId = b.BookId
where bc.ContributorId is null
");
}
}

View file

@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DataLayer.Configurations
{
internal class BookConfig : IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> entity)
{
entity.HasKey(b => b.BookId);
entity.HasIndex(b => b.AudibleProductId);
entity.OwnsOne(b => b.Rating);
//
// CRUCIAL: ignore unmapped collections, even get-only
//
entity.Ignore(nameof(Book.Authors));
entity.Ignore(nameof(Book.Narrators));
//// these don't seem to matter
//entity.Ignore(nameof(Book.AuthorNames));
//entity.Ignore(nameof(Book.NarratorNames));
//entity.Ignore(nameof(Book.HasPdfs));
// OwnsMany: "Can only ever appear on navigation properties of other entity types.
// Are automatically loaded, and can only be tracked by a DbContext alongside their owner."
entity.OwnsMany(b => b.Supplements);
// even though it's owned, we need to map its backing field
entity
.Metadata
.FindNavigation(nameof(Book.Supplements))
.SetPropertyAccessMode(PropertyAccessMode.Field);
// owns it 1:1, but store in separate table
entity
.OwnsOne(b => b.UserDefinedItem, b_udi => b_udi.ToTable(nameof(Book.UserDefinedItem)));
// UserDefinedItem must link back to book so we know how to log changed tags.
// ie: when a tag changes, we need to get the parent book's product id
entity
.HasOne(b => b.UserDefinedItem)
.WithOne(udi => udi.Book)
.HasForeignKey<UserDefinedItem>(udi => udi.BookId);
entity
.Metadata
.FindNavigation(nameof(Book.ContributorsLink))
// PropertyAccessMode.Field : Contributions is a get-only property, not a field, so use its backing field
.SetPropertyAccessMode(PropertyAccessMode.Field);
entity
.Metadata
.FindNavigation(nameof(Book.SeriesLink))
// PropertyAccessMode.Field : Series is a get-only property, not a field, so use its backing field
.SetPropertyAccessMode(PropertyAccessMode.Field);
entity
.HasOne(b => b.Category)
.WithMany()
.HasForeignKey(b => b.CategoryId);
}
}
}

View file

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DataLayer.Configurations
{
internal class BookContributorConfig : IEntityTypeConfiguration<BookContributor>
{
public void Configure(EntityTypeBuilder<BookContributor> entity)
{
entity.HasKey(bc => new { bc.BookId, bc.ContributorId, bc.Role });
entity.HasIndex(b => b.BookId);
entity.HasIndex(b => b.ContributorId);
entity
.HasOne(bc => bc.Book)
.WithMany(b => b.ContributorsLink)
.HasForeignKey(bc => bc.BookId);
entity
.HasOne(bc => bc.Contributor)
.WithMany(c => c.BooksLink)
.HasForeignKey(bc => bc.ContributorId);
}
}
}

View file

@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DataLayer.Configurations
{
internal class CategoryConfig : IEntityTypeConfiguration<Category>
{
public void Configure(EntityTypeBuilder<Category> entity)
{
entity.HasKey(c => c.CategoryId);
entity.HasIndex(c => c.AudibleCategoryId);
}
}
}

View file

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DataLayer.Configurations
{
internal class ContributorConfig : IEntityTypeConfiguration<Contributor>
{
public void Configure(EntityTypeBuilder<Contributor> entity)
{
entity.HasKey(c => c.ContributorId);
entity.HasIndex(c => c.Name);
//entity.OwnsOne(b => b.AuthorProperty);
// ... in separate table
entity
.Metadata
.FindNavigation(nameof(Contributor.BooksLink))
.SetPropertyAccessMode(PropertyAccessMode.Field);
}
}
}

View file

@ -0,0 +1,18 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DataLayer.Configurations
{
internal class LibraryBookConfig : IEntityTypeConfiguration<LibraryBook>
{
public void Configure(EntityTypeBuilder<LibraryBook> entity)
{
entity.HasKey(b => b.BookId);
entity
.HasOne(le => le.Book)
.WithOne()
.HasForeignKey<LibraryBook>(le => le.BookId);
}
}
}

View file

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DataLayer.Configurations
{
internal class SeriesBookConfig : IEntityTypeConfiguration<SeriesBook>
{
public void Configure(EntityTypeBuilder<SeriesBook> entity)
{
entity.HasKey(bc => new { bc.SeriesId, bc.BookId });
entity.HasIndex(b => b.SeriesId);
entity.HasIndex(b => b.BookId);
entity
.HasOne(sb => sb.Series)
.WithMany(s => s.BooksLink)
.HasForeignKey(sb => sb.SeriesId);
entity
.HasOne(sb => sb.Book)
.WithMany(b => b.SeriesLink)
.HasForeignKey(sb => sb.BookId);
}
}
}

View file

@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DataLayer.Configurations
{
internal class SeriesConfig : IEntityTypeConfiguration<Series>
{
public void Configure(EntityTypeBuilder<Series> entity)
{
entity.HasKey(b => b.SeriesId);
entity.HasIndex(b => b.AudibleSeriesId);
entity
.Metadata
.FindNavigation(nameof(Series.BooksLink))
.SetPropertyAccessMode(PropertyAccessMode.Field);
}
}
}

View file

@ -0,0 +1,17 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DataLayer.Configurations
{
internal class SupplementConfig : IEntityTypeConfiguration<Supplement>
{
public void Configure(EntityTypeBuilder<Supplement> entity)
{
entity.HasKey(s => s.SupplementId);
entity
.HasOne(s => s.Book)
.WithMany(b => b.Supplements)
.HasForeignKey(s => s.BookId);
}
}
}

View file

@ -0,0 +1,13 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DataLayer.Configurations
{
internal class UserDefinedItemConfig : IEntityTypeConfiguration<UserDefinedItem>
{
public void Configure(EntityTypeBuilder<UserDefinedItem> entity)
{
entity.OwnsOne(p => p.Rating);
}
}
}

View file

@ -0,0 +1,256 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dinah.Core;
using Microsoft.EntityFrameworkCore;
namespace DataLayer
{
public class AudibleProductId
{
public string Id { get; }
public AudibleProductId(string id)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
Id = id;
}
}
public class Book
{
// implementation detail. set by db only. only used by data layer
internal int BookId { get; private set; }
// immutable
public string AudibleProductId { get; private set; }
public string Title { get; private set; }
public string Description { get; private set; }
public int LengthInMinutes { get; private set; }
// mutable
public string PictureId { get; set; }
// book details
public bool HasBookDetails { get; private set; }
public bool IsAbridged { get; private set; }
public DateTime? DatePublished { get; private set; }
// non-null. use "empty pattern"
internal int CategoryId { get; private set; }
public Category Category { get; private set; }
public string[] CategoriesNames
=> Category == null ? new string[0]
: Category.ParentCategory == null ? new[] { Category.Name }
: new[] { Category.ParentCategory.Name, Category.Name };
public string[] CategoriesIds
=> Category == null ? null
: Category.ParentCategory == null ? new[] { Category.AudibleCategoryId }
: new[] { Category.ParentCategory.AudibleCategoryId, Category.AudibleCategoryId };
// is owned, not optional 1:1
public UserDefinedItem UserDefinedItem { get; private set; }
// is owned, not optional 1:1
/// <summary>The product's aggregate community rating</summary>
public Rating Rating { get; private set; } = new Rating(0, 0, 0);
// ef-ctor
private Book() { }
// non-ef ctor
/// <param name="audibleProductId">special id class b/c it's too easy to get string order mixed up</param>
public Book(
AudibleProductId audibleProductId,
string title,
string description,
int lengthInMinutes,
IEnumerable<Contributor> authors,
IEnumerable<Contributor> narrators)
{
// validate
ArgumentValidator.EnsureNotNull(audibleProductId, nameof(audibleProductId));
var productId = audibleProductId.Id;
ArgumentValidator.EnsureNotNullOrWhiteSpace(productId, nameof(productId));
ArgumentValidator.EnsureNotNullOrWhiteSpace(title, nameof(title));
// non-ef-ctor init.s
UserDefinedItem = new UserDefinedItem(this);
_contributorsLink = new HashSet<BookContributor>();
_seriesLink = new HashSet<SeriesBook>();
_supplements = new HashSet<Supplement>();
// since category/id is never null, nullity means it hasn't been loaded
CategoryId = Category.GetEmpty().CategoryId;
// simple assigns
AudibleProductId = productId;
Title = title;
Description = description;
LengthInMinutes = lengthInMinutes;
// assigns with biz logic
ReplaceAuthors(authors);
ReplaceNarrators(narrators);
// import previously saved tags
// do this immediately. any save occurs before reloading tags will overwrite persistent tags with new blank entries; all old persisted tags will be lost
// if refactoring, DO NOT use "ProductId" before it's assigned to. to be safe, just use "productId"
UserDefinedItem = new UserDefinedItem(this) { Tags = FileManager.TagsPersistence.GetTags(productId) };
}
#region contributors, authors, narrators
// use uninitialised backing fields - this means we can detect if the collection was loaded
private HashSet<BookContributor> _contributorsLink;
// i'd like this to be internal but migration throws this exception when i try:
// Value cannot be null.
// Parameter name: property
public IEnumerable<BookContributor> ContributorsLink
=> _contributorsLink?
.OrderBy(bc => bc.Order)
.ToList();
public IEnumerable<Contributor> Authors => getContributions(Role.Author).Select(bc => bc.Contributor).ToList();
public string AuthorNames => string.Join(", ", Authors.Select(a => a.Name));
public IEnumerable<Contributor> Narrators => getContributions(Role.Narrator).Select(bc => bc.Contributor).ToList();
public string NarratorNames => string.Join(", ", Narrators.Select(n => n.Name));
public string Publisher => getContributions(Role.Publisher).SingleOrDefault()?.Contributor.Name;
public void ReplaceAuthors(IEnumerable<Contributor> authors, DbContext context = null)
=> replaceContributors(authors, Role.Author, context);
public void ReplaceNarrators(IEnumerable<Contributor> narrators, DbContext context = null)
=> replaceContributors(narrators, Role.Narrator, context);
public void ReplacePublisher(Contributor publisher, DbContext context = null)
=> replaceContributors(new List<Contributor> { publisher }, Role.Publisher, context);
private void replaceContributors(IEnumerable<Contributor> newContributors, Role role, DbContext context = null)
{
ArgumentValidator.EnsureEnumerableNotNullOrEmpty(newContributors, nameof(newContributors));
// the edge cases of doing local-loaded vs remote-only got weird. just load it
if (_contributorsLink == null)
{
ArgumentValidator.EnsureNotNull(context, nameof(context));
if (!context.Entry(this).IsKeySet)
throw new InvalidOperationException("Could not add contributors");
context.Entry(this).Collection(s => s.ContributorsLink).Load();
}
var roleContributions = getContributions(role);
var isIdentical = roleContributions.Select(c => c.Contributor).SequenceEqual(newContributors);
if (isIdentical)
return;
_contributorsLink.RemoveWhere(bc => bc.Role == role);
addNewContributors(newContributors, role);
}
private void addNewContributors(IEnumerable<Contributor> newContributors, Role role)
{
byte order = 0;
var newContributionsEnum = newContributors.Select(c => new BookContributor(this, c, role, order++));
var newContributions = new HashSet<BookContributor>(newContributionsEnum);
_contributorsLink.UnionWith(newContributions);
}
private List<BookContributor> getContributions(Role role)
=> ContributorsLink
.Where(a => a.Role == role)
.OrderBy(a => a.Order)
.ToList();
#endregion
#region series
private HashSet<SeriesBook> _seriesLink;
public IEnumerable<SeriesBook> SeriesLink => _seriesLink?.ToList();
public string SeriesNames
{
get
{
// first: alphabetical by name
var withNames = _seriesLink
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
.Select(s => s.Series.Name)
.OrderBy(a => a)
.ToList();
// then un-named are alpha by series id
var nullNames = _seriesLink
.Where(s => string.IsNullOrWhiteSpace(s.Series.Name))
.Select(s => s.Series.AudibleSeriesId)
.OrderBy(a => a)
.ToList();
var all = withNames.Union(nullNames).ToList();
return string.Join(", ", all);
}
}
public void UpsertSeries(Series series, float? index = null, DbContext context = null)
{
ArgumentValidator.EnsureNotNull(series, nameof(series));
// our add() is conditional upon what's already included in the collection.
// therefore if not loaded, a trip is required. might as well just load it
if (_seriesLink == null)
{
ArgumentValidator.EnsureNotNull(context, nameof(context));
if (!context.Entry(this).IsKeySet)
throw new InvalidOperationException("Could not add series");
context.Entry(this).Collection(s => s.SeriesLink).Load();
}
var singleSeriesBook = _seriesLink.SingleOrDefault(sb => sb.Series == series);
if (singleSeriesBook == null)
_seriesLink.Add(new SeriesBook(series, this, index));
else
singleSeriesBook.UpdateIndex(index);
}
#endregion
#region supplements
private HashSet<Supplement> _supplements;
public IEnumerable<Supplement> Supplements => _supplements?.ToList();
public bool HasPdfs => Supplements.Any();
public void AddSupplementDownloadUrl(string url)
{
// supplements are owned by Book, so no need to Load():
// OwnsMany: "Can only ever appear on navigation properties of other entity types.
// Are automatically loaded, and can only be tracked by a DbContext alongside their owner."
ArgumentValidator.EnsureNotNullOrWhiteSpace(url, nameof(url));
url = FileManager.FileUtility.RestoreDeclawed(url);
if (!_supplements.Any(s => url.EqualsInsensitive(url)))
_supplements.Add(new Supplement(this, url));
}
#endregion
public void UpdateProductRating(float overallRating, float performanceRating, float storyRating)
=> Rating.Update(overallRating, performanceRating, storyRating);
public void UpdateBookDetails(bool isAbridged, DateTime? datePublished)
{
// don't overwrite with default values
IsAbridged |= isAbridged;
DatePublished = datePublished ?? DatePublished;
HasBookDetails = true;
}
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)
{
Category = category;
return;
}
if (context == null)
throw new Exception("need context");
context.Entry(this).Reference(s => s.Category).Load();
Category = category;
}
}
}

View file

@ -0,0 +1,27 @@
using Dinah.Core;
namespace DataLayer
{
public class BookContributor
{
internal int BookId { get; private set; }
internal int ContributorId { get; private set; }
public Role Role { get; private set; }
public byte Order { get; private set; }
public Book Book { get; private set; }
public Contributor Contributor { get; private set; }
private BookContributor() { }
internal BookContributor(Book book, Contributor contributor, Role role, byte order)
{
ArgumentValidator.EnsureNotNull(book, nameof(book));
ArgumentValidator.EnsureNotNull(contributor, nameof(contributor));
Book = book;
Contributor = contributor;
Role = role;
Order = order;
}
}
}

View file

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dinah.Core;
using Microsoft.EntityFrameworkCore;
namespace DataLayer
{
public class AudibleCategoryId
{
public string Id { get; }
public AudibleCategoryId(string id)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
Id = id;
}
}
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;
internal int CategoryId { get; private set; }
public string AudibleCategoryId { get; private set; }
public string Name { get; private set; }
public Category ParentCategory { get; private set; }
private Category() { }
/// <summary>special id class b/c it's too easy to get string order mixed up</summary>
public Category(AudibleCategoryId audibleSeriesId, string name, Category parentCategory = null)
{
ArgumentValidator.EnsureNotNull(audibleSeriesId, nameof(audibleSeriesId));
var id = audibleSeriesId.Id;
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
ArgumentValidator.EnsureNotNullOrWhiteSpace(name, nameof(name));
AudibleCategoryId = id;
Name = name;
UpdateParentCategory(parentCategory);
}
public void UpdateParentCategory(Category parentCategory)
{
// don't overwrite with null but not an error
if (parentCategory != null)
ParentCategory = parentCategory;
}
}
}

View file

@ -0,0 +1,82 @@
using System.Collections.Generic;
using System.Linq;
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
// 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<BookContributor> _booksLink;
public IEnumerable<BookContributor> BooksLink => _booksLink?.ToList();
private Contributor() { }
public Contributor(string name)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(name, nameof(name));
_booksLink = new HashSet<BookContributor>();
Name = name;
}
public string AudibleAuthorId { get; private set; }
public void UpdateAudibleAuthorId(string authorId)
{
// don't overwrite with null or whitespace but not an error
if (!string.IsNullOrWhiteSpace(authorId))
AudibleAuthorId = authorId;
}
#region // AudibleAuthorId refactor: separate author-specific info. overkill for a single optional string
///// <summary>Most authors in Audible have a unique id</summary>
//public AudibleAuthorProperty AudibleAuthorProperty { get; private set; }
//public void UpdateAuthorId(string authorId, LibationContext context = null)
//{
// if (authorId == null)
// return;
// if (AudibleAuthorProperty != null)
// {
// AudibleAuthorProperty.UpdateAudibleAuthorId(authorId);
// return;
// }
// if (context == null)
// throw new ArgumentNullException(nameof(context), "You must provide a context");
// if (context.Contributors.Find(ContributorId) == null)
// throw new InvalidOperationException("Could not update audible author id.");
// var audibleAuthorProperty = new AudibleAuthorProperty();
// audibleAuthorProperty.UpdateAudibleAuthorId(authorId);
// context.AuthorProperties.Add(audibleAuthorProperty);
//}
//public class AudibleAuthorProperty
//{
// public int ContributorId { get; private set; }
// public Contributor Contributor { get; set; }
// public string AudibleAuthorId { get; private set; }
// public void UpdateAudibleAuthorId(string authorId)
// {
// if (!string.IsNullOrWhiteSpace(authorId))
// AudibleAuthorId = authorId;
// }
//}
//// ...and create EF table config
#endregion
}
}

View file

@ -0,0 +1,25 @@
using System;
using Dinah.Core;
namespace DataLayer
{
public class LibraryBook
{
internal int BookId { get; private set; }
public Book Book { get; private set; }
public DateTime DateAdded { get; private set; }
/// <summary>For downloading AAX file</summary>
public string DownloadBookLink { get; private set; }
private LibraryBook() { }
public LibraryBook(Book book, DateTime dateAdded, string downloadBookLink)
{
ArgumentValidator.EnsureNotNull(book, nameof(book));
Book = book;
DateAdded = dateAdded;
DownloadBookLink = downloadBookLink;
}
}
}

View file

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using Dinah.Core;
namespace DataLayer
{
/// <summary>Parameterless ctor and setters should be used by EF only. Everything else should treat it as immutable</summary>
public class Rating : ValueObject_Static<Rating>
{
public float OverallRating { get; private set; }
public float PerformanceRating { get; private set; }
public float StoryRating { get; private set; }
private Rating() { }
internal Rating(float overallRating, float performanceRating, float storyRating)
{
OverallRating = overallRating;
PerformanceRating = performanceRating;
StoryRating = storyRating;
}
// EF magically tracks this owned object. by replacing it with a new() immutable object, stuff gets weird. update instead
internal void Update(float overallRating, float performanceRating, float storyRating)
{
// don't overwrite with all 0
if (overallRating + performanceRating + storyRating == 0)
return;
OverallRating = overallRating;
PerformanceRating = performanceRating;
StoryRating = storyRating;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return OverallRating;
yield return PerformanceRating;
yield return StoryRating;
}
public float FirstScore
=> OverallRating > 0 ? OverallRating
: PerformanceRating > 0 ? PerformanceRating
: StoryRating;
/// <summary>character: ★</summary>
const char STAR = '\u2605';
/// <summary>character: ½</summary>
const char HALF = '\u00BD';
string getStars(float score)
{
var fullStars = (int)Math.Floor(score);
var starString = "".PadLeft(fullStars, STAR);
if (score - fullStars == 0.5f)
starString += HALF;
return starString;
}
public string ToStarString()
{
var items = new List<string>();
if (OverallRating > 0)
items.Add($"Overall: {getStars(OverallRating)}");
if (PerformanceRating > 0)
items.Add($"Perform: {getStars(PerformanceRating)}");
if (StoryRating > 0)
items.Add($"Story: {getStars(StoryRating)}");
return string.Join("\r\n", items);
}
}
}

View file

@ -0,0 +1,4 @@
namespace DataLayer
{
public enum Role { Author = 1, Narrator = 2, Publisher = 3 }
}

View file

@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dinah.Core;
using Microsoft.EntityFrameworkCore;
namespace DataLayer
{
public class AudibleSeriesId
{
public string Id { get; }
public AudibleSeriesId(string id)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
Id = id;
}
}
public class Series
{
internal int SeriesId { get; private set; }
public string AudibleSeriesId { get; private set; }
/// <summary>optional</summary>
public string Name { get; private set; }
private HashSet<SeriesBook> _booksLink;
public IEnumerable<SeriesBook> BooksLink
=> _booksLink?
.OrderBy(sb => sb.Index)
.ToList();
private Series() { }
/// <summary>special id class b/c it's too easy to get string order mixed up</summary>
public Series(AudibleSeriesId audibleSeriesId, string name = null)
{
ArgumentValidator.EnsureNotNull(audibleSeriesId, nameof(audibleSeriesId));
var id = audibleSeriesId.Id;
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
AudibleSeriesId = id;
_booksLink = new HashSet<SeriesBook>();
UpdateName(name);
}
public void UpdateName(string name)
{
// don't overwrite with null or whitespace but not an error
if (!string.IsNullOrWhiteSpace(name))
Name = name;
}
public void AddBook(Book book, float? index = null, DbContext context = null)
{
ArgumentValidator.EnsureNotNull(book, nameof(book));
// our add() is conditional upon what's already included in the collection.
// therefore if not loaded, a trip is required. might as well just load it
if (_booksLink == null)
{
ArgumentValidator.EnsureNotNull(context, nameof(context));
if (!context.Entry(this).IsKeySet)
throw new InvalidOperationException("Could not add series");
context.Entry(this).Collection(s => s.BooksLink).Load();
}
if (_booksLink.SingleOrDefault(sb => sb.Book == book) == null)
_booksLink.Add(new SeriesBook(this, book, index));
}
}
}

View file

@ -0,0 +1,38 @@
using Dinah.Core;
namespace DataLayer
{
public class SeriesBook
{
internal int SeriesId { get; private set; }
internal int BookId { get; private set; }
/// <summary>
/// <para>"index" not "order". This is both for sequence and display</para>
/// <para>Float allows for in-between books. eg: 2.5</para>
/// <para>To show 2 editions as the same book in a series, give them the same index</para>
/// <para>null IS NOT the same as 0. Some series call a book "book 0"</para>
/// </summary>
public float? Index { get; private set; }
public Series Series { get; private set; }
public Book Book { get; private set; }
private SeriesBook() { }
internal SeriesBook(Series series, Book book, float? index = null)
{
ArgumentValidator.EnsureNotNull(series, nameof(series));
ArgumentValidator.EnsureNotNull(book, nameof(book));
Series = series;
Book = book;
Index = index;
}
public void UpdateIndex(float? index)
{
if (index.HasValue)
Index = index.Value;
}
}
}

View file

@ -0,0 +1,24 @@
using Dinah.Core;
namespace DataLayer
{
/// <summary>PDF/ZIP files only. Although book download info could be the same format, they're substantially different and subject to change</summary>
public class Supplement
{
internal int SupplementId { get; private set; }
internal int BookId { get; private set; }
public Book Book { get; private set; }
public string Url { get; private set; }
private Supplement() { }
public Supplement(Book book, string url)
{
ArgumentValidator.EnsureNotNull(book, nameof(book));
ArgumentValidator.EnsureNotNullOrWhiteSpace(url, nameof(url));
Book = book;
Url = url;
}
}
}

View file

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Dinah.Core;
namespace DataLayer
{
public class UserDefinedItem
{
internal int BookId { get; private set; }
public Book Book { get; private set; }
private UserDefinedItem() { }
internal UserDefinedItem(Book book)
{
ArgumentValidator.EnsureNotNull(book, nameof(book));
Book = book;
}
private string _tags = "";
public string Tags
{
get => _tags;
set => _tags = sanitize(value);
}
#region sanitize tags: space delimited. Inline/denormalized. Lower case. Alpha numeric and hyphen
// only legal chars are letters numbers underscores and separating whitespace
//
// technically, the only char.s which aren't easily supported are \ [ ]
// however, whitelisting is far safer than blacklisting (eg: new lines, non-printable character)
// it's easy to expand whitelist as needed
// for lucene, ToLower() isn't needed because search is case-inspecific. for here, it prevents duplicates
//
// there are also other allowed but misleading characters. eg: the ^ operator defines a 'boost' score
// full list of characters which must be escaped:
// + - && || ! ( ) { } [ ] ^ " ~ * ? : \
static Regex regex = new Regex(@"[^\w\d\s_]", RegexOptions.Compiled);
private static string sanitize(string input)
{
if (string.IsNullOrWhiteSpace(input))
return "";
var str = input
.Trim()
.ToLowerInvariant()
// assume a hyphen is supposed to be an underscore
.Replace("-", "_");
var unique = regex
// turn illegal characters into a space. this will also take care of turning new lines into spaces
.Replace(str, " ")
// split and remove excess spaces
.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
// de-dup
.Distinct()
// this will prevent order from being relevant
.OrderBy(a => a);
// currently, the string is the canonical set. if we later make the collection into the canonical set:
// var tags = new Hashset<string>(list); // de-dup, order doesn't matter but can seem random due to hashing algo
// var isEqual = tagsNew.SetEquals(tagsOld);
return string.Join(" ", unique);
}
public IEnumerable<string> TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
#endregion
// owned: not an optional one-to-one
/// <summary>The user's individual book rating</summary>
public Rating Rating { get; private set; } = new Rating(0, 0, 0);
public void UpdateRating(float overallRating, float performanceRating, float storyRating)
=> Rating.Update(overallRating, performanceRating, storyRating);
}
}

View file

@ -0,0 +1,69 @@
using DataLayer.Configurations;
using Dinah.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace DataLayer
{
public class LibationContext : InterceptableDbContext
{
// IMPORTANT: USING DbSet<>
// ========================
// these run against the db. linq queries against these MUST be translatable to sql. primatives only. no POCOs or this error occurs:
// Unable to create a constant value of type 'DataLayer.Contributor'. Only primitive types or enumeration types are supported in this context.
// to use full object-linq, load and use local
// load full table:
// List<Contributor> contributors = ...;
// Contributors.Load();
// Contributors.Local.Where(a => contributors.Contains(a));
// load only those in object:
// // overwrite collection
// Entry(product).Collection(x => x.Narrators).Load();
// product.Narrators = narrators;
public DbSet<LibraryBook> Library { get; private set; }
public DbSet<Book> Books { get; private set; }
public DbSet<Contributor> Contributors { get; private set; }
public DbSet<Series> Series { get; private set; }
public DbSet<Category> Categories { get; private set; }
public static LibationContext Create()
{
var factory = new LibationContextFactory();
var context = factory.Create();
return context;
}
// see DesignTimeDbContextFactoryBase for info about ctors and connection strings/OnConfiguring()
internal LibationContext(DbContextOptions options) : base(options) { }
// called on each instantiation
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
AddInterceptor(new TagPersistenceInterceptor());
base.OnConfiguring(optionsBuilder);
}
// typically only called once per execution; NOT once per instantiation
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfiguration(new BookConfig());
modelBuilder.ApplyConfiguration(new ContributorConfig());
modelBuilder.ApplyConfiguration(new BookContributorConfig());
modelBuilder.ApplyConfiguration(new SupplementConfig());
modelBuilder.ApplyConfiguration(new UserDefinedItemConfig());
modelBuilder.ApplyConfiguration(new LibraryBookConfig());
modelBuilder.ApplyConfiguration(new SeriesConfig());
modelBuilder.ApplyConfiguration(new SeriesBookConfig());
modelBuilder.ApplyConfiguration(new CategoryConfig());
// seeds go here. examples in scratch pad
modelBuilder
.Entity<Category>()
.HasData(Category.GetEmpty());
// views are now supported via "query types" (instead of "entity types"): https://docs.microsoft.com/en-us/ef/core/modeling/query-types
}
}
}

View file

@ -0,0 +1,11 @@
using Dinah.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace DataLayer
{
public class LibationContextFactory : DesignTimeDbContextFactoryBase<LibationContext>
{
protected override LibationContext CreateNewInstance(DbContextOptions<LibationContext> options) => new LibationContext(options);
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) => optionsBuilder.UseSqlServer(connectionString);
}
}

View file

@ -0,0 +1,294 @@
// <auto-generated />
using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20190114190811_Initial")]
partial class Initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.0-rtm-35687")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleProductId");
b.Property<int>("CategoryId");
b.Property<DateTime?>("DatePublished");
b.Property<string>("Description");
b.Property<bool>("HasBookDetails");
b.Property<bool>("IsAbridged");
b.Property<int>("LengthInMinutes");
b.Property<string>("PictureId");
b.Property<string>("Publisher");
b.Property<string>("Title");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId");
b.Property<int>("ContributorId");
b.Property<int>("Role");
b.Property<byte>("Order");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleCategoryId");
b.Property<string>("Name");
b.Property<int?>("ParentCategoryCategoryId");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.HasIndex("ParentCategoryCategoryId");
b.ToTable("Categories");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleAuthorId");
b.Property<string>("Name");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId");
b.Property<DateTime>("DateAdded");
b.Property<string>("DownloadBookLink");
b.HasKey("BookId");
b.ToTable("Library");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleSeriesId");
b.Property<string>("Name");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId");
b.Property<int>("BookId");
b.Property<float?>("Index");
b.HasKey("SeriesId", "BookId");
b.HasIndex("BookId");
b.HasIndex("SeriesId");
b.ToTable("SeriesBook");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.HasOne("DataLayer.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade);
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b1.Property<int>("BookId");
b1.Property<string>("Url");
b1.HasKey("SupplementId");
b1.HasIndex("BookId");
b1.ToTable("Supplement");
b1.HasOne("DataLayer.Book", "Book")
.WithMany("Supplements")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId");
b1.Property<string>("Tags");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem");
b1.HasOne("DataLayer.Book", "Book")
.WithOne("UserDefinedItem")
.HasForeignKey("DataLayer.UserDefinedItem", "BookId")
.OnDelete(DeleteBehavior.Cascade);
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId");
b2.Property<float>("OverallRating");
b2.Property<float>("PerformanceRating");
b2.Property<float>("StoryRating");
b2.HasKey("UserDefinedItemBookId");
b2.ToTable("UserDefinedItem");
b2.HasOne("DataLayer.UserDefinedItem")
.WithOne("Rating")
.HasForeignKey("DataLayer.Rating", "UserDefinedItemBookId")
.OnDelete(DeleteBehavior.Cascade);
});
});
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b1.Property<float>("OverallRating");
b1.Property<float>("PerformanceRating");
b1.Property<float>("StoryRating");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.HasOne("DataLayer.Book")
.WithOne("Rating")
.HasForeignKey("DataLayer.Rating", "BookId")
.OnDelete(DeleteBehavior.Cascade);
});
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("ContributorsLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("DataLayer.Contributor", "Contributor")
.WithMany("BooksLink")
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithOne()
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("SeriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("DataLayer.Series", "Series")
.WithMany("BooksLink")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,288 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
namespace DataLayer.Migrations
{
public partial class Initial : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Categories",
columns: table => new
{
CategoryId = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
AudibleCategoryId = table.Column<string>(nullable: true),
Name = table.Column<string>(nullable: true),
ParentCategoryCategoryId = table.Column<int>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Categories", x => x.CategoryId);
table.ForeignKey(
name: "FK_Categories_Categories_ParentCategoryCategoryId",
column: x => x.ParentCategoryCategoryId,
principalTable: "Categories",
principalColumn: "CategoryId",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Contributors",
columns: table => new
{
ContributorId = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
Name = table.Column<string>(nullable: true),
AudibleAuthorId = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Contributors", x => x.ContributorId);
});
migrationBuilder.CreateTable(
name: "Series",
columns: table => new
{
SeriesId = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
AudibleSeriesId = table.Column<string>(nullable: true),
Name = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Series", x => x.SeriesId);
});
migrationBuilder.CreateTable(
name: "Books",
columns: table => new
{
BookId = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
AudibleProductId = table.Column<string>(nullable: true),
Title = table.Column<string>(nullable: true),
Description = table.Column<string>(nullable: true),
LengthInMinutes = table.Column<int>(nullable: false),
PictureId = table.Column<string>(nullable: true),
HasBookDetails = table.Column<bool>(nullable: false),
IsAbridged = table.Column<bool>(nullable: false),
Publisher = table.Column<string>(nullable: true),
DatePublished = table.Column<DateTime>(nullable: true),
CategoryId = table.Column<int>(nullable: false),
Rating_OverallRating = table.Column<float>(nullable: false),
Rating_PerformanceRating = table.Column<float>(nullable: false),
Rating_StoryRating = table.Column<float>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Books", x => x.BookId);
table.ForeignKey(
name: "FK_Books_Categories_CategoryId",
column: x => x.CategoryId,
principalTable: "Categories",
principalColumn: "CategoryId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "BookContributor",
columns: table => new
{
BookId = table.Column<int>(nullable: false),
ContributorId = table.Column<int>(nullable: false),
Role = table.Column<int>(nullable: false),
Order = table.Column<byte>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BookContributor", x => new { x.BookId, x.ContributorId, x.Role });
table.ForeignKey(
name: "FK_BookContributor_Books_BookId",
column: x => x.BookId,
principalTable: "Books",
principalColumn: "BookId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_BookContributor_Contributors_ContributorId",
column: x => x.ContributorId,
principalTable: "Contributors",
principalColumn: "ContributorId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Library",
columns: table => new
{
BookId = table.Column<int>(nullable: false),
DateAdded = table.Column<DateTime>(nullable: false),
DownloadBookLink = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Library", x => x.BookId);
table.ForeignKey(
name: "FK_Library_Books_BookId",
column: x => x.BookId,
principalTable: "Books",
principalColumn: "BookId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "SeriesBook",
columns: table => new
{
SeriesId = table.Column<int>(nullable: false),
BookId = table.Column<int>(nullable: false),
Index = table.Column<float>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_SeriesBook", x => new { x.SeriesId, x.BookId });
table.ForeignKey(
name: "FK_SeriesBook_Books_BookId",
column: x => x.BookId,
principalTable: "Books",
principalColumn: "BookId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_SeriesBook_Series_SeriesId",
column: x => x.SeriesId,
principalTable: "Series",
principalColumn: "SeriesId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Supplement",
columns: table => new
{
SupplementId = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
BookId = table.Column<int>(nullable: false),
Url = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Supplement", x => x.SupplementId);
table.ForeignKey(
name: "FK_Supplement_Books_BookId",
column: x => x.BookId,
principalTable: "Books",
principalColumn: "BookId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "UserDefinedItem",
columns: table => new
{
BookId = table.Column<int>(nullable: false),
Tags = table.Column<string>(nullable: true),
Rating_OverallRating = table.Column<float>(nullable: false),
Rating_PerformanceRating = table.Column<float>(nullable: false),
Rating_StoryRating = table.Column<float>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserDefinedItem", x => x.BookId);
table.ForeignKey(
name: "FK_UserDefinedItem_Books_BookId",
column: x => x.BookId,
principalTable: "Books",
principalColumn: "BookId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_BookContributor_BookId",
table: "BookContributor",
column: "BookId");
migrationBuilder.CreateIndex(
name: "IX_BookContributor_ContributorId",
table: "BookContributor",
column: "ContributorId");
migrationBuilder.CreateIndex(
name: "IX_Books_AudibleProductId",
table: "Books",
column: "AudibleProductId");
migrationBuilder.CreateIndex(
name: "IX_Books_CategoryId",
table: "Books",
column: "CategoryId");
migrationBuilder.CreateIndex(
name: "IX_Categories_AudibleCategoryId",
table: "Categories",
column: "AudibleCategoryId");
migrationBuilder.CreateIndex(
name: "IX_Categories_ParentCategoryCategoryId",
table: "Categories",
column: "ParentCategoryCategoryId");
migrationBuilder.CreateIndex(
name: "IX_Contributors_Name",
table: "Contributors",
column: "Name");
migrationBuilder.CreateIndex(
name: "IX_Series_AudibleSeriesId",
table: "Series",
column: "AudibleSeriesId");
migrationBuilder.CreateIndex(
name: "IX_SeriesBook_BookId",
table: "SeriesBook",
column: "BookId");
migrationBuilder.CreateIndex(
name: "IX_SeriesBook_SeriesId",
table: "SeriesBook",
column: "SeriesId");
migrationBuilder.CreateIndex(
name: "IX_Supplement_BookId",
table: "Supplement",
column: "BookId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "BookContributor");
migrationBuilder.DropTable(
name: "Library");
migrationBuilder.DropTable(
name: "SeriesBook");
migrationBuilder.DropTable(
name: "Supplement");
migrationBuilder.DropTable(
name: "UserDefinedItem");
migrationBuilder.DropTable(
name: "Contributors");
migrationBuilder.DropTable(
name: "Series");
migrationBuilder.DropTable(
name: "Books");
migrationBuilder.DropTable(
name: "Categories");
}
}
}

View file

@ -0,0 +1,293 @@
// <auto-generated />
using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20190114191724_NullableCategory")]
partial class NullableCategory
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.0-rtm-35687")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleProductId");
b.Property<int?>("CategoryId");
b.Property<DateTime?>("DatePublished");
b.Property<string>("Description");
b.Property<bool>("HasBookDetails");
b.Property<bool>("IsAbridged");
b.Property<int>("LengthInMinutes");
b.Property<string>("PictureId");
b.Property<string>("Publisher");
b.Property<string>("Title");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId");
b.Property<int>("ContributorId");
b.Property<int>("Role");
b.Property<byte>("Order");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleCategoryId");
b.Property<string>("Name");
b.Property<int?>("ParentCategoryCategoryId");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.HasIndex("ParentCategoryCategoryId");
b.ToTable("Categories");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleAuthorId");
b.Property<string>("Name");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId");
b.Property<DateTime>("DateAdded");
b.Property<string>("DownloadBookLink");
b.HasKey("BookId");
b.ToTable("Library");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleSeriesId");
b.Property<string>("Name");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId");
b.Property<int>("BookId");
b.Property<float?>("Index");
b.HasKey("SeriesId", "BookId");
b.HasIndex("BookId");
b.HasIndex("SeriesId");
b.ToTable("SeriesBook");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.HasOne("DataLayer.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId");
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b1.Property<int>("BookId");
b1.Property<string>("Url");
b1.HasKey("SupplementId");
b1.HasIndex("BookId");
b1.ToTable("Supplement");
b1.HasOne("DataLayer.Book", "Book")
.WithMany("Supplements")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId");
b1.Property<string>("Tags");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem");
b1.HasOne("DataLayer.Book", "Book")
.WithOne("UserDefinedItem")
.HasForeignKey("DataLayer.UserDefinedItem", "BookId")
.OnDelete(DeleteBehavior.Cascade);
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId");
b2.Property<float>("OverallRating");
b2.Property<float>("PerformanceRating");
b2.Property<float>("StoryRating");
b2.HasKey("UserDefinedItemBookId");
b2.ToTable("UserDefinedItem");
b2.HasOne("DataLayer.UserDefinedItem")
.WithOne("Rating")
.HasForeignKey("DataLayer.Rating", "UserDefinedItemBookId")
.OnDelete(DeleteBehavior.Cascade);
});
});
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b1.Property<float>("OverallRating");
b1.Property<float>("PerformanceRating");
b1.Property<float>("StoryRating");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.HasOne("DataLayer.Book")
.WithOne("Rating")
.HasForeignKey("DataLayer.Rating", "BookId")
.OnDelete(DeleteBehavior.Cascade);
});
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("ContributorsLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("DataLayer.Contributor", "Contributor")
.WithMany("BooksLink")
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithOne()
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("SeriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("DataLayer.Series", "Series")
.WithMany("BooksLink")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace DataLayer.Migrations
{
public partial class NullableCategory : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Books_Categories_CategoryId",
table: "Books");
migrationBuilder.AlterColumn<int>(
name: "CategoryId",
table: "Books",
nullable: true,
oldClrType: typeof(int));
migrationBuilder.AddForeignKey(
name: "FK_Books_Categories_CategoryId",
table: "Books",
column: "CategoryId",
principalTable: "Categories",
principalColumn: "CategoryId",
onDelete: ReferentialAction.Restrict);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Books_Categories_CategoryId",
table: "Books");
migrationBuilder.AlterColumn<int>(
name: "CategoryId",
table: "Books",
nullable: false,
oldClrType: typeof(int),
oldNullable: true);
migrationBuilder.AddForeignKey(
name: "FK_Books_Categories_CategoryId",
table: "Books",
column: "CategoryId",
principalTable: "Categories",
principalColumn: "CategoryId",
onDelete: ReferentialAction.Cascade);
}
}
}

View file

@ -0,0 +1,293 @@
// <auto-generated />
using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20190124190012_NullCategory")]
partial class NullCategory
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.0-rtm-35687")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleProductId");
b.Property<int?>("CategoryId");
b.Property<DateTime?>("DatePublished");
b.Property<string>("Description");
b.Property<bool>("HasBookDetails");
b.Property<bool>("IsAbridged");
b.Property<int>("LengthInMinutes");
b.Property<string>("PictureId");
b.Property<string>("Publisher");
b.Property<string>("Title");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId");
b.Property<int>("ContributorId");
b.Property<int>("Role");
b.Property<byte>("Order");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleCategoryId");
b.Property<string>("Name");
b.Property<int?>("ParentCategoryCategoryId");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.HasIndex("ParentCategoryCategoryId");
b.ToTable("Categories");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleAuthorId");
b.Property<string>("Name");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId");
b.Property<DateTime>("DateAdded");
b.Property<string>("DownloadBookLink");
b.HasKey("BookId");
b.ToTable("Library");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleSeriesId");
b.Property<string>("Name");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId");
b.Property<int>("BookId");
b.Property<float?>("Index");
b.HasKey("SeriesId", "BookId");
b.HasIndex("BookId");
b.HasIndex("SeriesId");
b.ToTable("SeriesBook");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.HasOne("DataLayer.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId");
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b1.Property<int>("BookId");
b1.Property<string>("Url");
b1.HasKey("SupplementId");
b1.HasIndex("BookId");
b1.ToTable("Supplement");
b1.HasOne("DataLayer.Book", "Book")
.WithMany("Supplements")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId");
b1.Property<string>("Tags");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem");
b1.HasOne("DataLayer.Book", "Book")
.WithOne("UserDefinedItem")
.HasForeignKey("DataLayer.UserDefinedItem", "BookId")
.OnDelete(DeleteBehavior.Cascade);
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId");
b2.Property<float>("OverallRating");
b2.Property<float>("PerformanceRating");
b2.Property<float>("StoryRating");
b2.HasKey("UserDefinedItemBookId");
b2.ToTable("UserDefinedItem");
b2.HasOne("DataLayer.UserDefinedItem")
.WithOne("Rating")
.HasForeignKey("DataLayer.Rating", "UserDefinedItemBookId")
.OnDelete(DeleteBehavior.Cascade);
});
});
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b1.Property<float>("OverallRating");
b1.Property<float>("PerformanceRating");
b1.Property<float>("StoryRating");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.HasOne("DataLayer.Book")
.WithOne("Rating")
.HasForeignKey("DataLayer.Rating", "BookId")
.OnDelete(DeleteBehavior.Cascade);
});
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("ContributorsLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("DataLayer.Contributor", "Contributor")
.WithMany("BooksLink")
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithOne()
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("SeriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("DataLayer.Series", "Series")
.WithMany("BooksLink")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,17 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace DataLayer.Migrations
{
public partial class NullCategory : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View file

@ -0,0 +1,294 @@
// <auto-generated />
using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20190124190057_NonNullCategory")]
partial class NonNullCategory
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.0-rtm-35687")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleProductId");
b.Property<int>("CategoryId");
b.Property<DateTime?>("DatePublished");
b.Property<string>("Description");
b.Property<bool>("HasBookDetails");
b.Property<bool>("IsAbridged");
b.Property<int>("LengthInMinutes");
b.Property<string>("PictureId");
b.Property<string>("Publisher");
b.Property<string>("Title");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId");
b.Property<int>("ContributorId");
b.Property<int>("Role");
b.Property<byte>("Order");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleCategoryId");
b.Property<string>("Name");
b.Property<int?>("ParentCategoryCategoryId");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.HasIndex("ParentCategoryCategoryId");
b.ToTable("Categories");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleAuthorId");
b.Property<string>("Name");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId");
b.Property<DateTime>("DateAdded");
b.Property<string>("DownloadBookLink");
b.HasKey("BookId");
b.ToTable("Library");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleSeriesId");
b.Property<string>("Name");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId");
b.Property<int>("BookId");
b.Property<float?>("Index");
b.HasKey("SeriesId", "BookId");
b.HasIndex("BookId");
b.HasIndex("SeriesId");
b.ToTable("SeriesBook");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.HasOne("DataLayer.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade);
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b1.Property<int>("BookId");
b1.Property<string>("Url");
b1.HasKey("SupplementId");
b1.HasIndex("BookId");
b1.ToTable("Supplement");
b1.HasOne("DataLayer.Book", "Book")
.WithMany("Supplements")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId");
b1.Property<string>("Tags");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem");
b1.HasOne("DataLayer.Book", "Book")
.WithOne("UserDefinedItem")
.HasForeignKey("DataLayer.UserDefinedItem", "BookId")
.OnDelete(DeleteBehavior.Cascade);
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId");
b2.Property<float>("OverallRating");
b2.Property<float>("PerformanceRating");
b2.Property<float>("StoryRating");
b2.HasKey("UserDefinedItemBookId");
b2.ToTable("UserDefinedItem");
b2.HasOne("DataLayer.UserDefinedItem")
.WithOne("Rating")
.HasForeignKey("DataLayer.Rating", "UserDefinedItemBookId")
.OnDelete(DeleteBehavior.Cascade);
});
});
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b1.Property<float>("OverallRating");
b1.Property<float>("PerformanceRating");
b1.Property<float>("StoryRating");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.HasOne("DataLayer.Book")
.WithOne("Rating")
.HasForeignKey("DataLayer.Rating", "BookId")
.OnDelete(DeleteBehavior.Cascade);
});
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("ContributorsLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("DataLayer.Contributor", "Contributor")
.WithMany("BooksLink")
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithOne()
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("SeriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("DataLayer.Series", "Series")
.WithMany("BooksLink")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace DataLayer.Migrations
{
public partial class NonNullCategory : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Books_Categories_CategoryId",
table: "Books");
migrationBuilder.AlterColumn<int>(
name: "CategoryId",
table: "Books",
nullable: false,
oldClrType: typeof(int),
oldNullable: true);
migrationBuilder.AddForeignKey(
name: "FK_Books_Categories_CategoryId",
table: "Books",
column: "CategoryId",
principalTable: "Categories",
principalColumn: "CategoryId",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Books_Categories_CategoryId",
table: "Books");
migrationBuilder.AlterColumn<int>(
name: "CategoryId",
table: "Books",
nullable: true,
oldClrType: typeof(int));
migrationBuilder.AddForeignKey(
name: "FK_Books_Categories_CategoryId",
table: "Books",
column: "CategoryId",
principalTable: "Categories",
principalColumn: "CategoryId",
onDelete: ReferentialAction.Restrict);
}
}
}

View file

@ -0,0 +1,302 @@
// <auto-generated />
using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20190124192324_EmptyCategorySeed")]
partial class EmptyCategorySeed
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.0-rtm-35687")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleProductId");
b.Property<int>("CategoryId");
b.Property<DateTime?>("DatePublished");
b.Property<string>("Description");
b.Property<bool>("HasBookDetails");
b.Property<bool>("IsAbridged");
b.Property<int>("LengthInMinutes");
b.Property<string>("PictureId");
b.Property<string>("Publisher");
b.Property<string>("Title");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId");
b.Property<int>("ContributorId");
b.Property<int>("Role");
b.Property<byte>("Order");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleCategoryId");
b.Property<string>("Name");
b.Property<int?>("ParentCategoryCategoryId");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.HasIndex("ParentCategoryCategoryId");
b.ToTable("Categories");
b.HasData(
new
{
CategoryId = -1,
AudibleCategoryId = "",
Name = ""
});
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleAuthorId");
b.Property<string>("Name");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId");
b.Property<DateTime>("DateAdded");
b.Property<string>("DownloadBookLink");
b.HasKey("BookId");
b.ToTable("Library");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleSeriesId");
b.Property<string>("Name");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId");
b.Property<int>("BookId");
b.Property<float?>("Index");
b.HasKey("SeriesId", "BookId");
b.HasIndex("BookId");
b.HasIndex("SeriesId");
b.ToTable("SeriesBook");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.HasOne("DataLayer.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade);
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b1.Property<int>("BookId");
b1.Property<string>("Url");
b1.HasKey("SupplementId");
b1.HasIndex("BookId");
b1.ToTable("Supplement");
b1.HasOne("DataLayer.Book", "Book")
.WithMany("Supplements")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId");
b1.Property<string>("Tags");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem");
b1.HasOne("DataLayer.Book", "Book")
.WithOne("UserDefinedItem")
.HasForeignKey("DataLayer.UserDefinedItem", "BookId")
.OnDelete(DeleteBehavior.Cascade);
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId");
b2.Property<float>("OverallRating");
b2.Property<float>("PerformanceRating");
b2.Property<float>("StoryRating");
b2.HasKey("UserDefinedItemBookId");
b2.ToTable("UserDefinedItem");
b2.HasOne("DataLayer.UserDefinedItem")
.WithOne("Rating")
.HasForeignKey("DataLayer.Rating", "UserDefinedItemBookId")
.OnDelete(DeleteBehavior.Cascade);
});
});
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b1.Property<float>("OverallRating");
b1.Property<float>("PerformanceRating");
b1.Property<float>("StoryRating");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.HasOne("DataLayer.Book")
.WithOne("Rating")
.HasForeignKey("DataLayer.Rating", "BookId")
.OnDelete(DeleteBehavior.Cascade);
});
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("ContributorsLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("DataLayer.Contributor", "Contributor")
.WithMany("BooksLink")
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithOne()
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("SeriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("DataLayer.Series", "Series")
.WithMany("BooksLink")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace DataLayer.Migrations
{
public partial class EmptyCategorySeed : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.InsertData(
table: "Categories",
columns: new[] { "CategoryId", "AudibleCategoryId", "Name", "ParentCategoryCategoryId" },
values: new object[] { -1, "", "", null });
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "Categories",
keyColumn: "CategoryId",
keyValue: -1);
}
}
}

View file

@ -0,0 +1,300 @@
// <auto-generated />
using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20190124214317_PublisherContrib")]
partial class PublisherContrib
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.0-rtm-35687")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleProductId");
b.Property<int>("CategoryId");
b.Property<DateTime?>("DatePublished");
b.Property<string>("Description");
b.Property<bool>("HasBookDetails");
b.Property<bool>("IsAbridged");
b.Property<int>("LengthInMinutes");
b.Property<string>("PictureId");
b.Property<string>("Title");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId");
b.Property<int>("ContributorId");
b.Property<int>("Role");
b.Property<byte>("Order");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleCategoryId");
b.Property<string>("Name");
b.Property<int?>("ParentCategoryCategoryId");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.HasIndex("ParentCategoryCategoryId");
b.ToTable("Categories");
b.HasData(
new
{
CategoryId = -1,
AudibleCategoryId = "",
Name = ""
});
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleAuthorId");
b.Property<string>("Name");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId");
b.Property<DateTime>("DateAdded");
b.Property<string>("DownloadBookLink");
b.HasKey("BookId");
b.ToTable("Library");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleSeriesId");
b.Property<string>("Name");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId");
b.Property<int>("BookId");
b.Property<float?>("Index");
b.HasKey("SeriesId", "BookId");
b.HasIndex("BookId");
b.HasIndex("SeriesId");
b.ToTable("SeriesBook");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.HasOne("DataLayer.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade);
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b1.Property<int>("BookId");
b1.Property<string>("Url");
b1.HasKey("SupplementId");
b1.HasIndex("BookId");
b1.ToTable("Supplement");
b1.HasOne("DataLayer.Book", "Book")
.WithMany("Supplements")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId");
b1.Property<string>("Tags");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem");
b1.HasOne("DataLayer.Book", "Book")
.WithOne("UserDefinedItem")
.HasForeignKey("DataLayer.UserDefinedItem", "BookId")
.OnDelete(DeleteBehavior.Cascade);
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId");
b2.Property<float>("OverallRating");
b2.Property<float>("PerformanceRating");
b2.Property<float>("StoryRating");
b2.HasKey("UserDefinedItemBookId");
b2.ToTable("UserDefinedItem");
b2.HasOne("DataLayer.UserDefinedItem")
.WithOne("Rating")
.HasForeignKey("DataLayer.Rating", "UserDefinedItemBookId")
.OnDelete(DeleteBehavior.Cascade);
});
});
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b1.Property<float>("OverallRating");
b1.Property<float>("PerformanceRating");
b1.Property<float>("StoryRating");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.HasOne("DataLayer.Book")
.WithOne("Rating")
.HasForeignKey("DataLayer.Rating", "BookId")
.OnDelete(DeleteBehavior.Cascade);
});
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("ContributorsLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("DataLayer.Contributor", "Contributor")
.WithMany("BooksLink")
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithOne()
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("SeriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("DataLayer.Series", "Series")
.WithMany("BooksLink")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace DataLayer.Migrations
{
public partial class PublisherContrib : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Publisher",
table: "Books");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Publisher",
table: "Books",
nullable: true);
}
}
}

View file

@ -0,0 +1,298 @@
// <auto-generated />
using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
partial class LibationContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.0-rtm-35687")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleProductId");
b.Property<int>("CategoryId");
b.Property<DateTime?>("DatePublished");
b.Property<string>("Description");
b.Property<bool>("HasBookDetails");
b.Property<bool>("IsAbridged");
b.Property<int>("LengthInMinutes");
b.Property<string>("PictureId");
b.Property<string>("Title");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId");
b.Property<int>("ContributorId");
b.Property<int>("Role");
b.Property<byte>("Order");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleCategoryId");
b.Property<string>("Name");
b.Property<int?>("ParentCategoryCategoryId");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.HasIndex("ParentCategoryCategoryId");
b.ToTable("Categories");
b.HasData(
new
{
CategoryId = -1,
AudibleCategoryId = "",
Name = ""
});
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleAuthorId");
b.Property<string>("Name");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId");
b.Property<DateTime>("DateAdded");
b.Property<string>("DownloadBookLink");
b.HasKey("BookId");
b.ToTable("Library");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("AudibleSeriesId");
b.Property<string>("Name");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId");
b.Property<int>("BookId");
b.Property<float?>("Index");
b.HasKey("SeriesId", "BookId");
b.HasIndex("BookId");
b.HasIndex("SeriesId");
b.ToTable("SeriesBook");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.HasOne("DataLayer.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade);
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b1.Property<int>("BookId");
b1.Property<string>("Url");
b1.HasKey("SupplementId");
b1.HasIndex("BookId");
b1.ToTable("Supplement");
b1.HasOne("DataLayer.Book", "Book")
.WithMany("Supplements")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId");
b1.Property<string>("Tags");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem");
b1.HasOne("DataLayer.Book", "Book")
.WithOne("UserDefinedItem")
.HasForeignKey("DataLayer.UserDefinedItem", "BookId")
.OnDelete(DeleteBehavior.Cascade);
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId");
b2.Property<float>("OverallRating");
b2.Property<float>("PerformanceRating");
b2.Property<float>("StoryRating");
b2.HasKey("UserDefinedItemBookId");
b2.ToTable("UserDefinedItem");
b2.HasOne("DataLayer.UserDefinedItem")
.WithOne("Rating")
.HasForeignKey("DataLayer.Rating", "UserDefinedItemBookId")
.OnDelete(DeleteBehavior.Cascade);
});
});
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b1.Property<float>("OverallRating");
b1.Property<float>("PerformanceRating");
b1.Property<float>("StoryRating");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.HasOne("DataLayer.Book")
.WithOne("Rating")
.HasForeignKey("DataLayer.Rating", "BookId")
.OnDelete(DeleteBehavior.Cascade);
});
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("ContributorsLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("DataLayer.Contributor", "Contributor")
.WithMany("BooksLink")
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithOne()
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("SeriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("DataLayer.Series", "Series")
.WithMany("BooksLink")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
namespace DataLayer
{
public static class BookQueries
{
public static int BooksWithoutDetailsCount()
{
using (var context = LibationContext.Create())
return context
.Books
.Count(b => !b.HasBookDetails);
}
public static Book GetBook_Flat_NoTracking(string productId)
{
using (var context = LibationContext.Create())
return context
.Books
.AsNoTracking()
.GetBook(productId);
}
public static Book GetBook(this IQueryable<Book> books, string productId)
=> books
.GetBooks()
.SingleOrDefault(b => b.AudibleProductId == productId);
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
public static IQueryable<Book> GetBooks(this IQueryable<Book> books, Expression<Func<Book, bool>> predicate)
=> books
.GetBooks()
.Where(predicate);
public static IQueryable<Book> GetBooks(this IQueryable<Book> books)
=> books
// owned items are always loaded. eg: book.UserDefinedItem, book.Supplements
.Include(b => b.SeriesLink).ThenInclude(sb => sb.Series)
.Include(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
.Include(b => b.Category).ThenInclude(c => c.ParentCategory);
}
}

View file

@ -0,0 +1,19 @@
using System;
using System.Linq;
namespace DataLayer
{
public static class GenericPaging
{
public static IQueryable<T> Page<T>(this IQueryable<T> query, int pageNumZeroStart, int pageSize)
{
if (pageSize < 1)
throw new ArgumentOutOfRangeException(nameof(pageSize), "pageSize must be at least 1");
if (pageNumZeroStart > 0)
query = query.Skip(pageNumZeroStart * pageSize);
return query.Take(pageSize);
}
}
}

View file

@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
namespace DataLayer
{
public static class LibraryQueries
{
public static List<LibraryBook> GetLibrary_Flat_NoTracking()
{
using (var context = LibationContext.Create())
return context
.Library
.AsNoTracking()
.GetLibrary()
.ToList();
}
public static LibraryBook GetLibraryBook_Flat_NoTracking(string productId)
{
using (var context = LibationContext.Create())
return context
.Library
.AsNoTracking()
.GetLibraryBook(productId);
}
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
public static IQueryable<LibraryBook> GetLibrary(this IQueryable<LibraryBook> library)
=> library
// owned items are always loaded. eg: book.UserDefinedItem, book.Supplements
.Include(le => le.Book).ThenInclude(b => b.SeriesLink).ThenInclude(sb => sb.Series)
.Include(le => le.Book).ThenInclude(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
.Include(le => le.Book).ThenInclude(b => b.Category).ThenInclude(c => c.ParentCategory);
public static LibraryBook GetLibraryBook(this IQueryable<LibraryBook> library, string productId)
=> library
.GetLibrary()
.SingleOrDefault(le => le.Book.AudibleProductId == productId);
}
}

View file

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dinah.Core.Collections.Generic;
using Dinah.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace DataLayer
{
internal class TagPersistenceInterceptor : IDbInterceptor
{
public void Executing(DbContext context)
{
doWork__EFCore(context);
}
public void Executed(DbContext context) { }
static void doWork__EFCore(DbContext context)
{
// persist tags:
var modifiedEntities = context.ChangeTracker.Entries().Where(p => p.State.In(EntityState.Modified, EntityState.Added)).ToList();
var tagSets = modifiedEntities.Select(e => e.Entity as UserDefinedItem).Where(a => a != null).ToList();
foreach (var t in tagSets)
FileManager.TagsPersistence.Save(t.Book.AudibleProductId, t.Tags);
}
#region // notes: working with proxies, esp EF 6
// EF 6: entities are proxied with lazy loading when collections are virtual
// EF Core: lazy loading is supported in 2.1 (there is a version of lazy loading with proxy-wrapping and a proxy-less version with DI) but not on by default and are not supported here
//static void doWork_EF6(DbContext context)
//{
// var modifiedEntities = context.ChangeTracker.Entries().Where(p => p.State == EntityState.Modified).ToList();
// var unproxiedEntities = modifiedEntities.Select(me => UnProxy(context, me.Entity)).ToList();
// // persist tags
// var tagSets = unproxiedEntities.Select(ue => ue as UserDefinedItem).Where(a => a != null).ToList();
// foreach (var t in tagSets)
// FileManager.TagsPersistence.Save(t.ProductId, t.TagsRaw);
//}
//// https://stackoverflow.com/a/25774651
//private static T UnProxy<T>(DbContext context, T proxyObject) where T : class
//{
// // alternative: https://docs.microsoft.com/en-us/ef/ef6/fundamentals/proxies
// var proxyCreationEnabled = context.Configuration.ProxyCreationEnabled;
// try
// {
// context.Configuration.ProxyCreationEnabled = false;
// return context.Entry(proxyObject).CurrentValues.ToObject() as T;
// }
// finally
// {
// context.Configuration.ProxyCreationEnabled = proxyCreationEnabled;
// }
//}
#endregion
}
}

View file

@ -0,0 +1,124 @@
using System;
using Dinah.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace _scratch_pad
{
////// to use this as a console, open properties and change from class library => console
//// DON'T FORGET TO REVERT IT
//public class Program
//{
// public static void Main(string[] args)
// {
// var user = new Student() { Name = "Dinah Cheshire" };
// var udi = new UserDef { UserDefId = 1, TagsRaw = "my,tags" };
// using (var context = new MyTestContextDesignTimeDbContextFactory().Create())
// {
// context.Add(user);
// //context.Add(udi);
// context.Update(udi);
// context.SaveChanges();
// }
// Console.WriteLine($"Student was saved in the database with id: {user.Id}");
// }
//}
public class MyTestContextDesignTimeDbContextFactory : DesignTimeDbContextFactoryBase<MyTestContext>
{
protected override MyTestContext CreateNewInstance(DbContextOptions<MyTestContext> options) => new MyTestContext(options);
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) => optionsBuilder.UseSqlite(connectionString);
}
public class MyTestContext : DbContext
{
// see DesignTimeDbContextFactoryBase for info about ctors and connection strings/OnConfiguring()
public MyTestContext(DbContextOptions<MyTestContext> options) : base(options) { }
#region classes for OnModelCreating() seed example
class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public System.Collections.Generic.ICollection<Post> Posts { get; set; }
}
class Post
{
public int PostId { get; set; }
public string Content { get; set; }
public string Title { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
public Name AuthorName { get; set; }
}
class Name
{
public string First { get; set; }
public string Last { get; set; }
}
#endregion
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// config
modelBuilder.Entity<Blog>(entity => entity.Property(e => e.Url).IsRequired());
modelBuilder.Entity<Order>().OwnsOne(p => p.OrderDetails, cb =>
{
cb.OwnsOne(c => c.BillingAddress);
cb.OwnsOne(c => c.ShippingAddress);
});
modelBuilder.Entity<Post>(entity =>
entity
.HasOne(d => d.Blog)
.WithMany(p => p.Posts)
.HasForeignKey("BlogId"));
// BlogSeed
modelBuilder.Entity<Blog>().HasData(new Blog { BlogId = 1, Url = "http://sample.com" });
// PostSeed
modelBuilder.Entity<Post>().HasData(new Post() { BlogId = 1, PostId = 1, Title = "First post", Content = "Test 1" });
// AnonymousPostSeed
modelBuilder.Entity<Post>().HasData(new { BlogId = 1, PostId = 2, Title = "Second post", Content = "Test 2" });
// OwnedTypeSeed
modelBuilder.Entity<Post>().OwnsOne(p => p.AuthorName).HasData(
new { PostId = 1, First = "Andriy", Last = "Svyryd" },
new { PostId = 2, First = "Diego", Last = "Vega" });
}
public DbSet<Student> Students { get; set; }
public DbSet<UserDef> UserDefs { get; set; }
public DbSet<Order> Orders { get; set; }
}
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
}
public class UserDef
{
public int UserDefId { get; set; }
public string TagsRaw { get; set; }
}
public class Order
{
public int Id { get; set; }
public OrderDetails OrderDetails { get; set; }
}
public class OrderDetails
{
public StreetAddress BillingAddress { get; set; }
public StreetAddress ShippingAddress { get; set; }
}
public class StreetAddress
{
public string Street { get; set; }
public string City { get; set; }
}
}

View file

@ -0,0 +1,48 @@
HOW TO CREATE: EF CORE PROJECT
==============================
easiest with .NET Core but there's also a work-around for .NET Standard
example is for sqlite but the same works with MsSql
nuget
Microsoft.EntityFrameworkCore.Tools (needed for using Package Manager Console)
Microsoft.EntityFrameworkCore.Sqlite
MIGRATIONS require standard, not core
using standard instead of core. edit 3 things in csproj
1of3: pluralize xml TargetFramework tag to TargetFrameworks
2of2: TargetFrameworks from: netstandard2.0
to: netcoreapp2.1;netstandard2.0
3of3: add
<PropertyGroup>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
</PropertyGroup>
run. error
SQLite Error 1: 'no such table: Blogs'.
set project "Set as StartUp Project"
Tools >> Nuget Package Manager >> Package Manager Console
default project: Examples\SQLite_NETCore2_0
note: in EFCore, Enable-Migrations is no longer used. start with add-migration
PM> add-migration InitialCreate
PM> Update-Database
if add-migration xyz throws and error, don't take the error msg at face value. try again with add-migration xyz -verbose
new sqlite .db file created: Copy always/Copy if newer
or copy .db file to destination
relative:
optionsBuilder.UseSqlite("Data Source=blogging.db");
absolute (use fwd slashes):
optionsBuilder.UseSqlite("Data Source=C:/foo/bar/blogging.db");
REFERENCE ARTICLES
------------------
https://docs.microsoft.com/en-us/ef/core/get-started/netcore/new-db-sqlite
https://carlos.mendible.com/2016/07/11/step-by-step-dotnet-core-and-entity-framework-core/
https://www.benday.com/2017/12/19/ef-core-2-0-migrations-without-hard-coded-connection-strings/

View file

@ -0,0 +1,55 @@
proposed extensible schema to generalize beyond audible
problems
0) reeks of premature optimization
- i'm currently only doing audible audiobooks. this adds several layers of abstraction for the sake of possible expansion
- there's a good chance that supporting another platform may not conform to this schema, in which case i'd have done this for nothing. genres are one likely pain point
- libation is currently single-user. hopefully the below would suffice for adding users, but if i'm wrong it might be all pain and no gain
1) very thorough == very complex
2) there are some books which would still be difficult to taxonimize
- joy of cooking. has become more of a brand
- the bible. has different versions that aren't just editions
- dictionary. authored by a publisher
3) "books" vs "editions" is a confusing problem waiting to happen
[AIPK=auto increm PK]
(libation) users [AIPK id, name, join date]
audible users [AIPK id, AUDIBLE-PK username]
libation audible users [PK user id, PK audible user id -- cluster PK across all FKs]
- potential danger in multi-user environment. wouldn't want one libation user getting access to a different libation user's audible info
contributors [AIPK id, name]. prev people. incl publishers
audible authors [PK/FK contributor id, AUDIBLE-PK author id]
roles [AIPK id, name]. seeded: author, narrator, publisher. could expand (eg: translator, editor) without each needing a new table
books [AIPK id, title, desc]
book contributors [FK book id, FK contributor id, FK role id, order -- cluster PK across all FKs]
- likely only authors
editions [AIPK id, FK book id, title]. could expand to include year, is first edition, is abridged
- reasons for optional different title: "Ender's Game: Special 20th Anniversary Edition", "Harry Potter and the Sorcerer's Stone" vs "Harry Potter and the Philosopher's Stone" vs "Harry Potter y la piedra filosofal", "Midnight Riot" vs "Rivers of London"
edition contributors [FK edition id, FK contributor id, FK role id, order -- cluster PK across all FKs]
- likely everything except authors. eg narrators, publisher
audiobooks [PK/FK edition id, lengthInMinutes]
- could expand to other formats by adding other similar tables. eg: print with #pages and isbn, ebook with mb
audible origins [AIPK id, name]. seeded: library. detail. json. series
audible books [PK/FK edition id, AUDIBLE-PK product id, picture id, sku, 3 ratings, audible category id, audible origin id]
- could expand to other vendors by adding other similar tables
audible user ratings [PK/FK edition id, audible user id, 3 ratings]
audible supplements [AIPK id, FK edition id, download url]
- pdfs only. although book download info could be the same format, they're substantially different and subject to change
audible book downloads [PK/FK edition id, audible user id, bookdownloadlink]
pictures [AIPK id, FK edition id, filename (xyz.jpg -- not incl path)]
audible categories [AIPK id, AUDIBLE-PK category id, name, parent]. may only nest 1 deep
(libation) library [FK libation user id, FK edition id, date added -- cluster PK across all FKs]
(libation) user defined [FK libation user id, FK edition id, tagsRaw (, notes...) -- cluster PK across all FKs]
- there's no reason to restrict tags to library items, so don't combine/link this table with library
series [AIPK id, name]
audible series [FK series id, AUDIBLE-PK series id/asin, audible origin id]
- could also include a 'name' field for what audible calls this series
series books [FK series id, FK book id (NOT edition id), index -- cluster PK across all FKs]
- "index" not "order". display this number; don't just put in this sequence
- index is float instead of int to allow for in-between books. eg 2.5
- if only using "editions" (ie: getting rid of the "books" table), to show 2 editions as the same book in a series, give them the same index
(libation) user shelves [AIPK id, FK libation user id, name, desc]
- custom shelf. similar to library but very different in philosophy. likely different in evolving details
(libation) shelf books [AIPK id, FK user shelf id, date added, order]
- technically, it's no violation to list a book more than once so use AIPK

View file

@ -0,0 +1,76 @@
ignore for now:
authorProperties [PK/FK contributor id, AUDIBLE-PK author id]
notes in Contributor.cs for later refactoring
c# enum only, not their own tables:
roles [AIPK id, name]. seeded: author, narrator, publisher. could expand (eg: translator, editor) without each needing a new table
origins [AIPK id, name]. seeded: library. detail. json. series
-- begin SCHEMA ---------------------------------------------------------------------------------------------------------------------
any audible keys should be indexed
SCHEMA
======
contributors [AIPK id, name]. people and publishers
books [AIPK id, AUDIBLE-PK product id, title, desc, lengthInMinutes, picture id, 3 ratings, category id, origin id]
- product instances. each edition and version is discrete: unique and disconnected from different editions of the same book
- on book re-import
update:
update book origin and series origin with the new source type
overwrite simple fields
invoke complex contributor updates
details page gets
un/abridged
release date
language
publisher
series info incl name
categories
if new == series: ignore. do update series info. do not update book info
else if old == json: update (incl if new == json)
else if old == library && new == detail: update
else: ignore
book contributors [FK book id, FK contributor id, FK role id, order -- cluster PK across all FKs]
supplements [AIPK id, FK book id, download url]
categories [AIPK id, AUDIBLE-PK category id, name, parent]. may only nest 1 deep
user defined [PK/FK book id, 3 ratings, tagsRaw]
series [AIPK id, AUDIBLE-PK series id/asin, name, origin id]
series books [FK series id, FK book id, index -- cluster PK across all FKs]
- "index" not "order". display this number; don't just put in this sequence
- index is float instead of int to allow for in-between books. eg 2.5
- to show 2 editions as the same book in a series, give them the same index
- re-import using series page, there will need to be a re-eval of import logic
library [PK/FK book id, date added, bookdownloadlink]
-- end SCHEMA ---------------------------------------------------------------------------------------------------------------------
-- begin SIMPLIFIED DDD ---------------------------------------------------------------------------------------------------------------------
combine domain and persistence (C(r)UD). no repository pattern. encapsulated in domain objects; direct calls to EF Core
https://www.thereformedprogrammer.net/creating-domain-driven-design-entity-classes-with-entity-framework-core/
// pattern for x-to-many
public void AddReview(int numStars, DbContext context = null)
{
if (_reviews != null) _reviews.Add(new Review(numStars));
else if (context == null) throw new Exception("need context");
else if (context.Entry(this).IsKeySet) context.Add(new Review(numStars, BookId));
else throw new Exception("Could not add");
}
// pattern for optional one-to-one
MyPropClass MyProps { get; private set; }
public void AddMyProps(string s, int i, DbContext context = null)
{
// avoid a trip to the db
if (MyProps != null) { MyProps.Update(s, i); return; }
if (BookId == 0) { MyProps = new MyPropClass(s, i); return; }
if (context == null) throw new Exception("need context");
// per Jon P Smith, this single trip to db loads the property if there is one
// note: .Reference() is for single object references. for collections use .Collection()
context.Entry(this).Reference(s => s.MyProps).Load();
if (MyProps != null) MyProps.Update(s, i);
else MyProps = new MyPropClass(s, i);
}
repository reads are 'query object'-like extension methods
https://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework-core/#1-query-objects-a-way-to-isolate-and-hide-database-read-code
-- and SIMPLIFIED DDD ---------------------------------------------------------------------------------------------------------------------

View file

@ -0,0 +1,8 @@
{
"ConnectionStrings": {
"LibationContext": "Server=(LocalDb)\\MSSQLLocalDB;Database=DataLayer.LibationContext;Integrated Security=true;",
"// on windows sqlite paths accept windows and/or unix slashes": "",
"MyTestContext": "Data Source=%DESKTOP%/sample.db"
}
}