diff --git a/exercise.webapi/Data/DataContext.cs b/exercise.webapi/Data/DataContext.cs index b6be7a9..790818a 100644 --- a/exercise.webapi/Data/DataContext.cs +++ b/exercise.webapi/Data/DataContext.cs @@ -1,21 +1,22 @@ using exercise.webapi.Models; using Microsoft.EntityFrameworkCore; -using System.Collections.Generic; -using System.Reflection.Emit; +using Microsoft.EntityFrameworkCore.Diagnostics; namespace exercise.webapi.Data { public class DataContext : DbContext { + private readonly IConfiguration _configuration; - public DataContext(DbContextOptions options) : base(options) + public DataContext(DbContextOptions options, IConfiguration configuration) : base(options) { - + _configuration = configuration; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); - optionsBuilder.UseInMemoryDatabase("Library"); + optionsBuilder.UseNpgsql(_configuration.GetConnectionString("DefaultConnection")); + optionsBuilder.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)); } protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/exercise.webapi/Dto/AuthorResponse.cs b/exercise.webapi/Dto/AuthorResponse.cs new file mode 100644 index 0000000..e7250ed --- /dev/null +++ b/exercise.webapi/Dto/AuthorResponse.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +namespace exercise.webapi.Dto; + +public class AuthorResponse +{ + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public BookResponse[]? Books { get; set; } + + public AuthorResponse(Models.Author author) + { + Id = author.Id; + FirstName = author.FirstName; + LastName = author.LastName; + Email = author.Email; + if (author.Books.Count > 0) + { + Books = author.Books.Select((b) => + { + b.Author = null; + return new BookResponse(b); + }).ToArray(); + } + } +} \ No newline at end of file diff --git a/exercise.webapi/Dto/BookPost.cs b/exercise.webapi/Dto/BookPost.cs new file mode 100644 index 0000000..ae84d09 --- /dev/null +++ b/exercise.webapi/Dto/BookPost.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace exercise.webapi.Dto; + +public class BookPost +{ + [Required] + [MinLength(1)] + [MaxLength(100)] + public string Title { get; set; } + [Required] + public int AuthorId { get; set; } +} \ No newline at end of file diff --git a/exercise.webapi/Dto/BookPut.cs b/exercise.webapi/Dto/BookPut.cs new file mode 100644 index 0000000..ba36ff3 --- /dev/null +++ b/exercise.webapi/Dto/BookPut.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace exercise.webapi.Dto; + +public class BookPut +{ + [MinLength(1)] + [MaxLength(100)] + public string Title { get; set; } + public int AuthorId { get; set; } +} \ No newline at end of file diff --git a/exercise.webapi/Dto/BookResponse.cs b/exercise.webapi/Dto/BookResponse.cs new file mode 100644 index 0000000..560a37e --- /dev/null +++ b/exercise.webapi/Dto/BookResponse.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; +using exercise.webapi.Models; + +namespace exercise.webapi.Dto; + +public class BookResponse +{ + public int Id { get; set; } + public string Title { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public AuthorResponse? Author { get; set; } + + public BookResponse(Book book) + { + Id = book.Id; + Title = book.Title; + // TODO: Throws NullReferenceException when not in the if statement. FIX! + if (book.Author is not null) + { + Author = new AuthorResponse(book.Author); + } + } +} \ No newline at end of file diff --git a/exercise.webapi/Endpoints/AuthorApi.cs b/exercise.webapi/Endpoints/AuthorApi.cs new file mode 100644 index 0000000..f476710 --- /dev/null +++ b/exercise.webapi/Endpoints/AuthorApi.cs @@ -0,0 +1,36 @@ +using exercise.webapi.Dto; +using exercise.webapi.Repository; +using Microsoft.AspNetCore.Mvc; + +namespace exercise.webapi.Endpoints; + +public static class AuthorApi +{ + public static void ConfigureAuthorsApi(this WebApplication app) + { + app.MapGet("/authors", GetAuthors); + app.MapGet("/authors/{id}", GetAuthor); + } + + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + private static async Task GetAuthors(IAuthorRepository authorRepository) + { + var authors = await authorRepository.GetAllAuthors(); + var response = authors.Select(a => new AuthorResponse(a)); + + return TypedResults.Ok(response); + } + + [ProducesResponseType(typeof(AuthorResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + private static async Task GetAuthor(IAuthorRepository authorRepository, int id) + { + var author = await authorRepository.GetAuthorById(id); + if (author == null) + { + return TypedResults.NotFound(); + } + + return TypedResults.Ok(new AuthorResponse(author)); + } +} \ No newline at end of file diff --git a/exercise.webapi/Endpoints/BookApi.cs b/exercise.webapi/Endpoints/BookApi.cs index 6758215..4ae12a5 100644 --- a/exercise.webapi/Endpoints/BookApi.cs +++ b/exercise.webapi/Endpoints/BookApi.cs @@ -1,6 +1,8 @@ -using exercise.webapi.Models; +using System.ComponentModel.DataAnnotations; +using exercise.webapi.Dto; +using exercise.webapi.Models; using exercise.webapi.Repository; -using static System.Reflection.Metadata.BlobBuilder; +using Microsoft.AspNetCore.Mvc; namespace exercise.webapi.Endpoints { @@ -9,12 +11,99 @@ public static class BookApi public static void ConfigureBooksApi(this WebApplication app) { app.MapGet("/books", GetBooks); + app.MapGet("/books/{id}", GetBook); + app.MapPost("/books", CreateBook); + app.MapDelete("/books/{id}", DeleteBook); + app.MapPut("/books/{id}", UpdateBook); } + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] private static async Task GetBooks(IBookRepository bookRepository) { var books = await bookRepository.GetAllBooks(); - return TypedResults.Ok(books); + var response = books.Select(b => new BookResponse(b)); + + foreach (var book in books) + { + book.Author.Books = new List(); + } + return TypedResults.Ok(response); + } + + [ProducesResponseType(typeof(BookResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + private static async Task GetBook(IBookRepository bookRepository, int id) + { + var book = await bookRepository.GetBookById(id); + if (book == null) + { + return TypedResults.NotFound(); + } + book.Author.Books = new List(); + return TypedResults.Ok(new BookResponse(book)); + } + + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + private static async Task DeleteBook(IBookRepository bookRepository, int id) + { + var book = await bookRepository.GetBookById(id); + if (book == null) + { + return TypedResults.NotFound(); + } + bookRepository.DeleteBook(book); + return TypedResults.NoContent(); + } + + [ProducesResponseType(typeof(BookResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + private static async Task UpdateBook(IBookRepository bookRepository, IAuthorRepository authorRepository, int id, [FromBody] BookPut book) + { + var existingBook = await bookRepository.GetBookById(id); + if (existingBook == null) + { + return TypedResults.NotFound(); + } + + existingBook.Title = book.Title; + Console.WriteLine(book.AuthorId); + existingBook.AuthorId = book.AuthorId; + var newBook = await bookRepository.UpdateBook(existingBook); + + newBook.Author.Books = new List(); + return TypedResults.Ok(new BookResponse(newBook)); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + private static async Task CreateBook(IBookRepository bookRepository, + IAuthorRepository authorRepository, [FromBody] BookPost book) + { + try + { + Validator.ValidateObject(book, new ValidationContext(book), true); + } + catch (ValidationException e) + { + return TypedResults.BadRequest(e.Message); + } + + var author = await authorRepository.GetAuthorById(book.AuthorId); + if (author == null) + { + return TypedResults.NotFound(); + } + + var newBook = new Book + { + Title = book.Title, + AuthorId = book.AuthorId + }; + var createdBook = await bookRepository.AddBook(newBook); + createdBook.Author.Books = new List(); + return TypedResults.Created("/books/" + createdBook.Id); } } } diff --git a/exercise.webapi/Program.cs b/exercise.webapi/Program.cs index 43dec56..a8351be 100644 --- a/exercise.webapi/Program.cs +++ b/exercise.webapi/Program.cs @@ -9,13 +9,17 @@ // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("Library")); +builder.Services.AddDbContext(options => + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddScoped(); +builder.Services.AddScoped(); + var app = builder.Build(); -using (var dbContext = new DataContext(new DbContextOptions())) +using (var scope = app.Services.CreateScope()) { + var dbContext = scope.ServiceProvider.GetRequiredService(); dbContext.Database.EnsureCreated(); } @@ -29,4 +33,5 @@ app.UseHttpsRedirection(); app.ConfigureBooksApi(); +app.ConfigureAuthorsApi(); app.Run(); diff --git a/exercise.webapi/Repository/AuthorRepository.cs b/exercise.webapi/Repository/AuthorRepository.cs new file mode 100644 index 0000000..23009f4 --- /dev/null +++ b/exercise.webapi/Repository/AuthorRepository.cs @@ -0,0 +1,31 @@ +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 GetAuthorById(int id) + { + var author = await _db.Authors + .Include(a => a.Books) + .FirstOrDefaultAsync(a => a.Id == id); + + return author; + } +} \ No newline at end of file diff --git a/exercise.webapi/Repository/BookRepository.cs b/exercise.webapi/Repository/BookRepository.cs index 1f5e64a..1e196c7 100644 --- a/exercise.webapi/Repository/BookRepository.cs +++ b/exercise.webapi/Repository/BookRepository.cs @@ -12,11 +12,50 @@ public BookRepository(DataContext db) { _db = db; } + + public async Task AddBook(Book book) + { + await _db.Books.AddAsync(book); + await _db.SaveChangesAsync(); + return book; + } public async Task> GetAllBooks() { - return await _db.Books.Include(b => b.Author).ToListAsync(); + return await _db.Books + .Include(b => b.Author) + .ToListAsync(); + + } + + public async Task GetBookById(int id) + { + return await _db.Books.Select(b => new Book + { + Id = b.Id, + Title = b.Title, + Author = new Author + { + Id = b.Author.Id, + FirstName = b.Author.FirstName, + LastName = b.Author.LastName, + Email = b.Author.Email + } + }).FirstOrDefaultAsync(b => b.Id == id); + } + + public void DeleteBook(Book book) + { + _db.Books.Remove(book); + _db.SaveChanges(); + } + public async Task UpdateBook(Book book) + { + //_db.Books.Update(book); + _db.Attach(book).State = EntityState.Modified; + await _db.SaveChangesAsync(); + return book; } } } diff --git a/exercise.webapi/Repository/IAuthorRepository.cs b/exercise.webapi/Repository/IAuthorRepository.cs new file mode 100644 index 0000000..95cbda5 --- /dev/null +++ b/exercise.webapi/Repository/IAuthorRepository.cs @@ -0,0 +1,9 @@ +using exercise.webapi.Models; + +namespace exercise.webapi.Repository; + +public interface IAuthorRepository +{ + public Task> GetAllAuthors(); + Task GetAuthorById(int id); +} \ No newline at end of file diff --git a/exercise.webapi/Repository/IBookRepository.cs b/exercise.webapi/Repository/IBookRepository.cs index f860016..cd66446 100644 --- a/exercise.webapi/Repository/IBookRepository.cs +++ b/exercise.webapi/Repository/IBookRepository.cs @@ -4,6 +4,10 @@ namespace exercise.webapi.Repository { public interface IBookRepository { + Task AddBook(Book book); public Task> GetAllBooks(); + Task GetBookById(int id); + void DeleteBook(Book book); + Task UpdateBook(Book book); } } diff --git a/exercise.webapi/appsettings.Development.json b/exercise.webapi/appsettings.Development.json index 63d13d3..3c094af 100644 --- a/exercise.webapi/appsettings.Development.json +++ b/exercise.webapi/appsettings.Development.json @@ -8,7 +8,6 @@ "AllowedHosts": "*", "ConnectionStrings": { - "DefaultConnectionString": "Host=ep-winter-silence-a8e5oju7.eastus2.azure.neon.tech; Database=neondb; Username=neondb_owner; Password=npg_nBmYdDo0Lr3z;" - + "DefaultConnection": "Host=localhost;Database=db;Username=user;Password=pass;" } } \ No newline at end of file diff --git a/exercise.webapi/appsettings.json b/exercise.webapi/appsettings.json index 63d13d3..3c094af 100644 --- a/exercise.webapi/appsettings.json +++ b/exercise.webapi/appsettings.json @@ -8,7 +8,6 @@ "AllowedHosts": "*", "ConnectionStrings": { - "DefaultConnectionString": "Host=ep-winter-silence-a8e5oju7.eastus2.azure.neon.tech; Database=neondb; Username=neondb_owner; Password=npg_nBmYdDo0Lr3z;" - + "DefaultConnection": "Host=localhost;Database=db;Username=user;Password=pass;" } } \ No newline at end of file diff --git a/exercise.webapi/exercise.webapi.csproj b/exercise.webapi/exercise.webapi.csproj index 0ff4269..d90ae5d 100644 --- a/exercise.webapi/exercise.webapi.csproj +++ b/exercise.webapi/exercise.webapi.csproj @@ -9,7 +9,12 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + +