feat(dir): migrate QueryDbContext

This commit is contained in:
Collin Barrett 2024-05-30 17:18:27 -05:00
parent d87473bfd6
commit 2dbbdd981e
21 changed files with 539 additions and 0 deletions

View file

@ -0,0 +1,14 @@
using FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
public interface IQueryContext
{
IQueryable<FilterList> FilterLists { get; }
IQueryable<Language> Languages { get; }
IQueryable<License> Licenses { get; }
IQueryable<Maintainer> Maintainers { get; }
IQueryable<Software> Software { get; }
IQueryable<Syntax> Syntaxes { get; }
IQueryable<Tag> Tags { get; }
}

View file

@ -0,0 +1,25 @@
using FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
using Microsoft.EntityFrameworkCore;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
internal sealed class QueryContext(QueryDbContext dbContext) : IQueryContext, IAsyncDisposable, IDisposable
{
public ValueTask DisposeAsync()
{
return dbContext.DisposeAsync();
}
void IDisposable.Dispose()
{
dbContext.Dispose();
}
public IQueryable<FilterList> FilterLists => dbContext.FilterLists.AsNoTracking();
public IQueryable<Language> Languages => dbContext.Languages.AsNoTracking();
public IQueryable<License> Licenses => dbContext.Licenses.AsNoTracking();
public IQueryable<Maintainer> Maintainers => dbContext.Maintainers.AsNoTracking();
public IQueryable<Software> Software => dbContext.Software.AsNoTracking();
public IQueryable<Syntax> Syntaxes => dbContext.Syntaxes.AsNoTracking();
public IQueryable<Tag> Tags => dbContext.Tags.AsNoTracking();
}

View file

@ -0,0 +1,34 @@
using FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
using Microsoft.EntityFrameworkCore;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
public class QueryDbContext(DbContextOptions<QueryDbContext> options) : DbContext(options)
{
public DbSet<FilterList> FilterLists => Set<FilterList>();
public DbSet<Language> Languages => Set<Language>();
public DbSet<License> Licenses => Set<License>();
public DbSet<Maintainer> Maintainers => Set<Maintainer>();
public DbSet<Software> Software => Set<Software>();
public DbSet<Syntax> Syntaxes => Set<Syntax>();
public DbSet<Tag> Tags => Set<Tag>();
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
throw new InvalidOperationException("This context is read-only.");
}
public override Task<int> SaveChangesAsync(
bool acceptAllChangesOnSuccess,
CancellationToken cancellationToken = default)
{
throw new InvalidOperationException("This context is read-only.");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(
GetType().Assembly,
type => type.Namespace == typeof(FilterListTypeConfiguration).Namespace);
}
}

View file

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
public record Dependent
{
public long DependencyFilterListId { get; init; }
public FilterList DependencyFilterList { get; init; } = default!;
public long DependentFilterListId { get; init; }
public FilterList DependentFilterList { get; init; } = default!;
}
internal class DependentTypeConfiguration : IEntityTypeConfiguration<Dependent>
{
public virtual void Configure(EntityTypeBuilder<Dependent> builder)
{
builder.HasKey(d => new { d.DependencyFilterListId, d.DependentFilterListId });
builder.HasOne(d => d.DependencyFilterList)
.WithMany(fl => fl.DependentFilterLists)
.HasForeignKey(d => d.DependencyFilterListId);
builder.HasOne(d => d.DependentFilterList)
.WithMany(fl => fl.DependencyFilterLists)
.HasForeignKey(d => d.DependentFilterListId);
builder.HasDataJsonFile<Dependent>();
}
}

View file

@ -0,0 +1,6 @@
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
public abstract record Entity
{
public long Id { get; init; }
}

View file

@ -0,0 +1,59 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
public record FilterList : Entity
{
public string Name { get; init; } = default!;
public string? Description { get; init; }
public long LicenseId { get; init; }
public License License { get; init; } = default!;
public IEnumerable<FilterListSyntax> FilterListSyntaxes { get; init; } = new HashSet<FilterListSyntax>();
public IEnumerable<FilterListLanguage> FilterListLanguages { get; init; } = new HashSet<FilterListLanguage>();
public IEnumerable<FilterListTag> FilterListTags { get; init; } = new HashSet<FilterListTag>();
public IEnumerable<FilterListViewUrl> ViewUrls { get; init; } = new HashSet<FilterListViewUrl>();
public Uri? HomeUrl { get; init; }
public Uri? OnionUrl { get; init; }
public Uri? PolicyUrl { get; init; }
public Uri? SubmissionUrl { get; init; }
public Uri? IssuesUrl { get; init; }
public Uri? ForumUrl { get; init; }
public Uri? ChatUrl { get; init; }
public string? EmailAddress { get; init; }
public Uri? DonateUrl { get; init; }
public IEnumerable<FilterListMaintainer> FilterListMaintainers { get; init; } = new HashSet<FilterListMaintainer>();
public IEnumerable<Fork> UpstreamFilterLists { get; init; } = new HashSet<Fork>();
public IEnumerable<Fork> ForkFilterLists { get; init; } = new HashSet<Fork>();
public IEnumerable<Merge> IncludedInFilterLists { get; init; } = new HashSet<Merge>();
public IEnumerable<Merge> IncludesFilterLists { get; init; } = new HashSet<Merge>();
public IEnumerable<Dependent> DependencyFilterLists { get; init; } = new HashSet<Dependent>();
public IEnumerable<Dependent> DependentFilterLists { get; init; } = new HashSet<Dependent>();
}
internal class FilterListTypeConfiguration : IEntityTypeConfiguration<FilterList>
{
public virtual void Configure(EntityTypeBuilder<FilterList> builder)
{
builder.HasIndex(f => f.Name)
.IsUnique();
builder.Property(f => f.LicenseId)
.HasDefaultValue(5);
builder.HasOne(f => f.License)
.WithMany(l => l.FilterLists)
.OnDelete(DeleteBehavior.Restrict);
builder.OwnsMany(
f => f.ViewUrls,
b =>
{
b.Property(u => u.SegmentNumber)
.HasDefaultValue(1);
b.Property(u => u.Primariness)
.HasDefaultValue(1);
b.HasIndex(u => new { u.FilterListId, u.SegmentNumber, u.Primariness })
.IsUnique();
b.HasDataJsonFile<FilterListViewUrl>();
});
builder.HasDataJsonFile<FilterList>();
}
}

View file

@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
public record FilterListLanguage
{
public long FilterListId { get; init; }
public FilterList FilterList { get; init; } = default!;
public long LanguageId { get; init; }
public Language Language { get; init; } = default!;
}
internal class FilterListLanguageTypeConfiguration : IEntityTypeConfiguration<FilterListLanguage>
{
public virtual void Configure(EntityTypeBuilder<FilterListLanguage> builder)
{
builder.HasKey(fll => new { fll.FilterListId, fll.LanguageId });
builder.HasDataJsonFile<FilterListLanguage>();
}
}

View file

@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
public record FilterListMaintainer
{
public long FilterListId { get; init; }
public FilterList FilterList { get; init; } = default!;
public long MaintainerId { get; init; }
public Maintainer Maintainer { get; init; } = default!;
}
internal class FilterListMaintainerTypeConfiguration : IEntityTypeConfiguration<FilterListMaintainer>
{
public virtual void Configure(EntityTypeBuilder<FilterListMaintainer> builder)
{
builder.HasKey(flm => new { flm.FilterListId, flm.MaintainerId });
builder.HasDataJsonFile<FilterListMaintainer>();
}
}

View file

@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
public record FilterListSyntax
{
public long FilterListId { get; init; }
public FilterList FilterList { get; init; } = default!;
public long SyntaxId { get; init; }
public Syntax Syntax { get; init; } = default!;
}
internal class FilterListSyntaxTypeConfiguration : IEntityTypeConfiguration<FilterListSyntax>
{
public virtual void Configure(EntityTypeBuilder<FilterListSyntax> builder)
{
builder.HasKey(fls => new { fls.FilterListId, fls.SyntaxId });
builder.HasDataJsonFile<FilterListSyntax>();
}
}

View file

@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
public record FilterListTag
{
public long FilterListId { get; init; }
public FilterList FilterList { get; init; } = default!;
public long TagId { get; init; }
public Tag Tag { get; init; } = default!;
}
internal class FilterListTagTypeConfiguration : IEntityTypeConfiguration<FilterListTag>
{
public virtual void Configure(EntityTypeBuilder<FilterListTag> builder)
{
builder.HasKey(flt => new { flt.FilterListId, flt.TagId });
builder.HasDataJsonFile<FilterListTag>();
}
}

