diff --git a/exercise.webapi/DTO/AddBookDto.cs b/exercise.webapi/DTO/AddBookDto.cs new file mode 100644 index 0000000..3b61ad9 --- /dev/null +++ b/exercise.webapi/DTO/AddBookDto.cs @@ -0,0 +1,9 @@ +namespace exercise.webapi.DTO +{ + public class AddBookDto + { + public string Title { get; set; } + + public int AuthorId { get; set; } + } +} diff --git a/exercise.webapi/DTO/AuthorDto.cs b/exercise.webapi/DTO/AuthorDto.cs new file mode 100644 index 0000000..e9fba7c --- /dev/null +++ b/exercise.webapi/DTO/AuthorDto.cs @@ -0,0 +1,13 @@ +using exercise.webapi.Models; + +namespace exercise.webapi.DTO +{ + public class AuthorDto + { + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + + public ICollection Books { get; set; } = new List(); + } +} diff --git a/exercise.webapi/DTO/AuthorResponseDto.cs b/exercise.webapi/DTO/AuthorResponseDto.cs new file mode 100644 index 0000000..b9a4f5d --- /dev/null +++ b/exercise.webapi/DTO/AuthorResponseDto.cs @@ -0,0 +1,11 @@ +namespace exercise.webapi.DTO +{ + public class AuthorResponseDto + { + 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; } = new List(); + } +} diff --git a/exercise.webapi/DTO/BookDto.cs b/exercise.webapi/DTO/BookDto.cs new file mode 100644 index 0000000..22eea22 --- /dev/null +++ b/exercise.webapi/DTO/BookDto.cs @@ -0,0 +1,13 @@ +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 AuthorDto Author { get; set; } + } +} diff --git a/exercise.webapi/DTO/BookWithoutAuthorDto.cs b/exercise.webapi/DTO/BookWithoutAuthorDto.cs new file mode 100644 index 0000000..8f1fc38 --- /dev/null +++ b/exercise.webapi/DTO/BookWithoutAuthorDto.cs @@ -0,0 +1,8 @@ +namespace exercise.webapi.DTO +{ + public class BookWithoutAuthorDto + { + public int Id { get; set; } + public string Title { get; set; } + } +} diff --git a/exercise.webapi/Data/DataContext.cs b/exercise.webapi/Data/DataContext.cs index b6be7a9..029b8b5 100644 --- a/exercise.webapi/Data/DataContext.cs +++ b/exercise.webapi/Data/DataContext.cs @@ -1,5 +1,6 @@ using exercise.webapi.Models; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using System.Collections.Generic; using System.Reflection.Emit; @@ -7,15 +8,15 @@ namespace exercise.webapi.Data { public class DataContext : DbContext { - - public DataContext(DbContextOptions options) : base(options) + private readonly IConfiguration _configuration; + 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("DefaultConnectionString")); } protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/exercise.webapi/Endpoints/AuthorApi.cs b/exercise.webapi/Endpoints/AuthorApi.cs new file mode 100644 index 0000000..4eb5e8e --- /dev/null +++ b/exercise.webapi/Endpoints/AuthorApi.cs @@ -0,0 +1,71 @@ +using AutoMapper; +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) + { + app.MapGet("/authors", GetAuthors); + app.MapGet("/authors/{id}", GetAuthor); + + } + + private static async Task GetAuthors(IRepository repository, IRepository bookRepository, IMapper mapper) + { + try + { + var authors = await repository.GetAll(); + List response = new List(); + + foreach (var author in authors) + { + var books = bookRepository.FindAll(b => b.AuthorId.Equals(author.Id)); + + AuthorResponseDto authorDto = new AuthorResponseDto(); + authorDto.Id = author.Id; + authorDto.FirstName = author.FirstName; + authorDto.LastName = author.LastName; + authorDto.Email = author.Email; + authorDto.Books = (List)mapper.Map>(books.Result); + response.Add(authorDto); + } + + return TypedResults.Ok(response); + } + catch (Exception ex) + { + return TypedResults.NotFound(ex); + } + } + + private static async Task GetAuthor(IRepository repository, IRepository bookRepository, IMapper mapper, int id) + { + try + { + var author = await repository.Get(a => a.Id.Equals(id), a => a.Books); + var books = bookRepository.FindAll(b => b.AuthorId.Equals(author.Id)); + if (author == null) return TypedResults.NotFound($"Author with {id} was not found"); + + AuthorResponseDto authorDto = new AuthorResponseDto(); + authorDto.FirstName = author.FirstName; + authorDto.LastName = author.LastName; + authorDto.Email = author.Email; + authorDto.Books = (List)mapper.Map>(books.Result); + + + return TypedResults.Ok(mapper.Map(authorDto)); + } + catch (Exception ex) + { + return TypedResults.BadRequest(ex); + } + } + + + } +} diff --git a/exercise.webapi/Endpoints/BookApi.cs b/exercise.webapi/Endpoints/BookApi.cs index 6758215..2d04206 100644 --- a/exercise.webapi/Endpoints/BookApi.cs +++ b/exercise.webapi/Endpoints/BookApi.cs @@ -1,4 +1,6 @@ -using exercise.webapi.Models; +using AutoMapper; +using exercise.webapi.DTO; +using exercise.webapi.Models; using exercise.webapi.Repository; using static System.Reflection.Metadata.BlobBuilder; @@ -9,12 +11,93 @@ public static class BookApi public static void ConfigureBooksApi(this WebApplication app) { app.MapGet("/books", GetBooks); + app.MapGet("/books/{id}", GetBook); + app.MapPost("/books", AddBook); + app.MapPut("/books", UpdateBook); + app.MapDelete("/books/{id}", DeleteBook); } - private static async Task GetBooks(IBookRepository bookRepository) + private static async Task GetBooks(IRepository repository, IMapper mapper) { - var books = await bookRepository.GetAllBooks(); - return TypedResults.Ok(books); + try + { + var books = await repository.GetAll(b => b.Author); + var response = mapper.Map>(books); + return TypedResults.Ok(response); + } + catch (Exception ex) + { + return TypedResults.NotFound(ex); + } + } + + private static async Task GetBook(IRepository repository, IMapper mapper, int id) + { + try + { + var book = await repository.Get(b => b.Id.Equals(id), b => b.Author); + if (book == null) return TypedResults.NotFound($"Book with {id} was not found"); + return TypedResults.Ok(mapper.Map(book)); + } + catch (Exception ex) + { + return TypedResults.BadRequest(ex); + } + } + + private static async Task AddBook(IRepository repository, IRepository authorRepository, IMapper mapper, AddBookDto model) + { + try + { + var author = await authorRepository.Get(a => a.Id.Equals(model.AuthorId)); + if (author == null) return TypedResults.NotFound($"Author with {model.AuthorId} was not found"); + + Book newBook = new Book + { + Title = model.Title, + AuthorId = model.AuthorId, + Author = author, + }; + var book = await repository.Add(newBook); + return TypedResults.Created($"https://localhost:7054/books/", mapper.Map(book)); + } + catch (Exception ex) + { + return TypedResults.NotFound(ex); + } + } + + private static async Task UpdateBook(IRepository repository, IMapper mapper, int bookId, int authorId) + { + try + { + var book = await repository.Get(b => b.Id.Equals(bookId)); + if (book == null) return TypedResults.NotFound($"Book with {bookId} was not found"); + book.AuthorId = authorId; + await repository.Update(book); + var updatedBookWithInclude = await repository.Get(b => b.Id.Equals(bookId), b => b.Author); + return TypedResults.Ok(mapper.Map(updatedBookWithInclude)); + } + catch (Exception ex) + { + return TypedResults.NotFound(ex); + } + } + + private static async Task DeleteBook(IRepository repository, IMapper mapper, int bookId) + { + try + { + var book = await repository.Get(b => b.Id.Equals(bookId), b => b.Author); + if (book == null) return TypedResults.NotFound($"Book with {bookId} was not found"); + await repository.Delete(book); + + return TypedResults.Ok(mapper.Map(book)); + } + catch (Exception ex) + { + return TypedResults.NotFound(ex); + } } } } diff --git a/exercise.webapi/Mapper/AutoMapperProfile.cs b/exercise.webapi/Mapper/AutoMapperProfile.cs new file mode 100644 index 0000000..aca4c7e --- /dev/null +++ b/exercise.webapi/Mapper/AutoMapperProfile.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using exercise.webapi.DTO; +using exercise.webapi.Models; + +namespace exercise.webapi.Mapper +{ + public class AutoMapperProfile : Profile + { + public AutoMapperProfile() + { + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + } + } +} diff --git a/exercise.webapi/Program.cs b/exercise.webapi/Program.cs index 43dec56..6cb83d8 100644 --- a/exercise.webapi/Program.cs +++ b/exercise.webapi/Program.cs @@ -1,5 +1,8 @@ +using System.Diagnostics; using exercise.webapi.Data; using exercise.webapi.Endpoints; +using exercise.webapi.Mapper; +using exercise.webapi.Models; using exercise.webapi.Repository; using Microsoft.EntityFrameworkCore; @@ -9,17 +12,24 @@ // 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.AddAutoMapper(typeof(AutoMapperProfile)); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddDbContext(options => +{ + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")); + options.LogTo(message => Debug.WriteLine(message)); +}); + 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(); } - // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { @@ -27,6 +37,8 @@ app.UseSwaggerUI(); } + app.UseHttpsRedirection(); app.ConfigureBooksApi(); +app.ConfigureAuthorApi(); app.Run(); diff --git a/exercise.webapi/Repository/BookRepository.cs b/exercise.webapi/Repository/BookRepository.cs deleted file mode 100644 index 1f5e64a..0000000 --- a/exercise.webapi/Repository/BookRepository.cs +++ /dev/null @@ -1,22 +0,0 @@ -using exercise.webapi.Data; -using exercise.webapi.Models; -using Microsoft.EntityFrameworkCore; - -namespace exercise.webapi.Repository -{ - public class BookRepository: IBookRepository - { - DataContext _db; - - public BookRepository(DataContext db) - { - _db = db; - } - - public async Task> GetAllBooks() - { - return await _db.Books.Include(b => b.Author).ToListAsync(); - - } - } -} 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..5d9a432 --- /dev/null +++ b/exercise.webapi/Repository/IRepository.cs @@ -0,0 +1,15 @@ +using System.Linq.Expressions; +using exercise.webapi.Models; + +namespace exercise.webapi.Repository +{ + public interface IRepository where TEntity : class + { + Task> GetAll(params Expression>[] includeProperties); + Task> FindAll(Expression> predicate, params Expression>[] includeProperties); + Task Get(Expression> predicate, params Expression>[] includeProperties); + Task Add(TEntity entity); + Task Update(TEntity entity); + Task Delete(TEntity entity); + } +} diff --git a/exercise.webapi/Repository/Repository.cs b/exercise.webapi/Repository/Repository.cs new file mode 100644 index 0000000..790dd9c --- /dev/null +++ b/exercise.webapi/Repository/Repository.cs @@ -0,0 +1,84 @@ +using System.Linq.Expressions; +using exercise.webapi.Data; +using exercise.webapi.Models; +using Microsoft.EntityFrameworkCore; + +namespace exercise.webapi.Repository +{ + public class Repository : IRepository where TEntity : class + { + DataContext _context; + + public Repository(DataContext context) + { + _context = context; + } + + public async Task Add(TEntity entity) + { + await _context.Set().AddAsync(entity); + await _context.SaveChangesAsync(); + return entity; + } + + public async Task Delete(TEntity entity) + { + _context.Set().Remove(entity); + await _context.SaveChangesAsync(); + return entity; + } + + public async Task> FindAll(Expression> predicate, params Expression>[] includeProperties) + { + IQueryable query = _context.Set(); + + if (includeProperties != null) + { + foreach (var includeProperty in includeProperties) + { + query = query.Include(includeProperty); + } + } + + return await query.Where(predicate).ToListAsync(); + } + + public async Task Get(Expression> predicate, params Expression>[] includeProperties) + { + IQueryable query = _context.Set(); + + if (includeProperties != null) + { + foreach (var includeProperty in includeProperties) + { + query = query.Include(includeProperty); + } + } + + return await query.FirstOrDefaultAsync(predicate); + } + + public async Task> GetAll(params Expression>[] includeProperties) + { + IQueryable query = _context.Set(); + + if (includeProperties != null) + { + foreach (var includeProperty in includeProperties) + { + query = query.Include(includeProperty); + } + } + + return await query.ToListAsync(); + } + + + public async Task Update(TEntity entity) + { + _context.Set().Update(entity); + await _context.SaveChangesAsync(); + return entity; + } + } +} diff --git a/exercise.webapi/appsettings.Development.json b/exercise.webapi/appsettings.Development.json index 63d13d3..939892f 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-young-paper-a9ccy4lh-pooler.gwc.azure.neon.tech;Port=5432;Database=noahdb;Username=noahdb_owner;Password=npg_Gqyw9IBRMH0z;SSL Mode=Require;Trust Server Certificate=true;" - } + } } \ No newline at end of file diff --git a/exercise.webapi/appsettings.json b/exercise.webapi/appsettings.json index 63d13d3..e3803db 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-young-paper-a9ccy4lh-pooler.gwc.azure.neon.tech;Port=5432;Database=noahdb;Username=noahdb_owner;Password=npg_Gqyw9IBRMH0z;SSL Mode=Require;Trust Server Certificate=true;" - } + } } \ No newline at end of file diff --git a/exercise.webapi/exercise.webapi.csproj b/exercise.webapi/exercise.webapi.csproj index 0ff4269..b3f9497 100644 --- a/exercise.webapi/exercise.webapi.csproj +++ b/exercise.webapi/exercise.webapi.csproj @@ -8,10 +8,21 @@ + - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - +