diff --git a/src/GitHubActionsDemo.Api/Controllers/AuthorsController.cs b/src/GitHubActionsDemo.Api/Controllers/AuthorsController.cs index 0f4bd1e..2fcde26 100644 --- a/src/GitHubActionsDemo.Api/Controllers/AuthorsController.cs +++ b/src/GitHubActionsDemo.Api/Controllers/AuthorsController.cs @@ -7,24 +7,46 @@ namespace GitHubActionsDemo.Api.Controllers; [ApiController] [Route("[controller]")] -public class AuthorsController : ControllerBase +public class AuthorsController : BaseController { - private readonly ILogger _logger; private readonly ILibraryService _libraryService; public AuthorsController( - ILogger logger, ILibraryService libraryService ) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _libraryService = libraryService ?? throw new ArgumentNullException(nameof(libraryService)); } [HttpPost] - public async Task AddAuthorAsync(AuthorRequest authorRequest) + public async Task AddAuthorAsync([FromBody] AuthorRequest authorRequest) { - var author = await _libraryService.AddAuthorAsync(authorRequest.Map()); - return author.Map(); + var result = await _libraryService.AddAuthorAsync(authorRequest.Map()); + return result.Match( + success => Ok(success.Value.Map()), + error => InternalError() + ); + } + + [HttpGet("{authorId}")] + public async Task 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 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() + ); } } diff --git a/src/GitHubActionsDemo.Api/Controllers/BaseController.cs b/src/GitHubActionsDemo.Api/Controllers/BaseController.cs new file mode 100644 index 0000000..857a05e --- /dev/null +++ b/src/GitHubActionsDemo.Api/Controllers/BaseController.cs @@ -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(int page, int pageSize, T result) where T : IList + { + return Ok(new PagedResponse(page, pageSize, result)); + } +} \ No newline at end of file diff --git a/src/GitHubActionsDemo.Api/Controllers/BooksController.cs b/src/GitHubActionsDemo.Api/Controllers/BooksController.cs index 3d20072..dea4b18 100644 --- a/src/GitHubActionsDemo.Api/Controllers/BooksController.cs +++ b/src/GitHubActionsDemo.Api/Controllers/BooksController.cs @@ -7,38 +7,46 @@ namespace GitHubActionsDemo.Api.Controllers; [ApiController] [Route("[controller]")] -public class BooksController : ControllerBase +public class BooksController : BaseController { - private readonly ILogger _logger; private readonly ILibraryService _libraryService; public BooksController( - ILogger logger, ILibraryService libraryService ) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _libraryService = libraryService ?? throw new ArgumentNullException(nameof(libraryService)); } [HttpGet] - public async Task> GetBooksAsync(int page = 0, int pageSize = 10) + public async Task GetBooksAsync([FromQuery(Name = "page")] int page = 1, [FromQuery(Name = "pageSize")] int pageSize = 10) { - var books = await _libraryService.GetBooksAsync(page, pageSize); - return books.Select(x => x.Map()); + var result = await _libraryService.GetBooksAsync(page, pageSize); + + return result.Match( + success => PagedResult(page, pageSize, success.Value.Select(x => x.Map()).ToList()), + error => InternalError() + ); } [HttpGet("{bookId}")] - public async Task GetBookAsync(int bookId) + public async Task GetBookAsync(int bookId) { - var book = await _libraryService.GetBookAsync(bookId); - return book.Map(); + var result = await _libraryService.GetBookAsync(bookId); + return result.Match( + success => Ok(success.Value.Map()), + notfound => NotFound(), + error => InternalError() + ); } [HttpPost] - public async Task AddBookAsync(BookRequest bookRequest) + public async Task AddBookAsync([FromBody] BookRequest bookRequest) { - var book = await _libraryService.AddBookAsync(bookRequest.Map()); - return book.Map(); + var result = await _libraryService.AddBookAsync(bookRequest.Map()); + return result.Match( + success => Ok(success.Value.Map()), + error => InternalError() + ); } } diff --git a/src/GitHubActionsDemo.Api/GitHubActionsDemo.Api.csproj b/src/GitHubActionsDemo.Api/GitHubActionsDemo.Api.csproj index 9a28efe..1cb6438 100644 --- a/src/GitHubActionsDemo.Api/GitHubActionsDemo.Api.csproj +++ b/src/GitHubActionsDemo.Api/GitHubActionsDemo.Api.csproj @@ -9,6 +9,11 @@ + + + + + diff --git a/src/GitHubActionsDemo.Api/Models/BookRequest.cs b/src/GitHubActionsDemo.Api/Models/BookRequest.cs index 1d099a8..d84d225 100644 --- a/src/GitHubActionsDemo.Api/Models/BookRequest.cs +++ b/src/GitHubActionsDemo.Api/Models/BookRequest.cs @@ -5,5 +5,5 @@ public class BookRequest public string Title { get; set; } public int AuthorId { get; set; } public string Isbn { get; set; } - public DateOnly DatePublished { get; set; } + public DateTime DatePublished { get; set; } } diff --git a/src/GitHubActionsDemo.Api/Models/BookResponse.cs b/src/GitHubActionsDemo.Api/Models/BookResponse.cs index 9b340ad..afc0dcd 100644 --- a/src/GitHubActionsDemo.Api/Models/BookResponse.cs +++ b/src/GitHubActionsDemo.Api/Models/BookResponse.cs @@ -7,7 +7,7 @@ public class BookResponse string title, AuthorResponse author, string isbn, - DateOnly datePublished, + DateTime datePublished, DateTime dateCreated, DateTime dateModified ) @@ -25,7 +25,7 @@ public class BookResponse public string Title { get; } public AuthorResponse Author { get; } public string Isbn { get; } - public DateOnly DatePublished { get; } + public DateTime DatePublished { get; } public DateTime DateCreated { get; } public DateTime DateModified { get; } } diff --git a/src/GitHubActionsDemo.Api/Models/PagedResponse.cs b/src/GitHubActionsDemo.Api/Models/PagedResponse.cs new file mode 100644 index 0000000..8125a9c --- /dev/null +++ b/src/GitHubActionsDemo.Api/Models/PagedResponse.cs @@ -0,0 +1,18 @@ +using System.Collections; +namespace GitHubActionsDemo.Api.Models; + +public class PagedResponse 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; } +} \ No newline at end of file diff --git a/src/GitHubActionsDemo.Api/Models/PagedResult.cs b/src/GitHubActionsDemo.Api/Models/PagedResult.cs new file mode 100644 index 0000000..e69de29 diff --git a/src/GitHubActionsDemo.Api/Program.cs b/src/GitHubActionsDemo.Api/Program.cs index 60385d4..6b9e4ac 100644 --- a/src/GitHubActionsDemo.Api/Program.cs +++ b/src/GitHubActionsDemo.Api/Program.cs @@ -1,8 +1,15 @@ using GitHubActionsDemo.Service.Infrastructure; using GitHubActionsDemo.Persistance.Infrastructure; +using Serilog; var builder = WebApplication.CreateBuilder(args); +builder.Logging.ClearProviders(); +var logger = new LoggerConfiguration() + .WriteTo.Console() + .CreateLogger(); +builder.Logging.AddSerilog(logger); + builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); diff --git a/src/GitHubActionsDemo.Api/appsettings.Development.json b/src/GitHubActionsDemo.Api/appsettings.Development.json index 971f863..fff4625 100644 --- a/src/GitHubActionsDemo.Api/appsettings.Development.json +++ b/src/GitHubActionsDemo.Api/appsettings.Development.json @@ -8,5 +8,11 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "Serilog": { + "Using": ["Serilog.Sinks.Console"], + "MinimumLevel": { + "Default": "Information" + } } } diff --git a/src/GitHubActionsDemo.Api/appsettings.json b/src/GitHubActionsDemo.Api/appsettings.json index b3330a5..75480cb 100644 --- a/src/GitHubActionsDemo.Api/appsettings.json +++ b/src/GitHubActionsDemo.Api/appsettings.json @@ -9,5 +9,11 @@ "Microsoft.AspNetCore": "Warning" } }, + "Serilog": { + "Using": ["Serilog.Sinks.Console"], + "MinimumLevel": { + "Default": "Information" + } + }, "AllowedHosts": "*" } diff --git a/src/GitHubActionsDemo.Persistance/ILibraryRespository.cs b/src/GitHubActionsDemo.Persistance/ILibraryRespository.cs index 097624a..143c819 100644 --- a/src/GitHubActionsDemo.Persistance/ILibraryRespository.cs +++ b/src/GitHubActionsDemo.Persistance/ILibraryRespository.cs @@ -6,7 +6,8 @@ public interface ILibraryRespository { Task> GetBooksAsync(int page, int pageSize); Task GetBookAsync(int bookId); - Task AddBookAsync(NewBookDb book); - Task AddAuthorAsync(NewAuthorDb author); + Task> GetAuthorsAsync(int page, int pageSize); + Task AddBookAsync(NewBookDb book); + Task AddAuthorAsync(NewAuthorDb author); Task GetAuthorAsync(int authorId); } \ No newline at end of file diff --git a/src/GitHubActionsDemo.Persistance/LibraryRespository.cs b/src/GitHubActionsDemo.Persistance/LibraryRespository.cs index 23591ef..614e10a 100644 --- a/src/GitHubActionsDemo.Persistance/LibraryRespository.cs +++ b/src/GitHubActionsDemo.Persistance/LibraryRespository.cs @@ -12,19 +12,61 @@ public class LibraryRespository : ILibraryRespository _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); } - public async Task AddAuthorAsync(NewAuthorDb author) + public async Task 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(sql, author); } public async Task 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(sql, param); } - public async Task AddBookAsync(NewBookDb book) + public async Task> 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(sql); + } + + public async Task 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(sql, book); } public async Task GetBookAsync(int bookId) @@ -50,16 +92,14 @@ public class LibraryRespository : ILibraryRespository BookId = bookId }; - using (var connection = _dbContext.CreateConnection()) + using var connection = _dbContext.CreateConnection(); + var books = await connection.QueryAsync(sql, (book, author) => { - var books = await connection.QueryAsync(sql, (book, author) => - { - book.Author = author; - return book; - }, param, splitOn: nameof(AuthorDb.AuthorId)); + book.Author = author; + return book; + }, param, splitOn: nameof(AuthorDb.AuthorId)); - return books?.FirstOrDefault(); - } + return books?.FirstOrDefault(); } public async Task> GetBooksAsync(int page, int pageSize) @@ -78,15 +118,16 @@ public class LibraryRespository : ILibraryRespository a.date_modified AS Author{nameof(AuthorDb.DateModified)} FROM books b 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(sql, (book, author) => { - return await connection.QueryAsync(sql, (book, author) => - { - book.Author = author; - return book; - }, splitOn: nameof(AuthorDb.AuthorId)); - } + book.Author = author; + return book; + }, splitOn: nameof(AuthorDb.AuthorId)); } } diff --git a/src/GitHubActionsDemo.Persistance/Models/BookDb.cs b/src/GitHubActionsDemo.Persistance/Models/BookDb.cs index 23bb74a..60c6dc6 100644 --- a/src/GitHubActionsDemo.Persistance/Models/BookDb.cs +++ b/src/GitHubActionsDemo.Persistance/Models/BookDb.cs @@ -6,7 +6,7 @@ public class BookDb public string Title { get; set; } public AuthorDb Author { get; set; } public string Isbn { get; set; } - public DateOnly DatePublished { get; set; } + public DateTime DatePublished { get; set; } public DateTime DateCreated { get; set; } public DateTime DateModified { get; set; } } diff --git a/src/GitHubActionsDemo.Persistance/Models/NewBookDb.cs b/src/GitHubActionsDemo.Persistance/Models/NewBookDb.cs index 6144cf6..2dbad6c 100644 --- a/src/GitHubActionsDemo.Persistance/Models/NewBookDb.cs +++ b/src/GitHubActionsDemo.Persistance/Models/NewBookDb.cs @@ -6,7 +6,7 @@ public class NewBookDb string title, int authorId, string isbn, - DateOnly datePublished, + DateTime datePublished, DateTime dateCreated, DateTime dateModified ) @@ -23,7 +23,7 @@ public class NewBookDb public string Title { get; } public int AuthorId { get; } public string Isbn { get; } - public DateOnly DatePublished { get; } + public DateTime DatePublished { get; } public DateTime DateCreated { get; } public DateTime DateModified { get; } } \ No newline at end of file diff --git a/src/GitHubActionsDemo.Service/GitHubActionsDemo.Service.csproj b/src/GitHubActionsDemo.Service/GitHubActionsDemo.Service.csproj index 5216cb4..f5199b4 100644 --- a/src/GitHubActionsDemo.Service/GitHubActionsDemo.Service.csproj +++ b/src/GitHubActionsDemo.Service/GitHubActionsDemo.Service.csproj @@ -8,6 +8,7 @@ + diff --git a/src/GitHubActionsDemo.Service/ILibraryService.cs b/src/GitHubActionsDemo.Service/ILibraryService.cs index 8b52d9d..387e7fa 100644 --- a/src/GitHubActionsDemo.Service/ILibraryService.cs +++ b/src/GitHubActionsDemo.Service/ILibraryService.cs @@ -1,11 +1,16 @@ using GitHubActionsDemo.Service.Models; +using OneOf; +using OneOf.Types; +using NotFound = OneOf.Types.NotFound; namespace GitHubActionsDemo.Service; public interface ILibraryService { - Task> GetBooksAsync(int page, int pageSize); - Task GetBookAsync(int bookId); - Task AddBookAsync(NewBook book); - Task AddAuthorAsync(NewAuthor author); + Task>, Error>> GetBooksAsync(int page, int pageSize); + Task, NotFound, Error>> GetBookAsync(int bookId); + Task, Error>> AddBookAsync(NewBook newBook); + Task, Error>> AddAuthorAsync(NewAuthor newAuthor); + Task, NotFound, Error>> GetAuthorAsync(int authorId); + Task>, Error>> GetAuthorsAsync(int page, int pageSize); } \ No newline at end of file diff --git a/src/GitHubActionsDemo.Service/LibraryService.cs b/src/GitHubActionsDemo.Service/LibraryService.cs index 1bff707..436d052 100644 --- a/src/GitHubActionsDemo.Service/LibraryService.cs +++ b/src/GitHubActionsDemo.Service/LibraryService.cs @@ -1,39 +1,122 @@ using GitHubActionsDemo.Persistance; using GitHubActionsDemo.Service.Mappers; using GitHubActionsDemo.Service.Models; +using Microsoft.Extensions.Logging; +using OneOf; +using OneOf.Types; +using NotFound = OneOf.Types.NotFound; namespace GitHubActionsDemo.Service; public class LibraryService : ILibraryService { private readonly ILibraryRespository _libraryRepository; + private readonly ILogger _logger; - public LibraryService(ILibraryRespository libraryRepository) + public LibraryService( + ILibraryRespository libraryRepository, + ILogger logger + ) { _libraryRepository = libraryRepository ?? throw new ArgumentNullException(nameof(libraryRepository)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public async Task AddAuthorAsync(NewAuthor newAuthor) + public async Task, Error>> AddAuthorAsync(NewAuthor newAuthor) { - var author = await _libraryRepository.AddAuthorAsync(newAuthor.Map()); - return author.Map(); + try + { + var authorId = await _libraryRepository.AddAuthorAsync(newAuthor.Map()); + var author = await _libraryRepository.GetAuthorAsync(authorId); + return new Success(author.Map()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error adding author"); + return new Error(); + } } - public async Task AddBookAsync(NewBook newBook) + public async Task, Error>> AddBookAsync(NewBook newBook) { - var book = await _libraryRepository.AddBookAsync(newBook.Map()); - return book.Map(); + try + { + var bookId = await _libraryRepository.AddBookAsync(newBook.Map()); + var book = await _libraryRepository.GetBookAsync(bookId); + return new Success(book.Map()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error adding book"); + return new Error(); + } } - public async Task GetBookAsync(int bookId) + public async Task, NotFound, Error>> GetAuthorAsync(int authorId) { - var book = await _libraryRepository.GetBookAsync(bookId); - return book.Map(); + try + { + var author = await _libraryRepository.GetAuthorAsync(authorId); + if (author == null) + return new NotFound(); + + return new Success(author.Map()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting author"); + return new Error(); + } } - public async Task> GetBooksAsync(int page, int pageSize) + public async Task>, Error>> GetAuthorsAsync(int page, int pageSize) { - var books = await _libraryRepository.GetBooksAsync(page, pageSize); - return books?.Select(book => book.Map()) ?? new List(); + try + { + var authors = await _libraryRepository.GetAuthorsAsync(page, pageSize); + if (authors == null) + return new Success>(); + + return new Success>(authors?.Select(author => author.Map())); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting authors"); + return new Error(); + } + } + + public async Task, NotFound, Error>> GetBookAsync(int bookId) + { + try + { + var book = await _libraryRepository.GetBookAsync(bookId); + if (book == null) + return new NotFound(); + + return new Success(book.Map()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting book"); + return new Error(); + } + } + + public async Task>, Error>> GetBooksAsync(int page, int pageSize) + { + try + { + var books = await _libraryRepository.GetBooksAsync(page, pageSize); + if (books == null) + return new Success>(); + + return new Success>(books?.Select(book => book.Map())); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting books"); + return new Error(); + } } } \ No newline at end of file diff --git a/src/GitHubActionsDemo.Service/Models/Book.cs b/src/GitHubActionsDemo.Service/Models/Book.cs index d334c29..91e6742 100644 --- a/src/GitHubActionsDemo.Service/Models/Book.cs +++ b/src/GitHubActionsDemo.Service/Models/Book.cs @@ -7,7 +7,7 @@ public class Book string title, Author author, string isbn, - DateOnly datePublished, + DateTime datePublished, DateTime dateCreated, DateTime dateModified ) @@ -25,7 +25,7 @@ public class Book public string Title { get; } public Author Author { get; } public string Isbn { get; } - public DateOnly DatePublished { get; } + public DateTime DatePublished { get; } public DateTime DateCreated { get; } public DateTime DateModified { get; } } diff --git a/src/GitHubActionsDemo.Service/Models/NewBook.cs b/src/GitHubActionsDemo.Service/Models/NewBook.cs index 1679f5f..6234ca6 100644 --- a/src/GitHubActionsDemo.Service/Models/NewBook.cs +++ b/src/GitHubActionsDemo.Service/Models/NewBook.cs @@ -6,7 +6,7 @@ public class NewBook string title, int authorId, string isbn, - DateOnly datePublished + DateTime datePublished ) { Title = title; @@ -18,5 +18,5 @@ public class NewBook public string Title { get; } public int AuthorId { get; } public string Isbn { get; } - public DateOnly DatePublished { get; } + public DateTime DatePublished { get; } } \ No newline at end of file diff --git a/src/GitHubActionsDemo.Service/Models/NotFound.cs b/src/GitHubActionsDemo.Service/Models/NotFound.cs new file mode 100644 index 0000000..6ea7b8c --- /dev/null +++ b/src/GitHubActionsDemo.Service/Models/NotFound.cs @@ -0,0 +1,6 @@ +namespace GitHubActionsDemo.Service.Models; + +public class NotFound +{ + +} \ No newline at end of file