vote-for-link (#22)
This commit is contained in:
parent
2325522b98
commit
9650d6dc86
@ -4,7 +4,7 @@ using MediatR;
|
||||
|
||||
namespace HN.Application
|
||||
{
|
||||
public sealed class GetLinkQuery : IRequest<LinkDTO>
|
||||
public sealed class GetLinkQuery : IRequest<LinkDto>
|
||||
{
|
||||
[Required]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
@ -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<GetLinkQuery, LinkDTO>
|
||||
public sealed class GetLinkQueryHandler : IRequestHandler<GetLinkQuery, LinkDto>
|
||||
{
|
||||
private readonly IDbContext _context;
|
||||
|
||||
@ -14,13 +15,20 @@ namespace HN.Application
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public Task<LinkDTO> Handle(GetLinkQuery request, CancellationToken cancellationToken)
|
||||
public Task<LinkDto> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@ using MediatR;
|
||||
|
||||
namespace HN.Application
|
||||
{
|
||||
public sealed class ListLinksQuery : IRequest<LinkDTO[]>
|
||||
public sealed class ListLinksQuery : IRequest<LinkDto[]>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@ -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<ListLinksQuery, LinkDTO[]>
|
||||
// public static class LinkQueriesExtensions
|
||||
// {
|
||||
// public static IQueryable<LinkDto> AllAsDto(this DbSet<Link> 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<LinkDto> ToLinkDto(this IQueryable<Link> 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<ListLinksQuery, LinkDto[]>
|
||||
{
|
||||
private readonly IDbContext _context;
|
||||
|
||||
@ -14,10 +43,17 @@ namespace HN.Application
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public Task<LinkDTO[]> Handle(ListLinksQuery request, CancellationToken cancellationToken)
|
||||
public Task<LinkDto[]> 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());
|
||||
}
|
||||
|
||||
22
Application/VoteForLink/VoteForLinkCommand.cs
Normal file
22
Application/VoteForLink/VoteForLinkCommand.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using HN.Domain;
|
||||
using MediatR;
|
||||
|
||||
namespace HN.Application
|
||||
{
|
||||
public sealed class VoteForLinkCommand : IRequest
|
||||
{
|
||||
[Required]
|
||||
public Guid LinkId { get; set; }
|
||||
|
||||
[Required]
|
||||
public VoteType Type { get; set; }
|
||||
|
||||
public VoteForLinkCommand(Guid linkId, VoteType type)
|
||||
{
|
||||
LinkId = linkId;
|
||||
Type = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Application/VoteForLink/VoteForLinkCommandHandler.cs
Normal file
36
Application/VoteForLink/VoteForLinkCommandHandler.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using HN.Domain;
|
||||
using MediatR;
|
||||
|
||||
namespace HN.Application
|
||||
{
|
||||
public sealed class VoteForLinkCommandHandler : IRequestHandler<VoteForLinkCommand>
|
||||
{
|
||||
private readonly ILinkRepository _linkRepository;
|
||||
|
||||
public VoteForLinkCommandHandler(ILinkRepository linkRepository)
|
||||
{
|
||||
_linkRepository = linkRepository;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(VoteForLinkCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var link = await _linkRepository.GetByIdAsync(request.LinkId);
|
||||
|
||||
switch (request.Type)
|
||||
{
|
||||
case VoteType.Up:
|
||||
link.Upvote();
|
||||
break;
|
||||
case VoteType.Down:
|
||||
link.Downvote();
|
||||
break;
|
||||
}
|
||||
|
||||
await _linkRepository.UpdateAsync(link);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ using HN.Application;
|
||||
using MediatR;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using HN.Domain;
|
||||
|
||||
namespace Website.Controllers
|
||||
{
|
||||
@ -32,6 +33,16 @@ namespace Website.Controllers
|
||||
return View(new AddLinkCommand());
|
||||
}
|
||||
|
||||
[HttpPost("{controller}/{id:guid}/vote")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Vote(Guid id, string url, VoteType type)
|
||||
{
|
||||
await _bus.Send(new VoteForLinkCommand(id, type));
|
||||
|
||||
SetFlash($"Successfuly {type} for {url}!");
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create(AddLinkCommand command)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
@model HN.Application.LinkDTO[]
|
||||
@model HN.Application.LinkDto[]
|
||||
@{
|
||||
ViewData["Title"] = "Latest Links";
|
||||
}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
@model HN.Application.LinkDTO
|
||||
@model HN.Application.LinkDto
|
||||
|
||||
<partial name="_LinkItem" model="@Model" />
|
||||
@ -1,3 +1,8 @@
|
||||
@model HN.Application.LinkDTO
|
||||
@model HN.Application.LinkDto
|
||||
|
||||
<a asp-action="Show" asp-controller="Links" asp-route-id="@Model.Id">@Model.Url - created at @Model.CreatedAt.ToLocalTime()</a>
|
||||
<a asp-action="Show" asp-controller="Links" asp-route-id="@Model.Id">@Model.Url - created at @Model.CreatedAt.ToLocalTime() (👍: @Model.UpVotes / 👎: @Model.DownVotes)</a>
|
||||
|
||||
<form asp-controller="Links" asp-action="Vote" asp-route-id="@Model.Id" asp-route-url="@Model.Url" method="post">
|
||||
<input type="submit" name="type" value="up">👍</button>
|
||||
<input type="submit" name="type" value="down">👎</button>
|
||||
</form>
|
||||
@ -3,7 +3,8 @@
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"Microsoft.EntityFrameworkCore": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace HN.Domain
|
||||
@ -5,5 +6,7 @@ namespace HN.Domain
|
||||
public interface ILinkRepository
|
||||
{
|
||||
Task AddAsync(Link link);
|
||||
Task UpdateAsync(Link link);
|
||||
Task<Link> GetByIdAsync(Guid id);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace HN.Domain
|
||||
{
|
||||
@ -7,17 +8,30 @@ namespace HN.Domain
|
||||
public Guid Id { get; }
|
||||
public string Url { get; }
|
||||
public DateTime CreatedAt { get; }
|
||||
private List<Vote> _votes;
|
||||
public IReadOnlyList<Vote> Votes { get { return _votes; } }
|
||||
|
||||
private Link(string url)
|
||||
{
|
||||
this.Id = Guid.NewGuid();
|
||||
this.CreatedAt = DateTime.UtcNow;
|
||||
this.Url = url;
|
||||
this._votes = new List<Vote>();
|
||||
}
|
||||
|
||||
public static Link FromUrl(string url)
|
||||
{
|
||||
return new Link(url);
|
||||
}
|
||||
|
||||
public void Upvote()
|
||||
{
|
||||
_votes.Add(new Vote(VoteType.Up));
|
||||
}
|
||||
|
||||
public void Downvote()
|
||||
{
|
||||
_votes.Add(new Vote(VoteType.Down));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
Domain/Vote.cs
Normal file
16
Domain/Vote.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Domain/VoteType.cs
Normal file
8
Domain/VoteType.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace HN.Domain
|
||||
{
|
||||
public enum VoteType
|
||||
{
|
||||
Up,
|
||||
Down
|
||||
}
|
||||
}
|
||||
@ -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<Vote>(o => o.Votes)
|
||||
.WithOne()
|
||||
.HasForeignKey("LinkId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
Infrastructure/EntityTypes/VoteEntityType.cs
Normal file
18
Infrastructure/EntityTypes/VoteEntityType.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using HN.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace HN.Infrastructure.EntityTypes
|
||||
{
|
||||
public sealed class VoteEntityType : IEntityTypeConfiguration<Vote>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Vote> builder)
|
||||
{
|
||||
builder.ToTable("link_votes");
|
||||
builder.HasKey("LinkId");
|
||||
builder.Property(o => o.Type).IsRequired();
|
||||
builder.Property(o => o.CreatedAt).IsRequired();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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<Link> Links { get; set; }
|
||||
|
||||
public HNDbContext()
|
||||
@ -13,9 +15,9 @@ namespace HN.Infrastructure
|
||||
|
||||
}
|
||||
|
||||
public HNDbContext(DbContextOptions<HNDbContext> options) : base(options)
|
||||
public HNDbContext(DbContextOptions<HNDbContext> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using HN.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HN.Infrastructure
|
||||
{
|
||||
@ -13,5 +15,15 @@ namespace HN.Infrastructure
|
||||
{
|
||||
await base.AddAsync(link);
|
||||
}
|
||||
|
||||
public Task<Link> GetByIdAsync(Guid id)
|
||||
{
|
||||
return Entries.SingleOrDefaultAsync(o => o.Id == id);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Link link)
|
||||
{
|
||||
await base.UpdateAsync(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
75
Infrastructure/Migrations/20201210094301_CreateLinkVote.Designer.cs
generated
Normal file
75
Infrastructure/Migrations/20201210094301_CreateLinkVote.Designer.cs
generated
Normal file
@ -0,0 +1,75 @@
|
||||
// <auto-generated />
|
||||
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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Url")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("links");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("HN.Domain.Vote", b =>
|
||||
{
|
||||
b.Property<Guid>("LinkId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Infrastructure/Migrations/20201210094301_CreateLinkVote.cs
Normal file
36
Infrastructure/Migrations/20201210094301_CreateLinkVote.cs
Normal file
@ -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<Guid>(type: "TEXT", nullable: false),
|
||||
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -37,6 +37,36 @@ namespace Infrastructure.Migrations
|
||||
|
||||
b.ToTable("links");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("HN.Domain.Vote", b =>
|
||||
{
|
||||
b.Property<Guid>("LinkId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("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
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,5 +20,10 @@ namespace HN.Infrastructure
|
||||
await Entries.AddRangeAsync(entities);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
protected async Task UpdateAsync(params TEntity[] entities)
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user