Skip to content

Commit

Permalink
feat: add validation with result error pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
OrangeBurrito committed Jul 26, 2024
1 parent 30ce544 commit dd572da
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 23 deletions.
20 changes: 20 additions & 0 deletions src/Common/Error.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

using FluentValidation.Results;

namespace WomensWiki;

public class Error {
public string Code { get; private set; } = null!;
public string? Message { get; private set; }

public Error(string code, string? message = null) {
Code = code;
Message = message;
}
}

public static class ErrorMapper {
public static List<Error> Map(ValidationResult validationResult) {
return validationResult.Errors.Select(e => new Error(e.ErrorCode, e.ErrorMessage)).ToList();
}
}
23 changes: 23 additions & 0 deletions src/Common/Result.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace WomensWiki;

public static class Result {
public static Result<T> Success<T>(T data) => Result<T>.Success(data);
public static Result<T> Failure<T>(List<Error> errors) => Result<T>.Failure(errors);
}

public class Result<T> {
public bool IsSuccess { get; private set; }
public bool IsFailure => !IsSuccess;

public T? Data { get; private set; }
public List<Error>? Errors { get; private set; }

private Result(bool isSuccess, T? data, List<Error>? errors = null) {
IsSuccess = isSuccess;
Data = data;
Errors = errors;
}

public static Result<T> Success(T data) => new(true, data, null);
public static Result<T> Failure(List<Error> errors) => new(false, default, errors);
}
16 changes: 16 additions & 0 deletions src/Domain/Articles/ArticleValidators.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using FluentValidation;
using FluentValidation.Results;

namespace WomensWiki.Domain.Articles;

public static class ArticleValidators {
public static IRuleBuilderOptions<T, Guid> ArticleExists<T>(this IRuleBuilder<T, Guid> ruleBuilder) {
return (IRuleBuilderOptions<T, Guid>)ruleBuilder.Custom((value, context) => {
if (context.RootContextData["Article"] == null) {
var failure = new ValidationFailure("ArticleId", "Article with that ID does not exist");
failure.ErrorCode = "ArticleNotFound";
context.AddFailure(failure);
}
});
}
}
25 changes: 20 additions & 5 deletions src/Features/Articles/CreateArticle.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore;
using WomensWiki.Common;
Expand All @@ -7,23 +8,37 @@
namespace WomensWiki.Features.Articles;

