Add fluent validation

This commit is contained in:
Alex Hyett 2023-07-04 14:48:06 +01:00
parent d81f8eb46f
commit ad6e0d2015
9 changed files with 118 additions and 22 deletions

View file

@ -1,7 +1,9 @@
using System.Runtime.Intrinsics.X86;
using GitHubActionsDemo.Api.Models; using GitHubActionsDemo.Api.Models;
using GitHubActionsDemo.Api.Mappers; using GitHubActionsDemo.Api.Mappers;
using GitHubActionsDemo.Service; using GitHubActionsDemo.Service;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using FluentValidation;
namespace GitHubActionsDemo.Api.Controllers; namespace GitHubActionsDemo.Api.Controllers;
@ -10,42 +12,59 @@ namespace GitHubActionsDemo.Api.Controllers;
public class AuthorsController : BaseController public class AuthorsController : BaseController
{ {
private readonly ILibraryService _libraryService; private readonly ILibraryService _libraryService;
private readonly IValidator<AuthorRequest> _authorValidator;
private readonly IValidator<PageParameters> _pageValidator;
public AuthorsController( public AuthorsController(
ILibraryService libraryService ILibraryService libraryService,
IValidator<AuthorRequest> authorValidator,
IValidator<PageParameters> pageValidator
) )
{ {
_libraryService = libraryService ?? throw new ArgumentNullException(nameof(libraryService)); _libraryService = libraryService ?? throw new ArgumentNullException(nameof(libraryService));
_authorValidator = authorValidator ?? throw new ArgumentNullException(nameof(authorValidator));
_pageValidator = pageValidator ?? throw new ArgumentNullException(nameof(pageValidator));
} }
[HttpPost] [HttpPost]
public async Task<IActionResult> AddAuthorAsync([FromBody] AuthorRequest authorRequest) public async Task<IResult> AddAuthorAsync([FromBody] AuthorRequest authorRequest)
{ {
var validationResult = await _authorValidator.ValidateAsync(authorRequest);
if (!validationResult.IsValid)
return Results.ValidationProblem(validationResult.ToDictionary());
var result = await _libraryService.AddAuthorAsync(authorRequest.Map()); var result = await _libraryService.AddAuthorAsync(authorRequest.Map());
return result.Match( return result.Match(
success => Ok(success.Value.Map()), success => Results.Ok(success.Value.Map()),
error => InternalError() error => InternalError()
); );
} }
[HttpGet("{authorId}")] [HttpGet("{authorId}")]
public async Task<IActionResult> GetAuthorAsync(int authorId) public async Task<IResult> GetAuthorAsync(int authorId)
{ {
var result = await _libraryService.GetAuthorAsync(authorId); var result = await _libraryService.GetAuthorAsync(authorId);
return result.Match( return result.Match(
success => Ok(success.Value.Map()), success => Results.Ok(success.Value.Map()),
notfound => NotFound(), notfound => Results.NotFound(),
error => InternalError() error => InternalError()
); );
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> GetAuthorsAsync([FromQuery(Name = "page")] int page = 1, [FromQuery(Name = "pageSize")] int pageSize = 10) public async Task<IResult> GetAuthorsAsync([FromQuery] PageParameters parameters)
{ {
var result = await _libraryService.GetAuthorsAsync(page, pageSize); var validationResult = await _pageValidator.ValidateAsync(parameters);
if (!validationResult.IsValid)
return Results.ValidationProblem(validationResult.ToDictionary());
var result = await _libraryService.GetAuthorsAsync(parameters.Page, parameters.PageSize);
return result.Match( return result.Match(
success => PagedResult(page, pageSize, success.Value.Select(x => x.Map()).ToList()), success => PagedResult(parameters.Page, parameters.PageSize, success.Value.Select(x => x.Map()).ToList()),
error => InternalError() error => InternalError()
); );
} }

View file

@ -7,13 +7,13 @@ namespace GitHubActionsDemo.Api.Controllers;
public class BaseController : ControllerBase public class BaseController : ControllerBase
{ {
public IActionResult InternalError() public IResult InternalError()
{ {
return new StatusCodeResult((int)HttpStatusCode.InternalServerError); return Results.StatusCode((int)HttpStatusCode.InternalServerError);
} }
public IActionResult PagedResult<T>(int page, int pageSize, T result) where T : IList public IResult PagedResult<T>(int page, int pageSize, T result) where T : IList
{ {
return Ok(new PagedResponse<T>(page, pageSize, result)); return Results.Ok(new PagedResponse<T>(page, pageSize, result));
} }
} }

View file

@ -2,6 +2,7 @@ using GitHubActionsDemo.Api.Models;
using GitHubActionsDemo.Api.Mappers; using GitHubActionsDemo.Api.Mappers;
using GitHubActionsDemo.Service; using GitHubActionsDemo.Service;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using FluentValidation;
namespace GitHubActionsDemo.Api.Controllers; namespace GitHubActionsDemo.Api.Controllers;
@ -10,42 +11,58 @@ namespace GitHubActionsDemo.Api.Controllers;
public class BooksController : BaseController public class BooksController : BaseController
{ {
private readonly ILibraryService _libraryService; private readonly ILibraryService _libraryService;
private readonly IValidator<BookRequest> _bookValidator;
private readonly IValidator<PageParameters> _pageValidator;
public BooksController( public BooksController(
ILibraryService libraryService ILibraryService libraryService,
IValidator<BookRequest> bookValidator,
IValidator<PageParameters> pageValidator
) )
{ {
_libraryService = libraryService ?? throw new ArgumentNullException(nameof(libraryService)); _libraryService = libraryService ?? throw new ArgumentNullException(nameof(libraryService));
_bookValidator = bookValidator ?? throw new ArgumentNullException(nameof(bookValidator));
_pageValidator = pageValidator ?? throw new ArgumentNullException(nameof(pageValidator));
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> GetBooksAsync([FromQuery(Name = "page")] int page = 1, [FromQuery(Name = "pageSize")] int pageSize = 10) public async Task<IResult> GetBooksAsync([FromQuery] PageParameters parameters)
{ {
var result = await _libraryService.GetBooksAsync(page, pageSize); var validationResult = await _pageValidator.ValidateAsync(parameters);
if (!validationResult.IsValid)
return Results.ValidationProblem(validationResult.ToDictionary());
var result = await _libraryService.GetBooksAsync(parameters.Page, parameters.PageSize);
return result.Match( return result.Match(
success => PagedResult(page, pageSize, success.Value.Select(x => x.Map()).ToList()), success => PagedResult(parameters.Page, parameters.PageSize, success.Value.Select(x => x.Map()).ToList()),
error => InternalError() error => InternalError()
); );
} }
[HttpGet("{bookId}")] [HttpGet("{bookId}")]
public async Task<IActionResult> GetBookAsync(int bookId) public async Task<IResult> GetBookAsync(int bookId)
{ {
var result = await _libraryService.GetBookAsync(bookId); var result = await _libraryService.GetBookAsync(bookId);
return result.Match( return result.Match(
success => Ok(success.Value.Map()), success => Results.Ok(success.Value.Map()),
notfound => NotFound(), notfound => Results.NotFound(),
error => InternalError() error => InternalError()
); );
} }
[HttpPost] [HttpPost]
public async Task<IActionResult> AddBookAsync([FromBody] BookRequest bookRequest) public async Task<IResult> AddBookAsync([FromBody] BookRequest bookRequest)
{ {
var validationResult = await _bookValidator.ValidateAsync(bookRequest);
if (!validationResult.IsValid)
return Results.ValidationProblem(validationResult.ToDictionary());
var result = await _libraryService.AddBookAsync(bookRequest.Map()); var result = await _libraryService.AddBookAsync(bookRequest.Map());
return result.Match( return result.Match(
success => Ok(success.Value.Map()), success => Results.Ok(success.Value.Map()),
error => InternalError() error => InternalError()
); );
} }

View file

@ -8,6 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentValidation" Version="11.5.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.5" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.5" />
<PackageReference Include="OneOf" Version="3.0.255" /> <PackageReference Include="OneOf" Version="3.0.255" />
<PackageReference Include="Serilog" Version="3.0.1" /> <PackageReference Include="Serilog" Version="3.0.1" />

View file

@ -0,0 +1,7 @@
namespace GitHubActionsDemo.Api.Models;
public class PageParameters
{
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 10;
}

View file

@ -0,0 +1,12 @@
using FluentValidation;
namespace GitHubActionsDemo.Api.Models.Validators;
public class AuthorRequestValidator : AbstractValidator<AuthorRequest>
{
public AuthorRequestValidator()
{
RuleFor(x => x.FirstName).NotEmpty();
RuleFor(x => x.LastName).NotEmpty();
}
}

View file

@ -0,0 +1,21 @@
using FluentValidation;
namespace GitHubActionsDemo.Api.Models.Validators;
public class BookRequestValidator : AbstractValidator<BookRequest>
{
public BookRequestValidator()
{
RuleFor(x => x.Title).NotEmpty();
RuleFor(x => x.AuthorId).NotEmpty();
RuleFor(x => x.Isbn)
.NotEmpty()
.MinimumLength(10)
.MaximumLength(17)
.Matches(@"^(?:ISBN(?:-1[03])?:? )?(?=[0-9X]{10}$|(?=(?:[0-9]+[- ]){3})[- 0 - 9X]{ 13}$| 97[89][0 - 9]{ 10}$| (?= (?:[0 - 9] +[- ]){ 4})[- 0 - 9]{ 17}$)(?: 97[89][- ] ?)?[0 - 9]{ 1,5}[- ]?[0 - 9]+[- ]?[0 - 9] +[- ]?[0 - 9X]$");
RuleFor(x => x.DatePublished)
.NotEmpty();
}
}

View file

@ -0,0 +1,12 @@
using FluentValidation;
namespace GitHubActionsDemo.Api.Models.Validators;
public class PageParametersValidator : AbstractValidator<PageParameters>
{
public PageParametersValidator()
{
RuleFor(x => x.Page).GreaterThan(0);
RuleFor(x => x.PageSize).InclusiveBetween(1, 100);
}
}

View file

@ -1,6 +1,9 @@
using GitHubActionsDemo.Service.Infrastructure; using GitHubActionsDemo.Service.Infrastructure;
using GitHubActionsDemo.Persistance.Infrastructure; using GitHubActionsDemo.Persistance.Infrastructure;
using Serilog; using Serilog;
using FluentValidation;
using GitHubActionsDemo.Api.Models;
using GitHubActionsDemo.Api.Models.Validators;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -10,6 +13,10 @@ var logger = new LoggerConfiguration()
.CreateLogger(); .CreateLogger();
builder.Logging.AddSerilog(logger); builder.Logging.AddSerilog(logger);
builder.Services.AddScoped<IValidator<AuthorRequest>, AuthorRequestValidator>();
builder.Services.AddScoped<IValidator<BookRequest>, BookRequestValidator>();
builder.Services.AddScoped<IValidator<PageParameters>, PageParametersValidator>();
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();