diff --git a/README.md b/README.md
index aa062f3..8479b6a 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,21 @@
# C# Entity Framework Intro
-1. Fork this repository
-2. Clone your fork to your machine
-3. Open the ef.intro.sln in Visual Studio
-
+## Completed
+Completed everything but super extensions. To run the project, make sure to
+update / create an appsettings.json with a connection string. E.g.
+
+
+"ConnectionStrings": {
+ "DefaultConnectionString": "Host=your; Database=values; Username=goes; Password=here;"
+}
+
+
+Then run migrations and update the database. Test the endpoints in scalar or swagger.
+(I recommend scalar)
+
+1. `add-migration init`
+1. `update-database init`
+1. Start the project
## Setup
diff --git a/exercise.webapi/DTO/Author.cs b/exercise.webapi/DTO/Author.cs
new file mode 100644
index 0000000..85ae7e8
--- /dev/null
+++ b/exercise.webapi/DTO/Author.cs
@@ -0,0 +1,7 @@
+namespace exercise.webapi.DTO
+{
+ public record AuthorPost(string FirstName, string LastName, string Email);
+ public record AuthorPut(string? FirstName, string? LastName, string? Email);
+ public record AuthorInternal(int Id, string FirstName, string LastName, string Email);
+ public record AuthorView(int Id, string FirstName, string LastName, string Email, IEnumerable Books);
+}
diff --git a/exercise.webapi/DTO/Book.cs b/exercise.webapi/DTO/Book.cs
new file mode 100644
index 0000000..a3da65c
--- /dev/null
+++ b/exercise.webapi/DTO/Book.cs
@@ -0,0 +1,10 @@
+namespace exercise.webapi.DTO
+{
+ public record BookPost(string Title, int AuthorId, int PublisherId);
+ public record BookPut(string? Title);
+ public record BookView(int Id, string Title, IEnumerable Authors, PublisherInternal Publisher, CheckoutInternal? Checkout);
+ public record BookInternal(int Id, string Title);
+ public record BookInternalPublisher(int Id, string Title, PublisherInternal Publisher);
+ public record BookInternalAuthor(int Id, string Title, IEnumerable Authors);
+ public record BookInternalAuthorPublisher(int Id, string Title, IEnumerable Authors, PublisherInternal Publisher);
+}
diff --git a/exercise.webapi/DTO/Checkout.cs b/exercise.webapi/DTO/Checkout.cs
new file mode 100644
index 0000000..d965711
--- /dev/null
+++ b/exercise.webapi/DTO/Checkout.cs
@@ -0,0 +1,6 @@
+namespace exercise.webapi.DTO
+{
+ public record CheckoutPost(int BookId);
+ public record CheckoutView(int Id, DateTime CheckoutTime, DateTime? ReturnTime, DateTime ExpectedReturnTime, BookInternalAuthorPublisher Book);
+ public record CheckoutInternal(int Id, DateTime CheckoutTime, DateTime? ReturnTime, DateTime ExpectedReturnTime);
+}
diff --git a/exercise.webapi/DTO/Publisher.cs b/exercise.webapi/DTO/Publisher.cs
new file mode 100644
index 0000000..54b09cd
--- /dev/null
+++ b/exercise.webapi/DTO/Publisher.cs
@@ -0,0 +1,7 @@
+namespace exercise.webapi.DTO
+{
+ public record PublisherPost(string Name);
+ public record PublisherPut(string? Name);
+ public record PublisherView(int Id, string Name, IEnumerable Books);
+ public record PublisherInternal(int Id, string Name);
+}
diff --git a/exercise.webapi/Data/DataContext.cs b/exercise.webapi/Data/DataContext.cs
index b6be7a9..1230de6 100644
--- a/exercise.webapi/Data/DataContext.cs
+++ b/exercise.webapi/Data/DataContext.cs
@@ -1,32 +1,33 @@
using exercise.webapi.Models;
using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Collections.Generic;
using System.Reflection.Emit;
namespace exercise.webapi.Data
{
- public class DataContext : DbContext
+ public class DataContext(DbContextOptions options) : DbContext(options)
{
-
- public DataContext(DbContextOptions options) : base(options)
- {
-
- }
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- {
- base.OnConfiguring(optionsBuilder);
- optionsBuilder.UseInMemoryDatabase("Library");
- }
-
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
+ modelBuilder.Entity().HasMany(x => x.Authors).WithMany(x => x.Books).UsingEntity();
+ modelBuilder.Entity().HasOne(x => x.Publisher).WithMany(x => x.Books).HasForeignKey(x => x.PublisherId).OnDelete(DeleteBehavior.SetNull);
+ modelBuilder.Entity().HasKey(x => new { x.AuthorId, x.BookId });
+ modelBuilder.Entity().HasOne(x => x.Book).WithMany(x => x.Checkouts).HasForeignKey(x => x.BookId).OnDelete(DeleteBehavior.SetNull);
+
Seeder seeder = new Seeder();
modelBuilder.Entity().HasData(seeder.Authors);
+ modelBuilder.Entity().HasData(seeder.Publishers);
modelBuilder.Entity().HasData(seeder.Books);
-
+ modelBuilder.Entity().HasData(seeder.AuthorBooks);
+ modelBuilder.Entity().HasData(seeder.Checkouts);
}
public DbSet Authors { get; set; }
public DbSet Books { get; set; }
+ public DbSet AuthorBooks { get; set; }
+ public DbSet Publishers { get; set; }
+ public DbSet Checkouts { get; set; }
+
}
}
diff --git a/exercise.webapi/Data/Seeder.cs b/exercise.webapi/Data/Seeder.cs
index 955e3c8..6ee255c 100644
--- a/exercise.webapi/Data/Seeder.cs
+++ b/exercise.webapi/Data/Seeder.cs
@@ -79,13 +79,17 @@ public class Seeder
private List _authors = new List();
private List _books = new List();
+ private List _publishers = new List();
+ private List _authorBooks = new List();
+ private List _checkouts = new List();
public Seeder()
{
Random authorRandom = new Random();
Random bookRandom = new Random();
-
+ Random publisherRandom = new Random();
+ Random checkoutRandom = new Random();
for (int x = 1; x < 250; x++)
@@ -98,20 +102,76 @@ public Seeder()
_authors.Add(author);
}
+ for (int y = 1; y < 250; y++)
+ {
+ Publisher publisher = new Publisher();
+ publisher.Id = y;
+ publisher.Name = $"{_lastnames[publisherRandom.Next(_lastnames.Count)]} {_thirdword[bookRandom.Next(_thirdword.Count)]}";
+ _publishers.Add(publisher);
+ }
+
+ HashSet> authorBookSet = new HashSet>();
for (int y = 1; y < 250; y++)
{
Book book = new Book();
book.Id = y;
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;
+ AuthorBook authorBook = new AuthorBook();
+ var ids = new Tuple(book.Id, _authors[authorRandom.Next(_authors.Count)].Id);
+ // Horrible stuff, cant be bothered to do better.
+ while (authorBookSet.Contains(ids)) ids = new Tuple(book.Id, _authors[authorRandom.Next(_authors.Count)].Id);
+ authorBookSet.Add(ids);
+ authorBook.BookId = ids.Item1;
+ authorBook.AuthorId = ids.Item2;
+ _authorBooks.Add(authorBook);
_books.Add(book);
}
+ for (int y = 1; y < 250; y++)
+ {
+ AuthorBook authorBook = new AuthorBook();
+ var ids = new Tuple(_books[bookRandom.Next(_books.Count)].Id, _authors[authorRandom.Next(_authors.Count)].Id);
+ if (authorBookSet.Contains(ids)) continue;
+ authorBookSet.Add(ids);
+ authorBook.BookId = ids.Item1;
+ authorBook.AuthorId = ids.Item2;
+ _authorBooks.Add(authorBook);
+ }
+ HashSet checkedOutBooks = new HashSet();
+ for (int y = 1; y < 100; y++)
+ {
+ Checkout checkout = new Checkout();
+ var bookId = _books[bookRandom.Next(_books.Count)].Id;
+ if (checkedOutBooks.Contains(bookId)) continue;
+ bool isReturned = checkoutRandom.NextDouble() < .7;
+ DateTime checkoutTime = DateTime.UtcNow.AddDays(-checkoutRandom.Next(180));
+ if (isReturned)
+ {
+ bool isOnTime = checkoutRandom.NextDouble() < .65;
+ if (isOnTime)
+ {
+ checkout.ReturnTime = checkoutTime.AddDays(checkoutRandom.Next(14));
+ } else
+ {
+ checkout.ReturnTime = checkoutTime.AddDays(checkoutRandom.Next(15, 60));
+ }
+ } else
+ {
+ checkedOutBooks.Add(bookId);
+ }
+ checkout.Id = y;
+ checkout.CheckoutTime = checkoutTime;
+ checkout.BookId = bookId;
+ _checkouts.Add(checkout);
+ }
}
public List Authors { get { return _authors; } }
public List Books { get { return _books; } }
+ public List Publishers { get { return _publishers; } }
+ public List AuthorBooks { get { return _authorBooks; } }
+ public List Checkouts { get { return _checkouts; } }
}
}
diff --git a/exercise.webapi/Endpoints/AuthorEndpoints.cs b/exercise.webapi/Endpoints/AuthorEndpoints.cs
new file mode 100644
index 0000000..5f35317
--- /dev/null
+++ b/exercise.webapi/Endpoints/AuthorEndpoints.cs
@@ -0,0 +1,164 @@
+using exercise.webapi.DTO;
+using exercise.webapi.Exceptions;
+using exercise.webapi.Models;
+using exercise.webapi.Repository;
+
+namespace exercise.webapi.Endpoints
+{
+ public static class AuthorEndpoints
+ {
+ public static string Path { get; private set; } = "authors";
+
+ public static void ConfigureAuthorsEndpoints(this WebApplication app)
+ {
+ var group = app.MapGroup(Path);
+
+ group.MapPost("/", CreateAuthor);
+ group.MapGet("/", GetAuthors);
+ group.MapGet("/{id}", GetAuthor);
+ group.MapPut("/{id}", UpdateAuthor);
+ group.MapDelete("/{id}", DeleteAuthor);
+ }
+ public static async Task CreateAuthor(IRepository repository, AuthorPost entity)
+ {
+ try
+ {
+ Author author = await repository.Add(new Author
+ {
+ FirstName = entity.FirstName,
+ LastName = entity.LastName,
+ Email = entity.Email,
+ });
+ return TypedResults.Ok(new AuthorView(
+ author.Id,
+ author.FirstName,
+ author.LastName,
+ author.Email,
+ author.Books.Select(b => new BookInternalPublisher(
+ b.Id,
+ b.Title,
+ new PublisherInternal(
+ b.Publisher.Id,
+ b.Publisher.Name
+ )
+ ))
+ ));
+ }
+ catch (IdNotFoundException ex)
+ {
+ return TypedResults.NotFound(new { ex.Message });
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+ public static async Task GetAuthors(IRepository repository)
+ {
+ try
+ {
+ IEnumerable authors = await repository.GetAll();
+ return TypedResults.Ok(authors.Select(a =>
+ {
+ return new AuthorView(
+ a.Id,
+ a.FirstName,
+ a.LastName,
+ a.Email,
+ a.Books.Select(b => new BookInternalPublisher(
+ b.Id,
+ b.Title,
+ new PublisherInternal(
+ b.Publisher.Id,
+ b.Publisher.Name
+ )
+ )));
+ }));
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+
+ public static async Task GetAuthor(IRepository repository, int id)
+ {
+ try
+ {
+ Author author = await repository.Get(id);
+ return TypedResults.Ok(new AuthorView(
+ author.Id,
+ author.FirstName,
+ author.LastName,
+ author.Email,
+ author.Books.Select(b => new BookInternalPublisher(
+ b.Id,
+ b.Title,
+ new PublisherInternal(
+ b.Publisher.Id,
+ b.Publisher.Name
+ )
+ ))
+ ));
+ }
+ catch (IdNotFoundException ex)
+ {
+ return TypedResults.NotFound(new { ex.Message });
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+ public static async Task UpdateAuthor(IRepository bookRepository, IRepository authorRepository, int id, AuthorPut entity)
+ {
+ try
+ {
+ Author author = await bookRepository.Get(id);
+ if (entity.FirstName != null) author.FirstName = entity.FirstName;
+ if (entity.LastName != null) author.LastName = entity.LastName;
+ if (entity.Email != null) author.Email = entity.Email;
+
+ author = await bookRepository.Update(author);
+ return TypedResults.Ok(new AuthorView(
+ author.Id,
+ author.FirstName,
+ author.LastName,
+ author.Email,
+ author.Books.Select(b => new BookInternalPublisher(
+ b.Id,
+ b.Title,
+ new PublisherInternal(
+ b.Publisher.Id,
+ b.Publisher.Name
+ )
+ ))
+ ));
+ }
+ catch (IdNotFoundException ex)
+ {
+ return TypedResults.NotFound(new { ex.Message });
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+ public static async Task DeleteAuthor(IRepository repository, int id)
+ {
+ try
+ {
+ Author author = await repository.Delete(id);
+ return TypedResults.Ok(new { Message = $"Deleted Author with FirstName = {author.FirstName}" });
+ }
+ catch (IdNotFoundException ex)
+ {
+ return TypedResults.NotFound(new { ex.Message });
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+ }
+}
diff --git a/exercise.webapi/Endpoints/BookApi.cs b/exercise.webapi/Endpoints/BookApi.cs
deleted file mode 100644
index 6758215..0000000
--- a/exercise.webapi/Endpoints/BookApi.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using exercise.webapi.Models;
-using exercise.webapi.Repository;
-using static System.Reflection.Metadata.BlobBuilder;
-
-namespace exercise.webapi.Endpoints
-{
- public static class BookApi
- {
- public static void ConfigureBooksApi(this WebApplication app)
- {
- app.MapGet("/books", GetBooks);
- }
-
- private static async Task GetBooks(IBookRepository bookRepository)
- {
- var books = await bookRepository.GetAllBooks();
- return TypedResults.Ok(books);
- }
- }
-}
diff --git a/exercise.webapi/Endpoints/BookEndpoints.cs b/exercise.webapi/Endpoints/BookEndpoints.cs
new file mode 100644
index 0000000..91fd600
--- /dev/null
+++ b/exercise.webapi/Endpoints/BookEndpoints.cs
@@ -0,0 +1,325 @@
+using exercise.webapi.DTO;
+using exercise.webapi.Exceptions;
+using exercise.webapi.Models;
+using exercise.webapi.Repository;
+using Microsoft.AspNetCore.Mvc;
+
+namespace exercise.webapi.Endpoints
+{
+ public static class BookEndpoints
+ {
+ public static string Path { get; private set; } = "books";
+
+ public static void ConfigureBooksEndpoints(this WebApplication app)
+ {
+ var group = app.MapGroup(Path);
+
+ group.MapPost("/", CreateBook);
+ group.MapGet("/", GetBooks);
+ group.MapGet("/{id}", GetBook);
+ group.MapPut("/{id}", UpdateBook);
+ group.MapDelete("/{id}", DeleteBook);
+ group.MapPost("/author/add/{id}", AddAuthor);
+ group.MapPost("/author/remove/{id}", RemoveAuthor);
+ }
+
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public static async Task CreateBook(IRepository bookRepository, IRepository authorRepository, IRepository publisherRepository, BookPost entity)
+ {
+ try
+ {
+ Author author = await authorRepository.Get(entity.AuthorId);
+ Publisher publisher = await publisherRepository.Get(entity.PublisherId);
+ Book book = await bookRepository.Add(new Book
+ {
+ Title = entity.Title,
+ PublisherId = entity.PublisherId,
+ });
+ book.Authors.Add(author);
+ await bookRepository.Update(book);
+ return TypedResults.Created($"/{Path}/{book.Id}", new BookView(
+ book.Id,
+ book.Title,
+ [new AuthorInternal(
+ author.Id,
+ author.FirstName,
+ author.LastName,
+ author.Email
+ )],
+ new PublisherInternal(
+ publisher.Id,
+ publisher.Name
+ ),
+ null
+ ));
+ }
+ catch (IdNotFoundException ex)
+ {
+ return TypedResults.NotFound(new {ex.Message});
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public static async Task GetBooks(IRepository repository, string? isCheckedOut, string? isOverdue)
+ {
+ try
+ {
+ IEnumerable books = await repository.GetAll();
+ if (!string.IsNullOrEmpty(isCheckedOut))
+ {
+ bool value = false;
+ if (!bool.TryParse(isCheckedOut, out value))
+ {
+ return TypedResults.BadRequest(new { Message = "The query param 'isCheckedOut' should be true / false" });
+ }
+ if (value)
+ {
+ books = books.Where(book => book.Checkouts.Count != 0 && book.Checkouts.Any(checkout => checkout.ReturnTime == null));
+ } else
+ {
+ books = books.Where(book => book.Checkouts.Count == 0 || book.Checkouts.All(checkout => checkout.ReturnTime != null));
+ }
+ }
+ if (!string.IsNullOrEmpty(isOverdue))
+ {
+ bool value = false;
+ if (!bool.TryParse(isOverdue, out value))
+ {
+ return TypedResults.BadRequest(new { Message = "The query param 'isOverdue' should be true / false" });
+ }
+ if (value)
+ {
+ books = books.Where(book => book.Checkouts.Count != 0 && book.Checkouts.Any(checkout => checkout.ReturnTime == null && checkout.ExpectedReturnTime < DateTime.UtcNow));
+ } else
+ {
+ books = books.Where(book => book.Checkouts.Count == 0 || book.Checkouts.All(checkout =>
+ (checkout.ReturnTime == null && checkout.ExpectedReturnTime > DateTime.UtcNow)
+ || (checkout.ReturnTime != null && checkout.ExpectedReturnTime > checkout.ReturnTime)
+ ));
+ }
+ }
+ return TypedResults.Ok(books.Select(b =>
+ {
+ Checkout? checkout = b.Checkouts.OrderByDescending(c => c.CheckoutTime).FirstOrDefault();
+
+ return new BookView(
+ b.Id,
+ b.Title,
+ b.Authors.Select(author => new AuthorInternal(
+ author.Id,
+ author.FirstName,
+ author.LastName,
+ author.Email
+ )),
+ new PublisherInternal(
+ b.Publisher.Id,
+ b.Publisher.Name
+ ),
+ checkout == null ? null : new CheckoutInternal(
+ checkout.Id,
+ checkout.CheckoutTime,
+ checkout.ReturnTime,
+ checkout.ExpectedReturnTime
+ )
+ );
+ }));
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public static async Task GetBook(IRepository repository, int id)
+ {
+ try
+ {
+ Book book = await repository.Get(id);
+ Checkout? checkout = book.Checkouts.OrderByDescending(c => c.CheckoutTime).FirstOrDefault();
+ return TypedResults.Ok(new BookView(
+ book.Id,
+ book.Title,
+ book.Authors.Select(author => new AuthorInternal(
+ author.Id,
+ author.FirstName,
+ author.LastName,
+ author.Email
+ )),
+ new PublisherInternal(
+ book.Publisher.Id,
+ book.Publisher.Name
+ ),
+ checkout == null ? null : new CheckoutInternal(
+ checkout.Id,
+ checkout.CheckoutTime,
+ checkout.ReturnTime,
+ checkout.ExpectedReturnTime
+ )
+ ));
+ }
+ catch (IdNotFoundException ex)
+ {
+ return TypedResults.NotFound(new { ex.Message });
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public static async Task UpdateBook(IRepository repository, int id, BookPut entity)
+ {
+ try
+ {
+ Book book = await repository.Get(id);
+ if (entity.Title != null) book.Title = entity.Title;
+
+ book = await repository.Update(book);
+ Checkout? checkout = book.Checkouts.OrderByDescending(c => c.CheckoutTime).FirstOrDefault();
+ return TypedResults.Ok(new BookView(
+ book.Id,
+ book.Title,
+ book.Authors.Select(author => new AuthorInternal(
+ author.Id,
+ author.FirstName,
+ author.LastName,
+ author.Email
+ )),
+ new PublisherInternal(
+ book.Publisher.Id,
+ book.Publisher.Name
+ ),
+ checkout == null ? null : new CheckoutInternal(
+ checkout.Id,
+ checkout.CheckoutTime,
+ checkout.ReturnTime,
+ checkout.ExpectedReturnTime
+ )
+ ));
+ }
+ catch (IdNotFoundException ex)
+ {
+ return TypedResults.NotFound(new { ex.Message });
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public static async Task DeleteBook(IRepository repository, int id)
+ {
+ try
+ {
+ Book book = await repository.Delete(id);
+ return TypedResults.Ok(new { Message = $"Deleted Book with Title = {book.Title}" });
+ }
+ catch (IdNotFoundException ex)
+ {
+ return TypedResults.NotFound(new { ex.Message });
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+ public static async Task AddAuthor(IRepository bookRepository, IRepository authorRepository, int id, int authorId)
+ {
+ try
+ {
+ Book book = await bookRepository.Get(id);
+ if (book.Authors.Any(a => a.Id == authorId)) return TypedResults.BadRequest(new { Message = $"Author with ID = {authorId} is already registered to this book!" });
+ Author author = await authorRepository.Get(authorId);
+ book.Authors.Add(author);
+ book = await bookRepository.Update(book);
+ Checkout? checkout = book.Checkouts.OrderByDescending(c => c.CheckoutTime).FirstOrDefault();
+
+ return TypedResults.Ok(new BookView(
+ book.Id,
+ book.Title,
+ book.Authors.Select(a => new AuthorInternal(
+ a.Id,
+ a.FirstName,
+ a.LastName,
+ a.Email
+ )),
+ new PublisherInternal(
+ book.Publisher.Id,
+ book.Publisher.Name
+ ),
+ checkout == null ? null : new CheckoutInternal(
+ checkout.Id,
+ checkout.CheckoutTime,
+ checkout.ReturnTime,
+ checkout.ExpectedReturnTime
+ )
+ ));
+ }
+ catch (IdNotFoundException ex)
+ {
+ return TypedResults.NotFound(new { ex.Message });
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+ public static async Task RemoveAuthor(IRepository bookRepository, IRepository authorRepository, int id, int authorId)
+ {
+ try
+ {
+ Book book = await bookRepository.Get(id);
+ if (book.Authors.Count <= 1) return TypedResults.BadRequest(new { Message = $"Book must have at least one author!" });
+ Author author = await authorRepository.Get(authorId);
+ book.Authors.Remove(author);
+ book = await bookRepository.Update(book);
+ Checkout? checkout = book.Checkouts.OrderByDescending(c => c.CheckoutTime).FirstOrDefault();
+
+ return TypedResults.Ok(new BookView(
+ book.Id,
+ book.Title,
+ book.Authors.Select(a => new AuthorInternal(
+ a.Id,
+ a.FirstName,
+ a.LastName,
+ a.Email
+ )),
+ new PublisherInternal(
+ book.Publisher.Id,
+ book.Publisher.Name
+ ),
+ checkout == null ? null : new CheckoutInternal(
+ checkout.Id,
+ checkout.CheckoutTime,
+ checkout.ReturnTime,
+ checkout.ExpectedReturnTime
+ )
+ ));
+ }
+ catch (IdNotFoundException ex)
+ {
+ return TypedResults.NotFound(new { ex.Message });
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+ }
+}
diff --git a/exercise.webapi/Endpoints/CheckoutEndpoints.cs b/exercise.webapi/Endpoints/CheckoutEndpoints.cs
new file mode 100644
index 0000000..4d8b98f
--- /dev/null
+++ b/exercise.webapi/Endpoints/CheckoutEndpoints.cs
@@ -0,0 +1,187 @@
+
+using exercise.webapi.DTO;
+using exercise.webapi.Exceptions;
+using exercise.webapi.Models;
+using exercise.webapi.Repository;
+using Microsoft.AspNetCore.Mvc;
+
+namespace exercise.webapi.Endpoints
+{
+ public static class CheckoutEndpoints
+ {
+ public static string Path { get; private set; } = "checkouts";
+
+ public static void ConfigureCheckoutEndpoints(this WebApplication app)
+ {
+ var group = app.MapGroup(Path);
+
+ group.MapGet("/", GetCheckouts);
+ group.MapGet("/{id}", GetCheckout);
+ group.MapPost("/checkout", CheckoutBook);
+ group.MapPost("/return", ReturnBook);
+ }
+
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ private static async Task GetCheckouts(IRepository repository)
+ {
+ try
+ {
+ IEnumerable checkouts = await repository.GetAll();
+ return TypedResults.Ok(checkouts.Select(checkout =>
+ {
+ return new CheckoutView(
+ checkout.Id,
+ checkout.CheckoutTime,
+ checkout.ReturnTime,
+ checkout.ExpectedReturnTime,
+ new BookInternalAuthorPublisher(
+ checkout.Book.Id,
+ checkout.Book.Title,
+ checkout.Book.Authors.Select(author => new AuthorInternal(
+ author.Id,
+ author.FirstName,
+ author.LastName,
+ author.Email
+ )),
+ new PublisherInternal(
+ checkout.Book.Publisher.Id,
+ checkout.Book.Publisher.Name
+ )
+ )
+ );
+ }));
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ private static async Task GetCheckout(IRepository repository, int id)
+ {
+ try
+ {
+ Checkout checkout = await repository.Get(id);
+ return TypedResults.Ok(new CheckoutView(
+ checkout.Id,
+ checkout.CheckoutTime,
+ checkout.ReturnTime,
+ checkout.ExpectedReturnTime,
+ new BookInternalAuthorPublisher(
+ checkout.Book.Id,
+ checkout.Book.Title,
+ checkout.Book.Authors.Select(author => new AuthorInternal(
+ author.Id,
+ author.FirstName,
+ author.LastName,
+ author.Email
+ )),
+ new PublisherInternal(
+ checkout.Book.Publisher.Id,
+ checkout.Book.Publisher.Name
+ )
+ )));
+ }
+ catch (IdNotFoundException ex)
+ {
+ return TypedResults.NotFound(new { ex.Message });
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ private static async Task CheckoutBook(IRepository checkoutRepository, IRepository bookRepository, CheckoutPost entity)
+ {
+ try
+ {
+ Book book = await bookRepository.Get(entity.BookId);
+ if (book.Checkouts.Any(checkout => checkout.ReturnTime == null)) return TypedResults.Conflict(new { Message = "That book is already checked out!" });
+ Checkout checkout = await checkoutRepository.Add(new Checkout
+ {
+ BookId = book.Id,
+ });
+ return TypedResults.Created($"/{Path}/{checkout.Id}", new CheckoutView(
+ checkout.Id,
+ checkout.CheckoutTime,
+ checkout.ReturnTime,
+ checkout.ExpectedReturnTime,
+ new BookInternalAuthorPublisher(
+ checkout.Book.Id,
+ checkout.Book.Title,
+ checkout.Book.Authors.Select(author => new AuthorInternal(
+ author.Id,
+ author.FirstName,
+ author.LastName,
+ author.Email
+ )),
+ new PublisherInternal(
+ checkout.Book.Publisher.Id,
+ checkout.Book.Publisher.Name
+ )
+ )));
+ }
+ catch (IdNotFoundException ex)
+ {
+ return TypedResults.NotFound(new { ex.Message });
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ private static async Task ReturnBook(IRepository checkoutRepository, IRepository bookRepository, CheckoutPost entity)
+ {
+ try
+ {
+ Book book = await bookRepository.Get(entity.BookId);
+ if (book.Checkouts.All(checkout => checkout.ReturnTime != null)) return TypedResults.Conflict(new { Message = "That book is not checked out!" });
+ Checkout? checkout = book.Checkouts.Where(c => c.ReturnTime == null).FirstOrDefault();
+ if (checkout == null)
+ {
+ return TypedResults.Problem("This should never happen but okay...");
+ }
+ checkout.ReturnTime = DateTime.UtcNow;
+ checkout = await checkoutRepository.Update(checkout);
+ return TypedResults.Ok(new CheckoutView(
+ checkout.Id,
+ checkout.CheckoutTime,
+ checkout.ReturnTime,
+ checkout.ExpectedReturnTime,
+ new BookInternalAuthorPublisher(
+ checkout.Book.Id,
+ checkout.Book.Title,
+ checkout.Book.Authors.Select(author => new AuthorInternal(
+ author.Id,
+ author.FirstName,
+ author.LastName,
+ author.Email
+ )),
+ new PublisherInternal(
+ checkout.Book.Publisher.Id,
+ checkout.Book.Publisher.Name
+ )
+ )));
+ }
+ catch (IdNotFoundException ex)
+ {
+ return TypedResults.NotFound(new { ex.Message });
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+ }
+}
diff --git a/exercise.webapi/Endpoints/PublisherEndpoints.cs b/exercise.webapi/Endpoints/PublisherEndpoints.cs
new file mode 100644
index 0000000..039fa13
--- /dev/null
+++ b/exercise.webapi/Endpoints/PublisherEndpoints.cs
@@ -0,0 +1,75 @@
+using exercise.webapi.DTO;
+using exercise.webapi.Exceptions;
+using exercise.webapi.Models;
+using exercise.webapi.Repository;
+
+namespace exercise.webapi.Endpoints
+{
+ public static class PublisherEndpoints
+ {
+ public static string Path { get; private set; } = "publishers";
+
+ public static void ConfigurePublisherEndpoints(this WebApplication app)
+ {
+ var group = app.MapGroup(Path);
+
+ group.MapGet("/", GetPublishers);
+ group.MapGet("/{id}", GetPublisher);
+ }
+ public static async Task GetPublishers(IRepository repository)
+ {
+ try
+ {
+ IEnumerable publishers = await repository.GetAll();
+ return TypedResults.Ok(publishers.Select(publisher => new PublisherView(
+ publisher.Id,
+ publisher.Name,
+ publisher.Books.Select(book => new BookInternalAuthor(
+ book.Id,
+ book.Title,
+ book.Authors.Select(author => new AuthorInternal(
+ author.Id,
+ author.FirstName,
+ author.LastName,
+ author.Email
+ ))
+ ))
+ )));
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+
+ public static async Task GetPublisher(IRepository repository, int id)
+ {
+ try
+ {
+ Publisher publisher = await repository.Get(id);
+ return TypedResults.Ok(new PublisherView(
+ publisher.Id,
+ publisher.Name,
+ publisher.Books.Select(book => new BookInternalAuthor(
+ book.Id,
+ book.Title,
+ book.Authors.Select(author => new AuthorInternal(
+ author.Id,
+ author.FirstName,
+ author.LastName,
+ author.Email
+ ))
+ ))
+ ));
+ }
+ catch (IdNotFoundException ex)
+ {
+ return TypedResults.NotFound(new { ex.Message });
+ }
+ catch (Exception ex)
+ {
+ return TypedResults.Problem(ex.Message);
+ }
+ }
+ }
+}
diff --git a/exercise.webapi/Exceptions/IdNotFoundException.cs b/exercise.webapi/Exceptions/IdNotFoundException.cs
new file mode 100644
index 0000000..82f7793
--- /dev/null
+++ b/exercise.webapi/Exceptions/IdNotFoundException.cs
@@ -0,0 +1,11 @@
+namespace exercise.webapi.Exceptions
+{
+ public class IdNotFoundException : ArgumentException
+ {
+ public IdNotFoundException() { }
+
+ public IdNotFoundException(string? message) : base(message) { }
+
+ public IdNotFoundException(string? message, Exception? innerException) : base(message, innerException) { }
+ }
+}
diff --git a/exercise.webapi/Models/Author.cs b/exercise.webapi/Models/Author.cs
index 9f47878..99b00b6 100644
--- a/exercise.webapi/Models/Author.cs
+++ b/exercise.webapi/Models/Author.cs
@@ -9,7 +9,7 @@ public class Author
public string LastName { get; set; }
public string Email { get; set; }
- [JsonIgnore] // Todo: replace this with DTO approach
- public ICollection Books { get; set; } = new List();
+ public List Books { get; set; } = [];
+ public List AuthorBooks { get; set; } = [];
}
}
diff --git a/exercise.webapi/Models/AuthorBook.cs b/exercise.webapi/Models/AuthorBook.cs
new file mode 100644
index 0000000..6de7c30
--- /dev/null
+++ b/exercise.webapi/Models/AuthorBook.cs
@@ -0,0 +1,10 @@
+namespace exercise.webapi.Models
+{
+ public class AuthorBook
+ {
+ public int AuthorId { get; set; }
+ public Author Author { get; set; }
+ public int BookId { get; set; }
+ public Book Book { get; set; }
+ }
+}
diff --git a/exercise.webapi/Models/Book.cs b/exercise.webapi/Models/Book.cs
index 9534929..4fc1dee 100644
--- a/exercise.webapi/Models/Book.cs
+++ b/exercise.webapi/Models/Book.cs
@@ -7,7 +7,12 @@ public class Book
public int Id { get; set; }
public string Title { get; set; }
- public int AuthorId { get; set; }
- public Author Author { get; set; }
+ public List Authors { get; set; } = [];
+ public List AuthorBooks { get; set; } = [];
+
+ public int PublisherId { get; set; }
+ public Publisher Publisher { get; set; }
+
+ public List Checkouts { get; set; }
}
}
diff --git a/exercise.webapi/Models/Checkout.cs b/exercise.webapi/Models/Checkout.cs
new file mode 100644
index 0000000..814a9ac
--- /dev/null
+++ b/exercise.webapi/Models/Checkout.cs
@@ -0,0 +1,16 @@
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace exercise.webapi.Models
+{
+ public class Checkout
+ {
+ public int Id { get; set; }
+ public int BookId { get; set; }
+ public Book Book { get; set; }
+ public DateTime CheckoutTime { get; set; } = DateTime.UtcNow;
+ public DateTime? ReturnTime { get; set; }
+
+ [NotMapped]
+ public DateTime ExpectedReturnTime { get { return CheckoutTime.AddDays(14); } }
+ }
+}
diff --git a/exercise.webapi/Models/Publisher.cs b/exercise.webapi/Models/Publisher.cs
new file mode 100644
index 0000000..65596c4
--- /dev/null
+++ b/exercise.webapi/Models/Publisher.cs
@@ -0,0 +1,10 @@
+namespace exercise.webapi.Models
+{
+ public class Publisher
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+
+ public List Books { get; set; } = [];
+ }
+}
diff --git a/exercise.webapi/Program.cs b/exercise.webapi/Program.cs
index 43dec56..d6306d5 100644
--- a/exercise.webapi/Program.cs
+++ b/exercise.webapi/Program.cs
@@ -1,32 +1,50 @@
using exercise.webapi.Data;
using exercise.webapi.Endpoints;
+using exercise.webapi.Models;
using exercise.webapi.Repository;
using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
+using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// 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.AddScoped();
+builder.Services.AddOpenApi();
-var app = builder.Build();
+var configuration = new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json")
+ .Build();
-using (var dbContext = new DataContext(new DbContextOptions()))
+builder.Services.AddDbContext(options =>
{
- dbContext.Database.EnsureCreated();
-}
+ var connectionString = configuration.GetConnectionString("DefaultConnectionString");
+ options.UseNpgsql(connectionString);
+
+ options.ConfigureWarnings(warnings =>
+ warnings.Ignore(RelationalEventId.PendingModelChangesWarning));
+});
+
+
+builder.Services.AddScoped, BookRepository>();
+builder.Services.AddScoped, AuthorRepository>();
+builder.Services.AddScoped, PublisherRepository>();
+builder.Services.AddScoped, CheckoutRepository>();
+
+var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
- app.UseSwagger();
- app.UseSwaggerUI();
+ app.MapOpenApi();
+ app.MapScalarApiReference();
+ app.MapSwagger();
}
app.UseHttpsRedirection();
-app.ConfigureBooksApi();
+app.ConfigureBooksEndpoints();
+app.ConfigureAuthorsEndpoints();
+app.ConfigureCheckoutEndpoints();
+app.ConfigurePublisherEndpoints();
app.Run();
diff --git a/exercise.webapi/Properties/launchSettings.json b/exercise.webapi/Properties/launchSettings.json
index 27b9975..60aac02 100644
--- a/exercise.webapi/Properties/launchSettings.json
+++ b/exercise.webapi/Properties/launchSettings.json
@@ -1,41 +1,24 @@
{
- "$schema": "http://json.schemastore.org/launchsettings.json",
- "iisSettings": {
- "windowsAuthentication": false,
- "anonymousAuthentication": true,
- "iisExpress": {
- "applicationUrl": "http://localhost:7277",
- "sslPort": 44338
- }
- },
"profiles": {
"http": {
- "commandName": "Project",
- "dotnetRunMessages": true,
- "launchBrowser": true,
- "launchUrl": "swagger",
- "applicationUrl": "http://localhost:5201",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "launchUrl": "scalar/v1",
+ "applicationUrl": "http://localhost:5201",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
- "launchBrowser": true,
- "launchUrl": "swagger",
+ "launchBrowser": false,
+ "launchUrl": "scalar/v1",
"applicationUrl": "https://localhost:7054;http://localhost:5201",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
- },
- "IIS Express": {
- "commandName": "IISExpress",
- "launchBrowser": true,
- "launchUrl": "swagger",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
}
}
}
diff --git a/exercise.webapi/Repository/AuthorRepository.cs b/exercise.webapi/Repository/AuthorRepository.cs
new file mode 100644
index 0000000..d203135
--- /dev/null
+++ b/exercise.webapi/Repository/AuthorRepository.cs
@@ -0,0 +1,52 @@
+using exercise.webapi.Data;
+using exercise.webapi.DTO;
+using exercise.webapi.Exceptions;
+using exercise.webapi.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace exercise.webapi.Repository
+{
+ public class AuthorRepository(DataContext db) : IRepository
+ {
+ private DataContext _db = db;
+
+ public async Task Add(Author entity)
+ {
+ await _db.AddAsync(entity);
+ await _db.SaveChangesAsync();
+ return entity;
+ }
+
+ public async Task Delete(int id)
+ {
+ Author author = await Get(id);
+ _db.Remove(author);
+ await _db.SaveChangesAsync();
+ return author;
+ }
+
+ public async Task Get(int id)
+ {
+ Author? author = await _db.Authors.Include(a => a.Books).ThenInclude(b => b.Publisher).SingleOrDefaultAsync(x => x.Id == id);
+ if (author == null) throw new IdNotFoundException("The provided author Id does not exist!");
+ return author;
+ }
+
+ public async Task> GetAll()
+ {
+ return await _db.Authors.Include(a => a.Books).ThenInclude(b => b.Publisher).ToListAsync();
+ }
+
+ public async Task Update(Author entity)
+ {
+ if (_db.Entry(entity).State == EntityState.Detached)
+ {
+ _db.Authors.Attach(entity);
+ _db.Entry(entity).State = EntityState.Modified;
+ }
+
+ await _db.SaveChangesAsync();
+ return entity;
+ }
+ }
+}
diff --git a/exercise.webapi/Repository/BookRepository.cs b/exercise.webapi/Repository/BookRepository.cs
index 1f5e64a..3635071 100644
--- a/exercise.webapi/Repository/BookRepository.cs
+++ b/exercise.webapi/Repository/BookRepository.cs
@@ -1,10 +1,12 @@
using exercise.webapi.Data;
+using exercise.webapi.DTO;
+using exercise.webapi.Exceptions;
using exercise.webapi.Models;
using Microsoft.EntityFrameworkCore;
namespace exercise.webapi.Repository
{
- public class BookRepository: IBookRepository
+ public class BookRepository : IRepository
{
DataContext _db;
@@ -13,10 +15,43 @@ public BookRepository(DataContext db)
_db = db;
}
- public async Task> GetAllBooks()
+ public async Task Add(Book entity)
{
- return await _db.Books.Include(b => b.Author).ToListAsync();
+ await _db.Books.AddAsync(entity);
+ await _db.SaveChangesAsync();
+ return entity;
+ }
+
+ public async Task Delete(int id)
+ {
+ Book book = await Get(id);
+ _db.Books.Remove(book);
+ await _db.SaveChangesAsync();
+ return book;
+ }
+
+ public async Task Get(int id)
+ {
+ Book? book = await _db.Books.Include(b => b.Authors).Include(b => b.Publisher).Include(b => b.Checkouts).SingleOrDefaultAsync(x => x.Id == id);
+ if (book == null) throw new IdNotFoundException("The provided book Id does not exist!");
+ return book;
+ }
+
+ public async Task> GetAll()
+ {
+ return await _db.Books.Include(book => book.Authors).Include(b => b.Publisher).Include(b => b.Checkouts).ToListAsync();
+ }
+
+ public async Task Update(Book entity)
+ {
+ if (_db.Entry(entity).State == EntityState.Detached)
+ {
+ _db.Books.Attach(entity);
+ _db.Entry(entity).State = EntityState.Modified;
+ }
+ await _db.SaveChangesAsync();
+ return entity;
}
}
}
diff --git a/exercise.webapi/Repository/CheckoutRepository.cs b/exercise.webapi/Repository/CheckoutRepository.cs
new file mode 100644
index 0000000..15b1b48
--- /dev/null
+++ b/exercise.webapi/Repository/CheckoutRepository.cs
@@ -0,0 +1,52 @@
+using exercise.webapi.Data;
+using exercise.webapi.DTO;
+using exercise.webapi.Exceptions;
+using exercise.webapi.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace exercise.webapi.Repository
+{
+ public class CheckoutRepository(DataContext db) : IRepository
+ {
+ DataContext _db = db;
+
+ public async Task Add(Checkout entity)
+ {
+ await _db.Checkouts.AddAsync(entity);
+ await _db.SaveChangesAsync();
+ return entity;
+ }
+
+ public async Task Delete(int id)
+ {
+ Checkout checkout = await Get(id);
+ _db.Checkouts.Remove(checkout);
+ await _db.SaveChangesAsync();
+ return checkout;
+ }
+
+ public async Task Get(int id)
+ {
+ Checkout? checkout = await _db.Checkouts.Include(p => p.Book).ThenInclude(b => b.Authors).Include(p => p.Book).ThenInclude(b => b.Publisher).SingleOrDefaultAsync(x => x.Id == id);
+ if (checkout == null) throw new IdNotFoundException("The provided checkout Id does not exist!");
+ return checkout;
+ }
+
+ public async Task> GetAll()
+ {
+ return await _db.Checkouts.Include(checkout => checkout.Book).ThenInclude(b => b.Authors).Include(p => p.Book).ThenInclude(b => b.Publisher).ToListAsync();
+ }
+
+ public async Task Update(Checkout entity)
+ {
+ if (_db.Entry(entity).State == EntityState.Detached)
+ {
+ _db.Checkouts.Attach(entity);
+ _db.Entry(entity).State = EntityState.Modified;
+ }
+
+ await _db.SaveChangesAsync();
+ return entity;
+ }
+ }
+}
diff --git a/exercise.webapi/Repository/IBookRepository.cs b/exercise.webapi/Repository/IBookRepository.cs
deleted file mode 100644
index f860016..0000000
--- a/exercise.webapi/Repository/IBookRepository.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using exercise.webapi.Models;
-
-namespace exercise.webapi.Repository
-{
- public interface IBookRepository
- {
- public Task> GetAllBooks();
- }
-}
diff --git a/exercise.webapi/Repository/IRepository.cs b/exercise.webapi/Repository/IRepository.cs
new file mode 100644
index 0000000..37c2cb6
--- /dev/null
+++ b/exercise.webapi/Repository/IRepository.cs
@@ -0,0 +1,11 @@
+namespace exercise.webapi.Repository
+{
+ public interface IRepository where T : class
+ {
+ Task Get(int id);
+ Task> GetAll();
+ Task Add(T entity);
+ Task Update(T entity);
+ Task Delete(int id);
+ }
+}
diff --git a/exercise.webapi/Repository/PublisherRepository.cs b/exercise.webapi/Repository/PublisherRepository.cs
new file mode 100644
index 0000000..1018413
--- /dev/null
+++ b/exercise.webapi/Repository/PublisherRepository.cs
@@ -0,0 +1,52 @@
+using exercise.webapi.Data;
+using exercise.webapi.DTO;
+using exercise.webapi.Exceptions;
+using exercise.webapi.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace exercise.webapi.Repository
+{
+ public class PublisherRepository(DataContext db) : IRepository
+ {
+ DataContext _db = db;
+
+ public async Task Add(Publisher entity)
+ {
+ await _db.Publishers.AddAsync(entity);
+ await _db.SaveChangesAsync();
+ return entity;
+ }
+
+ public async Task Delete(int id)
+ {
+ Publisher publisher = await Get(id);
+ _db.Publishers.Remove(publisher);
+ await _db.SaveChangesAsync();
+ return publisher;
+ }
+
+ public async Task Get(int id)
+ {
+ Publisher? publisher = await _db.Publishers.Include(b => b.Books).ThenInclude(book => book.Authors).SingleOrDefaultAsync(x => x.Id == id);
+ if (publisher == null) throw new IdNotFoundException("The provided publisher Id does not exist!");
+ return publisher;
+ }
+
+ public async Task> GetAll()
+ {
+ return await _db.Publishers.Include(publisher => publisher.Books).ThenInclude(book => book.Authors).ToListAsync();
+ }
+
+ public async Task Update(Publisher entity)
+ {
+ if (_db.Entry(entity).State == EntityState.Detached)
+ {
+ _db.Publishers.Attach(entity);
+ _db.Entry(entity).State = EntityState.Modified;
+ }
+
+ await _db.SaveChangesAsync();
+ return entity;
+ }
+ }
+}
diff --git a/exercise.webapi/exercise.webapi.csproj b/exercise.webapi/exercise.webapi.csproj
index 0ff4269..5ca671c 100644
--- a/exercise.webapi/exercise.webapi.csproj
+++ b/exercise.webapi/exercise.webapi.csproj
@@ -8,10 +8,16 @@
-
-
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
-
+