public static class CreateArticle {
public record CreateArticleCommand(string Author, string Title, string Content) : IRequest<CreateArticleResponse>;
public record CreateArticleCommand(string Author, string Title, string Content) : IRequest<Result<CreateArticleResponse>>;

internal sealed class CreateArticleHandler(AppDbContext dbContext) : IRequestHandler<CreateArticleCommand, CreateArticleResponse> {
public async Task<CreateArticleResponse> Handle(CreateArticleCommand request, CancellationToken cancellationToken) {
public class CreateArticleValidator : AbstractValidator<CreateArticleCommand> {
public CreateArticleValidator() {
RuleFor(x => x.Author).NotEmpty();
RuleFor(x => x.Title).NotEmpty().MinimumLength(3);
RuleFor(x => x.Content).NotEmpty().MinimumLength(25);
}
}

internal sealed class CreateArticleHandler(AppDbContext dbContext, CreateArticleValidator validator)
: IRequestHandler<CreateArticleCommand, Result<CreateArticleResponse>> {
public async Task<Result<CreateArticleResponse>> Handle(CreateArticleCommand request, CancellationToken cancellationToken) {
var author = await dbContext.Users.SingleAsync(u => u.Username == request.Author);

var validationResult = await validator.ValidateAsync(request, cancellationToken);
if (!validationResult.IsValid) {
return Result.Failure<CreateArticleResponse>(ErrorMapper.Map(validationResult));
}

var article = Article.Create(request.Title, request.Content);
await dbContext.Articles.AddAsync(article);
await dbContext.SaveChangesAsync();

return new CreateArticleResponse(article.Id, article.CreatedAt, article.Title, article.Content);
return Result.Success(new CreateArticleResponse(article.Id, article.CreatedAt, article.Title, article.Content));
}
}

[MutationType]
public class CreateArticleMutation {
public async Task<CreateArticleResponse> CreateArticleAsync([Service] ISender sender, CreateArticleCommand input) {
public async Task<Result<CreateArticleResponse>> CreateArticleAsync([Service] ISender sender, CreateArticleCommand input) {
return await sender.Send(input);
}
}
Expand Down
27 changes: 22 additions & 5 deletions src/Features/Articles/GetArticleById.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,40 @@
using FluentValidation;
using MediatR;
using WomensWiki.Common;
using WomensWiki.Contracts;
using WomensWiki.Domain.Articles;

namespace WomensWiki.Features.Articles;

public static class GetArticleById {
public record GetArticleByIdRequest(Guid Id) : IRequest<ArticleResponse>;
public record GetArticleByIdRequest(Guid Id) : IRequest<Result<ArticleResponse>>;

internal sealed class GetArticleByIdHandler(AppDbContext dbContext) : IRequestHandler<GetArticleByIdRequest, ArticleResponse> {
public async Task<ArticleResponse> Handle(GetArticleByIdRequest request, CancellationToken cancellationToken) {
public class GetArticleByIdValidator : AbstractValidator<GetArticleByIdRequest> {
public GetArticleByIdValidator() {
RuleFor(x => x.Id).ArticleExists();
}
}

internal sealed class GetArticleByIdHandler(AppDbContext dbContext, GetArticleByIdValidator validator)
: IRequestHandler<GetArticleByIdRequest, Result<ArticleResponse>> {
public async Task<Result<ArticleResponse>> Handle(GetArticleByIdRequest request, CancellationToken cancellationToken) {
var article = await dbContext.Articles.FindAsync(request.Id);

var context = new ValidationContext<GetArticleByIdRequest>(request);
context.RootContextData["Article"] = article;
var validationResult = await validator.ValidateAsync(context);

if (!validationResult.IsValid) {
return Result.Failure<ArticleResponse>(ErrorMapper.Map(validationResult));
}

return ArticleResponse.FromArticle(article);
return Result.Success(ArticleResponse.FromArticle(article));

Check warning on line 31 in src/Features/Articles/GetArticleById.cs

View workflow job for this annotation

GitHub Actions / publish

Possible null reference argument for parameter 'article' in 'ArticleResponse ArticleResponse.FromArticle(Article article)'.

Check warning on line 31 in src/Features/Articles/GetArticleById.cs

View workflow job for this annotation

GitHub Actions / publish

Possible null reference argument for parameter 'article' in 'ArticleResponse ArticleResponse.FromArticle(Article article)'.
}
}

[QueryType]
public class GetArticleByIdQuery {
public async Task<ArticleResponse> GetArticleByIdAsync([Service] ISender sender, GetArticleByIdRequest input) {
public async Task<Result<ArticleResponse>> GetArticleByIdAsync([Service] ISender sender, GetArticleByIdRequest input) {
return await sender.Send(input);
}
}
Expand Down
37 changes: 24 additions & 13 deletions src/Features/Articles/UpdateArticle.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,47 @@
using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore;
using WomensWiki.Common;
using WomensWiki.Contracts;
using WomensWiki.Domain.Articles;

namespace WomensWiki.Features;

public static class UpdateArticle {
public record UpdateArticleCommand(Guid ArticleId, string Author, string Content, string? Summary = null) : IRequest<ArticleResponse>;
public record UpdateArticleCommand(Guid ArticleId, string Author, string Content, string? Summary = null) : IRequest<Result<ArticleResponse>>;

internal sealed class UpdateArticleHandler(AppDbContext dbContext) : IRequestHandler<UpdateArticleCommand, ArticleResponse> {
public async Task<ArticleResponse> Handle(UpdateArticleCommand request, CancellationToken cancellationToken) {
public class UpdateArticleValidator : AbstractValidator<UpdateArticleCommand> {
public UpdateArticleValidator() {
RuleFor(x => x.ArticleId).ArticleExists();
RuleFor(x => x.Author).NotEmpty();
RuleFor(x => x.Content).NotEmpty().MinimumLength(25);
}
}

internal sealed class UpdateArticleHandler(AppDbContext dbContext, UpdateArticleValidator validator)
: IRequestHandler<UpdateArticleCommand, Result<ArticleResponse>> {
public async Task<Result<ArticleResponse>> Handle(UpdateArticleCommand request, CancellationToken cancellationToken) {
var article = await dbContext.Articles.Include(a => a.History).SingleAsync(a => a.Id == request.ArticleId);
// if article
if (article == null) {
Console.WriteLine($"Article is null");
} else {
Console.WriteLine($"Article: {article.Id}");
}
var author = await dbContext.Users.SingleAsync(u => u.Username == request.Author);

var context = new ValidationContext<UpdateArticleCommand>(request);
context.RootContextData["Article"] = article;
var validationResult = await validator.ValidateAsync(context);

if (!validationResult.IsValid) {
return Result.Failure<ArticleResponse>(ErrorMapper.Map(validationResult));
}

article.Update(author, request.Content, request.Summary);
await dbContext.SaveChangesAsync();
Console.WriteLine("Updated article");

return ArticleResponse.FromArticle(article);
Console.WriteLine("Returned response");
return Result.Success(ArticleResponse.FromArticle(article));
}
}

[MutationType]
public class UpdateArticleMutation {
public async Task<ArticleResponse> UpdateArticleAsync([Service] ISender sender, UpdateArticleCommand input) {
public async Task<Result<ArticleResponse>> UpdateArticleAsync([Service] ISender sender, UpdateArticleCommand input) {
return await sender.Send(input);
}
}
Expand Down

0 comments on commit dd572da

Please sign in to comment.