ajout auteur des liens / commentaires

This commit is contained in:
Julien LEICHER 2021-12-15 16:05:50 +01:00
parent 9399b6d92c
commit 5b1f99bae1
No known key found for this signature in database
GPG Key ID: BE0761B6A007EB96
17 changed files with 601 additions and 22 deletions

View File

@ -1,6 +1,9 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using HackerNet.Api.Models; using HackerNet.Api.Models;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
namespace HackerNet.Api.Controllers; namespace HackerNet.Api.Controllers;
@ -10,17 +13,21 @@ public class AccountsController : ControllerBase
{ {
private readonly UserManager<IdentityUser> _userManager; private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager; private readonly SignInManager<IdentityUser> _signInManager;
private readonly IUserClaimsPrincipalFactory<IdentityUser> _claimsFactory;
private readonly TokenValidation _tokenValidation;
public AccountsController(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager) public AccountsController(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, IUserClaimsPrincipalFactory<IdentityUser> claimsFactory, TokenValidation tokenValidation)
{ {
_userManager = userManager; _userManager = userManager;
_signInManager = signInManager; _signInManager = signInManager;
_claimsFactory = claimsFactory;
_tokenValidation = tokenValidation;
} }
[HttpGet("me")] [HttpGet("me")]
public ActionResult Me() public ActionResult<string> Me()
{ {
throw new NotImplementedException(); return User.Identity.Name;
} }
[HttpPost] [HttpPost]
@ -36,4 +43,36 @@ public class AccountsController : ControllerBase
return CreatedAtAction(nameof(Me), null); return CreatedAtAction(nameof(Me), null);
} }
[HttpPost("token")]
public async Task<ActionResult<string>> Signin(SignupLoginViewModel cmd)
{
var user = await _userManager.FindByNameAsync(cmd.Username);
if (user == null)
{
return BadRequest();
}
var result = await _signInManager.CheckPasswordSignInAsync(user, cmd.Password, false);
if (!result.Succeeded)
{
return BadRequest();
}
var principal = await _claimsFactory.CreateAsync(user);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = (ClaimsIdentity)principal.Identity,
Expires = DateTime.UtcNow.AddDays(7),
Issuer = _tokenValidation.Issuer,
Audience = _tokenValidation.Audience,
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(_tokenValidation.Key))
, SecurityAlgorithms.HmacSha256Signature)
};
return new JwtSecurityTokenHandler().CreateEncodedJwt(tokenDescriptor);
}
} }

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using HackerNet.Application; using HackerNet.Application;
using HackerNet.Infrastructure.AspNet.Filters; using HackerNet.Infrastructure.AspNet.Filters;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace HackerNet.Api.Controllers; namespace HackerNet.Api.Controllers;
@ -52,6 +53,7 @@ public class LinksController : ControllerBase
} }
[HttpPost("{id:guid}/comments")] [HttpPost("{id:guid}/comments")]
[Authorize]
[ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult PublishComment(Guid id, PublishCommentBody cmd) public ActionResult PublishComment(Guid id, PublishCommentBody cmd)
@ -76,6 +78,7 @@ public class LinksController : ControllerBase
/// <param name="cmd"></param> /// <param name="cmd"></param>
/// <returns></returns> /// <returns></returns>
[HttpPost] [HttpPost]
[Authorize]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status201Created)]
public ActionResult PublishLink(PublishLinkCommand cmd) public ActionResult PublishLink(PublishLinkCommand cmd)

View File

@ -12,6 +12,7 @@ var tokenValidation = builder.Configuration
.GetSection("TokenValidation") .GetSection("TokenValidation")
.Get<TokenValidation>(); .Get<TokenValidation>();
builder.Services.AddSingleton(tokenValidation);
builder.Services.AddHackerNetServicesEntityFramework(builder.Configuration); builder.Services.AddHackerNetServicesEntityFramework(builder.Configuration);
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services builder.Services

View File

@ -6,6 +6,7 @@ GET {{url}}/api/links
POST {{url}}/api/links POST {{url}}/api/links
Content-Type: application/json Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiJiMGFiMDU0Zi1lMjI0LTRlNGMtYmY0MC0xY2UxNzU0ZWNiMTciLCJuYW1lIjoidGVzdCIsIkFzcE5ldC5JZGVudGl0eS5TZWN1cml0eVN0YW1wIjoiRlNHVk01U0ZFRUpSTzVORFIzR0ZPN1ZRWkRJQ1c2QVEiLCJuYmYiOjE2Mzk1Nzg3NjEsImV4cCI6MTY0MDE4MzU2MSwiaWF0IjoxNjM5NTc4NzYxLCJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo3MjUyLyIsImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0OjcyNTIvIn0.wjL5ALncw5JtNe5YpZD-uQAz2-BvbznOqqN1xgZpjAU
{ {
"url": "https://localhost:7252/api/links", "url": "https://localhost:7252/api/links",
@ -30,6 +31,16 @@ Content-Type: application/json
POST {{url}}/api/accounts POST {{url}}/api/accounts
Content-Type: application/json Content-Type: application/json
{
"username": "test",
"password": "G6:c`bzr2h#Pq;4"
}
###
POST {{url}}/api/accounts/token
Content-Type: application/json
{ {
"username": "test", "username": "test",
"password": "G6:c`bzr2h#Pq;4" "password": "G6:c`bzr2h#Pq;4"

View File

@ -1,7 +1,7 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Debug",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },

View File

@ -9,5 +9,6 @@
<a class="text-indigo-500 hover:underline" asp-controller="Links" asp-action="Detail" asp-route-id="@Model.Id">Détail du lien</a> <a class="text-indigo-500 hover:underline" asp-controller="Links" asp-action="Detail" asp-route-id="@Model.Id">Détail du lien</a>
<a class="text-indigo-500 hover:underline" asp-controller="Comments" asp-action="New" asp-route-id="@Model.Id">Ajouter un commentaire</a> <a class="text-indigo-500 hover:underline" asp-controller="Comments" asp-action="New" asp-route-id="@Model.Id">Ajouter un commentaire</a>
<p>@Model.CommentsCount commentaire@(Model.CommentsCount > 1 ? "s" : "")</p> <p>@Model.CommentsCount commentaire@(Model.CommentsCount > 1 ? "s" : "")</p>
<p>@Model.Author</p>
</div> </div>
</article> </article>

View File

@ -7,20 +7,25 @@ public class LinkService
private readonly ILinkRepository _repository; private readonly ILinkRepository _repository;
private readonly ICommentRepository _commentRepository; private readonly ICommentRepository _commentRepository;
private readonly IReadStore _readStore; private readonly IReadStore _readStore;
private readonly ICurrentUser _currentUser;
public LinkService( public LinkService(
ILinkRepository repository, ILinkRepository repository,
ICommentRepository commentRepository, ICommentRepository commentRepository,
IReadStore readStore) IReadStore readStore,
ICurrentUser currentUser)
{ {
_repository = repository; _repository = repository;
_commentRepository = commentRepository; _commentRepository = commentRepository;
_readStore = readStore; _readStore = readStore;
_currentUser = currentUser;
} }
public Guid PublishLink(PublishLinkCommand cmd) public Guid PublishLink(PublishLinkCommand cmd)
{ {
var link = new Link(cmd.Url, cmd.Description); var link = new Link(_currentUser.GetCurrentUserId(),
cmd.Url,
cmd.Description);
_repository.Add(link); _repository.Add(link);
@ -29,7 +34,7 @@ public class LinkService
public Guid PublishComment(PublishCommentCommand cmd) public Guid PublishComment(PublishCommentCommand cmd)
{ {
var comment = new Comment(cmd.LinkId, cmd.Content); var comment = new Comment(_currentUser.GetCurrentUserId(), cmd.LinkId, cmd.Content);
_commentRepository.Add(comment); _commentRepository.Add(comment);
@ -53,17 +58,24 @@ public interface IReadStore
LinkComment[] GetLinkComments(Guid linkId); LinkComment[] GetLinkComments(Guid linkId);
} }
public interface ICurrentUser
{
string GetCurrentUserId();
}
public class LinkHomePage public class LinkHomePage
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public string Url { get; set; } public string Url { get; set; }
public string Description { get; set; } public string Description { get; set; }
public int CommentsCount { get; set; } public int CommentsCount { get; set; }
public string Author { get; set; }
} }
public class LinkComment public class LinkComment
{ {
public string Content { get; set; } public string Content { get; set; }
public string User { get; set; }
} }
public class PublishLinkCommand public class PublishLinkCommand

View File

@ -6,12 +6,14 @@ public class Comment
public Guid LinkId { get; } public Guid LinkId { get; }
public string Content { get; } public string Content { get; }
public DateTime CreatedAt { get; } public DateTime CreatedAt { get; }
public string CreatedBy { get; }
public Comment(Guid linkId, string content) public Comment(string createdBy, Guid linkId, string content)
{ {
Id = Guid.NewGuid(); Id = Guid.NewGuid();
LinkId = linkId; LinkId = linkId;
Content = content; Content = content;
CreatedAt = DateTime.UtcNow; CreatedAt = DateTime.UtcNow;
CreatedBy = createdBy;
} }
} }

View File

@ -6,12 +6,14 @@ public class Link
public string Url { get; } public string Url { get; }
public string Description { get; } public string Description { get; }
public DateTime CreatedAt { get; } public DateTime CreatedAt { get; }
public string CreatedBy { get; }
public Link(string url, string description) public Link(string createdBy, string url, string description)
{ {
Id = Guid.NewGuid(); Id = Guid.NewGuid();
Url = url; Url = url;
Description = description; Description = description;
CreatedAt = DateTime.UtcNow; CreatedAt = DateTime.UtcNow;
CreatedBy = createdBy;
} }
} }

