Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • xbily/pv293-socialnetwork-social
1 result
Show changes
Commits on Source (3)
......@@ -4,44 +4,47 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Ardalis.GuardClauses" Version="4.3.0"/>
<PackageVersion Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1"/>
<PackageVersion Include="coverlet.collector" Version="6.0.0"/>
<PackageVersion Include="FluentAssertions" Version="6.12.0"/>
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.0"/>
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0"/>
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.1"/>
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1"/>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.1"/>
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="8.0.1"/>
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.0"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.1"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.1"/>
<PackageVersion Include="Ardalis.GuardClauses" Version="4.3.0" />
<PackageVersion Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
<PackageVersion Include="ErrorOr" Version="1.9.0" />
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
<PackageVersion Include="MediatR" Version="12.2.0" />
<PackageVersion Include="MediatR.Extensions.FluentValidation.AspNetCore" Version="5.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.1"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/>
<PackageVersion Include="NSubstitute" Version="5.1.0"/>
<PackageVersion Include="NSwag.AspNetCore" Version="14.0.1"/>
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageVersion Include="NSubstitute" Version="5.1.0" />
<PackageVersion Include="NSwag.AspNetCore" Version="14.0.1" />
<PackageVersion Include="NSwag.MSBuild" Version="14.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
<PackageVersion Include="nunit" Version="4.0.1"/>
<PackageVersion Include="nunit" Version="4.0.1" />
<PackageVersion Include="NUnit.Analyzers" Version="3.10.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0"/>
<PackageVersion Include="Respawn" Version="6.2.0"/>
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0"/>
<PackageVersion Include="Testcontainers.MsSql" Version="3.7.0"/>
<PackageVersion Include="ZymLabs.NSwag.FluentValidation.AspNetCore" Version="0.6.2"/>
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageVersion Include="Respawn" Version="6.2.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageVersion Include="Testcontainers.MsSql" Version="3.7.0" />
<PackageVersion Include="ZymLabs.NSwag.FluentValidation.AspNetCore" Version="0.6.2" />
</ItemGroup>
</Project>
\ No newline at end of file
services:
webapi:
image: webapi
build:
context: .
dockerfile: src/WebApi/Dockerfile
......
......@@ -8,7 +8,10 @@
<ItemGroup>
<PackageReference Include="Ardalis.GuardClauses" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" />
<PackageReference Include="ErrorOr" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" />
<PackageReference Include="MediatR" />
<PackageReference Include="MediatR.Extensions.FluentValidation.AspNetCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore" />
</ItemGroup>
......
using System.Reflection;
using MediatR;
using MediatR.Extensions.FluentValidation.AspNetCore;
using Microsoft.Extensions.DependencyInjection;
namespace SocialNetwork.Social.Application;
......@@ -11,6 +13,12 @@ public static class DependencyInjection
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
});
return services;
}
}
using ErrorOr;
using MediatR;
using SocialNetwork.Social.Application.Common.Interfaces;
using SocialNetwork.Social.Domain.Entities.Feed;
namespace SocialNetwork.Social.Application.Posts.Commands;
public record CommentPostCommand(Guid PostId, Guid CommenterId, string Content) : IRequest<ErrorOr<Comment>>;
public class CommentPostCommandHandler(IApplicationDbContext dbContext) : IRequestHandler<CommentPostCommand, ErrorOr<Comment>>
{
public async Task<ErrorOr<Comment>> Handle(CommentPostCommand request, CancellationToken cancellationToken)
{
var postId = request.PostId;
var post = await dbContext.Posts.FindAsync([postId], cancellationToken: cancellationToken);
if (post is null) return PostErrors.PostNotFound(postId);
var comment = new Comment {AuthorId = request.CommenterId, Content = request.Content, PostId = request.PostId,};
await dbContext.Comments.AddAsync(comment, cancellationToken);
post.Comments.Add(comment);
await dbContext.SaveChangesAsync(cancellationToken);
dbContext.Posts.Update(post);
await dbContext.SaveChangesAsync(cancellationToken);
return comment;
}
}
public class CommentPostCommandValidator : AbstractValidator<CommentPostCommand>
{
public CommentPostCommandValidator()
{
RuleFor(p => p.PostId)
.NotEmpty();
RuleFor(p => p.CommenterId)
.NotEmpty();
RuleFor(p => p.Content)
.NotEmpty()
.Length(3, 512);
}
}
using ErrorOr;
using MediatR;
using SocialNetwork.Social.Application.Common.Interfaces;
using SocialNetwork.Social.Domain.Entities.Feed;
namespace SocialNetwork.Social.Application.Posts.Commands;
public record PublishPostCommand(Guid AuthorId, string Title, string Content) : IRequest<ErrorOr<Post>>;
public class PublishPostCommandHandler(IApplicationDbContext dbContext) : IRequestHandler<PublishPostCommand, ErrorOr<Post>>
{
public async Task<ErrorOr<Post>> Handle(PublishPostCommand request, CancellationToken cancellationToken)
{
var post = new Post {AuthorId = request.AuthorId, Title = request.Title, Content = request.Content};
await dbContext.Posts.AddAsync(post, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
// TODO fire event PostPublished
return post;
}
}
public class PublishPostCommandValidator : AbstractValidator<PublishPostCommand>
{
public PublishPostCommandValidator()
{
RuleFor(p => p.AuthorId)
.NotEmpty();
RuleFor(p => p.Title)
.NotEmpty()
.Length(3, 64);
RuleFor(p => p.Content)
.NotEmpty()
.Length(3, 512);
}
}
using ErrorOr;
using MediatR;
using SocialNetwork.Social.Application.Common.Interfaces;
using SocialNetwork.Social.Domain.Entities.Feed;
namespace SocialNetwork.Social.Application.Posts.Commands;
public record GetPostByIdCommand(Guid PostId) : IRequest<ErrorOr<Post>>;
public class GetPostByIdCommandHandler(IApplicationDbContext dbContext) : IRequestHandler<GetPostByIdCommand, ErrorOr<Post>>
{
public async Task<ErrorOr<Post>> Handle(GetPostByIdCommand request, CancellationToken cancellationToken)
{
var postIdToFind = request.PostId;
var post = await dbContext
.Posts
.FindAsync(new object?[] {postIdToFind}, cancellationToken: cancellationToken);
if (post is null) return PostErrors.PostNotFound(postIdToFind);
return post;
}
}
using MediatR;
using SocialNetwork.Social.Application.Common.Interfaces;
using SocialNetwork.Social.Domain.Entities.Feed;
namespace SocialNetwork.Social.Application.Posts.Commands;
public class GetPostsCommand : IRequest<List<Post>>; // Could include a filter and pagination here
public class GetPostsCommandHandler(IApplicationDbContext dbContext) : IRequestHandler<GetPostsCommand, List<Post>>
{
public Task<List<Post>> Handle(GetPostsCommand request, CancellationToken cancellationToken) =>
dbContext
.Posts
.ToListAsync(cancellationToken: cancellationToken);
}
using ErrorOr;
using MediatR;
using SocialNetwork.Social.Application.Common.Interfaces;
namespace SocialNetwork.Social.Application.Posts.Commands;
public record LikePostCommand(Guid PostId, Guid LikerId) : IRequest<ErrorOr<Success>>;
public class LikePostCommandHandler(IApplicationDbContext dbContext) : IRequestHandler<LikePostCommand, ErrorOr<Success>>
{
public async Task<ErrorOr<Success>> Handle(LikePostCommand request, CancellationToken cancellationToken)
{
var requestPostId = request.PostId;
var post = await dbContext.Posts.FindAsync(requestPostId);
if (post is null) return PostErrors.PostNotFound(requestPostId);
post.LikesCount++; // I would create a whole table for likers and postId to add who liked the post
// TODO fire post liked event
await dbContext.SaveChangesAsync(cancellationToken);
return Result.Success;
}
}
using ErrorOr;
namespace SocialNetwork.Social.Application.Posts;
public static class PostErrors
{
public static Error PostNotFound(Guid postId) =>
Error.NotFound("PostNotFound", $"Post with id: {postId} not found");
}
......@@ -5,8 +5,4 @@
<AssemblyName>SocialNetwork.Social.Domain</AssemblyName>
</PropertyGroup>
<ItemGroup>
<Folder Include="ValueObjects\" />
</ItemGroup>
</Project>
namespace SocialNetwork.Social.Domain.ValueObjects;
public class SocialProfile
{
public Guid Id { get; set; }
public required string Name { get; set; }
public required string Email { get; set; }
}
using Microsoft.EntityFrameworkCore;
using SocialNetwork.Social.Application.Common.Interfaces;
using SocialNetwork.Social.Domain.Entities.Feed;
using ErrorOr;
using MediatR;
using SocialNetwork.Social.Application.Posts.Commands;
namespace WebApi.Endpoints;
......@@ -19,58 +19,61 @@ public class PostEndpoints
builder.MapPost("/{postId:guid}/like", LikePost);
}
private async Task<IResult> GetAllPosts(IApplicationDbContext dbContext) =>
Results.Ok(await dbContext.Posts
.Include(post => post.Comments) // normally we wouldn't include comments here, but for demo purposes it's fine
.ToListAsync());
private async Task<IResult> GetAllPosts(ISender sender) =>
Results.Ok(await sender.Send(new GetPostsCommand()));
private async Task<IResult> GetPostById(Guid postId, IApplicationDbContext dbContext)
private async Task<IResult> GetPostById(Guid postId, ISender sender)
{
var post = await dbContext.Posts
.Include(post => post.Comments)
.FirstOrDefaultAsync(post => post.Id == postId);
if (post is null) return Results.NotFound();
return Results.Ok(post);
var errorOrPost = await sender.Send(new GetPostByIdCommand(postId));
return errorOrPost.MatchFirst(
Results.Ok,
error => error.Type switch
{
ErrorType.NotFound => Results.NotFound(error.Description),
_ => Results.BadRequest(error.Description)
}
);
}
private async Task<IResult> CommentPost(Guid postId, Comment comment, IApplicationDbContext dbContext)
private async Task<IResult> CreatePost(PublishPostCommand command, ISender sender)
{
var post = await dbContext.Posts.FindAsync(postId);
if (post is null) return Results.NotFound("Post not found");
if (post.Comments.Any(c => c.Id == comment.Id)) return Results.Conflict("Comment already exists");
comment.PostId = postId;
await dbContext.Comments.AddAsync(comment);
post.Comments.Add(comment);
await dbContext.SaveChangesAsync();
dbContext.Posts.Update(post);
await dbContext.SaveChangesAsync();
return Results.Created($"api/posts/{postId}", comment);
var errorOrCreatedPost = await sender.Send(command);
return errorOrCreatedPost.MatchFirst(
createdPost => Results.Created($"/api/posts/{createdPost.Id}", createdPost),
error => error.Type switch
{
ErrorType.Validation => Results.BadRequest(error.Description),
ErrorType.Failure => Results.Problem(error.Description),
_ => Results.BadRequest(error.Description)
}
);
}
private async Task<IResult> CreatePost(Post post, IApplicationDbContext dbContext)
private async Task<IResult> CommentPost(Guid postId, CommentPostCommand command, ISender sender)
{
var doesPostExist = await dbContext.Posts.AnyAsync(p => p.Id == post.Id);
if (doesPostExist) return Results.Conflict();
var entry = dbContext.Posts.Add(post);
await dbContext.SaveChangesAsync();
var persistedPost = entry.Entity;
return Results.Created($"/api/posts/{persistedPost.Id}", persistedPost);
var errorOrComment = await sender.Send(command with {PostId = postId});
return errorOrComment.MatchFirst(
comment => Results.Created($"api/posts/{postId}", comment),
error => error.Type switch
{
ErrorType.Validation => Results.BadRequest(error.Description),
ErrorType.NotFound => Results.NotFound(error.Description),
_ => Results.BadRequest(error.Description)
}
);
}
private async Task<IResult> LikePost(Guid postId, Guid likerId, IApplicationDbContext dbContext)
private async Task<IResult> LikePost(Guid postId, LikePostCommand command, ISender sender)
{
var post = await dbContext.Posts.FindAsync(postId);
if (post is null) return Results.NotFound();
post.LikesCount++;
// TODO add who liked the post
await dbContext.SaveChangesAsync();
return Results.Ok();
var errorOrSuccess = await sender.Send(command with {PostId = postId});
return errorOrSuccess.MatchFirst(
_ => Results.Ok(),
error => error.Type switch
{
ErrorType.Validation => Results.BadRequest(error.Description),
ErrorType.NotFound => Results.NotFound(error.Description),
_ => Results.BadRequest(error.Description)
}
);
}
}