Basics setup, going to change how repos work

This commit is contained in:
2022-07-14 18:12:04 -04:00
commit 4c42b765e1
64 changed files with 2385 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
using System.Reflection;
using DBConnection.Interfaces;
using DBConnection.ModelBuilders;
using DBConnection.Models;
using DBConnection.Seeders;
using Microsoft.EntityFrameworkCore;
namespace DBConnection;
public class AppDbContext : DbContext
{
public DbSet<Novel> Novels { get; set; }
public DbSet<Chapter> Chapters { get; set; }
public DbSet<Author> Authors { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<UserNovel> UserNovels { get; set; }
private readonly IEnumerable<ISeeder> _seeders =
from t in Assembly.GetExecutingAssembly().GetTypes()
where t.IsClass && t.Namespace.Contains(nameof(DBConnection.Seeders)) && typeof(ISeeder).IsAssignableFrom(t)
select (ISeeder) Activator.CreateInstance(t);
private static readonly IEnumerable<IModelBuilder> ModelBuilders =
from t in Assembly.GetExecutingAssembly().GetTypes()
where t.IsClass && t.Namespace.Contains(nameof(DBConnection.ModelBuilders)) && typeof(IModelBuilder).IsAssignableFrom(t)
select (IModelBuilder) Activator.CreateInstance(t);
public AppDbContext(DbContextOptions options) : base(options)
{
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
{
UpdateAuditInfo();
return base.SaveChangesAsync(cancellationToken);
}
private void UpdateAuditInfo()
{
var entries = ChangeTracker.Entries().Where(x =>
x.Entity is BaseEntity && (x.State == EntityState.Added || x.State == EntityState.Modified));
foreach(var entry in entries) {
if (entry.State == EntityState.Added)
{
((BaseEntity)entry.Entity).DateCreated = DateTime.Now;
}
((BaseEntity)entry.Entity).DateModified = DateTime.Now;
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
foreach (var builder in ModelBuilders)
{
builder.BuildModel(modelBuilder);
}
foreach (var seeder in _seeders)
{
seeder.SeedData(modelBuilder);
}
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
}
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.7" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" />
</ItemGroup>
<ItemGroup>
<Folder Include="Interfaces" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,9 @@
namespace DBConnection.Enums;
public enum NovelStatus
{
InProgress,
Completed,
Hiatus,
Unknown
}

View File

@@ -0,0 +1,40 @@
using System.Reflection;
using DBConnection.Repositories.Interfaces;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace DBConnection.Extensions;
public static class BuilderExtensions
{
/// <summary>
/// Adds dbcontext and all repositories in repository namespace
/// </summary>
/// <param name="collection">service collection</param>
/// <param name="config">configuration</param>
public static void AddDbServices(this IServiceCollection collection, IConfiguration config)
{
string dbConnectionString = config.GetConnectionString("DefaultConnection");
collection.AddDbContext<AppDbContext>(opt =>
{
opt.UseNpgsql(dbConnectionString);
});
Type[] repositories = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => t.IsClass && !t.IsAbstract && t.Namespace.Contains(nameof(DBConnection.Repositories)) && typeof(IRepository).IsAssignableFrom(t)).ToArray();
foreach (var repo in repositories)
{
var repoInterface = repo.GetInterfaces()
.FirstOrDefault(repoInterface => typeof(IRepository).IsAssignableFrom(repoInterface) && repoInterface != typeof(IRepository));
if (repoInterface != null)
{
collection.AddScoped(repoInterface, repo);
}
else
{
collection.AddScoped(repo);
}
}
}
}

View File

@@ -0,0 +1,11 @@
using Microsoft.EntityFrameworkCore;
namespace DBConnection.Interfaces;
/// <summary>
/// ModelBuilders are used for customizing DB models with properties not settable via annotations
/// </summary>
public interface IModelBuilder
{
public ModelBuilder BuildModel(ModelBuilder builder);
}

View File

@@ -0,0 +1,15 @@
using DBConnection.Interfaces;
using DBConnection.Models;
using Microsoft.EntityFrameworkCore;
namespace DBConnection.ModelBuilders;
public class UserNovelBuilder : IModelBuilder
{
public ModelBuilder BuildModel(ModelBuilder builder)
{
var userNovelModelBuilder = builder.Entity<UserNovel>();
userNovelModelBuilder.HasKey(un => new {un.NovelUrl, un.UserId});
return builder;
}
}

View File

@@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace DBConnection.Models;
public class Author : BaseEntity
{
[Key]
public string Url { get; set; }
public string Name { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace DBConnection.Models;
public abstract class BaseEntity
{
public DateTime DateCreated { get; set; }
public DateTime DateModified { get; set; }
}

View File

@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
namespace DBConnection.Models;
public class Chapter : BaseEntity
{
[Key]
public int ChapterNumber { get; set; }
public string Name { get; set; }
public string? Content { get; set; }
public string? RawContent { get; set; }
public string Url { get; set; }
public DateTime DatePosted { get; set; }
public DateTime DateUpdated { get; set; }
}

View File

@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
namespace DBConnection.Models;
public class Novel : BaseEntity
{
[Key]
public string Url { get; set; }
public string Title { get; set; }
public Author Author { get; set; }
public List<Tag> Tags { get; set; }
public List<Chapter> Chapters { get; set; }
public DateTime LastUpdated { get; set; }
public DateTime DatePosted { get; set; }
}

View File

@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
namespace DBConnection.Models;
public class Tag : BaseEntity
{
[Key]
public string TagValue { get; set; }
[JsonIgnore]
public List<Novel> Novels { get; set; }
}

View File

@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace DBConnection.Models;
public class User : BaseEntity
{
[Key]
public int Id { get; set; }
public string Email { get; set; }
public List<UserNovel> WatchedNovels { get; set; }
}

View File

@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
namespace DBConnection.Models;
public class UserNovel
{
[JsonIgnore]
public int UserId { get; set; }
public string NovelUrl { get; set; }
public Novel Novel { get; set; }
public User User { get; set; }
public int LastChapterRead { get; set; }
}

View File

@@ -0,0 +1,59 @@
using System.Reflection;
using DBConnection.Models;
using DBConnection.Repositories.Interfaces;
using Microsoft.EntityFrameworkCore;
using NuGet.Configuration;
namespace DBConnection.Repositories;
public abstract class BaseRepository<TEntityType> : IRepository<TEntityType> where TEntityType : BaseEntity
{
protected readonly AppDbContext DbContext;
private object?[]? GetPrimaryKey(TEntityType entity)
{
var keyProperties = DbContext.Model.FindEntityType(typeof(TEntityType))?.FindPrimaryKey()?.Properties.Select(p => p.Name);
return keyProperties?.Select(p => entity.GetType().GetProperty(p)?.GetValue(entity, null)).ToArray();
}
protected abstract IQueryable<TEntityType> GetAllIncludedQueryable();
public BaseRepository(AppDbContext dbContext)
{
DbContext = dbContext;
}
public virtual TEntityType Delete(TEntityType entity)
{
DbContext.Set<TEntityType>().Remove(entity);
return entity;
}
public virtual async Task<TEntityType> Upsert(TEntityType entity)
{
bool exists = await DbContext.Set<TEntityType>().ContainsAsync(entity);
if (!exists)
{
DbContext.Set<TEntityType>().Add(entity);
}
else
{
var dbEntry = await Get(entity);
DbContext.Entry(dbEntry).CurrentValues.SetValues(entity);
}
await DbContext.SaveChangesAsync();
return entity;
}
public virtual async Task<TEntityType?> Get(TEntityType entity)
{
var keyValues = GetPrimaryKey(entity);
return await Get(keyValues);
}
public virtual async Task<TEntityType?> Get(params object?[]? keyValues)
{
return await DbContext.Set<TEntityType>().Include(j => j.DateCreated).FindAsync(keyValues);
}
}

View File

@@ -0,0 +1,8 @@
using DBConnection.Models;
namespace DBConnection.Repositories.Interfaces;
public interface INovelRepository : IRepository<Novel>
{
}

View File

@@ -0,0 +1,14 @@
namespace DBConnection.Repositories.Interfaces;
public interface IRepository
{
}
public interface IRepository<TEntityType> : IRepository where TEntityType : class
{
TEntityType Delete(TEntityType entity);
Task<TEntityType> Upsert(TEntityType entity);
Task<TEntityType?> Get(TEntityType entity);
Task<TEntityType?> Get(params object?[]? keyValues);
}

View File

@@ -0,0 +1,22 @@
using DBConnection.Models;
using DBConnection.Repositories.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace DBConnection.Repositories;
public class NovelRepository : BaseRepository<Novel>, INovelRepository
{
public NovelRepository(AppDbContext dbContext) : base(dbContext)
{
}
protected override IQueryable<Novel> GetAllIncludedQueryable()
{
return DbContext.Novels
.Include(i => i.Author)
.Include(i => i.Chapters)
.Include(i => i.Tags);
}
}

View File

@@ -0,0 +1,12 @@
using Microsoft.EntityFrameworkCore;
namespace DBConnection.Seeders;
/// <summary>
/// Used for seeding the database with pre-existing data
/// </summary>
public interface ISeeder
{
public static string SeedDataDirectory = @"Data/SeedData/";
public ModelBuilder SeedData(ModelBuilder builder);
}