add Vote entity inside Link + update queries

This commit is contained in:
Julien Leicher 2020-12-10 11:15:09 +01:00
parent 2325522b98
commit df00e15002
No known key found for this signature in database
GPG Key ID: F2A187E5D2F626A9
18 changed files with 306 additions and 65 deletions

View File

@ -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; }

View File

@ -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());
}
}
}

View File

@ -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;
}
}
}

View File

@ -2,7 +2,7 @@ using MediatR;
namespace HN.Application
{
public sealed class ListLinksQuery : IRequest<LinkDTO[]>
public sealed class ListLinksQuery : IRequest<LinkDto[]>
{
}

View File

@ -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());
}

View File

@ -1,4 +1,4 @@
@model HN.Application.LinkDTO[]
@model HN.Application.LinkDto[]
@{
ViewData["Title"] = "Latest Links";
}

View File

@ -1,3 +1,3 @@
@model HN.Application.LinkDTO
@model HN.Application.LinkDto
<partial name="_LinkItem" model="@Model" />

View File

@ -1,3 +1,3 @@
@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>

View File

@ -3,7 +3,8 @@
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore": "Information"
}
}
}

View File

@ -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<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)

16
Domain/Vote.cs Normal file
View 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
View File

@ -0,0 +1,8 @@
namespace HN.Domain
{
public enum VoteType
{
Up,
Down
}
}

View File

@ -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();
}
}
}

View 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();
}
}
}

View File

@ -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);
}
}
}

View 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
}
}
}

View 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");
}
}
}

View File

@ -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
}
}