View file

@ -0,0 +1,10 @@
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
public record FilterListViewUrl : Entity
{
public long FilterListId { get; init; }
public FilterList FilterList { get; init; } = default!;
public short SegmentNumber { get; init; }
public short Primariness { get; init; }
public Uri Url { get; init; } = default!;
}

View file

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
public record Fork
{
public long UpstreamFilterListId { get; init; }
public FilterList UpstreamFilterList { get; init; } = default!;
public long ForkFilterListId { get; init; }
public FilterList ForkFilterList { get; init; } = default!;
}
internal class ForkTypeConfiguration : IEntityTypeConfiguration<Fork>
{
public virtual void Configure(EntityTypeBuilder<Fork> builder)
{
builder.HasKey(f => new { f.UpstreamFilterListId, f.ForkFilterListId });
builder.HasOne(f => f.UpstreamFilterList)
.WithMany(fl => fl.ForkFilterLists)
.HasForeignKey(f => f.UpstreamFilterListId);
builder.HasOne(f => f.ForkFilterList)
.WithMany(fl => fl.UpstreamFilterLists)
.HasForeignKey(f => f.ForkFilterListId);
builder.HasDataJsonFile<Fork>();
}
}

View file

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
public record Language : Entity
{
public string Iso6391 { get; init; } = default!;
public string Name { get; init; } = default!;
public IEnumerable<FilterListLanguage> FilterListLanguages { get; init; } = new HashSet<FilterListLanguage>();
}
internal class LanguageTypeConfiguration : IEntityTypeConfiguration<Language>
{
public virtual void Configure(EntityTypeBuilder<Language> builder)
{
builder.Property(l => l.Iso6391)
.IsFixedLength()
.HasMaxLength(2);
builder.HasIndex(l => l.Iso6391)
.IsUnique();
builder.HasIndex(l => l.Name)
.IsUnique();
builder.HasDataJsonFile<Language>();
}
}

View file

@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
public record License : Entity
{
public string Name { get; init; } = default!;
public Uri? Url { get; init; }
public bool PermitsModification { get; init; }
public bool PermitsDistribution { get; init; }
public bool PermitsCommercialUse { get; init; }
public IEnumerable<FilterList> FilterLists { get; init; } = new HashSet<FilterList>();
}
internal class LicenseTypeConfiguration : IEntityTypeConfiguration<License>
{
public virtual void Configure(EntityTypeBuilder<License> builder)
{
builder.HasIndex(l => l.Name)
.IsUnique();
builder.Property(l => l.PermitsModification)
.HasDefaultValue(false);
builder.Property(l => l.PermitsDistribution)
.HasDefaultValue(false);
builder.Property(l => l.PermitsCommercialUse)
.HasDefaultValue(false);
builder.HasDataJsonFile<License>();
}
}

View file

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
public record Maintainer : Entity
{
public string Name { get; init; } = default!;
public Uri? Url { get; init; }
public string? EmailAddress { get; init; }
public string? TwitterHandle { get; init; }
public IEnumerable<FilterListMaintainer> FilterListMaintainers { get; init; } = new HashSet<FilterListMaintainer>();
}
internal class MaintainerTypeConfiguration : IEntityTypeConfiguration<Maintainer>
{
public virtual void Configure(EntityTypeBuilder<Maintainer> builder)
{
builder.HasIndex(m => m.Name)
.IsUnique();
builder.HasDataJsonFile<Maintainer>();
}
}

View file

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
public record Merge
{
public long IncludedInFilterListId { get; init; }
public FilterList IncludedInFilterList { get; init; } = default!;
public long IncludesFilterListId { get; init; }
public FilterList IncludesFilterList { get; init; } = default!;
}
internal class MergeTypeConfiguration : IEntityTypeConfiguration<Merge>
{
public virtual void Configure(EntityTypeBuilder<Merge> builder)
{
builder.HasKey(m => new { m.IncludedInFilterListId, m.IncludesFilterListId });
builder.HasOne(m => m.IncludedInFilterList)
.WithMany(fl => fl.IncludesFilterLists)
.HasForeignKey(m => m.IncludedInFilterListId);
builder.HasOne(m => m.IncludesFilterList)
.WithMany(fl => fl.IncludedInFilterLists)
.HasForeignKey(m => m.IncludesFilterListId);
builder.HasDataJsonFile<Merge>();
}
}

