From 9c4b867472570001c90944482806ddfcda624994 Mon Sep 17 00:00:00 2001 From: Erik Bergvall Date: Thu, 23 Jan 2025 15:52:10 +0100 Subject: [PATCH] Core exercise and 1 extension --- .gitignore | 4 +- exercise.webapi/DTO/AuthorDTO.cs | 13 ++ exercise.webapi/DTO/BookDTO.cs | 12 ++ exercise.webapi/DTO/BookPost.cs | 8 + exercise.webapi/DTO/BookPut.cs | 7 + exercise.webapi/Endpoints/AuthorAPI.cs | 48 ++++++ exercise.webapi/Endpoints/BookApi.cs | 141 +++++++++++++++++- exercise.webapi/Program.cs | 2 + .../Repository/AuthorRepository.cs | 27 ++++ exercise.webapi/Repository/BookRepository.cs | 57 +++++++ .../Repository/IAuthorRepository.cs | 11 ++ exercise.webapi/Repository/IBookRepository.cs | 11 +- exercise.webapi/appsettings.Development.json | 6 +- exercise.webapi/appsettings.json | 6 +- exercise.webapi/exercise.webapi.csproj | 9 +- 15 files changed, 348 insertions(+), 14 deletions(-) create mode 100644 exercise.webapi/DTO/AuthorDTO.cs create mode 100644 exercise.webapi/DTO/BookDTO.cs create mode 100644 exercise.webapi/DTO/BookPost.cs create mode 100644 exercise.webapi/DTO/BookPut.cs create mode 100644 exercise.webapi/Endpoints/AuthorAPI.cs create mode 100644 exercise.webapi/Repository/AuthorRepository.cs create mode 100644 exercise.webapi/Repository/IAuthorRepository.cs diff --git a/.gitignore b/.gitignore index 46d3f4b..5c58dbb 100644 --- a/.gitignore +++ b/.gitignore @@ -367,5 +367,5 @@ FodyWeavers.xsd */**/obj/Debug */**/obj/Release */Migrations -/workshop.wwwapi/appsettings.json -/workshop.wwwapi/appsettings.Development.json \ No newline at end of file +/exercise.wwwapi/appsettings.json +/exercise.wwwapi/appsettings.Development.json \ No newline at end of file diff --git a/exercise.webapi/DTO/AuthorDTO.cs b/exercise.webapi/DTO/AuthorDTO.cs new file mode 100644 index 0000000..8c9f913 --- /dev/null +++ b/exercise.webapi/DTO/AuthorDTO.cs @@ -0,0 +1,13 @@ +using exercise.webapi.Models; + +namespace exercise.webapi.DTO +{ + public class AuthorDTO + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public IEnumerable Books { get; set; } = new List(); + } +} diff --git a/exercise.webapi/DTO/BookDTO.cs b/exercise.webapi/DTO/BookDTO.cs new file mode 100644 index 0000000..cca3bc5 --- /dev/null +++ b/exercise.webapi/DTO/BookDTO.cs @@ -0,0 +1,12 @@ +using exercise.webapi.Models; + +namespace exercise.webapi.DTO +{ + public class BookDTO + { + public int Id { get; set; } + public string Title { get; set; } + public int AuthorId { get; set; } + public string AuthorName { get; set; } + } +} diff --git a/exercise.webapi/DTO/BookPost.cs b/exercise.webapi/DTO/BookPost.cs new file mode 100644 index 0000000..a93de84 --- /dev/null +++ b/exercise.webapi/DTO/BookPost.cs @@ -0,0 +1,8 @@ +namespace exercise.webapi.DTO +{ + public class BookPost + { + public string Title { get; set; } + public int AuthorId { get; set; } + } +} diff --git a/exercise.webapi/DTO/BookPut.cs b/exercise.webapi/DTO/BookPut.cs new file mode 100644 index 0000000..c70f26a --- /dev/null +++ b/exercise.webapi/DTO/BookPut.cs @@ -0,0 +1,7 @@ +namespace exercise.webapi.DTO +{ + public class BookPut + { + public int? AuthorId { get; set; } + } +} diff --git a/exercise.webapi/Endpoints/AuthorAPI.cs b/exercise.webapi/Endpoints/AuthorAPI.cs new file mode 100644 index 0000000..2fc2059 --- /dev/null +++ b/exercise.webapi/Endpoints/AuthorAPI.cs @@ -0,0 +1,48 @@ +using exercise.webapi.DTO; +using exercise.webapi.Models; +using exercise.webapi.Repository; +using static System.Reflection.Metadata.BlobBuilder; + +namespace exercise.webapi.Endpoints +{ + public static class AuthorAPI + { + public static void ConfigureAuthorApi(this WebApplication app) + { + var authors = app.MapGroup("authors"); + + authors.MapGet("/", GetAuthors); + authors.MapGet("/{id}", GetAuthor); + } + + private static async Task GetAuthors(IAuthorRepository authorRepository) + { + List authorsDTO = new List(); + var authors = await authorRepository.GetAllAuthors(); + foreach (var author in authors) + { + authorsDTO.Add(new AuthorDTO + { + Id = author.Id, + FirstName = author.FirstName, + LastName = author.LastName, + Email = author.Email, + Books = author.Books.Select(b => b.Title), + }); + } + return TypedResults.Ok(authorsDTO); + } + + private static async Task GetAuthor(IAuthorRepository authorRepository, int id) + { + AuthorDTO authorDTO = new AuthorDTO(); + var author = authorRepository.GetAuthor(id); + authorDTO.Id = author.Result.Id; + authorDTO.FirstName = author.Result.FirstName; + authorDTO.LastName = author.Result.LastName; + authorDTO.Email = author.Result.Email; + authorDTO.Books = author.Result.Books.Select(b => b.Title); + return TypedResults.Ok(authorDTO); + } + } +} diff --git a/exercise.webapi/Endpoints/BookApi.cs b/exercise.webapi/Endpoints/BookApi.cs index 6758215..a3f0ce8 100644 --- a/exercise.webapi/Endpoints/BookApi.cs +++ b/exercise.webapi/Endpoints/BookApi.cs @@ -1,5 +1,9 @@ -using exercise.webapi.Models; +using System.Reflection; +using exercise.webapi.Data; +using exercise.webapi.DTO; +using exercise.webapi.Models; using exercise.webapi.Repository; +using Microsoft.AspNetCore.Mvc; using static System.Reflection.Metadata.BlobBuilder; namespace exercise.webapi.Endpoints @@ -8,13 +12,144 @@ public static class BookApi { public static void ConfigureBooksApi(this WebApplication app) { - app.MapGet("/books", GetBooks); + var books = app.MapGroup("books"); + + books.MapGet("/", GetBooks); + books.MapGet("/{id}", GetBook); + books.MapPut("/{id}", UpdateBook); + books.MapDelete("/{id}", DeleteBook); + books.MapPost("/", AddBook); + books.MapPut("/removeAuthor/{id}", RemoveAuthor); + books.MapPut("/addAuthor/{id}", AddAuthor); + } + private static async Task AddAuthor(IBookRepository bookRepository, IAuthorRepository authorRepository, int id, int authId) + { + BookDTO bookDTO = new BookDTO(); + Book target = await bookRepository.GetBook(id); + + + var result = await bookRepository.AssignAuthor(id, authId); + + Author author = await authorRepository.GetAuthor(target.AuthorId); + author.Books.Add(target); + + bookDTO.Id = result.Id; + bookDTO.Title = result.Title; + bookDTO.AuthorId = result.AuthorId; + bookDTO.AuthorName = result.Author.FirstName + " " + result.Author.LastName; + + + return TypedResults.Ok(bookDTO); + } + private static async Task RemoveAuthor(IBookRepository bookRepository, IAuthorRepository authorRepository, int id) + { + BookDTO bookDTO = new BookDTO(); + Book target = await bookRepository.GetBook(id); + Author author = await authorRepository.GetAuthor(target.AuthorId); + author.Books.Remove(target); + + var result = await bookRepository.RemoveAuthor(id); + bookDTO.Id = result.Id; + bookDTO.Title = result.Title; + bookDTO.AuthorId = result.AuthorId; + bookDTO.AuthorName = ""; + + + return TypedResults.Ok(bookDTO); + } + private static async Task AddBook(IBookRepository bookRepository, BookPost model) + { + BookDTO bookDTO = new BookDTO(); + + Book book = new Book() + { + Title = model.Title, + AuthorId = model.AuthorId + }; + + if( await bookRepository.AddBook(book) == null) + { + return TypedResults.NotFound("Author not found"); + } + + bookDTO.Id = book.Id; + bookDTO.Title = book.Title; + bookDTO.AuthorId = book.AuthorId; + bookDTO.AuthorName = book.Author.FirstName + " " + book.Author.LastName; + + + return TypedResults.Created($"https://localhost:7188/products/{bookDTO.Id}", bookDTO); } private static async Task GetBooks(IBookRepository bookRepository) { + List booksDTO = new List(); var books = await bookRepository.GetAllBooks(); - return TypedResults.Ok(books); + foreach (var book in books) + { + booksDTO.Add(new BookDTO + { + Id = book.Id, + Title = book.Title, + AuthorId = book.AuthorId, + AuthorName = book.Author.FirstName + " " + book.Author.LastName, + }); + } + return TypedResults.Ok(booksDTO); + } + + private static async Task GetBook(IBookRepository bookRepository, int id) + { + BookDTO bookDTO = new BookDTO(); + var book = bookRepository.GetBook(id); + bookDTO.Id = book.Result.Id; + bookDTO.Title = book.Result.Title; + bookDTO.AuthorId = book.Result.AuthorId; + if(bookDTO.AuthorId != 0) + bookDTO.AuthorName = book.Result.Author.FirstName + " " + book.Result.Author.LastName; + return TypedResults.Ok(bookDTO); + } + private static async Task UpdateBook(IBookRepository bookRepository, int id, BookPut model) + { + + try + { + BookDTO bookDTO = new BookDTO(); + var target = await bookRepository.GetBook(id); + if (target == null) return TypedResults.NotFound("Product was not found"); + + if (model.AuthorId != null) target.AuthorId = model.AuthorId.Value; + + var result = await bookRepository.UpdateBook(target); + bookDTO.Id = result.Id; + bookDTO.Title = result.Title; + bookDTO.AuthorId = result.AuthorId; + bookDTO.AuthorName = result.Author.FirstName + " " + result.Author.LastName; + return Results.Created($"https://localhost:7188/books/{id}", bookDTO); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public static async Task DeleteBook(IBookRepository repository, int id) + { + try + { + var model = await repository.GetBook(id); + if (model == null) + return TypedResults.NotFound("Book was not found"); + if (await repository.DeleteBook(id)) return Results.Ok(new { When = DateTime.Now, Status = "Deleted", Name = model.Title, AuthorId = model.AuthorId, Author = model.Author.FirstName + " " + model.Author.LastName }); + return TypedResults.NotFound(); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } } } } diff --git a/exercise.webapi/Program.cs b/exercise.webapi/Program.cs index 43dec56..ea294b4 100644 --- a/exercise.webapi/Program.cs +++ b/exercise.webapi/Program.cs @@ -11,6 +11,7 @@ builder.Services.AddSwaggerGen(); builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("Library")); builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); @@ -29,4 +30,5 @@ app.UseHttpsRedirection(); app.ConfigureBooksApi(); +app.ConfigureAuthorApi(); app.Run(); diff --git a/exercise.webapi/Repository/AuthorRepository.cs b/exercise.webapi/Repository/AuthorRepository.cs new file mode 100644 index 0000000..7c81fb7 --- /dev/null +++ b/exercise.webapi/Repository/AuthorRepository.cs @@ -0,0 +1,27 @@ +using exercise.webapi.Data; +using exercise.webapi.Models; +using Microsoft.EntityFrameworkCore; + +namespace exercise.webapi.Repository +{ + public class AuthorRepository : IAuthorRepository + { + DataContext _db; + + public AuthorRepository(DataContext db) + { + _db = db; + } + public async Task> GetAllAuthors() + { + return await _db.Authors.Include(a => a.Books).ToListAsync(); + } + + public async Task GetAuthor(int id) + { + var target = await _db.Authors.FindAsync(id); + _db.Entry(target).Collection(x => x.Books).Load(); + return target; + } + } +} diff --git a/exercise.webapi/Repository/BookRepository.cs b/exercise.webapi/Repository/BookRepository.cs index 1f5e64a..b36242d 100644 --- a/exercise.webapi/Repository/BookRepository.cs +++ b/exercise.webapi/Repository/BookRepository.cs @@ -1,4 +1,5 @@ using exercise.webapi.Data; +using exercise.webapi.DTO; using exercise.webapi.Models; using Microsoft.EntityFrameworkCore; @@ -13,10 +14,66 @@ public BookRepository(DataContext db) _db = db; } + public async Task AddBook(Book Book) + { + if (_db.Authors.Find(Book.Author) == null) + { + return Book = null; + } + await _db.Books.AddAsync(Book); + _db.Entry(Book).Reference(x => x.Author).Load(); + await _db.SaveChangesAsync(); + return Book; + } + + public async Task AssignAuthor(int id, int authId) + { + var target = await _db.Books.FindAsync(id); + target.AuthorId = authId; + await _db.SaveChangesAsync(); + return target; + } + + public async Task DeleteBook(int id) + { + var target = await _db.Books.FindAsync(id); + _db.Books.Remove(target); + await _db.SaveChangesAsync(); + return true; + } + public async Task> GetAllBooks() { return await _db.Books.Include(b => b.Author).ToListAsync(); + } + + public async Task GetBook(int id) + { + var target = await _db.Books.FindAsync(id); + _db.Entry(target).Reference(x => x.Author).Load(); + return target; + } + + public async Task RemoveAuthor(int id) + { + var target = await _db.Books.FindAsync(id); + target.Author = null; + target.AuthorId = 0; + await _db.SaveChangesAsync(); + + return target; + } + + public async Task UpdateBook(Book Book) + { + var target = await _db.Books.FindAsync(Book.Id); + _db.Books.Remove(target); + await _db.SaveChangesAsync(); + await _db.Books.AddAsync(Book); + await _db.SaveChangesAsync(); + _db.Entry(Book).Reference(x => x.Author).Load(); + return Book; } } } diff --git a/exercise.webapi/Repository/IAuthorRepository.cs b/exercise.webapi/Repository/IAuthorRepository.cs new file mode 100644 index 0000000..3690f1b --- /dev/null +++ b/exercise.webapi/Repository/IAuthorRepository.cs @@ -0,0 +1,11 @@ +using exercise.webapi.Models; + +namespace exercise.webapi.Repository +{ + public interface IAuthorRepository + { + public Task> GetAllAuthors(); + + Task GetAuthor(int id); + } +} diff --git a/exercise.webapi/Repository/IBookRepository.cs b/exercise.webapi/Repository/IBookRepository.cs index f860016..2b60eb6 100644 --- a/exercise.webapi/Repository/IBookRepository.cs +++ b/exercise.webapi/Repository/IBookRepository.cs @@ -1,9 +1,18 @@ -using exercise.webapi.Models; +using exercise.webapi.DTO; +using exercise.webapi.Models; namespace exercise.webapi.Repository { public interface IBookRepository { public Task> GetAllBooks(); + + Task GetBook(int id); + Task DeleteBook(int id); + Task AddBook(Book Book); + Task UpdateBook(Book Book); + Task RemoveAuthor(int id); + Task AssignAuthor(int id, int authId); + } } diff --git a/exercise.webapi/appsettings.Development.json b/exercise.webapi/appsettings.Development.json index 63d13d3..b4a6720 100644 --- a/exercise.webapi/appsettings.Development.json +++ b/exercise.webapi/appsettings.Development.json @@ -7,8 +7,8 @@ }, "AllowedHosts": "*", - "ConnectionStrings": { - "DefaultConnectionString": "Host=ep-winter-silence-a8e5oju7.eastus2.azure.neon.tech; Database=neondb; Username=neondb_owner; Password=npg_nBmYdDo0Lr3z;" + "ConnectionStrings": { + "DefaultConnectionString": "Host=ep-white-mouse-a80xfjkp.eastus2.azure.neon.tech; Database=neondb; Username=neondb_owner; Password=npg_9yXKvBn4FpIb;" - } + } } \ No newline at end of file diff --git a/exercise.webapi/appsettings.json b/exercise.webapi/appsettings.json index 63d13d3..b4a6720 100644 --- a/exercise.webapi/appsettings.json +++ b/exercise.webapi/appsettings.json @@ -7,8 +7,8 @@ }, "AllowedHosts": "*", - "ConnectionStrings": { - "DefaultConnectionString": "Host=ep-winter-silence-a8e5oju7.eastus2.azure.neon.tech; Database=neondb; Username=neondb_owner; Password=npg_nBmYdDo0Lr3z;" + "ConnectionStrings": { + "DefaultConnectionString": "Host=ep-white-mouse-a80xfjkp.eastus2.azure.neon.tech; Database=neondb; Username=neondb_owner; Password=npg_9yXKvBn4FpIb;" - } + } } \ No newline at end of file diff --git a/exercise.webapi/exercise.webapi.csproj b/exercise.webapi/exercise.webapi.csproj index 0ff4269..ec8316d 100644 --- a/exercise.webapi/exercise.webapi.csproj +++ b/exercise.webapi/exercise.webapi.csproj @@ -9,9 +9,14 @@ - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - +