Add DB queries

This commit is contained in:
Alex Hyett 2023-07-04 12:36:35 +01:00
parent db9f7348fa
commit d81f8eb46f
21 changed files with 298 additions and 70 deletions

View file

@ -7,24 +7,46 @@ namespace GitHubActionsDemo.Api.Controllers;
[ApiController] [ApiController]
[Route("[controller]")] [Route("[controller]")]
public class AuthorsController : ControllerBase public class AuthorsController : BaseController
{ {
private readonly ILogger<AuthorsController> _logger;
private readonly ILibraryService _libraryService; private readonly ILibraryService _libraryService;
public AuthorsController( public AuthorsController(
ILogger<AuthorsController> logger,
ILibraryService libraryService ILibraryService libraryService
) )
{ {
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_libraryService = libraryService ?? throw new ArgumentNullException(nameof(libraryService)); _libraryService = libraryService ?? throw new ArgumentNullException(nameof(libraryService));
} }
[HttpPost] [HttpPost]
public async Task<AuthorResponse> AddAuthorAsync(AuthorRequest authorRequest) public async Task<IActionResult> AddAuthorAsync([FromBody] AuthorRequest authorRequest)
{ {
var author = await _libraryService.AddAuthorAsync(authorRequest.Map()); var result = await _libraryService.AddAuthorAsync(authorRequest.Map());
return author.Map(); return result.Match(
success => Ok(success.Value.Map()),
error => InternalError()
);
}
[HttpGet("{authorId}")]
public async Task<IActionResult> GetAuthorAsync(int authorId)
{
var result = await _libraryService.GetAuthorAsync(authorId);
return result.Match(
success => Ok(success.Value.Map()),
notfound => NotFound(),
error => InternalError()
);
}
[HttpGet]
public async Task<IActionResult> GetAuthorsAsync([FromQuery(Name = "page")] int page = 1, [FromQuery(Name = "pageSize")] int pageSize = 10)
{
var result = await _libraryService.GetAuthorsAsync(page, pageSize);
return result.Match(
success => PagedResult(page, pageSize, success.Value.Select(x => x.Map()).ToList()),
error => InternalError()
);
} }
} }

View file

@ -0,0 +1,19 @@
using System.Collections;
using System.Net;
using GitHubActionsDemo.Api.Models;
using Microsoft.AspNetCore.Mvc;
namespace GitHubActionsDemo.Api.Controllers;
public class BaseController : ControllerBase
{
public IActionResult InternalError()
{
return new StatusCodeResult((int)HttpStatusCode.InternalServerError);
}
public IActionResult PagedResult<T>(int page, int pageSize, T result) where T : IList
{
return Ok(new PagedResponse<T>(page, pageSize, result));
}
}

View file

@ -7,38 +7,46 @@ namespace GitHubActionsDemo.Api.Controllers;
[ApiController] [ApiController]
[Route("[controller]")] [Route("[controller]")]
public class BooksController : ControllerBase public class BooksController : BaseController
{ {
private readonly ILogger<BooksController> _logger;
private readonly ILibraryService _libraryService; private readonly ILibraryService _libraryService;
public BooksController( public BooksController(
ILogger<BooksController> logger,
ILibraryService libraryService ILibraryService libraryService
) )
{ {
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_libraryService = libraryService ?? throw new ArgumentNullException(nameof(libraryService)); _libraryService = libraryService ?? throw new ArgumentNullException(nameof(libraryService));
} }
[HttpGet] [HttpGet]
public async Task<IEnumerable<BookResponse>> GetBooksAsync(int page = 0, int pageSize = 10) public async Task<IActionResult> GetBooksAsync([FromQuery(Name = "page")] int page = 1, [FromQuery(Name = "pageSize")] int pageSize = 10)
{ {
var books = await _libraryService.GetBooksAsync(page, pageSize); var result = await _libraryService.GetBooksAsync(page, pageSize);
return books.Select(x => x.Map());
return result.Match(
success => PagedResult(page, pageSize, success.Value.Select(x => x.Map()).ToList()),
error => InternalError()
);
} }
[HttpGet("{bookId}")] [HttpGet("{bookId}")]
public async Task<BookResponse> GetBookAsync(int bookId) public async Task<IActionResult> GetBookAsync(int bookId)
{ {
var book = await _libraryService.GetBookAsync(bookId); var result = await _libraryService.GetBookAsync(bookId);
return book.Map(); return result.Match(
success => Ok(success.Value.Map()),
notfound => NotFound(),
error => InternalError()
);
} }
[HttpPost] [HttpPost]
public async Task<BookResponse> AddBookAsync(BookRequest bookRequest) public async Task<IActionResult> AddBookAsync([FromBody] BookRequest bookRequest)
{ {
var book = await _libraryService.AddBookAsync(bookRequest.Map()); var result = await _libraryService.AddBookAsync(bookRequest.Map());
return book.Map(); return result.Match(
success => Ok(success.Value.Map()),
error => InternalError()
);
} }
} }

View file

@ -9,6 +9,11 @@
<ItemGroup> <ItemGroup>
<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="Serilog" Version="3.0.1" />
<PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup> </ItemGroup>

View file

@ -5,5 +5,5 @@ public class BookRequest
public string Title { get; set; } public string Title { get; set; }
public int AuthorId { get; set; } public int AuthorId { get; set; }
public string Isbn { get; set; } public string Isbn { get; set; }
public DateOnly DatePublished { get; set; } public DateTime DatePublished { get; set; }
} }

View file

@ -7,7 +7,7 @@ public class BookResponse
string title, string title,
AuthorResponse author, AuthorResponse author,
string isbn, string isbn,
DateOnly datePublished, DateTime datePublished,
DateTime dateCreated, DateTime dateCreated,
DateTime dateModified DateTime dateModified
) )
@ -25,7 +25,7 @@ public class BookResponse
public string Title { get; } public string Title { get; }
public AuthorResponse Author { get; } public AuthorResponse Author { get; }
public string Isbn { get; } public string Isbn { get; }
public DateOnly DatePublished { get; } public DateTime DatePublished { get; }
public DateTime DateCreated { get; } public DateTime DateCreated { get; }
public DateTime DateModified { get; } public DateTime DateModified { get; }
} }

View file

@ -0,0 +1,18 @@
using System.Collections;
namespace GitHubActionsDemo.Api.Models;
public class PagedResponse<T> where T : IList
{
public PagedResponse(int page, int pageSize, T result)
{
Page = page;
PageSize = pageSize;
Result = result;
Count = result.Count;
}
public int Page { get; }
public int PageSize { get; }
public int Count { get; }
public T Result { get; }
}

View file

@ -1,8 +1,15 @@
using GitHubActionsDemo.Service.Infrastructure; using GitHubActionsDemo.Service.Infrastructure;
using GitHubActionsDemo.Persistance.Infrastructure; using GitHubActionsDemo.Persistance.Infrastructure;
using Serilog;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
var logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateLogger();
builder.Logging.AddSerilog(logger);
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();

View file

@ -8,5 +8,11 @@
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
},
"Serilog": {
"Using": ["Serilog.Sinks.Console"],
"MinimumLevel": {
"Default": "Information"
}
} }
} }

View file

@ -9,5 +9,11 @@
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"Serilog": {
"Using": ["Serilog.Sinks.Console"],
"MinimumLevel": {
"Default": "Information"
}
},
"AllowedHosts": "*" "AllowedHosts": "*"
} }

View file

@ -6,7 +6,8 @@ public interface ILibraryRespository
{ {
Task<IEnumerable<BookDb>> GetBooksAsync(int page, int pageSize); Task<IEnumerable<BookDb>> GetBooksAsync(int page, int pageSize);
Task<BookDb> GetBookAsync(int bookId); Task<BookDb> GetBookAsync(int bookId);
Task<BookDb> AddBookAsync(NewBookDb book); Task<IEnumerable<AuthorDb>> GetAuthorsAsync(int page, int pageSize);
Task<AuthorDb> AddAuthorAsync(NewAuthorDb author); Task<int> AddBookAsync(NewBookDb book);
Task<int> AddAuthorAsync(NewAuthorDb author);
Task<AuthorDb> GetAuthorAsync(int authorId); Task<AuthorDb> GetAuthorAsync(int authorId);
} }

View file

@ -12,19 +12,61 @@ public class LibraryRespository : ILibraryRespository
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
} }
public async Task<AuthorDb> AddAuthorAsync(NewAuthorDb author) public async Task<int> AddAuthorAsync(NewAuthorDb author)
{ {
throw new NotImplementedException(); var sql = @$"INSERT INTO authors (first_name, last_name, date_created, date_modified)
VALUES(@FirstName, @LastName, @DateCreated, @DateModified);
SELECT LAST_INSERT_ID();";
using var connection = _dbContext.CreateConnection();
return await connection.QueryFirstOrDefaultAsync<int>(sql, author);
} }
public async Task<AuthorDb> GetAuthorAsync(int authorId) public async Task<AuthorDb> GetAuthorAsync(int authorId)
{ {
throw new NotImplementedException(); var sql = @$"SELECT
a.author_id AS {nameof(AuthorDb.AuthorId)},
a.first_name AS {nameof(AuthorDb.FirstName)},
a.last_name AS {nameof(AuthorDb.LastName)},
a.date_created AS Author{nameof(AuthorDb.DateCreated)},
a.date_modified AS Author{nameof(AuthorDb.DateModified)}
FROM authors a
WHERE a.author_id = @AuthorId;";
var param = new
{
AuthorId = authorId
};
using var connection = _dbContext.CreateConnection();
return await connection.QueryFirstOrDefaultAsync<AuthorDb>(sql, param);
} }
public async Task<BookDb> AddBookAsync(NewBookDb book) public async Task<IEnumerable<AuthorDb>> GetAuthorsAsync(int page, int pageSize)
{ {
throw new NotImplementedException(); var sql = @$"SELECT
a.author_id AS {nameof(AuthorDb.AuthorId)},
a.first_name AS {nameof(AuthorDb.FirstName)},
a.last_name AS {nameof(AuthorDb.LastName)},
a.date_created AS Author{nameof(AuthorDb.DateCreated)},
a.date_modified AS Author{nameof(AuthorDb.DateModified)}
FROM authors a
ORDER BY author_id
LIMIT {pageSize}
OFFSET {pageSize * (page - 1)};";
using var connection = _dbContext.CreateConnection();
return await connection.QueryAsync<AuthorDb>(sql);
}
public async Task<int> AddBookAsync(NewBookDb book)
{
var sql = @$"INSERT INTO books (title, author_id, isbn, date_published, date_created, date_modified)
VALUES(@Title, @AuthorId, @Isbn, @DatePublished, @DateCreated, @DateModified);
SELECT LAST_INSERT_ID();";
using var connection = _dbContext.CreateConnection();
return await connection.QueryFirstOrDefaultAsync<int>(sql, book);
} }
public async Task<BookDb> GetBookAsync(int bookId) public async Task<BookDb> GetBookAsync(int bookId)
@ -50,8 +92,7 @@ public class LibraryRespository : ILibraryRespository
BookId = bookId BookId = bookId
}; };
using (var connection = _dbContext.CreateConnection()) using var connection = _dbContext.CreateConnection();
{
var books = await connection.QueryAsync<BookDb, AuthorDb, BookDb>(sql, (book, author) => var books = await connection.QueryAsync<BookDb, AuthorDb, BookDb>(sql, (book, author) =>
{ {
book.Author = author; book.Author = author;
@ -60,7 +101,6 @@ public class LibraryRespository : ILibraryRespository
return books?.FirstOrDefault(); return books?.FirstOrDefault();
} }
}
public async Task<IEnumerable<BookDb>> GetBooksAsync(int page, int pageSize) public async Task<IEnumerable<BookDb>> GetBooksAsync(int page, int pageSize)
{ {
@ -78,15 +118,16 @@ public class LibraryRespository : ILibraryRespository
a.date_modified AS Author{nameof(AuthorDb.DateModified)} a.date_modified AS Author{nameof(AuthorDb.DateModified)}
FROM books b FROM books b
INNER JOIN authors a ON a.author_id = b.author_id INNER JOIN authors a ON a.author_id = b.author_id
ORDER BY b.book_id;"; ORDER BY b.book_id
LIMIT {pageSize}
OFFSET {pageSize * (page - 1)};";
using var connection = _dbContext.CreateConnection();
using (var connection = _dbContext.CreateConnection())
{
return await connection.QueryAsync<BookDb, AuthorDb, BookDb>(sql, (book, author) => return await connection.QueryAsync<BookDb, AuthorDb, BookDb>(sql, (book, author) =>
{ {
book.Author = author; book.Author = author;
return book; return book;
}, splitOn: nameof(AuthorDb.AuthorId)); }, splitOn: nameof(AuthorDb.AuthorId));
} }
}
} }

