Files
jsmr/JSMR.Infrastructure/Data/Repositories/Circles/CircleSearchProvider.cs
Brian Bicknell 2418bd0a8f
All checks were successful
ci / build-test (push) Successful in 2m17s
ci / publish-image (push) Has been skipped
Updated search logic. More UI updates.
2025-11-17 21:05:55 -05:00

288 lines
12 KiB
C#

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<CircleSearchItem, CircleSearchCriteria, CircleSortField, CircleQuery>, ICircleSearchProvider
{
protected override bool UseSelectIdQuery => false;
protected override IQueryable<CircleQuery> 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<CircleSearchItem> 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<CircleQuery> ApplyFilters(IQueryable<CircleQuery> 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<Func<CircleQuery, object?>> 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<Func<CircleQuery, object>> Selector, SortDirection Dir)> GetDefaultSortChain()
{
yield return (x => x.Circle.Name, SortDirection.Ascending);
yield return (x => x.Circle.MakerId, SortDirection.Ascending);
}
protected override IQueryable<int> GetSelectIdQuery(IOrderedQueryable<CircleQuery> query)
{
return query.Select(x => x.Circle.CircleId);
}
protected override IQueryable<CircleSearchItem> GetSelectQuery(IOrderedQueryable<CircleQuery> 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<Dictionary<int, CircleSearchItem>> 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);
}
}