diff --git a/Application/CommentLink/CommentLinkCommand.cs b/Application/CommentLink/CommentLinkCommand.cs new file mode 100644 index 0000000..1edf25e --- /dev/null +++ b/Application/CommentLink/CommentLinkCommand.cs @@ -0,0 +1,15 @@ +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; } + } +} \ 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/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/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..dec8f08 100644 --- a/Domain/Link.cs +++ b/Domain/Link.cs @@ -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/LinkRepository.cs b/Infrastructure/LinkRepository.cs index 842f73c..6eb37c5 100644 --- a/Infrastructure/LinkRepository.cs +++ b/Infrastructure/LinkRepository.cs @@ -11,19 +11,19 @@ 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) { - return Entries.SingleOrDefaultAsync(o => o.Id == id); + return Entries.Include(l => l.Votes).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)