diff --git a/.gitignore b/.gitignore index b357a0c..5c58dbb 100644 --- a/.gitignore +++ b/.gitignore @@ -368,4 +368,4 @@ FodyWeavers.xsd */**/obj/Release */Migrations /exercise.wwwapi/appsettings.json -/exercise.wwwapi/appsettings.Development.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..7623cab --- /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 List Books { get; set; } + } +} diff --git a/exercise.webapi/DTO/BookDTO.cs b/exercise.webapi/DTO/BookDTO.cs new file mode 100644 index 0000000..e8c19c7 --- /dev/null +++ b/exercise.webapi/DTO/BookDTO.cs @@ -0,0 +1,26 @@ +using exercise.webapi.Models; + +namespace exercise.webapi.DTO +{ + public class BookDTO + { + public int Id { get; set; } + public string Title { get; set; } + public string AuthorName { get; set; } = string.Empty; + public string PublisherName { get; set; } = string.Empty; + } + + public class BookAuthorDTO + { + public int Id { get; set; } + public string Title { get; set; } + public string AuthorName { get; set; } = string.Empty; + } + + public class BookPublisherDTO + { + public int Id { get; set; } + public string Title { get; set; } + public string PublisherName { get; set; } = string.Empty; + } +} diff --git a/exercise.webapi/DTO/PublisherDTO.cs b/exercise.webapi/DTO/PublisherDTO.cs new file mode 100644 index 0000000..2e6e868 --- /dev/null +++ b/exercise.webapi/DTO/PublisherDTO.cs @@ -0,0 +1,9 @@ +namespace exercise.webapi.DTO +{ + public class PublisherDTO + { + public int Id { get; set; } + public string Name { get; set; } + public List Books { get; set; } + } +} diff --git a/exercise.webapi/Data/DataContext.cs b/exercise.webapi/Data/DataContext.cs index b6be7a9..3b3828f 100644 --- a/exercise.webapi/Data/DataContext.cs +++ b/exercise.webapi/Data/DataContext.cs @@ -24,9 +24,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasData(seeder.Authors); modelBuilder.Entity().HasData(seeder.Books); + modelBuilder.Entity().HasData(seeder.Publishers); } public DbSet Authors { get; set; } public DbSet Books { get; set; } + public DbSet Publishers { get; set; } } } diff --git a/exercise.webapi/Data/Seeder.cs b/exercise.webapi/Data/Seeder.cs index 955e3c8..4184fdb 100644 --- a/exercise.webapi/Data/Seeder.cs +++ b/exercise.webapi/Data/Seeder.cs @@ -76,17 +76,35 @@ public class Seeder "Flowers", "Leopards" }; + private List _publishernames = new List() + { + "Penguin Random House", + "Hacette Livre", + "Harper Collins", + "Simon and Schuster", + "Macmillan Publishers", + "Chronicle Books", + "Persea Books" + }; private List _authors = new List(); private List _books = new List(); + private List _publishers = new List(); public Seeder() { Random authorRandom = new Random(); Random bookRandom = new Random(); + Random publisherRandom = new Random(); - + for (int y = 1; y < _publishernames.Count; y++) + { + Publisher publisher = new Publisher(); + publisher.Id = y; + publisher.Name = _publishernames[y]; + _publishers.Add(publisher); + } for (int x = 1; x < 250; x++) { @@ -106,12 +124,13 @@ public Seeder() book.Title = $"{_firstword[bookRandom.Next(_firstword.Count)]} {_secondword[bookRandom.Next(_secondword.Count)]} {_thirdword[bookRandom.Next(_thirdword.Count)]}"; book.AuthorId = _authors[authorRandom.Next(_authors.Count)].Id; //book.Author = authors[book.AuthorId-1]; + book.PublisherId = _publishers[publisherRandom.Next(_publishers.Count)].Id; _books.Add(book); } - } public List Authors { get { return _authors; } } public List Books { get { return _books; } } + public List Publishers { get { return _publishers; } } } } diff --git a/exercise.webapi/Endpoints/AuthorApi.cs b/exercise.webapi/Endpoints/AuthorApi.cs new file mode 100644 index 0000000..067c65c --- /dev/null +++ b/exercise.webapi/Endpoints/AuthorApi.cs @@ -0,0 +1,78 @@ +using exercise.webapi.DTO; +using exercise.webapi.Models; +using exercise.webapi.Repository; +using Microsoft.AspNetCore.Mvc; + +namespace exercise.webapi.Endpoints +{ + public static class AuthorApi + { + public static void ConfigureAuthorsApi(this WebApplication app) + { + var author = app.MapGroup("authors"); + author.MapGet("/", GetAuthors); + author.MapGet("/{id}", GetAuthor); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + private static async Task GetAuthor(IAuthorRepository authorRepository, int id) + { + var author = await authorRepository.GetAuthor(id); + var result = new AuthorDTO() + { + Id = author.Id, + FirstName = author.FirstName, + LastName = author.LastName, + Email = author.Email, + Books = [] + }; + + foreach (var b in author.Books) + { + BookPublisherDTO bookDTO = new BookPublisherDTO() + { + Id = b.Id, + Title = b.Title, + PublisherName = b.Publisher.Name + }; + result.Books.Add(bookDTO); + } + + return TypedResults.Ok(result); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + private static async Task GetAuthors(IAuthorRepository authorRepository) + { + var authors = await authorRepository.GetAllAuthors(); + var result = new List(); + + foreach (var a in authors) + { + var authorDTO = new AuthorDTO() + { + Id = a.Id, + FirstName = a.FirstName, + LastName = a.LastName, + Email = a.Email, + Books = [] + }; + + foreach(var b in a.Books) + { + BookPublisherDTO bookDTO = new BookPublisherDTO() + { + Id = b.Id, + Title = b.Title, + PublisherName = b.Publisher.Name + }; + authorDTO.Books.Add(bookDTO); + } + result.Add(authorDTO); + } + + return TypedResults.Ok(result); + } + + } +} diff --git a/exercise.webapi/Endpoints/BookApi.cs b/exercise.webapi/Endpoints/BookApi.cs index 6758215..7b0ef0a 100644 --- a/exercise.webapi/Endpoints/BookApi.cs +++ b/exercise.webapi/Endpoints/BookApi.cs @@ -1,5 +1,9 @@ -using exercise.webapi.Models; +using System.Collections.Immutable; +using exercise.webapi.DTO; +using exercise.webapi.Models; using exercise.webapi.Repository; +using exercise.webapi.ViewModel; +using Microsoft.AspNetCore.Mvc; using static System.Reflection.Metadata.BlobBuilder; namespace exercise.webapi.Endpoints @@ -8,13 +12,137 @@ public static class BookApi { public static void ConfigureBooksApi(this WebApplication app) { - app.MapGet("/books", GetBooks); + var book = app.MapGroup("books"); + book.MapGet("/", GetBooks); + book.MapGet("/{id}", GetBook); + book.MapPut("/{id}", UpdateBook); + book.MapDelete("/{id}", DeleteBook); + book.MapPost("/{id}", CreateBook); } + [ProducesResponseType(StatusCodes.Status200OK)] + private static async Task GetBook(IBookRepository bookRepository, int id) + { + var book = await bookRepository.GetBook(id); + var result = new BookDTO() + { + Id = book.Id, + Title = book.Title, + AuthorName = $"{book.Author.FirstName} {book.Author.LastName}", + PublisherName = book.Publisher.Name + }; + + return TypedResults.Ok(result); + } + + [ProducesResponseType(StatusCodes.Status200OK)] private static async Task GetBooks(IBookRepository bookRepository) { var books = await bookRepository.GetAllBooks(); - return TypedResults.Ok(books); + var result = new List(); + + foreach (var b in books) + { + var book = new BookDTO() + { + Id = b.Id, + Title = b.Title, + AuthorName = $"{b.Author.FirstName} {b.Author.LastName}", + PublisherName = b.Publisher.Name + }; + result.Add(book); + + } + + return TypedResults.Ok(result); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + private static async Task UpdateBook(IBookRepository bookRepository, int id, BookPut model) + { + try + { + var target = await bookRepository.GetBook(id); + if (model.AuthorId != null) target.AuthorId = (int)model.AuthorId; + if (model.PublisherId != null) target.PublisherId = (int)model.PublisherId; + + await bookRepository.UpdateBook(target); + var update = await bookRepository.GetBook(id); + var result = new BookDTO() + { + Id = update.Id, + Title = update.Title, + AuthorName = $"{update.Author.FirstName} {update.Author.LastName}", + PublisherName = update.Publisher.Name + }; + + return TypedResults.Ok(result); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + private static async Task DeleteBook(IBookRepository bookRepository, int id) + { + try + { + var target = await bookRepository.GetBook(id); + if (target == null) return TypedResults.NotFound("Book not found."); + if (bookRepository.DeleteBook(id) != null) + { + var result = new BookDTO() + { + Id = target.Id, + Title = target.Title, + AuthorName = $"{target.Author.FirstName} {target.Author.LastName}", + PublisherName = target.Publisher.Name + }; + return TypedResults.Ok(result); + } + return TypedResults.NotFound("Book not found."); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } } + + [ProducesResponseType(StatusCodes.Status200OK)] + private static async Task CreateBook(IBookRepository bookRepository, IAuthorRepository authorRepository, IPublisherRepository publisherRepository, BookPost model) + { + try + { + if (string.IsNullOrEmpty(model.Title)) return TypedResults.BadRequest("Title is required."); + if (model.AuthorId <= 0) return TypedResults.BadRequest("Valid AuthorId is required."); + if (model.PublisherId <= 0) return TypedResults.BadRequest("Valid PublisherId is required."); + + var author = await authorRepository.GetAuthor(model.AuthorId); + if (author == null) return TypedResults.NotFound("Author does not exist in database"); + var publisher = await publisherRepository.GetPublisher(model.PublisherId); + if (publisher == null) return TypedResults.NotFound("Publiser does not exist in database"); + + var newBook = await bookRepository.CreateBook(new Book(){ Title = model.Title, AuthorId = model.AuthorId, PublisherId = model.PublisherId}); + + var createdBook = await bookRepository.GetBook(newBook.Id); + + BookDTO result = new BookDTO() + { + Id = createdBook.Id, + Title = createdBook.Title, + AuthorName = $"{createdBook.Author.FirstName} {createdBook.Author.LastName}", + PublisherName = createdBook.Publisher.Name + }; + return TypedResults.Created($"/{createdBook.Id}", result); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + } } diff --git a/exercise.webapi/Endpoints/PublisherApi.cs b/exercise.webapi/Endpoints/PublisherApi.cs new file mode 100644 index 0000000..1927598 --- /dev/null +++ b/exercise.webapi/Endpoints/PublisherApi.cs @@ -0,0 +1,76 @@ +using exercise.webapi.DTO; +using exercise.webapi.Repository; +using Microsoft.AspNetCore.Mvc; + +namespace exercise.webapi.Endpoints +{ + public static class PublisherApi + { + + public static void ConfigurePublishersApi(this WebApplication app) + { + var publisher = app.MapGroup("publishers"); + publisher.MapGet("/", GetPublishers); + publisher.MapGet("/{id}", GetPublisher); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + private static async Task GetPublisher(IPublisherRepository publisherRepository, int id) + { + var publisher = await publisherRepository.GetPublisher(id); + var result = new PublisherDTO() + { + Id = publisher.Id, + Name = publisher.Name, + Books = [] + }; + + foreach (var b in publisher.Books) + { + BookAuthorDTO bookDTO = new BookAuthorDTO() + { + Id = b.Id, + Title = b.Title, + AuthorName = $"{b.Author.FirstName} {b.Author.LastName}", + }; + result.Books.Add(bookDTO); + } + + return TypedResults.Ok(result); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + private static async Task GetPublishers(IPublisherRepository publisherRepository) + { + var publishers = await publisherRepository.GetAllPublishers(); + var result = new List(); + + foreach (var p in publishers) + { + var publisherDTO = new PublisherDTO() + { + Id = p.Id, + Name = p.Name, + Books = [] + }; + + foreach (var b in p.Books) + { + if (!p.Books.Any()) continue; + BookAuthorDTO bookDTO = new BookAuthorDTO() + { + Id = b.Id, + Title = b.Title, + AuthorName = $"{b.Author.FirstName} {b.Author.LastName}", + }; + publisherDTO.Books.Add(bookDTO); + } + result.Add(publisherDTO); + } + + return TypedResults.Ok(result); + } + + } +} + diff --git a/exercise.webapi/Models/Author.cs b/exercise.webapi/Models/Author.cs index 9f47878..6da6f42 100644 --- a/exercise.webapi/Models/Author.cs +++ b/exercise.webapi/Models/Author.cs @@ -8,8 +8,6 @@ public class Author public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } - - [JsonIgnore] // Todo: replace this with DTO approach public ICollection Books { get; set; } = new List(); } } diff --git a/exercise.webapi/Models/Book.cs b/exercise.webapi/Models/Book.cs index 9534929..e07d178 100644 --- a/exercise.webapi/Models/Book.cs +++ b/exercise.webapi/Models/Book.cs @@ -6,8 +6,10 @@ public class Book { public int Id { get; set; } public string Title { get; set; } - public int AuthorId { get; set; } public Author Author { get; set; } + + public int PublisherId { get; set; } + public Publisher Publisher { get; set; } } } diff --git a/exercise.webapi/Models/Publisher.cs b/exercise.webapi/Models/Publisher.cs new file mode 100644 index 0000000..98a7276 --- /dev/null +++ b/exercise.webapi/Models/Publisher.cs @@ -0,0 +1,9 @@ +namespace exercise.webapi.Models +{ + public class Publisher + { + public int Id { get; set; } + public string Name { get; set; } + public ICollection Books { get; set; } = new List(); + } +} diff --git a/exercise.webapi/Program.cs b/exercise.webapi/Program.cs index 43dec56..5fdb7b7 100644 --- a/exercise.webapi/Program.cs +++ b/exercise.webapi/Program.cs @@ -11,6 +11,9 @@ builder.Services.AddSwaggerGen(); builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("Library")); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + var app = builder.Build(); @@ -29,4 +32,6 @@ app.UseHttpsRedirection(); app.ConfigureBooksApi(); +app.ConfigureAuthorsApi(); +app.ConfigurePublishersApi(); app.Run(); diff --git a/exercise.webapi/Repository/AuthorRepository.cs b/exercise.webapi/Repository/AuthorRepository.cs new file mode 100644 index 0000000..8dd92a5 --- /dev/null +++ b/exercise.webapi/Repository/AuthorRepository.cs @@ -0,0 +1,24 @@ +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).ThenInclude(b => b.Publisher).ToListAsync(); + } + public async Task GetAuthor(int id) + { + return await _db.Authors.Include(a => a.Books).ThenInclude(b => b.Publisher).FirstOrDefaultAsync(a => a.Id == id); + } + } +} diff --git a/exercise.webapi/Repository/BookRepository.cs b/exercise.webapi/Repository/BookRepository.cs index 1f5e64a..9ea9bc5 100644 --- a/exercise.webapi/Repository/BookRepository.cs +++ b/exercise.webapi/Repository/BookRepository.cs @@ -15,8 +15,34 @@ public BookRepository(DataContext db) public async Task> GetAllBooks() { - return await _db.Books.Include(b => b.Author).ToListAsync(); + return await _db.Books.Include(b => b.Author).Include(b => b.Publisher).ToListAsync(); } + + public async Task GetBook(int id) + { + return await _db.Books.Include(b => b.Author).Include(b => b.Publisher).FirstOrDefaultAsync(b => b.Id == id); + } + + public async Task UpdateBook(Book entity) + { + _db.Books.Attach(entity); + _db.Entry(entity).State = EntityState.Modified; + await _db.SaveChangesAsync(); + return entity; + } + public async Task DeleteBook(int id) + { + var target = await _db.Books.FirstOrDefaultAsync(b => b.Id == id); + _db.Books.Remove(target); + _db.SaveChanges(); + return target; + } + public async Task CreateBook(Book entity) + { + await _db.Books.AddAsync(entity); + await _db.SaveChangesAsync(); + return entity; + } } } diff --git a/exercise.webapi/Repository/IAuthorRepository.cs b/exercise.webapi/Repository/IAuthorRepository.cs new file mode 100644 index 0000000..9dfd1ef --- /dev/null +++ b/exercise.webapi/Repository/IAuthorRepository.cs @@ -0,0 +1,10 @@ +using exercise.webapi.Models; + +namespace exercise.webapi.Repository +{ + public interface IAuthorRepository + { + Task GetAuthor(int id); + Task> GetAllAuthors(); + } +} diff --git a/exercise.webapi/Repository/IBookRepository.cs b/exercise.webapi/Repository/IBookRepository.cs index f860016..c442e30 100644 --- a/exercise.webapi/Repository/IBookRepository.cs +++ b/exercise.webapi/Repository/IBookRepository.cs @@ -5,5 +5,9 @@ namespace exercise.webapi.Repository public interface IBookRepository { public Task> GetAllBooks(); + public Task GetBook(int id); + public Task UpdateBook(Book entity); + public Task DeleteBook(int id); + public Task CreateBook(Book entity); } } diff --git a/exercise.webapi/Repository/IPublisherRepository.cs b/exercise.webapi/Repository/IPublisherRepository.cs new file mode 100644 index 0000000..32a1aab --- /dev/null +++ b/exercise.webapi/Repository/IPublisherRepository.cs @@ -0,0 +1,10 @@ +using exercise.webapi.Models; + +namespace exercise.webapi.Repository +{ + public interface IPublisherRepository + { + Task GetPublisher(int id); + Task> GetAllPublishers(); + } +} diff --git a/exercise.webapi/Repository/PublisherRepository.cs b/exercise.webapi/Repository/PublisherRepository.cs new file mode 100644 index 0000000..da00a36 --- /dev/null +++ b/exercise.webapi/Repository/PublisherRepository.cs @@ -0,0 +1,24 @@ +using exercise.webapi.Data; +using exercise.webapi.Models; +using Microsoft.EntityFrameworkCore; + +namespace exercise.webapi.Repository +{ + public class PublisherRepository : IPublisherRepository + { + DataContext _db; + + public PublisherRepository(DataContext db) + { + _db = db; + } + public async Task> GetAllPublishers() + { + return await _db.Publishers.Include(p => p.Books).ThenInclude(b => b.Author).ToListAsync(); + } + public async Task GetPublisher(int id) + { + return await _db.Publishers.Include(p => p.Books).ThenInclude(b => b.Author).FirstOrDefaultAsync(a => a.Id == id); + } + } +} diff --git a/exercise.webapi/ViewModel/BookPost.cs b/exercise.webapi/ViewModel/BookPost.cs new file mode 100644 index 0000000..169fd09 --- /dev/null +++ b/exercise.webapi/ViewModel/BookPost.cs @@ -0,0 +1,9 @@ +namespace exercise.webapi.ViewModel +{ + public class BookPost + { + public string Title { get; set; } + public int AuthorId { get; set; } + public int PublisherId { get; set; } + } +} diff --git a/exercise.webapi/ViewModel/BookPut.cs b/exercise.webapi/ViewModel/BookPut.cs new file mode 100644 index 0000000..33b34f7 --- /dev/null +++ b/exercise.webapi/ViewModel/BookPut.cs @@ -0,0 +1,10 @@ +using exercise.webapi.Models; + +namespace exercise.webapi.ViewModel +{ + public class BookPut + { + public int? AuthorId { get; set; } + public int? PublisherId { get; set; } + } +} diff --git a/exercise.webapi/exercise.webapi.csproj b/exercise.webapi/exercise.webapi.csproj index 0ff4269..00b0722 100644 --- a/exercise.webapi/exercise.webapi.csproj +++ b/exercise.webapi/exercise.webapi.csproj @@ -9,6 +9,11 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive +