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 @@ ...@@ -4,44 +4,47 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Ardalis.GuardClauses" Version="4.3.0"/> <PackageVersion Include="Ardalis.GuardClauses" Version="4.3.0" />
<PackageVersion Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1"/> <PackageVersion Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.0"/> <PackageVersion Include="coverlet.collector" Version="6.0.0" />
<PackageVersion Include="FluentAssertions" Version="6.12.0"/> <PackageVersion Include="ErrorOr" Version="1.9.0" />
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.0"/> <PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0"/> <PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.1"/> <PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1"/> <PackageVersion Include="MediatR" Version="12.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.1"/> <PackageVersion Include="MediatR.Extensions.FluentValidation.AspNetCore" Version="5.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="8.0.1"/> <PackageVersion Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.0"/> <PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.1"/> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" 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"> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion> </PackageVersion>
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0"/> <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" 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.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0"/> <PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageVersion Include="NSubstitute" Version="5.1.0"/> <PackageVersion Include="NSubstitute" Version="5.1.0" />
<PackageVersion Include="NSwag.AspNetCore" Version="14.0.1"/> <PackageVersion Include="NSwag.AspNetCore" Version="14.0.1" />
<PackageVersion Include="NSwag.MSBuild" Version="14.0.1"> <PackageVersion Include="NSwag.MSBuild" Version="14.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion> </PackageVersion>
<PackageVersion Include="nunit" Version="4.0.1"/> <PackageVersion Include="nunit" Version="4.0.1" />
<PackageVersion Include="NUnit.Analyzers" Version="3.10.0"> <PackageVersion Include="NUnit.Analyzers" Version="3.10.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion> </PackageVersion>
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0"/> <PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageVersion Include="Respawn" Version="6.2.0"/> <PackageVersion Include="Respawn" Version="6.2.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0"/> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageVersion Include="Testcontainers.MsSql" Version="3.7.0"/> <PackageVersion Include="Testcontainers.MsSql" Version="3.7.0" />
<PackageVersion Include="ZymLabs.NSwag.FluentValidation.AspNetCore" Version="0.6.2"/> <PackageVersion Include="ZymLabs.NSwag.FluentValidation.AspNetCore" Version="0.6.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>
\ No newline at end of file
services: services:
webapi: webapi:
image: webapi
build: build:
context: . context: .
dockerfile: src/WebApi/Dockerfile dockerfile: src/WebApi/Dockerfile
......
...@@ -8,7 +8,10 @@ ...@@ -8,7 +8,10 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Ardalis.GuardClauses" /> <PackageReference Include="Ardalis.GuardClauses" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" />
<PackageReference Include="ErrorOr" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" /> <PackageReference Include="FluentValidation.DependencyInjectionExtensions" />
<PackageReference Include="MediatR" />
<PackageReference Include="MediatR.Extensions.FluentValidation.AspNetCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore" /> <PackageReference Include="Microsoft.EntityFrameworkCore" />
</ItemGroup> </ItemGroup>
......
using System.Reflection; using System.Reflection;
using MediatR;
using MediatR.Extensions.FluentValidation.AspNetCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace SocialNetwork.Social.Application; namespace SocialNetwork.Social.Application;
...@@ -11,6 +13,12 @@ public static class DependencyInjection ...@@ -11,6 +13,12 @@ public static class DependencyInjection
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
});
return services; 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 @@ ...@@ -5,8 +5,4 @@
<AssemblyName>SocialNetwork.Social.Domain</AssemblyName> <AssemblyName>SocialNetwork.Social.Domain</AssemblyName>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Folder Include="ValueObjects\" />
</ItemGroup>
</Project> </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 ErrorOr;
using SocialNetwork.Social.Application.Common.Interfaces; using MediatR;
using SocialNetwork.Social.Domain.Entities.Feed; using SocialNetwork.Social.Application.Posts.Commands;
namespace WebApi.Endpoints; namespace WebApi.Endpoints;
...@@ -19,58 +19,61 @@ public class PostEndpoints ...@@ -19,58 +19,61 @@ public class PostEndpoints
builder.MapPost("/{postId:guid}/like", LikePost); builder.MapPost("/{postId:guid}/like", LikePost);
} }
private async Task<IResult> GetAllPosts(IApplicationDbContext dbContext) => private async Task<IResult> GetAllPosts(ISender sender) =>
Results.Ok(await dbContext.Posts Results.Ok(await sender.Send(new GetPostsCommand()));
.Include(post => post.Comments) // normally we wouldn't include comments here, but for demo purposes it's fine
.ToListAsync());
private async Task<IResult> GetPostById(Guid postId, IApplicationDbContext dbContext) private async Task<IResult> GetPostById(Guid postId, ISender sender)
{ {
var post = await dbContext.Posts var errorOrPost = await sender.Send(new GetPostByIdCommand(postId));
.Include(post => post.Comments) return errorOrPost.MatchFirst(
.FirstOrDefaultAsync(post => post.Id == postId); Results.Ok,
if (post is null) return Results.NotFound(); error => error.Type switch
{
return Results.Ok(post); 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); var errorOrCreatedPost = await sender.Send(command);
if (post is null) return Results.NotFound("Post not found"); return errorOrCreatedPost.MatchFirst(
if (post.Comments.Any(c => c.Id == comment.Id)) return Results.Conflict("Comment already exists"); createdPost => Results.Created($"/api/posts/{createdPost.Id}", createdPost),
error => error.Type switch
comment.PostId = postId; {
await dbContext.Comments.AddAsync(comment); ErrorType.Validation => Results.BadRequest(error.Description),
post.Comments.Add(comment); ErrorType.Failure => Results.Problem(error.Description),
await dbContext.SaveChangesAsync(); _ => Results.BadRequest(error.Description)
}
dbContext.Posts.Update(post); );
await dbContext.SaveChangesAsync();
return Results.Created($"api/posts/{postId}", comment);
} }
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); var errorOrComment = await sender.Send(command with {PostId = postId});
if (doesPostExist) return Results.Conflict(); return errorOrComment.MatchFirst(
comment => Results.Created($"api/posts/{postId}", comment),
var entry = dbContext.Posts.Add(post); error => error.Type switch
await dbContext.SaveChangesAsync(); {
ErrorType.Validation => Results.BadRequest(error.Description),
var persistedPost = entry.Entity; ErrorType.NotFound => Results.NotFound(error.Description),
return Results.Created($"/api/posts/{persistedPost.Id}", persistedPost); _ => 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); var errorOrSuccess = await sender.Send(command with {PostId = postId});
if (post is null) return Results.NotFound(); return errorOrSuccess.MatchFirst(
_ => Results.Ok(),
post.LikesCount++; error => error.Type switch
// TODO add who liked the post {
await dbContext.SaveChangesAsync(); ErrorType.Validation => Results.BadRequest(error.Description),
return Results.Ok(); ErrorType.NotFound => Results.NotFound(error.Description),
_ => Results.BadRequest(error.Description)
}
);
} }
} }