View file

@ -6,7 +6,7 @@ public class BookDb
public string Title { get; set; } public string Title { get; set; }
public AuthorDb Author { get; set; } public AuthorDb Author { get; set; }
public string Isbn { get; set; } public string Isbn { get; set; }
public DateOnly DatePublished { get; set; } public DateTime DatePublished { get; set; }
public DateTime DateCreated { get; set; } public DateTime DateCreated { get; set; }
public DateTime DateModified { get; set; } public DateTime DateModified { get; set; }
} }

View file

@ -6,7 +6,7 @@ public class NewBookDb
string title, string title,
int authorId, int authorId,
string isbn, string isbn,
DateOnly datePublished, DateTime datePublished,
DateTime dateCreated, DateTime dateCreated,
DateTime dateModified DateTime dateModified
) )
@ -23,7 +23,7 @@ public class NewBookDb
public string Title { get; } public string Title { get; }
public int AuthorId { get; } public int AuthorId { get; }
public string Isbn { get; } public string Isbn { get; }
public DateOnly DatePublished { get; } public DateTime DatePublished { get; }
public DateTime DateCreated { get; } public DateTime DateCreated { get; }
public DateTime DateModified { get; } public DateTime DateModified { get; }
} }

View file

@ -8,6 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="OneOf" Version="3.0.255" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -1,11 +1,16 @@
using GitHubActionsDemo.Service.Models; using GitHubActionsDemo.Service.Models;
using OneOf;
using OneOf.Types;
using NotFound = OneOf.Types.NotFound;
namespace GitHubActionsDemo.Service; namespace GitHubActionsDemo.Service;
public interface ILibraryService public interface ILibraryService
{ {
Task<IEnumerable<Book>> GetBooksAsync(int page, int pageSize); Task<OneOf<Success<IEnumerable<Book>>, Error>> GetBooksAsync(int page, int pageSize);
Task<Book> GetBookAsync(int bookId); Task<OneOf<Success<Book>, NotFound, Error>> GetBookAsync(int bookId);
Task<Book> AddBookAsync(NewBook book); Task<OneOf<Success<Book>, Error>> AddBookAsync(NewBook newBook);
Task<Author> AddAuthorAsync(NewAuthor author); Task<OneOf<Success<Author>, Error>> AddAuthorAsync(NewAuthor newAuthor);
Task<OneOf<Success<Author>, NotFound, Error>> GetAuthorAsync(int authorId);
Task<OneOf<Success<IEnumerable<Author>>, Error>> GetAuthorsAsync(int page, int pageSize);
} }

View file

@ -1,39 +1,122 @@
using GitHubActionsDemo.Persistance; using GitHubActionsDemo.Persistance;
using GitHubActionsDemo.Service.Mappers; using GitHubActionsDemo.Service.Mappers;
using GitHubActionsDemo.Service.Models; using GitHubActionsDemo.Service.Models;
using Microsoft.Extensions.Logging;
using OneOf;
using OneOf.Types;
using NotFound = OneOf.Types.NotFound;
namespace GitHubActionsDemo.Service; namespace GitHubActionsDemo.Service;
public class LibraryService : ILibraryService public class LibraryService : ILibraryService
{ {
private readonly ILibraryRespository _libraryRepository; private readonly ILibraryRespository _libraryRepository;
private readonly ILogger<LibraryService> _logger;
public LibraryService(ILibraryRespository libraryRepository) public LibraryService(
ILibraryRespository libraryRepository,
ILogger<LibraryService> logger
)
{ {
_libraryRepository = libraryRepository ?? throw new ArgumentNullException(nameof(libraryRepository)); _libraryRepository = libraryRepository ?? throw new ArgumentNullException(nameof(libraryRepository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
public async Task<Author> AddAuthorAsync(NewAuthor newAuthor) public async Task<OneOf<Success<Author>, Error>> AddAuthorAsync(NewAuthor newAuthor)
{ {
var author = await _libraryRepository.AddAuthorAsync(newAuthor.Map()); try
return author.Map();
}
public async Task<Book> AddBookAsync(NewBook newBook)
{ {
var book = await _libraryRepository.AddBookAsync(newBook.Map()); var authorId = await _libraryRepository.AddAuthorAsync(newAuthor.Map());
return book.Map(); var author = await _libraryRepository.GetAuthorAsync(authorId);
return new Success<Author>(author.Map());
}
catch (Exception ex)
{
_logger.LogError(ex, "Error adding author");
return new Error();
}
} }
public async Task<Book> GetBookAsync(int bookId) public async Task<OneOf<Success<Book>, Error>> AddBookAsync(NewBook newBook)
{
try
{
var bookId = await _libraryRepository.AddBookAsync(newBook.Map());
var book = await _libraryRepository.GetBookAsync(bookId);
return new Success<Book>(book.Map());
}
catch (Exception ex)
{
_logger.LogError(ex, "Error adding book");
return new Error();
}
}
public async Task<OneOf<Success<Author>, NotFound, Error>> GetAuthorAsync(int authorId)
{
try
{
var author = await _libraryRepository.GetAuthorAsync(authorId);
if (author == null)
return new NotFound();
return new Success<Author>(author.Map());
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting author");
return new Error();
}
}
public async Task<OneOf<Success<IEnumerable<Author>>, Error>> GetAuthorsAsync(int page, int pageSize)
{
try
{
var authors = await _libraryRepository.GetAuthorsAsync(page, pageSize);
if (authors == null)
return new Success<IEnumerable<Author>>();
return new Success<IEnumerable<Author>>(authors?.Select(author => author.Map()));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting authors");
return new Error();
}
}
public async Task<OneOf<Success<Book>, NotFound, Error>> GetBookAsync(int bookId)
{
try
{ {
var book = await _libraryRepository.GetBookAsync(bookId); var book = await _libraryRepository.GetBookAsync(bookId);
return book.Map(); if (book == null)
return new NotFound();
return new Success<Book>(book.Map());
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting book");
return new Error();
}
} }
public async Task<IEnumerable<Book>> GetBooksAsync(int page, int pageSize) public async Task<OneOf<Success<IEnumerable<Book>>, Error>> GetBooksAsync(int page, int pageSize)
{
try
{ {
var books = await _libraryRepository.GetBooksAsync(page, pageSize); var books = await _libraryRepository.GetBooksAsync(page, pageSize);
return books?.Select(book => book.Map()) ?? new List<Book>(); if (books == null)
return new Success<IEnumerable<Book>>();
return new Success<IEnumerable<Book>>(books?.Select(book => book.Map()));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting books");
return new Error();
}
} }
} }

View file

@ -7,7 +7,7 @@ public class Book
string title, string title,
Author author, Author author,
string isbn, string isbn,
DateOnly datePublished, DateTime datePublished,
DateTime dateCreated, DateTime dateCreated,
DateTime dateModified DateTime dateModified
) )
@ -25,7 +25,7 @@ public class Book
public string Title { get; } public string Title { get; }
public Author Author { get; } public Author Author { get; }
public string Isbn { get; } public string Isbn { get; }
public DateOnly DatePublished { get; } public DateTime DatePublished { get; }
public DateTime DateCreated { get; } public DateTime DateCreated { get; }
public DateTime DateModified { get; } public DateTime DateModified { get; }
} }

View file

@ -6,7 +6,7 @@ public class NewBook
string title, string title,
int authorId, int authorId,
string isbn, string isbn,
DateOnly datePublished DateTime datePublished
) )
{ {
Title = title; Title = title;
@ -18,5 +18,5 @@ public class NewBook
public string Title { get; } public string Title { get; }
public int AuthorId { get; } public int AuthorId { get; }
public string Isbn { get; } public string Isbn { get; }
public DateOnly DatePublished { get; } public DateTime DatePublished { get; }
} }

View file

@ -0,0 +1,6 @@
namespace GitHubActionsDemo.Service.Models;
public class NotFound
{
}