Commit 041d69cc authored by Almouhannad's avatar Almouhannad

(B) Link validation, implement Create employee case

parent 22134c5c
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" />
<PackageReference Include="MediatR" Version="12.4.0" /> <PackageReference Include="MediatR" Version="12.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
......
using API.Options.Database; using API.Options.Database;
using API.SeedDatabaseHelper; using API.SeedDatabaseHelper;
using Application.Behaviors;
using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Persistence.Context; using Persistence.Context;
...@@ -49,6 +52,12 @@ builder ...@@ -49,6 +52,12 @@ builder
#region Add MadiatR #region Add MadiatR
builder.Services.AddMediatR(configuration => builder.Services.AddMediatR(configuration =>
configuration.RegisterServicesFromAssembly(Application.AssemblyReference.Assembly)); configuration.RegisterServicesFromAssembly(Application.AssemblyReference.Assembly));
#region Add validation pipeline
builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationPipelineBehavior<,>));
builder.Services.AddValidatorsFromAssembly(Application.AssemblyReference.Assembly);
#endregion
#endregion #endregion
#region Link controllers with presentation layer #region Link controllers with presentation layer
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentValidation" Version="11.9.2" />
<PackageReference Include="MediatR" Version="12.4.0" /> <PackageReference Include="MediatR" Version="12.4.0" />
</ItemGroup> </ItemGroup>
......
using Domain.Shared.Validation;
using Domain.Shared;
using MediatR;
using FluentValidation;
namespace Application.Behaviors;
// Using MediatR pipeline to perform validation
// Using Fluent validation (IValidator) to make validators
public class ValidationPipelineBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse> // From MediatR
where TRequest : IRequest<TResponse> // Request sent by the pipeline (enforced by imp. of CQRS)
where TResponse : Result // Type of response (enforced by imp. of CQRS)
{
#region CTOR DI for validators
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationPipelineBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
#endregion
#region Create validation result
private static TResult CreateValidationResult<TResult>(Error[] errors)
where TResult : Result
{
// Simple case, Result only
if (typeof(TResult) == typeof(Result))
{
return (ValidationResult.WithErrors(errors) as TResult)!;
// This won't fail ever
}
// Other complicated case, we have a Result<Type>
// Using reflection
object validationResult = typeof(ValidationResult<>)
.GetGenericTypeDefinition()
.MakeGenericType(typeof(TResult).GenericTypeArguments[0])
.GetMethod(nameof(ValidationResult.WithErrors))!
.Invoke(null, [errors])!;
return (TResult)validationResult;
}
#endregion
#region Handle method (pipeline behavior)
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
// Validate request
// If any errors, return validation request
// Otherwise,return next() [Pipline ~ Middleware]
// Case of no validators
if (!_validators.Any())
{
return await next();
}
// Generate errors array
Error[] errors = _validators
.Select(validator => validator.Validate(request)) // Select the result of validate method
.SelectMany(validationResult => validationResult.Errors)
.Where(validationFailure => validationFailure is not null)
.Select(failure => new Error( // Project into Error object
failure.PropertyName,
failure.ErrorMessage))
.Distinct()
.ToArray();
if (errors.Length != 0)
{
return CreateValidationResult<TResponse>(errors);
// Static method in this class
}
return await next();
}
#endregion
}
using Domain.ValidationConstants.ErrorMessages;
using Domain.ValidationConstants.RegularExpressions;
using FluentValidation;
namespace Application.Employees.Commands.Create;
public class CreateEmployeeCommandValidator : AbstractValidator<CreateEmployeeCommand>
{
public CreateEmployeeCommandValidator()
{
#region First name
RuleFor(c => c.FirstName)
.NotEmpty()
.WithMessage(ValidationErrorMessages.Required);
RuleFor(c => c.FirstName)
.MaximumLength(50)
.WithMessage(ValidationErrorMessages.FixedLength);
RuleFor(c => c.FirstName)
.Matches(ValidationRegularExpressions.ArabicLettersOnly)
.WithMessage(ValidationErrorMessages.ArabicLettersOnly);
#endregion
#region Middle name
RuleFor(c => c.MiddleName)
.NotEmpty()
.WithMessage(ValidationErrorMessages.Required);
RuleFor(c => c.MiddleName)
.MaximumLength(50)
.WithMessage(ValidationErrorMessages.FixedLength);
RuleFor(c => c.MiddleName)
.Matches(ValidationRegularExpressions.ArabicLettersOnly)
.WithMessage(ValidationErrorMessages.ArabicLettersOnly);
#endregion
#region Last name
RuleFor(c => c.LastName)
.NotEmpty()
.WithMessage(ValidationErrorMessages.Required);
RuleFor(c => c.LastName)
.MaximumLength(50)
.WithMessage(ValidationErrorMessages.FixedLength);
RuleFor(c => c.LastName)
.Matches(ValidationRegularExpressions.ArabicLettersOnly)
.WithMessage(ValidationErrorMessages.ArabicLettersOnly);
#endregion
#region Serial number
RuleFor(c => c.SerialNumber)
.NotEmpty()
.WithMessage(ValidationErrorMessages.Required);
RuleFor(c => c.SerialNumber)
.Matches(ValidationRegularExpressions.NumbersOnly)
.WithMessage(ValidationErrorMessages.NumbersOnly);
RuleFor(c => c.SerialNumber)
.MaximumLength(20)
.WithMessage(ValidationErrorMessages.FixedLength);
#endregion
#region Center status
RuleFor(c => c.CenterStatus)
.NotEmpty()
.WithMessage(ValidationErrorMessages.Required);
RuleFor(c => c.CenterStatus)
.Matches(ValidationRegularExpressions.ArabicLettersOnly)
.WithMessage(ValidationErrorMessages.ArabicLettersOnly);
RuleFor(c => c.CenterStatus)
.MaximumLength(50)
.WithMessage(ValidationErrorMessages.FixedLength);
#endregion
// TODO: add validation rules for additional fields
}
}
namespace Domain.ValidationConstants.ErrorMessages;
public static class ValidationErrorMessages
{
public static string Required
=> "هذا الحقل مطلوب";
public static string ArabicOrEnglishLettersOnly
=> "يجب أن يحوي هذا الحقل على أحرف عربية فقط أو أحرف انكليزية فقط ولا يحوي أي رموز أو أرقام";
public static string ArabicLettersOnly
=> "يجب أن يحوي هذا الحقل على أحرف عربية فقط ولا يحوي أي رموز أو أرقام";
public static string FixedLength
=> "هذا الحقل طويل للغاية";
public static string NumbersOnly
=> "يجب أن يحوي هذا الحقل على أرقام فقط";
}
namespace Domain.ValidationConstants.RegularExpressions;
public static class ValidationRegularExpressions
{
public static string ArabicOrEnglishLettersOnly
=> @"^[\u0600-\u06ff\s]+$|^[a-zA-Z\s]+$";
public static string ArabicLettersOnly
=> @"^[א-׿ء-ي]+$";
public static string EmailAddress
=> @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$";
public static string NumbersOnly
=> @"^\d+$";
}
using Domain.Shared.Validation;
using Domain.Shared;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Presentation.Controllers.Base;
[ApiController]
public abstract class ApiController : ControllerBase
{
#region CTOR DI for MediatR sendse
protected readonly ISender _sender;
protected ApiController(ISender sender)
{
_sender = sender;
}
#endregion
protected IActionResult HandleFailure(Result result) =>
result switch
{
{ IsSuccess: true } => throw new InvalidOperationException(),
IValidationResult validationResult =>
BadRequest(
CreateProblemDetails(
"Validation Error", StatusCodes.Status400BadRequest,
result.Error,
validationResult.Errors)),
_ =>
BadRequest(
CreateProblemDetails(
"Bad Request",
StatusCodes.Status400BadRequest,
result.Error))
};
private static ProblemDetails CreateProblemDetails(
string title,
int status,
Error error,
Error[]? errors = null) =>
new()
{
Title = title,
Type = error.Code,
Detail = error.Message,
Status = status,
Extensions = { { nameof(errors), errors } }
};
}
\ No newline at end of file
using Application.Employees.Commands.Create; using Application.Employees.Commands.Create;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Presentation.Controllers.Base;
namespace Presentation.Controllers; namespace Presentation.Controllers;
[Route("api/[controller]")] [Route("api/Employees")]
[ApiController] public class EmployeesController : ApiController
public class EmployeesController : ControllerBase
{ {
#region DI for MeditR sender #region DI for MeditR sender
private readonly ISender _sender; public EmployeesController(ISender sender) : base(sender)
public EmployeesController(ISender sender)
{ {
_sender = sender;
} }
#endregion #endregion
...@@ -21,8 +20,8 @@ public class EmployeesController : ControllerBase ...@@ -21,8 +20,8 @@ public class EmployeesController : ControllerBase
public async Task<IActionResult> Create(CreateEmployeeCommand command) public async Task<IActionResult> Create(CreateEmployeeCommand command)
{ {
var result = await _sender.Send(command); var result = await _sender.Send(command);
if (result.IsSuccess) if (result.IsFailure)
return Created(); return HandleFailure(result);
else return BadRequest(result.Error.Message); return Created();
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment