Skip to content

Commit

Permalink
Add Validation Pipeline and CustomExceptions.
Browse files Browse the repository at this point in the history
  • Loading branch information
mohammadKarimi committed Jan 12, 2024
1 parent 502e360 commit 5cb16e2
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 38 deletions.
58 changes: 23 additions & 35 deletions src/SwiftLink.Application/Behaviors/ValidationBehavior.cs
Original file line number Diff line number Diff line change
@@ -1,47 +1,35 @@
using Azure;
using FluentValidation;
using FluentValidation;
using MediatR;
using System.Net;
using SwiftLink.Application.Common;
using SwiftLink.Application.Common.Exceptions;

namespace SwiftLink.Application.Behaviors;

/// <summary>
/// Pipie line for Validation.
/// </summary>
/// <typeparam name="TRequest"></typeparam>
/// <typeparam name="TResult"></typeparam>
public class ValidationBehavior<TRequest, TResult>(IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResult> where TRequest : notnull
public sealed class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators = validators;

public async Task<TResult> Handle(TRequest request, RequestHandlerDelegate<TResult> next,
CancellationToken ct = default)
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (!_validators.Any())
return await next();

var validationContext = new ValidationContext<TRequest>(request);

var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(validationContext, ct)));
var context = new ValidationContext<TRequest>(request);

var errorList = validationResults.Where(r => r.Errors.Count != 0)
.SelectMany(x => x.Errors)
.ToList();
var validationFailures = await Task.WhenAll(_validators.Select(validator => validator.ValidateAsync(context)));

if (errorList.Count > 0)
{
var message = string.Join(" | ", errorList.Select(e => e.ErrorMessage));
var errors = validationFailures
.Where(validationResult => !validationResult.IsValid)
.SelectMany(validationResult => validationResult.Errors)
.Select(validationFailure => new ValidationError(
validationFailure.PropertyName,
validationFailure.ErrorMessage))
.ToList();

if (Enum.TryParse(errorList.First().ErrorCode, out HttpStatusCode statusCode))
return GenerateResponse(message, statusCode);
if (errors.Count != 0)
throw new BusinessValidationException(errors);

return GenerateResponse(message);
}

return await next();
var response = await next();
return response;
}

private static TResult GenerateResponse(string message, HttpStatusCode statusCode = HttpStatusCode.BadRequest)
=> (TResult)Activator.CreateInstance(typeof(Result<object>), false, message, statusCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace SwiftLink.Application.Common.Exceptions;
public class BusinessValidationException(List<ValidationError> errors) : Exception
{
public readonly List<ValidationError> Errors = errors;
}
2 changes: 2 additions & 0 deletions src/SwiftLink.Application/Common/ValidationError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
namespace SwiftLink.Application.Common;
public record ValidationError(string PropertyName, string ErrorMessage);
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class GenerateShortCodeCommandHandler(IApplicationDbContext dbContext,

public async Task<Result<object>> Handle(GenerateShortCodeCommand request, CancellationToken cancellationToken = default)
{
var subscriber = _dbContext.Set<Subscriber>().Find(request.Token);
var link = new Link
{
OriginalUrl = request.Url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ public GenerateShortCodeValidator()
.NotNull().WithMessage(Constants.Link.UrlMustBeSent)
.Must(BeAValidUrl).WithMessage(Constants.Link.InvalidUrlFormat);

RuleFor(x => x.Token)
.NotNull().WithMessage(Constants.Link.UrlMustBeSent)
.Must(BeAValidUrl).WithMessage(Constants.Link.InvalidUrlFormat);
//RuleFor(x => x.Token)
// .NotNull().WithMessage(Constants.Link.UrlMustBeSent)
// .Must(BeAValidUrl).WithMessage(Constants.Link.InvalidUrlFormat);
}

private bool BeAValidUrl(string url)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using SwiftLink.Application.Common.Exceptions;
using System;

namespace SwiftLink.Presentation.Middleware;

public sealed class BusinessValidationExceptionHandlingMiddleware(RequestDelegate next)
{
private readonly RequestDelegate _next = next;

public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (BusinessValidationException exception)
{
var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status400BadRequest,
Type = "ValidationFailure",
Title = "Validation error",
Detail = "One or more validation errors has occurred"
};

if (exception.Errors is not null)
problemDetails.Extensions["errors"] = exception.Errors;

context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsJsonAsync(problemDetails);
}
}
}
2 changes: 2 additions & 0 deletions src/SwiftLink.Presentation/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using SwiftLink.Infrastructure;
using SwiftLink.Infrastructure.Persistence.Context;
using SwiftLink.Presentation;
using SwiftLink.Presentation.Middleware;
using SwiftLink.Shared;

var builder = WebApplication.CreateBuilder(args);
Expand Down Expand Up @@ -41,6 +42,7 @@

var app = builder.Build();
{
app.UseMiddleware<BusinessValidationExceptionHandlingMiddleware>();
app.UseExceptionHandler(error =>
{
error.Run(async context =>
Expand Down

0 comments on commit 5cb16e2

Please sign in to comment.