Included navigation fields in get queries

This commit is contained in:
Khwezi Mngoma
2026-05-25 23:00:17 +02:00
parent d55bf4f82f
commit 4a85d01d1a
8 changed files with 73 additions and 22 deletions
@@ -30,7 +30,7 @@ public class BooksService(IDbContextFactory<MidrandBooksDbContext> contextFactor
} }
} }
public async ValueTask<Result<long>> PublishBookAsync(long authorId, long productId, CancellationToken cancellationToken = default) public async ValueTask<Result<long>> CreateBookAsync(long authorId, long productId, CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -65,7 +65,11 @@ public class BooksService(IDbContextFactory<MidrandBooksDbContext> contextFactor
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var book = await context.Books var book = await context.Books
.AsNoTracking().FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken); .AsNoTracking()
.Include(b => b.Author)
.Include(b => b.Product!.Price)
.Include(b => b.Pages)
.FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken);
return book is null return book is null
? Result.Fail<AuthorBook>(new Error($"Book with ID {bookId} not found")) ? Result.Fail<AuthorBook>(new Error($"Book with ID {bookId} not found"))
@@ -88,6 +92,8 @@ public class BooksService(IDbContextFactory<MidrandBooksDbContext> contextFactor
var books = await context.Books var books = await context.Books
.AsNoTracking() .AsNoTracking()
.Include(b => b.Author)
.Include(b => b.Product!.Price)
.OrderByDescending(b => b.CreatedAt) .OrderByDescending(b => b.CreatedAt)
.Where(b => b.AuthorId == authorId) .Where(b => b.AuthorId == authorId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -101,4 +107,34 @@ public class BooksService(IDbContextFactory<MidrandBooksDbContext> contextFactor
return Result.Fail<AuthorBook[]>(new Error(ex.Message).CausedBy(ex)); return Result.Fail<AuthorBook[]>(new Error(ex.Message).CausedBy(ex));
} }
} }
public async ValueTask<Result<AuthorBook[]>> GetPublishedBooksAsync(int offset, int limit, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var books = await context.Books
.AsNoTracking()
.Include(b => b.Author)
.Include(b => b.Product!.Price)
.Include(b => b.Pages)
.Where(b => b.Enabled && b.Product!.Enabled && b.Author.Enabled)
.OrderByDescending(b => b.Ranking)
.ThenByDescending(b => b.Ranking)
.ThenByDescending(b => b.CreatedAt)
.ThenByDescending(b => b.UpdatedAt)
.Skip(offset).Take(limit)
.AsSplitQuery()
.ToArrayAsync(cancellationToken);
return books?.Length > 0
? Result.Ok(books.Select(b => b.ToModel()).ToArray())
: Result.Fail<AuthorBook[]>(new Error("No published books found."));
}
catch (Exception ex)
{
return Result.Fail<AuthorBook[]>(new Error(ex.Message).CausedBy(ex));
}
}
} }
@@ -8,7 +8,7 @@ public class AuthorBook : Models.AuthorBook
{ {
public virtual Author Author { get; set; } = new(); public virtual Author Author { get; set; } = new();
public virtual Product Book { get; set; } = new(); public new virtual Product? Product { get; set; }
public virtual ICollection<BookPage> Pages { get; set; } = []; public virtual ICollection<BookPage> Pages { get; set; } = [];
} }
@@ -20,7 +20,7 @@ public class AuthorBookConfiguration : IEntityTypeConfiguration<AuthorBook>
.HasForeignKey(f => f.AuthorId) .HasForeignKey(f => f.AuthorId)
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Book) builder.HasOne(f => f.Product)
.WithMany() .WithMany()
.HasForeignKey(f => f.ProductId) .HasForeignKey(f => f.ProductId)
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
@@ -1,4 +1,6 @@
namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Models; using LiteCharms.Features.MidrandBooks.Products.Models;
namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
public class AuthorBook public class AuthorBook
{ {
@@ -16,5 +18,7 @@ public class AuthorBook
public int Ranking { get; set; } public int Ranking { get; set; }
public Product? Product { get; set; }
public bool Enabled { get; set; } public bool Enabled { get; set; }
} }
@@ -1,4 +1,5 @@
using LiteCharms.Features.MidrandBooks.Authors.Models; using LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
using LiteCharms.Features.MidrandBooks.Authors.Models;
using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Postgres;
using LiteCharms.Features.MidrandBooks.Products.Models; using LiteCharms.Features.MidrandBooks.Products.Models;
@@ -8,7 +9,7 @@ namespace LiteCharms.Features.MidrandBooks.Authors;
public class AuthorService(IDbContextFactory<MidrandBooksDbContext> contextFactory) public class AuthorService(IDbContextFactory<MidrandBooksDbContext> contextFactory)
{ {
public async ValueTask<Result<Product[]>> GetAuthorBooksAsync(long authorId, CancellationToken cancellationToken) public async ValueTask<Result<AuthorBook[]>> GetAuthorBooksAsync(long authorId, CancellationToken cancellationToken)
{ {
try try
{ {
@@ -17,21 +18,24 @@ public class AuthorService(IDbContextFactory<MidrandBooksDbContext> contextFacto
var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken); var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken);
if (author is null) if (author is null)
return Result.Fail<Product[]>(new Error($"Author with ID {authorId} not found")); return Result.Fail<AuthorBook[]>(new Error($"Author with ID {authorId} not found"));
var books = await context.Books.AsNoTracking() var books = await context.Books
.AsNoTracking()
.Include(b => b.Author)
.Include(b => b.Product!.Price)
.OrderByDescending(b => b.CreatedAt) .OrderByDescending(b => b.CreatedAt)
.Where(p => p.AuthorId == authorId) .Where(p => p.AuthorId == authorId)
.Select(p => p.Book.ToModel()) .AsSplitQuery()
.ToArrayAsync(cancellationToken); .ToArrayAsync(cancellationToken);
return books?.Length > 0 return books?.Length > 0
? Result.Ok(books) ? Result.Ok(books.Select(b => b.ToModel()).ToArray())
: Result.Fail<Product[]>(new Error($"No books found for author with ID {authorId}")); : Result.Fail<AuthorBook[]>(new Error($"No books found for author with ID {authorId}"));
} }
catch (Exception ex) catch (Exception ex)
{ {
return Result.Fail<Product[]>(new Error(ex.Message).CausedBy(ex)); return Result.Fail<AuthorBook[]>(new Error(ex.Message).CausedBy(ex));
} }
} }
@@ -30,7 +30,8 @@ public static class Mappers
AuthorId = entity.AuthorId, AuthorId = entity.AuthorId,
Ranking = entity.Ranking, Ranking = entity.Ranking,
Rating = entity.Rating, Rating = entity.Rating,
Enabled = entity.Enabled Enabled = entity.Enabled,
Product = entity.Product?.ToModel(),
}; };
public static ProductPrice ToModel(this Products.Entities.ProductPrice entity) => new() public static ProductPrice ToModel(this Products.Entities.ProductPrice entity) => new()
@@ -59,7 +60,8 @@ public static class Mappers
ThumbnailUrls = entity.ThumbnailUrls, ThumbnailUrls = entity.ThumbnailUrls,
Metadata = entity.Metadata, Metadata = entity.Metadata,
Categories = entity.Categories, Categories = entity.Categories,
Enabled = entity.Enabled Enabled = entity.Enabled,
Price = entity.Prices?.FirstOrDefault(p => p.Enabled)?.ToModel() ?? null,
}; };
} }
@@ -26,5 +26,7 @@ public class Product
public ProductMetadata? Metadata { get; set; } public ProductMetadata? Metadata { get; set; }
public ProductPrice? Price { get; set; }
public bool Enabled { get; set; } public bool Enabled { get; set; }
} }
@@ -223,7 +223,7 @@ public class ProductService(IDbContextFactory<MidrandBooksDbContext> contextFact
} }
} }
public async ValueTask<Result<Product[]>> GetProductsAsync(DateRange range, CancellationToken cancellationToken = default) public async ValueTask<Result<Product[]>> GetProductsAsync(int offset, DateRange range, CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -234,10 +234,13 @@ public class ProductService(IDbContextFactory<MidrandBooksDbContext> contextFact
var products = await context.Products var products = await context.Products
.AsNoTracking() .AsNoTracking()
.Include(p => p.Prices)
.OrderByDescending(p => p.CreatedAt) .OrderByDescending(p => p.CreatedAt)
.ThenBy(p => p.UpdatedAt) .ThenByDescending(p => p.UpdatedAt)
.Where(p => p.CreatedAt >= fromDate && p.CreatedAt <= toDate) .Where(p => p.CreatedAt >= fromDate && p.CreatedAt <= toDate)
.Skip(offset)
.Take(range.MaxRecords) .Take(range.MaxRecords)
.AsSplitQuery()
.ToArrayAsync(cancellationToken); .ToArrayAsync(cancellationToken);
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0