View File

@ -13,8 +13,8 @@ public static class ServiceCollectionExtensions
{ {
public static IServiceCollection AddHackerNetServicesMemory(this IServiceCollection services) public static IServiceCollection AddHackerNetServicesMemory(this IServiceCollection services)
{ {
var link = new Link("https://localhost:7050/", "Youhouuu"); var link = new Link("", "https://localhost:7050/", "Youhouuu");
var comment = new Comment(link.Id, "Wow!"); var comment = new Comment("", link.Id, "Wow!");
var linksRepository = new MemoryLinkRepository(link); var linksRepository = new MemoryLinkRepository(link);
var commentsRepository = new MemoryCommentRepository(comment); var commentsRepository = new MemoryCommentRepository(comment);
@ -33,6 +33,7 @@ public static class ServiceCollectionExtensions
services.AddDbContext<HackerContext>(options services.AddDbContext<HackerContext>(options
=> options.UseSqlite(configuration.GetConnectionString("Default"))); => options.UseSqlite(configuration.GetConnectionString("Default")));
services.AddScoped<ICurrentUser, HttpCurrentUser>();
services.AddScoped<ILinkRepository, EFLinkRepository>(); services.AddScoped<ILinkRepository, EFLinkRepository>();
services.AddScoped<ICommentRepository, EFCommentRepository>(); services.AddScoped<ICommentRepository, EFCommentRepository>();
services.AddScoped<IReadStore, EFReadStore>(); services.AddScoped<IReadStore, EFReadStore>();

View File

@ -0,0 +1,23 @@
using HackerNet.Application;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
namespace HackerNet.Infrastructure.AspNet;
public class HttpCurrentUser : ICurrentUser
{
private readonly IHttpContextAccessor _contextAccessor;
private readonly UserManager<IdentityUser> _userManager;
public HttpCurrentUser(IHttpContextAccessor contextAccessor, UserManager<IdentityUser> userManager)
{
_contextAccessor = contextAccessor;
_userManager = userManager;
}
public string GetCurrentUserId()
{
var userPrincipal = _contextAccessor.HttpContext.User;
return _userManager.GetUserId(userPrincipal);
}
}

View File

@ -0,0 +1,349 @@
// <auto-generated />
using System;
using HackerNet.Infrastructure.Repositories.EntityFramework;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace HackerNet.Infrastructure.Migrations
{
[DbContext(typeof(HackerContext))]
[Migration("20211215145254_AddCreatedBy")]
partial class AddCreatedBy
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.1");
modelBuilder.Entity("HackerNet.Domain.Comment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("LinkId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedBy");
b.HasIndex("LinkId");
b.ToTable("Comments");
});
modelBuilder.Entity("HackerNet.Domain.Link", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedBy");
b.ToTable("Links");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("HackerNet.Domain.Comment", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("HackerNet.Domain.Link", null)
.WithMany()
.HasForeignKey("LinkId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("HackerNet.Domain.Link", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,81 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace HackerNet.Infrastructure.Migrations
{
public partial class AddCreatedBy : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("DELETE FROM Links; DELETE FROM Comments;");
migrationBuilder.AddColumn<string>(
name: "CreatedBy",
table: "Links",
type: "TEXT",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "CreatedBy",
table: "Comments",
type: "TEXT",
nullable: false,
defaultValue: "");
migrationBuilder.CreateIndex(
name: "IX_Links_CreatedBy",
table: "Links",
column: "CreatedBy");
migrationBuilder.CreateIndex(
name: "IX_Comments_CreatedBy",
table: "Comments",
column: "CreatedBy");
migrationBuilder.AddForeignKey(
name: "FK_Comments_AspNetUsers_CreatedBy",
table: "Comments",
column: "CreatedBy",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Links_AspNetUsers_CreatedBy",
table: "Links",
column: "CreatedBy",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Comments_AspNetUsers_CreatedBy",
table: "Comments");
migrationBuilder.DropForeignKey(
name: "FK_Links_AspNetUsers_CreatedBy",
table: "Links");
migrationBuilder.DropIndex(
name: "IX_Links_CreatedBy",
table: "Links");
migrationBuilder.DropIndex(
name: "IX_Comments_CreatedBy",
table: "Comments");
migrationBuilder.DropColumn(
name: "CreatedBy",
table: "Links");
migrationBuilder.DropColumn(
name: "CreatedBy",
table: "Comments");
}
}
}

View File

@ -30,11 +30,17 @@ namespace HackerNet.Infrastructure.Migrations
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("LinkId") b.Property<Guid>("LinkId")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("CreatedBy");
b.HasIndex("LinkId"); b.HasIndex("LinkId");
b.ToTable("Comments"); b.ToTable("Comments");
@ -49,6 +55,10 @@ namespace HackerNet.Infrastructure.Migrations
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("CreatedBy")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Description") b.Property<string>("Description")
.IsRequired() .IsRequired()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -60,6 +70,8 @@ namespace HackerNet.Infrastructure.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("CreatedBy");
b.ToTable("Links"); b.ToTable("Links");
}); });
@ -257,6 +269,12 @@ namespace HackerNet.Infrastructure.Migrations
modelBuilder.Entity("HackerNet.Domain.Comment", b => modelBuilder.Entity("HackerNet.Domain.Comment", b =>
{ {
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("HackerNet.Domain.Link", null) b.HasOne("HackerNet.Domain.Link", null)
.WithMany() .WithMany()
.HasForeignKey("LinkId") .HasForeignKey("LinkId")
@ -264,6 +282,15 @@ namespace HackerNet.Infrastructure.Migrations
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("HackerNet.Domain.Link", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{ {
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)

View File

@ -27,16 +27,30 @@ public class EFReadStore : IReadStore
private IQueryable<LinkHomePage> GetLinks(Guid? id = null) private IQueryable<LinkHomePage> GetLinks(Guid? id = null)
{ {
return _context.Links return (from link in _context.Links
.Where(l => !id.HasValue || l.Id == id) join user in _context.Users on link.CreatedBy equals user.Id
.OrderByDescending(l => l.CreatedAt) orderby link.CreatedAt descending
.Select(l => new LinkHomePage select new LinkHomePage
{ {
Id = l.Id, Id = link.Id,
Url = l.Url, Url = link.Url,
Description = l.Description, Description = link.Description,
CommentsCount = _context.Comments CommentsCount = _context.Comments.Count(c => c.LinkId == link.Id),
.Count(c => c.LinkId == l.Id), Author = user.UserName,
}); });
// return _context.Links
// .Where(l => !id.HasValue || l.Id == id)
// .OrderByDescending(l => l.CreatedAt)
// .Join()
// .Select(l => new LinkHomePage
// {
// Id = l.Id,
// Url = l.Url,
// Description = l.Description,
// CommentsCount = _context.Comments
// .Count(c => c.LinkId == l.Id),
// });
} }
} }

View File

@ -1,4 +1,5 @@
using HackerNet.Domain; using HackerNet.Domain;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Builders;
@ -16,5 +17,10 @@ public class CommentEntityTypeConfiguration : IEntityTypeConfiguration<Comment>
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
builder.Property(o => o.Content).IsRequired(); builder.Property(o => o.Content).IsRequired();
builder.Property(o => o.CreatedAt).IsRequired(); builder.Property(o => o.CreatedAt).IsRequired();
builder.HasOne<IdentityUser>()
.WithMany()
.HasForeignKey(o => o.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
} }
} }

View File

@ -1,4 +1,5 @@
using HackerNet.Domain; using HackerNet.Domain;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Builders;
@ -12,5 +13,11 @@ public class LinkEntityTypeConfiguration : IEntityTypeConfiguration<Link>
builder.Property(o => o.Url).IsRequired().HasMaxLength(250); builder.Property(o => o.Url).IsRequired().HasMaxLength(250);
builder.Property(o => o.Description).IsRequired(); builder.Property(o => o.Description).IsRequired();
builder.Property(o => o.CreatedAt).IsRequired(); builder.Property(o => o.CreatedAt).IsRequired();
builder.HasOne<IdentityUser>()
.WithMany()
.HasForeignKey(o => o.CreatedBy)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
} }
} }