From 66cc78d30ea342b156f814b1cd4175fef7ea8490 Mon Sep 17 00:00:00 2001 From: Julien Leicher Date: Fri, 11 Dec 2020 09:46:42 +0100 Subject: [PATCH] comment-a-link (#24) Closes #23 refactor to use OwnsMany --- Application/CommentLink/CommentLinkCommand.cs | 26 +++++ .../CommentLink/CommentLinkCommandHandler.cs | 30 +++++ .../Website/Controllers/CommentsController.cs | 34 ++++++ Apps/Website/Controllers/LinksController.cs | 7 +- Apps/Website/Models/ShowLinkViewModel.cs | 18 +++ Apps/Website/Startup.cs | 3 +- Apps/Website/Views/Links/Create.cshtml | 8 +- Apps/Website/Views/Links/Show.cshtml | 5 +- Apps/Website/Views/Shared/_CommentForm.cshtml | 12 ++ .../Views/{Links => Shared}/_LinkItem.cshtml | 1 + Domain/Comment.cs | 20 ++++ Domain/ICommentRepository.cs | 9 ++ Domain/Link.cs | 7 +- Infrastructure/CommentRepository.cs | 17 +++ .../EntityTypes/CommentEntityType.cs | 22 ++++ Infrastructure/EntityTypes/LinkEntityType.cs | 13 ++- Infrastructure/EntityTypes/VoteEntityType.cs | 18 --- Infrastructure/LinkRepository.cs | 8 +- .../20201210171100_CreateComment.Designer.cs | 107 ++++++++++++++++++ .../20201210171100_CreateComment.cs | 42 +++++++ .../Migrations/HNDbContextModelSnapshot.cs | 32 ++++++ 21 files changed, 403 insertions(+), 36 deletions(-) create mode 100644 Application/CommentLink/CommentLinkCommand.cs create mode 100644 Application/CommentLink/CommentLinkCommandHandler.cs create mode 100644 Apps/Website/Controllers/CommentsController.cs create mode 100644 Apps/Website/Models/ShowLinkViewModel.cs create mode 100644 Apps/Website/Views/Shared/_CommentForm.cshtml rename Apps/Website/Views/{Links => Shared}/_LinkItem.cshtml (84%) create mode 100644 Domain/Comment.cs create mode 100644 Domain/ICommentRepository.cs create mode 100644 Infrastructure/CommentRepository.cs create mode 100644 Infrastructure/EntityTypes/CommentEntityType.cs delete mode 100644 Infrastructure/EntityTypes/VoteEntityType.cs create mode 100644 Infrastructure/Migrations/20201210171100_CreateComment.Designer.cs create mode 100644 Infrastructure/Migrations/20201210171100_CreateComment.cs diff --git a/Application/CommentLink/CommentLinkCommand.cs b/Application/CommentLink/CommentLinkCommand.cs new file mode 100644 index 0000000..233be2a --- /dev/null +++ b/Application/CommentLink/CommentLinkCommand.cs @@ -0,0 +1,26 @@ +using System; +using System.ComponentModel.DataAnnotations; +using MediatR; + +namespace HN.Application +{ + public sealed class CommentLinkCommand : IRequest + { + [Required] + public Guid LinkId { get; set; } + + [Required] + public string Content { get; set; } + + // Constructeur vide nécessaire pour le model binding + public CommentLinkCommand() + { + + } + + public CommentLinkCommand(Guid linkId) + { + LinkId = linkId; + } + } +} \ No newline at end of file diff --git a/Application/CommentLink/CommentLinkCommandHandler.cs b/Application/CommentLink/CommentLinkCommandHandler.cs new file mode 100644 index 0000000..fb455b5 --- /dev/null +++ b/Application/CommentLink/CommentLinkCommandHandler.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using HN.Domain; +using MediatR; + +namespace HN.Application +{ + public sealed class CommentLinkCommandHandler : IRequestHandler + { + private readonly ILinkRepository _linkRepository; + private readonly ICommentRepository _commentRepository; + + public CommentLinkCommandHandler(ILinkRepository linkRepository, ICommentRepository commentRepository) + { + _linkRepository = linkRepository; + _commentRepository = commentRepository; + } + + public async Task Handle(CommentLinkCommand request, CancellationToken cancellationToken) + { + var link = await _linkRepository.GetByIdAsync(request.LinkId); + var comment = link.AddComment(request.Content); + + await _commentRepository.AddAsync(comment); + + return comment.Id; + } + } +} \ No newline at end of file diff --git a/Apps/Website/Controllers/CommentsController.cs b/Apps/Website/Controllers/CommentsController.cs new file mode 100644 index 0000000..1f73833 --- /dev/null +++ b/Apps/Website/Controllers/CommentsController.cs @@ -0,0 +1,34 @@ +using HN.Application; +using Website.Models; +using Microsoft.AspNetCore.Mvc; +using MediatR; +using System.Threading.Tasks; + +namespace Website.Controllers +{ + public sealed class CommentsController : BaseController + { + private readonly IMediator _bus; + + public CommentsController(IMediator bus) + { + _bus = bus; + } + + [ValidateAntiForgeryToken] + [HttpPost] + public async Task Create(CommentLinkCommand command) + { + if (!ModelState.IsValid) + { + return View("../Links/Show", new ShowLinkViewModel(await _bus.Send(new GetLinkQuery(command.LinkId)), command)); + } + + await _bus.Send(command); + + SetFlash("Comment added!"); + + return RedirectToAction(nameof(LinksController.Show), "Links", new { id = command.LinkId }); + } + } +} \ No newline at end of file diff --git a/Apps/Website/Controllers/LinksController.cs b/Apps/Website/Controllers/LinksController.cs index aa7e811..47b7d49 100644 --- a/Apps/Website/Controllers/LinksController.cs +++ b/Apps/Website/Controllers/LinksController.cs @@ -4,6 +4,7 @@ using MediatR; using System.Threading.Tasks; using System; using HN.Domain; +using Website.Models; namespace Website.Controllers { @@ -25,7 +26,7 @@ namespace Website.Controllers [HttpGet("{controller}/{id:guid}")] public async Task Show(Guid id) { - return View(await _bus.Send(new GetLinkQuery(id))); + return View(new ShowLinkViewModel(await _bus.Send(new GetLinkQuery(id)), new CommentLinkCommand(id))); } public IActionResult Create() @@ -35,12 +36,12 @@ namespace Website.Controllers [HttpPost("{controller}/{id:guid}/vote")] [ValidateAntiForgeryToken] - public async Task Vote(Guid id, string url, VoteType type) + public async Task Vote(Guid id, string url, VoteType type, string redirectTo) { await _bus.Send(new VoteForLinkCommand(id, type)); SetFlash($"Successfuly {type} for {url}!"); - return RedirectToAction(nameof(Index)); + return Redirect(redirectTo); } [HttpPost] diff --git a/Apps/Website/Models/ShowLinkViewModel.cs b/Apps/Website/Models/ShowLinkViewModel.cs new file mode 100644 index 0000000..739fc09 --- /dev/null +++ b/Apps/Website/Models/ShowLinkViewModel.cs @@ -0,0 +1,18 @@ +using HN.Application; + +namespace Website.Models +{ + public class ShowLinkViewModel + { + public LinkDto Link { get; set; } + + public CommentLinkCommand CommentForm { get; set; } + + public ShowLinkViewModel(LinkDto link, CommentLinkCommand commentForm) + { + Link = link; + CommentForm = commentForm; + } + } + +} \ No newline at end of file diff --git a/Apps/Website/Startup.cs b/Apps/Website/Startup.cs index 8b38b93..ac9ec92 100644 --- a/Apps/Website/Startup.cs +++ b/Apps/Website/Startup.cs @@ -27,7 +27,8 @@ namespace Website services.AddDbContext(options => options.UseSqlite(Configuration.GetConnectionString("Default"))); services.AddScoped(); services.AddScoped(); - services.AddMediatR(typeof(HN.Application.AddLinkCommand)); + services.AddScoped(); + services.AddMediatR(typeof(HN.Application.IDbContext)); services.Configure(options => { diff --git a/Apps/Website/Views/Links/Create.cshtml b/Apps/Website/Views/Links/Create.cshtml index 0de01d2..2161b0d 100644 --- a/Apps/Website/Views/Links/Create.cshtml +++ b/Apps/Website/Views/Links/Create.cshtml @@ -5,9 +5,13 @@
- @Html.LabelFor(m => m.Url) + @* @Html.LabelFor(m => m.Url) @Html.EditorFor(m => m.Url) - @Html.ValidationMessageFor(m => m.Url) + @Html.ValidationMessageFor(m => m.Url) *@ + + + +
diff --git a/Apps/Website/Views/Links/Show.cshtml b/Apps/Website/Views/Links/Show.cshtml index fced2aa..656418b 100644 --- a/Apps/Website/Views/Links/Show.cshtml +++ b/Apps/Website/Views/Links/Show.cshtml @@ -1,3 +1,4 @@ -@model HN.Application.LinkDto +@model ShowLinkViewModel - \ No newline at end of file + + \ No newline at end of file diff --git a/Apps/Website/Views/Shared/_CommentForm.cshtml b/Apps/Website/Views/Shared/_CommentForm.cshtml new file mode 100644 index 0000000..57fd06c --- /dev/null +++ b/Apps/Website/Views/Shared/_CommentForm.cshtml @@ -0,0 +1,12 @@ +@model HN.Application.CommentLinkCommand + +
+

Add a comment

+
+ + + + + +
+
\ No newline at end of file diff --git a/Apps/Website/Views/Links/_LinkItem.cshtml b/Apps/Website/Views/Shared/_LinkItem.cshtml similarity index 84% rename from Apps/Website/Views/Links/_LinkItem.cshtml rename to Apps/Website/Views/Shared/_LinkItem.cshtml index 7a6e43f..ecc19fc 100644 --- a/Apps/Website/Views/Links/_LinkItem.cshtml +++ b/Apps/Website/Views/Shared/_LinkItem.cshtml @@ -3,6 +3,7 @@ @Model.Url - created at @Model.CreatedAt.ToLocalTime() (👍: @Model.UpVotes / 👎: @Model.DownVotes)
+ 👍 👎
\ No newline at end of file diff --git a/Domain/Comment.cs b/Domain/Comment.cs new file mode 100644 index 0000000..c1e1ba7 --- /dev/null +++ b/Domain/Comment.cs @@ -0,0 +1,20 @@ +using System; + +namespace HN.Domain +{ + public sealed class Comment + { + public Guid Id { get; private set; } + public Guid LinkId { get; private set; } + public string Content { get; private set; } + public DateTime CreatedAt { get; private set; } + + internal Comment(Guid linkId, string content) + { + Id = Guid.NewGuid(); + LinkId = linkId; + Content = content; + CreatedAt = DateTime.UtcNow; + } + } +} \ No newline at end of file diff --git a/Domain/ICommentRepository.cs b/Domain/ICommentRepository.cs new file mode 100644 index 0000000..d5d48fb --- /dev/null +++ b/Domain/ICommentRepository.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace HN.Domain +{ + public interface ICommentRepository + { + Task AddAsync(Comment comment); + } +} \ No newline at end of file diff --git a/Domain/Link.cs b/Domain/Link.cs index 121dd40..363d111 100644 --- a/Domain/Link.cs +++ b/Domain/Link.cs @@ -9,7 +9,7 @@ namespace HN.Domain public string Url { get; } public DateTime CreatedAt { get; } private List _votes; - public IReadOnlyList Votes { get { return _votes; } } + public IReadOnlyList Votes => _votes; private Link(string url) { @@ -33,5 +33,10 @@ namespace HN.Domain { _votes.Add(new Vote(VoteType.Down)); } + + public Comment AddComment(string content) + { + return new Comment(Id, content); + } } } diff --git a/Infrastructure/CommentRepository.cs b/Infrastructure/CommentRepository.cs new file mode 100644 index 0000000..7652178 --- /dev/null +++ b/Infrastructure/CommentRepository.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using HN.Domain; + +namespace HN.Infrastructure +{ + public sealed class CommentRepository : Repository, ICommentRepository + { + public CommentRepository(HNDbContext context) : base(context) + { + } + + public Task AddAsync(Comment comment) + { + return base.AddAsync(comment); + } + } +} \ No newline at end of file diff --git a/Infrastructure/EntityTypes/CommentEntityType.cs b/Infrastructure/EntityTypes/CommentEntityType.cs new file mode 100644 index 0000000..610b27c --- /dev/null +++ b/Infrastructure/EntityTypes/CommentEntityType.cs @@ -0,0 +1,22 @@ +using HN.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace HN.Infrastructure +{ + public sealed class CommentEntityType : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("comments"); + builder.HasKey(o => o.Id); + builder.HasOne() + .WithMany() + .HasForeignKey(o => o.LinkId) + .IsRequired(); + builder.Property(o => o.Content).IsRequired(); + builder.Property(o => o.CreatedAt).IsRequired(); + } + } + +} \ No newline at end of file diff --git a/Infrastructure/EntityTypes/LinkEntityType.cs b/Infrastructure/EntityTypes/LinkEntityType.cs index 237b881..ad688cc 100644 --- a/Infrastructure/EntityTypes/LinkEntityType.cs +++ b/Infrastructure/EntityTypes/LinkEntityType.cs @@ -14,11 +14,14 @@ namespace HN.Infrastructure.EntityTypes builder.Property(o => o.CreatedAt).IsRequired(); builder.HasIndex(o => o.Url).IsUnique(); - builder.HasMany(o => o.Votes) - .WithOne() - .HasForeignKey("LinkId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + builder.OwnsMany(o => o.Votes, vote => + { + vote.ToTable("link_votes"); + vote.WithOwner().HasForeignKey("LinkId"); + vote.HasKey("LinkId"); + vote.Property(o => o.Type).IsRequired(); + vote.Property(o => o.CreatedAt).IsRequired(); + }); } } } diff --git a/Infrastructure/EntityTypes/VoteEntityType.cs b/Infrastructure/EntityTypes/VoteEntityType.cs deleted file mode 100644 index e57bf9f..0000000 --- a/Infrastructure/EntityTypes/VoteEntityType.cs +++ /dev/null @@ -1,18 +0,0 @@ -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/LinkRepository.cs b/Infrastructure/LinkRepository.cs index 842f73c..87b8614 100644 --- a/Infrastructure/LinkRepository.cs +++ b/Infrastructure/LinkRepository.cs @@ -11,9 +11,9 @@ namespace HN.Infrastructure { } - public async Task AddAsync(Link link) + public Task AddAsync(Link link) { - await base.AddAsync(link); + return base.AddAsync(link); } public Task GetByIdAsync(Guid id) @@ -21,9 +21,9 @@ namespace HN.Infrastructure return Entries.SingleOrDefaultAsync(o => o.Id == id); } - public async Task UpdateAsync(Link link) + public Task UpdateAsync(Link link) { - await base.UpdateAsync(link); + return base.UpdateAsync(link); } } } \ No newline at end of file diff --git a/Infrastructure/Migrations/20201210171100_CreateComment.Designer.cs b/Infrastructure/Migrations/20201210171100_CreateComment.Designer.cs new file mode 100644 index 0000000..1e5748a --- /dev/null +++ b/Infrastructure/Migrations/20201210171100_CreateComment.Designer.cs @@ -0,0 +1,107 @@ +// +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("20201210171100_CreateComment")] + partial class CreateComment + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.0"); + + modelBuilder.Entity("HN.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("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.Comment", b => + { + b.HasOne("HN.Domain.Link", null) + .WithMany() + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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/20201210171100_CreateComment.cs b/Infrastructure/Migrations/20201210171100_CreateComment.cs new file mode 100644 index 0000000..87ad8a7 --- /dev/null +++ b/Infrastructure/Migrations/20201210171100_CreateComment.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Infrastructure.Migrations +{ + public partial class CreateComment : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + 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"); + } + } +} diff --git a/Infrastructure/Migrations/HNDbContextModelSnapshot.cs b/Infrastructure/Migrations/HNDbContextModelSnapshot.cs index de280a5..16756d5 100644 --- a/Infrastructure/Migrations/HNDbContextModelSnapshot.cs +++ b/Infrastructure/Migrations/HNDbContextModelSnapshot.cs @@ -16,6 +16,29 @@ namespace Infrastructure.Migrations modelBuilder .HasAnnotation("ProductVersion", "5.0.0"); + modelBuilder.Entity("HN.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("HN.Domain.Link", b => { b.Property("Id") @@ -54,6 +77,15 @@ namespace Infrastructure.Migrations b.ToTable("link_votes"); }); + modelBuilder.Entity("HN.Domain.Comment", b => + { + b.HasOne("HN.Domain.Link", null) + .WithMany() + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("HN.Domain.Vote", b => { b.HasOne("HN.Domain.Link", null)