diff --git a/Application/GetLink/GetLinkQuery.cs b/Application/GetLink/GetLinkQuery.cs index 7abbedf..4ba9746 100644 --- a/Application/GetLink/GetLinkQuery.cs +++ b/Application/GetLink/GetLinkQuery.cs @@ -4,7 +4,7 @@ using MediatR; namespace HN.Application { - public sealed class GetLinkQuery : IRequest + public sealed class GetLinkQuery : IRequest { [Required] public Guid Id { get; set; } diff --git a/Application/GetLink/GetLinkQueryHandler.cs b/Application/GetLink/GetLinkQueryHandler.cs index b1beb34..fa6ab91 100644 --- a/Application/GetLink/GetLinkQueryHandler.cs +++ b/Application/GetLink/GetLinkQueryHandler.cs @@ -2,10 +2,11 @@ using System.Threading; using System.Linq; using System.Threading.Tasks; using MediatR; +using HN.Domain; namespace HN.Application { - public sealed class GetLinkQueryHandler : IRequestHandler + public sealed class GetLinkQueryHandler : IRequestHandler { private readonly IDbContext _context; @@ -14,13 +15,20 @@ namespace HN.Application _context = context; } - public Task Handle(GetLinkQuery request, CancellationToken cancellationToken) + public Task Handle(GetLinkQuery request, CancellationToken cancellationToken) { var result = from link in _context.Links where link.Id == request.Id - select new LinkDTO(link); + select new LinkDto + { + Id = link.Id, + Url = link.Url, + CreatedAt = link.CreatedAt, + UpVotes = link.Votes.Count(v => v.Type == VoteType.Up), + DownVotes = link.Votes.Count(v => v.Type == VoteType.Down) + }; - return Task.FromResult(result.FirstOrDefault()); + return Task.FromResult(result.SingleOrDefault()); } } } \ No newline at end of file diff --git a/Application/ListLinks/LinkDTO.cs b/Application/ListLinks/LinkDTO.cs index c8999cc..fa3bf7c 100644 --- a/Application/ListLinks/LinkDTO.cs +++ b/Application/ListLinks/LinkDTO.cs @@ -1,19 +1,18 @@ using System; -using HN.Domain; namespace HN.Application { - public sealed class LinkDTO + public sealed class LinkDto { public Guid Id { get; set; } public string Url { get; set; } public DateTime CreatedAt { get; set; } + public int UpVotes { get; set; } + public int DownVotes { get; set; } - public LinkDTO(Link link) + public LinkDto() { - Id = link.Id; - Url = link.Url; - CreatedAt = link.CreatedAt; + } } } \ No newline at end of file diff --git a/Application/ListLinks/ListLinksQuery.cs b/Application/ListLinks/ListLinksQuery.cs index 3a15de6..b15a36d 100644 --- a/Application/ListLinks/ListLinksQuery.cs +++ b/Application/ListLinks/ListLinksQuery.cs @@ -2,7 +2,7 @@ using MediatR; namespace HN.Application { - public sealed class ListLinksQuery : IRequest + public sealed class ListLinksQuery : IRequest { } diff --git a/Application/ListLinks/ListLinksQueryHandler.cs b/Application/ListLinks/ListLinksQueryHandler.cs index 6fa867e..5360ef2 100644 --- a/Application/ListLinks/ListLinksQueryHandler.cs +++ b/Application/ListLinks/ListLinksQueryHandler.cs @@ -2,10 +2,39 @@ using System.Threading; using System.Linq; using System.Threading.Tasks; using MediatR; +using HN.Domain; namespace HN.Application { - public sealed class ListLinksQueryHandler : IRequestHandler + // public static class LinkQueriesExtensions + // { + // public static IQueryable AllAsDto(this DbSet links) + // { + // return from link in links + // select new LinkDto + // { + // Id = link.Id, + // Url = link.Url, + // CreatedAt = link.CreatedAt, + // UpVotes = link.Votes.Count(v => v.Type == VoteType.Up), + // DownVotes = link.Votes.Count(v => v.Type == VoteType.Down) + // }; + // } + + // public static IQueryable ToLinkDto(this IQueryable links) + // { + // return links.Select(link => new LinkDto + // { + // Id = link.Id, + // Url = link.Url, + // CreatedAt = link.CreatedAt, + // UpVotes = link.Votes.Count(v => v.Type == VoteType.Up), + // DownVotes = link.Votes.Count(v => v.Type == VoteType.Down) + // }); + // } + // } + + public sealed class ListLinksQueryHandler : IRequestHandler { private readonly IDbContext _context; @@ -14,10 +43,17 @@ namespace HN.Application _context = context; } - public Task Handle(ListLinksQuery request, CancellationToken cancellationToken) + public Task Handle(ListLinksQuery request, CancellationToken cancellationToken) { var links = from link in _context.Links - select new LinkDTO(link); + select new LinkDto + { + Id = link.Id, + Url = link.Url, + CreatedAt = link.CreatedAt, + UpVotes = link.Votes.Count(v => v.Type == VoteType.Up), + DownVotes = link.Votes.Count(v => v.Type == VoteType.Down) + }; return Task.FromResult(links.ToArray()); } diff --git a/Apps/Website/Views/Links/Index.cshtml b/Apps/Website/Views/Links/Index.cshtml index 6123034..ffd5c2c 100644 --- a/Apps/Website/Views/Links/Index.cshtml +++ b/Apps/Website/Views/Links/Index.cshtml @@ -1,4 +1,4 @@ -@model HN.Application.LinkDTO[] +@model HN.Application.LinkDto[] @{ ViewData["Title"] = "Latest Links"; } diff --git a/Apps/Website/Views/Links/Show.cshtml b/Apps/Website/Views/Links/Show.cshtml index b880dd3..fced2aa 100644 --- a/Apps/Website/Views/Links/Show.cshtml +++ b/Apps/Website/Views/Links/Show.cshtml @@ -1,3 +1,3 @@ -@model HN.Application.LinkDTO +@model HN.Application.LinkDto \ No newline at end of file diff --git a/Apps/Website/Views/Links/_LinkItem.cshtml b/Apps/Website/Views/Links/_LinkItem.cshtml index ea83a29..2bd2b3f 100644 --- a/Apps/Website/Views/Links/_LinkItem.cshtml +++ b/Apps/Website/Views/Links/_LinkItem.cshtml @@ -1,3 +1,3 @@ -@model HN.Application.LinkDTO +@model HN.Application.LinkDto -@Model.Url - created at @Model.CreatedAt.ToLocalTime() \ No newline at end of file +@Model.Url - created at @Model.CreatedAt.ToLocalTime() (👍: @Model.UpVotes / 👎: @Model.DownVotes) \ No newline at end of file diff --git a/Apps/Website/appsettings.Development.json b/Apps/Website/appsettings.Development.json index 8983e0f..eaa8762 100644 --- a/Apps/Website/appsettings.Development.json +++ b/Apps/Website/appsettings.Development.json @@ -3,7 +3,8 @@ "LogLevel": { "Default": "Information", "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore": "Information" } } } diff --git a/Domain/Link.cs b/Domain/Link.cs index 64132e3..e17a0e0 100644 --- a/Domain/Link.cs +++ b/Domain/Link.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace HN.Domain { @@ -7,12 +8,15 @@ namespace HN.Domain public Guid Id { get; } public string Url { get; } public DateTime CreatedAt { get; } + private List _votes; + public IReadOnlyList Votes { get { return _votes; } } private Link(string url) { this.Id = Guid.NewGuid(); this.CreatedAt = DateTime.UtcNow; this.Url = url; + this._votes = new List(); } public static Link FromUrl(string url) diff --git a/Domain/Vote.cs b/Domain/Vote.cs new file mode 100644 index 0000000..e48b4ff --- /dev/null +++ b/Domain/Vote.cs @@ -0,0 +1,16 @@ +using System; + +namespace HN.Domain +{ + public sealed class Vote + { + public VoteType Type { get; private set; } + public DateTime CreatedAt { get; private set; } + + public Vote(VoteType type) + { + Type = type; + CreatedAt = DateTime.UtcNow; + } + } +} \ No newline at end of file diff --git a/Domain/VoteType.cs b/Domain/VoteType.cs new file mode 100644 index 0000000..baaec2a --- /dev/null +++ b/Domain/VoteType.cs @@ -0,0 +1,8 @@ +namespace HN.Domain +{ + public enum VoteType + { + Up, + Down + } +} \ No newline at end of file diff --git a/Infrastructure/EntityTypes/LinkEntityType.cs b/Infrastructure/EntityTypes/LinkEntityType.cs index ab26901..237b881 100644 --- a/Infrastructure/EntityTypes/LinkEntityType.cs +++ b/Infrastructure/EntityTypes/LinkEntityType.cs @@ -13,6 +13,12 @@ namespace HN.Infrastructure.EntityTypes builder.Property(o => o.Url).IsRequired().HasMaxLength(500); builder.Property(o => o.CreatedAt).IsRequired(); builder.HasIndex(o => o.Url).IsUnique(); + + builder.HasMany(o => o.Votes) + .WithOne() + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); } } } diff --git a/Infrastructure/EntityTypes/VoteEntityType.cs b/Infrastructure/EntityTypes/VoteEntityType.cs new file mode 100644 index 0000000..e57bf9f --- /dev/null +++ b/Infrastructure/EntityTypes/VoteEntityType.cs @@ -0,0 +1,18 @@ +using HN.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace HN.Infrastructure.EntityTypes +{ + public sealed class VoteEntityType : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("link_votes"); + builder.HasKey("LinkId"); + builder.Property(o => o.Type).IsRequired(); + builder.Property(o => o.CreatedAt).IsRequired(); + } + } + +} \ No newline at end of file diff --git a/Infrastructure/HNDbContext.cs b/Infrastructure/HNDbContext.cs index df3fc32..d51e40a 100644 --- a/Infrastructure/HNDbContext.cs +++ b/Infrastructure/HNDbContext.cs @@ -1,11 +1,13 @@ using HN.Application; using HN.Domain; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace HN.Infrastructure { public sealed class HNDbContext : DbContext, IDbContext { + private readonly ILoggerFactory _loggerFactory; public DbSet Links { get; set; } public HNDbContext() @@ -13,9 +15,9 @@ namespace HN.Infrastructure } - public HNDbContext(DbContextOptions options) : base(options) + public HNDbContext(DbContextOptions options, ILoggerFactory loggerFactory) : base(options) { - + _loggerFactory = loggerFactory; } protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly); @@ -27,6 +29,8 @@ namespace HN.Infrastructure { optionsBuilder.UseSqlite("Data Source=:memory:"); } + + optionsBuilder.UseLoggerFactory(_loggerFactory); } } } diff --git a/Infrastructure/Migrations/20201210094301_CreateLinkVote.Designer.cs b/Infrastructure/Migrations/20201210094301_CreateLinkVote.Designer.cs new file mode 100644 index 0000000..671213e --- /dev/null +++ b/Infrastructure/Migrations/20201210094301_CreateLinkVote.Designer.cs @@ -0,0 +1,75 @@ +// +using System; +using HN.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(HNDbContext))] + [Migration("20201210094301_CreateLinkVote")] + partial class CreateLinkVote + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.0"); + + modelBuilder.Entity("HN.Domain.Link", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Url") + .IsUnique(); + + b.ToTable("links"); + }); + + modelBuilder.Entity("HN.Domain.Vote", b => + { + b.Property("LinkId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("LinkId"); + + b.ToTable("link_votes"); + }); + + modelBuilder.Entity("HN.Domain.Vote", b => + { + b.HasOne("HN.Domain.Link", null) + .WithMany("Votes") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HN.Domain.Link", b => + { + b.Navigation("Votes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Migrations/20201210094301_CreateLinkVote.cs b/Infrastructure/Migrations/20201210094301_CreateLinkVote.cs new file mode 100644 index 0000000..5641614 --- /dev/null +++ b/Infrastructure/Migrations/20201210094301_CreateLinkVote.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Infrastructure.Migrations +{ + public partial class CreateLinkVote : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "link_votes", + columns: table => new + { + LinkId = table.Column(type: "TEXT", nullable: false), + Type = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_link_votes", x => x.LinkId); + table.ForeignKey( + name: "FK_link_votes_links_LinkId", + column: x => x.LinkId, + principalTable: "links", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "link_votes"); + } + } +} diff --git a/Infrastructure/Migrations/HNDbContextModelSnapshot.cs b/Infrastructure/Migrations/HNDbContextModelSnapshot.cs index c5f1f74..de280a5 100644 --- a/Infrastructure/Migrations/HNDbContextModelSnapshot.cs +++ b/Infrastructure/Migrations/HNDbContextModelSnapshot.cs @@ -1,43 +1,73 @@ -// -using System; -using HN.Infrastructure; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace Infrastructure.Migrations -{ - [DbContext(typeof(HNDbContext))] - partial class HNDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); - - modelBuilder.Entity("HN.Domain.Link", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("CreatedAt") - .HasColumnType("TEXT"); - - b.Property("Url") - .IsRequired() - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Url") - .IsUnique(); - - b.ToTable("links"); - }); -#pragma warning restore 612, 618 - } - } -} +// +using System; +using HN.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(HNDbContext))] + partial class HNDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.0"); + + modelBuilder.Entity("HN.Domain.Link", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Url") + .IsUnique(); + + b.ToTable("links"); + }); + + modelBuilder.Entity("HN.Domain.Vote", b => + { + b.Property("LinkId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("LinkId"); + + b.ToTable("link_votes"); + }); + + modelBuilder.Entity("HN.Domain.Vote", b => + { + b.HasOne("HN.Domain.Link", null) + .WithMany("Votes") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HN.Domain.Link", b => + { + b.Navigation("Votes"); + }); +#pragma warning restore 612, 618 + } + } +}