diff --git a/.gitignore b/.gitignore index 48cc008..2d8d5ff 100644 --- a/.gitignore +++ b/.gitignore @@ -452,3 +452,5 @@ $RECYCLE.BIN/ !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json + +hackernet.db* \ No newline at end of file diff --git a/Apps/HackerNet.Api/Program.cs b/Apps/HackerNet.Api/Program.cs index c6ed57b..fc9a031 100644 --- a/Apps/HackerNet.Api/Program.cs +++ b/Apps/HackerNet.Api/Program.cs @@ -2,7 +2,7 @@ using HackerNet.Infrastructure.AspNet; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddHackerNetServices(); +builder.Services.AddHackerNetServicesEntityFramework(); builder.Services.AddControllers(); builder.Services.AddOpenApiDocument(d => { @@ -11,6 +11,8 @@ builder.Services.AddOpenApiDocument(d => var app = builder.Build(); +app.MigrateDatabase(); + app.UseOpenApi(); app.UseSwaggerUi3(); diff --git a/Apps/HackerNet.Web/Program.cs b/Apps/HackerNet.Web/Program.cs index e89e6a4..5edd5e3 100644 --- a/Apps/HackerNet.Web/Program.cs +++ b/Apps/HackerNet.Web/Program.cs @@ -5,7 +5,8 @@ var builder = WebApplication.CreateBuilder(args); // Add services to the container. //ServiceCollectionExtensions.AddHackerNetServices(builder.Services); builder.Services - .AddHackerNetServices() + //.AddHackerNetServicesMemory() + .AddHackerNetServicesEntityFramework() .AddControllersWithViews(o => { //o.Filters.Add(); @@ -13,6 +14,8 @@ builder.Services var app = builder.Build(); +app.MigrateDatabase(); + // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { diff --git a/HackerNet.Infrastructure/AspNet/Extensions.cs b/HackerNet.Infrastructure/AspNet/Extensions.cs new file mode 100644 index 0000000..0cb7e98 --- /dev/null +++ b/HackerNet.Infrastructure/AspNet/Extensions.cs @@ -0,0 +1,49 @@ +using HackerNet.Application; +using HackerNet.Domain; +using HackerNet.Infrastructure.Repositories.EntityFramework; +using HackerNet.Infrastructure.Repositories.Memory; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace HackerNet.Infrastructure.AspNet; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddHackerNetServicesMemory(this IServiceCollection services) + { + var link = new Link("https://localhost:7050/", "Youhouuu"); + var comment = new Comment(link.Id, "Wow!"); + var linksRepository = new MemoryLinkRepository(link); + var commentsRepository = new MemoryCommentRepository(comment); + + services.AddSingleton(linksRepository); + services.AddSingleton(commentsRepository); + services.AddSingleton(new MemoryReadStore(linksRepository, commentsRepository)); + + services.AddSingleton(); + + return services; + } + + public static IServiceCollection AddHackerNetServicesEntityFramework(this IServiceCollection services) + { + services.AddDbContext(options + => options.UseSqlite("Data Source=hackernet.db")); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + + return services; + } + + public static void MigrateDatabase(this IApplicationBuilder app) + { + using var scope = app.ApplicationServices.CreateScope(); + using var ctx = scope.ServiceProvider.GetRequiredService(); + + ctx.Database.Migrate(); + } +} \ No newline at end of file diff --git a/HackerNet.Infrastructure/AspNet/ServiceCollectionExtensions.cs b/HackerNet.Infrastructure/AspNet/ServiceCollectionExtensions.cs deleted file mode 100644 index a6bf8de..0000000 --- a/HackerNet.Infrastructure/AspNet/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -using HackerNet.Application; -using HackerNet.Domain; -using HackerNet.Infrastructure.Repositories.Memory; -using Microsoft.Extensions.DependencyInjection; - -namespace HackerNet.Infrastructure.AspNet; - -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddHackerNetServices(this IServiceCollection services) - { - var link = new Link("https://localhost:7050/", "Youhouuu"); - var comment = new Comment(link.Id, "Wow!"); - var linksRepository = new MemoryLinkRepository(link); - var commentsRepository = new MemoryCommentRepository(comment); - - services.AddSingleton(linksRepository); - services.AddSingleton(commentsRepository); - services.AddSingleton(new MemoryReadStore(linksRepository, commentsRepository)); - - services.AddSingleton(); - - return services; - } -} \ No newline at end of file diff --git a/HackerNet.Infrastructure/HackerNet.Infrastructure.csproj b/HackerNet.Infrastructure/HackerNet.Infrastructure.csproj index 64e55bc..6d05fe8 100644 --- a/HackerNet.Infrastructure/HackerNet.Infrastructure.csproj +++ b/HackerNet.Infrastructure/HackerNet.Infrastructure.csproj @@ -5,6 +5,14 @@ + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + net6.0 enable diff --git a/HackerNet.Infrastructure/Migrations/20211215082223_CreateLinkAndComment.Designer.cs b/HackerNet.Infrastructure/Migrations/20211215082223_CreateLinkAndComment.Designer.cs new file mode 100644 index 0000000..af6073f --- /dev/null +++ b/HackerNet.Infrastructure/Migrations/20211215082223_CreateLinkAndComment.Designer.cs @@ -0,0 +1,79 @@ +// +using System; +using HackerNet.Infrastructure.Repositories.EntityFramework; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace HackerNet.Infrastructure.Migrations +{ + [DbContext(typeof(HackerContext))] + [Migration("20211215082223_CreateLinkAndComment")] + partial class CreateLinkAndComment + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); + + modelBuilder.Entity("HackerNet.Domain.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("LinkId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LinkId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("HackerNet.Domain.Link", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Links"); + }); + + modelBuilder.Entity("HackerNet.Domain.Comment", b => + { + b.HasOne("HackerNet.Domain.Link", null) + .WithMany() + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/HackerNet.Infrastructure/Migrations/20211215082223_CreateLinkAndComment.cs b/HackerNet.Infrastructure/Migrations/20211215082223_CreateLinkAndComment.cs new file mode 100644 index 0000000..2858f89 --- /dev/null +++ b/HackerNet.Infrastructure/Migrations/20211215082223_CreateLinkAndComment.cs @@ -0,0 +1,61 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace HackerNet.Infrastructure.Migrations +{ + public partial class CreateLinkAndComment : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Links", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Url = table.Column(type: "TEXT", maxLength: 250, nullable: false), + Description = table.Column(type: "TEXT", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Links", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Comments", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + LinkId = table.Column(type: "TEXT", nullable: false), + Content = table.Column(type: "TEXT", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Comments", x => x.Id); + table.ForeignKey( + name: "FK_Comments_Links_LinkId", + column: x => x.LinkId, + principalTable: "Links", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Comments_LinkId", + table: "Comments", + column: "LinkId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Comments"); + + migrationBuilder.DropTable( + name: "Links"); + } + } +} diff --git a/HackerNet.Infrastructure/Migrations/HackerContextModelSnapshot.cs b/HackerNet.Infrastructure/Migrations/HackerContextModelSnapshot.cs new file mode 100644 index 0000000..ffe531b --- /dev/null +++ b/HackerNet.Infrastructure/Migrations/HackerContextModelSnapshot.cs @@ -0,0 +1,77 @@ +// +using System; +using HackerNet.Infrastructure.Repositories.EntityFramework; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace HackerNet.Infrastructure.Migrations +{ + [DbContext(typeof(HackerContext))] + partial class HackerContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); + + modelBuilder.Entity("HackerNet.Domain.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("LinkId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LinkId"); + + b.ToTable("Comments", (string)null); + }); + + modelBuilder.Entity("HackerNet.Domain.Link", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Links", (string)null); + }); + + modelBuilder.Entity("HackerNet.Domain.Comment", b => + { + b.HasOne("HackerNet.Domain.Link", null) + .WithMany() + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/HackerNet.Infrastructure/Repositories/EntityFramework/EFCommentRepository.cs b/HackerNet.Infrastructure/Repositories/EntityFramework/EFCommentRepository.cs new file mode 100644 index 0000000..f5059d7 --- /dev/null +++ b/HackerNet.Infrastructure/Repositories/EntityFramework/EFCommentRepository.cs @@ -0,0 +1,19 @@ +using HackerNet.Domain; + +namespace HackerNet.Infrastructure.Repositories.EntityFramework; + +public class EFCommentRepository : ICommentRepository +{ + private readonly HackerContext _context; + + public EFCommentRepository(HackerContext context) + { + _context = context; + } + + public void Add(Comment comment) + { + _context.Comments.Add(comment); + _context.SaveChanges(); + } +} \ No newline at end of file diff --git a/HackerNet.Infrastructure/Repositories/EntityFramework/EFLinkRepository.cs b/HackerNet.Infrastructure/Repositories/EntityFramework/EFLinkRepository.cs new file mode 100644 index 0000000..c9cddb3 --- /dev/null +++ b/HackerNet.Infrastructure/Repositories/EntityFramework/EFLinkRepository.cs @@ -0,0 +1,19 @@ +using HackerNet.Domain; + +namespace HackerNet.Infrastructure.Repositories.EntityFramework; + +public class EFLinkRepository : ILinkRepository +{ + private readonly HackerContext _context; + + public EFLinkRepository(HackerContext context) + { + _context = context; + } + + public void Add(Link link) + { + _context.Links.Add(link); + _context.SaveChanges(); + } +} \ No newline at end of file diff --git a/HackerNet.Infrastructure/Repositories/EntityFramework/EFReadStore.cs b/HackerNet.Infrastructure/Repositories/EntityFramework/EFReadStore.cs new file mode 100644 index 0000000..2b3fa09 --- /dev/null +++ b/HackerNet.Infrastructure/Repositories/EntityFramework/EFReadStore.cs @@ -0,0 +1,42 @@ +using HackerNet.Application; + +namespace HackerNet.Infrastructure.Repositories.EntityFramework; + +public class EFReadStore : IReadStore +{ + private readonly HackerContext _context; + + public EFReadStore(HackerContext context) + { + _context = context; + } + + public LinkComment[] GetLinkComments(Guid linkId) + => _context.Comments + .OrderByDescending(c => c.CreatedAt) + .Select(c => new LinkComment + { + Content = c.Content + }).ToArray(); + + public LinkHomePage GetLinkDetail(Guid id) + => GetLinks(id).Single(); + + public LinkHomePage[] GetPublishedLinks() + => GetLinks().ToArray(); + + private IQueryable GetLinks(Guid? id = null) + { + return _context.Links + .Where(l => !id.HasValue || l.Id == id) + .OrderByDescending(l => l.CreatedAt) + .Select(l => new LinkHomePage + { + Id = l.Id, + Url = l.Url, + Description = l.Description, + CommentsCount = _context.Comments + .Count(c => c.LinkId == l.Id), + }); + } +} \ No newline at end of file diff --git a/HackerNet.Infrastructure/Repositories/EntityFramework/EntityTypeConfigurations/CommentEntityTypeConfiguration.cs b/HackerNet.Infrastructure/Repositories/EntityFramework/EntityTypeConfigurations/CommentEntityTypeConfiguration.cs new file mode 100644 index 0000000..7792ce6 --- /dev/null +++ b/HackerNet.Infrastructure/Repositories/EntityFramework/EntityTypeConfigurations/CommentEntityTypeConfiguration.cs @@ -0,0 +1,20 @@ +using HackerNet.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace HackerNet.Infrastructure.Repositories.EntityFramework.EntityTypeConfigurations; + +public class CommentEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(o => o.Id); + builder.HasOne() + .WithMany() + .HasForeignKey(o => o.LinkId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + builder.Property(o => o.Content).IsRequired(); + builder.Property(o => o.CreatedAt).IsRequired(); + } +} \ No newline at end of file diff --git a/HackerNet.Infrastructure/Repositories/EntityFramework/EntityTypeConfigurations/LinkEntityTypeConfiguration.cs b/HackerNet.Infrastructure/Repositories/EntityFramework/EntityTypeConfigurations/LinkEntityTypeConfiguration.cs new file mode 100644 index 0000000..8c26369 --- /dev/null +++ b/HackerNet.Infrastructure/Repositories/EntityFramework/EntityTypeConfigurations/LinkEntityTypeConfiguration.cs @@ -0,0 +1,16 @@ +using HackerNet.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace HackerNet.Infrastructure.Repositories.EntityFramework.EntityTypeConfigurations; + +public class LinkEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(o => o.Id); + builder.Property(o => o.Url).IsRequired().HasMaxLength(250); + builder.Property(o => o.Description).IsRequired(); + builder.Property(o => o.CreatedAt).IsRequired(); + } +} \ No newline at end of file diff --git a/HackerNet.Infrastructure/Repositories/EntityFramework/HackerContext.cs b/HackerNet.Infrastructure/Repositories/EntityFramework/HackerContext.cs new file mode 100644 index 0000000..ee8f0de --- /dev/null +++ b/HackerNet.Infrastructure/Repositories/EntityFramework/HackerContext.cs @@ -0,0 +1,58 @@ +using HackerNet.Application; +using HackerNet.Domain; +using HackerNet.Infrastructure.Repositories.EntityFramework.EntityTypeConfigurations; +using Microsoft.EntityFrameworkCore; + +namespace HackerNet.Infrastructure.Repositories.EntityFramework; + +public class HackerContext : DbContext +{ +#pragma warning disable CS8618 + + public DbSet Links { get; set; } + public DbSet Comments { get; set; } + +#pragma warning restore CS8618 + + public HackerContext() : base() + { + + } + + public HackerContext(DbContextOptions options) : base(options) + { + + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (!optionsBuilder.IsConfigured) + { + optionsBuilder.UseSqlite("Data Source=:memory:"); + } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // var link = modelBuilder.Entity(); + // link.HasKey(o => o.Id); + // link.Property(o => o.Url).IsRequired().HasMaxLength(250); + // link.Property(o => o.Description).IsRequired(); + // link.Property(o => o.CreatedAt).IsRequired(); + + // var comment = modelBuilder.Entity(); + // comment.HasKey(o => o.Id); + // comment.HasOne() + // .WithMany() + // .HasForeignKey(o => o.LinkId) + // .IsRequired() + // .OnDelete(DeleteBehavior.Cascade); + // comment.Property(o => o.Content).IsRequired(); + // comment.Property(o => o.CreatedAt).IsRequired(); + + //modelBuilder.ApplyConfiguration(new LinkEntityTypeConfiguration()); + //modelBuilder.ApplyConfiguration(new CommentEntityTypeConfiguration()); + + modelBuilder.ApplyConfigurationsFromAssembly(typeof(HackerContext).Assembly); + } +} \ No newline at end of file