using JSMR.Application.Circles.Queries.Search; using JSMR.Application.Common.Search; using JSMR.Domain.Entities; using JSMR.Infrastructure.Common.Queries; using Microsoft.EntityFrameworkCore; using System.Linq.Expressions; namespace JSMR.Infrastructure.Data.Repositories.Circles; public class CircleQuery { public required Circle Circle { get; init; } } public class CircleSearchProvider(AppDbContext context) : SearchProvider, ICircleSearchProvider { protected override bool UseSelectIdQuery => false; protected override IQueryable GetBaseQuery() { // Project from Circles so we can use correlated subqueries per CircleId. var q = from c in context.Circles.AsNoTracking() select new CircleQuery { Circle = c }; return q; } //protected override IQueryable GetBaseQuery() //{ // // Project from Circles so we can use correlated subqueries per CircleId. // var q = // from c in context.Circles.AsNoTracking() // select new CircleSearchItem // { // CircleId = c.CircleId, // Name = c.Name, // MakerId = c.MakerId, // Favorite = c.Favorite, // Blacklisted = c.Blacklisted, // Spam = c.Spam, // // Aggregates // Downloads = context.VoiceWorks // .Where(v => v.CircleId == c.CircleId) // .Select(v => (int?)v.Downloads) // make nullable for Sum over empty set // .Sum() ?? 0, // Releases = context.VoiceWorks // .Count(v => v.CircleId == c.CircleId && v.SalesDate != null), // Pending = context.VoiceWorks // .Count(v => v.CircleId == c.CircleId && v.ExpectedDate != null), // FirstReleaseDate = context.VoiceWorks // .Where(v => v.CircleId == c.CircleId) // .Select(v => v.SalesDate) // .Min(), // LatestReleaseDate = context.VoiceWorks // .Where(v => v.CircleId == c.CircleId) // .Select(v => v.SalesDate) // .Max(), // // "Latest" by ProductId length, then value // LatestProductId = context.VoiceWorks // .Where(v => v.CircleId == c.CircleId) // .OrderByDescending(v => v.ProductId.Length) // .ThenByDescending(v => v.ProductId) // .Select(v => v.ProductId) // .FirstOrDefault(), // // If you want these two in base query too: // LatestVoiceWorkHasImage = context.VoiceWorks // .Where(v => v.CircleId == c.CircleId) // .OrderByDescending(v => v.ProductId.Length) // .ThenByDescending(v => v.ProductId) // .Select(v => (bool?)v.HasImage) // .FirstOrDefault(), // LatestVoiceWorkSalesDate = context.VoiceWorks // .Where(v => v.CircleId == c.CircleId) // .OrderByDescending(v => v.ProductId.Length) // .ThenByDescending(v => v.ProductId) // .Select(v => v.SalesDate) // .FirstOrDefault() // }; // return q; //} protected override IQueryable ApplyFilters(IQueryable query, CircleSearchCriteria criteria) { if (!string.IsNullOrWhiteSpace(criteria.Name)) { var term = $"%{criteria.Name.Trim()}%"; query = query.Where(x => EF.Functions.Like(x.Circle.Name, term) || EF.Functions.Like(x.Circle.MakerId, term)); } switch (criteria.Status) { case CircleStatus.NotBlacklisted: query = query.Where(x => !x.Circle.Blacklisted); break; case CircleStatus.Favorited: query = query.Where(x => x.Circle.Favorite); break; case CircleStatus.Blacklisted: query = query.Where(x => x.Circle.Blacklisted); break; case CircleStatus.Spam: query = query.Where(x => x.Circle.Spam); break; } return query; } protected override Expression> GetSortExpression(CircleSortField field) => field switch { CircleSortField.Favorite => x => !x.Circle.Favorite, CircleSortField.Blacklisted => x => !x.Circle.Blacklisted, CircleSortField.Spam => x => !x.Circle.Spam, _ => x => x.Circle.Name }; protected override IEnumerable<(Expression> Selector, SortDirection Dir)> GetDefaultSortChain() { yield return (x => x.Circle.Name, SortDirection.Ascending); yield return (x => x.Circle.MakerId, SortDirection.Ascending); } protected override IQueryable GetSelectIdQuery(IOrderedQueryable query) { return query.Select(x => x.Circle.CircleId); } protected override IQueryable GetSelectQuery(IOrderedQueryable query) { // Join to VoiceWorks by LatestProductId to fill HasImage / SalesDate var selected = from item in query //join vw in context.VoiceWorks.AsNoTracking() on item.LatestProductId equals vw.ProductId into vws //from latest in vws.DefaultIfEmpty() select new CircleSearchItem { CircleId = item.Circle.CircleId, Name = item.Circle.Name, MakerId = item.Circle.MakerId, Favorite = item.Circle.Favorite, Blacklisted = item.Circle.Blacklisted, Spam = item.Circle.Spam, // Aggregates Downloads = context.VoiceWorks .Where(v => v.CircleId == item.Circle.CircleId) .Select(v => (int?)v.Downloads) // make nullable for Sum over empty set .Sum() ?? 0, Releases = context.VoiceWorks .Count(v => v.CircleId == item.Circle.CircleId && v.SalesDate != null), Pending = context.VoiceWorks .Count(v => v.CircleId == item.Circle.CircleId && v.ExpectedDate != null), FirstReleaseDate = context.VoiceWorks .Where(v => v.CircleId == item.Circle.CircleId) .Select(v => v.SalesDate) .Min(), LatestReleaseDate = context.VoiceWorks .Where(v => v.CircleId == item.Circle.CircleId) .Select(v => v.SalesDate) .Max(), // "Latest" by ProductId length, then value LatestProductId = context.VoiceWorks .Where(v => v.CircleId == item.Circle.CircleId) .OrderByDescending(v => v.ProductId.Length) .ThenByDescending(v => v.ProductId) .Select(v => v.ProductId) .FirstOrDefault(), // If you want these two in base query too: LatestVoiceWorkHasImage = context.VoiceWorks .Where(v => v.CircleId == item.Circle.CircleId) .OrderByDescending(v => v.ProductId.Length) .ThenByDescending(v => v.ProductId) .Select(v => (bool?)v.HasImage) .FirstOrDefault(), LatestVoiceWorkSalesDate = context.VoiceWorks .Where(v => v.CircleId == item.Circle.CircleId) .OrderByDescending(v => v.ProductId.Length) .ThenByDescending(v => v.ProductId) .Select(v => v.SalesDate) .FirstOrDefault(), //Downloads = item.Downloads, //Releases = item.Releases, //Pending = item.Pending, //FirstReleaseDate = item.FirstReleaseDate, //LatestReleaseDate = item.LatestReleaseDate, //LatestProductId = item.LatestProductId, //LatestVoiceWorkHasImage = latest != null ? latest.HasImage : (bool?)null, //LatestVoiceWorkSalesDate = latest != null ? latest.SalesDate : null }; return selected; } protected override async Task> GetItems(int[] ids) { // Join to VoiceWorks by LatestProductId to fill HasImage / SalesDate var selected = from circle in context.Circles.AsNoTracking() //join vw in context.VoiceWorks.AsNoTracking() on item.LatestProductId equals vw.ProductId into vws //from latest in vws.DefaultIfEmpty() where ids.Contains(circle.CircleId) select new CircleSearchItem { CircleId = circle.CircleId, Name = circle.Name, MakerId = circle.MakerId, Favorite = circle.Favorite, Blacklisted = circle.Blacklisted, Spam = circle.Spam, // Aggregates Downloads = context.VoiceWorks .Where(v => v.CircleId == circle.CircleId) .Select(v => (int?)v.Downloads) // make nullable for Sum over empty set .Sum() ?? 0, Releases = context.VoiceWorks .Count(v => v.CircleId == circle.CircleId && v.SalesDate != null), Pending = context.VoiceWorks .Count(v => v.CircleId == circle.CircleId && v.ExpectedDate != null), FirstReleaseDate = context.VoiceWorks .Where(v => v.CircleId == circle.CircleId) .Select(v => v.SalesDate) .Min(), LatestReleaseDate = context.VoiceWorks .Where(v => v.CircleId == circle.CircleId) .Select(v => v.SalesDate) .Max(), // "Latest" by ProductId length, then value LatestProductId = context.VoiceWorks .Where(v => v.CircleId == circle.CircleId) .OrderByDescending(v => v.ProductId.Length) .ThenByDescending(v => v.ProductId) .Select(v => v.ProductId) .FirstOrDefault(), // If you want these two in base query too: LatestVoiceWorkHasImage = context.VoiceWorks .Where(v => v.CircleId == circle.CircleId) .OrderByDescending(v => v.ProductId.Length) .ThenByDescending(v => v.ProductId) .Select(v => (bool?)v.HasImage) .FirstOrDefault(), LatestVoiceWorkSalesDate = context.VoiceWorks .Where(v => v.CircleId == circle.CircleId) .OrderByDescending(v => v.ProductId.Length) .ThenByDescending(v => v.ProductId) .Select(v => v.SalesDate) .FirstOrDefault(), //Downloads = item.Downloads, //Releases = item.Releases, //Pending = item.Pending, //FirstReleaseDate = item.FirstReleaseDate, //LatestReleaseDate = item.LatestReleaseDate, //LatestProductId = item.LatestProductId, //LatestVoiceWorkHasImage = latest != null ? latest.HasImage : (bool?)null, //LatestVoiceWorkSalesDate = latest != null ? latest.SalesDate : null }; return await selected.ToDictionaryAsync(x => x.CircleId); } }