View file

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
public record Software : Entity
{
public string Name { get; init; } = default!;
public string? Description { get; init; }
public Uri? HomeUrl { get; init; }
public Uri? DownloadUrl { get; init; }
public bool SupportsAbpUrlScheme { get; init; }
public IEnumerable<SoftwareSyntax> SoftwareSyntaxes { get; init; } = new HashSet<SoftwareSyntax>();
}
internal class SoftwareTypeConfiguration : IEntityTypeConfiguration<Software>
{
public virtual void Configure(EntityTypeBuilder<Software> builder)
{
builder.HasIndex(s => s.Name)
.IsUnique();
builder.Property(s => s.SupportsAbpUrlScheme)
.HasDefaultValue(false);
builder.HasDataJsonFile<Software>();
}
}

View file

@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
public record SoftwareSyntax
{
public long SoftwareId { get; init; }
public Software Software { get; init; } = default!;
public long SyntaxId { get; init; }
public Syntax Syntax { get; init; } = default!;
}
internal class SoftwareSyntaxTypeConfiguration : IEntityTypeConfiguration<SoftwareSyntax>
{
public virtual void Configure(EntityTypeBuilder<SoftwareSyntax> builder)
{
builder.HasKey(ss => new { ss.SoftwareId, ss.SyntaxId });
builder.HasDataJsonFile<SoftwareSyntax>();
}
}

View file

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
public record Syntax : Entity
{
public string Name { get; init; } = default!;
public string? Description { get; init; }
public Uri? Url { get; init; }
public IEnumerable<FilterListSyntax> FilterListSyntaxes { get; init; } = new HashSet<FilterListSyntax>();
public IEnumerable<SoftwareSyntax> SoftwareSyntaxes { get; init; } = new HashSet<SoftwareSyntax>();
}
internal class SyntaxTypeConfiguration : IEntityTypeConfiguration<Syntax>
{
public virtual void Configure(EntityTypeBuilder<Syntax> builder)
{
builder.HasIndex(s => s.Name)
.IsUnique();
builder.HasDataJsonFile<Syntax>();
}
}

View file

@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
public record Tag : Entity
{
public string Name { get; init; } = default!;
public string? Description { get; init; }
public IEnumerable<FilterListTag> FilterListTags { get; init; } = new HashSet<FilterListTag>();
}
internal class TagTypeConfiguration : IEntityTypeConfiguration<Tag>
{
public virtual void Configure(EntityTypeBuilder<Tag> builder)
{
builder.HasIndex(t => t.Name)
.IsUnique();
builder.HasDataJsonFile<Tag>();
}
}

View file

@ -0,0 +1,56 @@
using System.Text.Json;
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace FilterLists.Directory.Infrastructure.Persistence;
public static class SeedExtension
{
public static async Task MigrateAsync(this IHost host)
{
using var scope = host.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<QueryDbContext>();
await db.Database.MigrateAsync();
}
}
internal static class SeedConfigurationExtensions
{
private static readonly JsonSerializerOptions CamelCaseJsonSerializerOptions =
new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
public static void HasDataJsonFile<TEntity>(this EntityTypeBuilder entityTypeBuilder) where TEntity : class
{
var entities = Deserialize<TEntity>();
if (entities.Count == 0) return;
entityTypeBuilder.HasData(entities);
}
public static void HasDataJsonFile<TEntity>(this OwnedNavigationBuilder ownedNavigationBuilder)
where TEntity : class
{
var entities = Deserialize<TEntity>();
if (entities.Count == 0) return;
ownedNavigationBuilder.HasData(entities);
}
private static List<TEntity> Deserialize<TEntity>()
{
// uncomment to short-circuit HasData() when adding a migration
//return new List<TEntity>();
var path = Path.Combine("../data", $"{typeof(TEntity).Name}.json");
if (!File.Exists(path)) return [];
var entitiesJson = File.ReadAllText(path);
var entities = JsonSerializer.Deserialize<IEnumerable<TEntity>>(
entitiesJson,
CamelCaseJsonSerializerOptions);
return entities is null ? [] : entities.ToList();
}
}