Add project files.
This commit is contained in:
40
JSMR.Infrastructure/Caching/DistributedCacheAdapter.cs
Normal file
40
JSMR.Infrastructure/Caching/DistributedCacheAdapter.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using JSMR.Application.Common.Caching;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace JSMR.Infrastructure.Caching;
|
||||
|
||||
public class DistributedCacheAdapter(IDistributedCache distributedCache) : ICache
|
||||
{
|
||||
public async ValueTask<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
byte[]? bytes = await distributedCache.GetAsync(key, cancellationToken);
|
||||
|
||||
if (bytes == null)
|
||||
return default;
|
||||
|
||||
string json = Encoding.UTF8.GetString(bytes);
|
||||
|
||||
return JsonSerializer.Deserialize<T>(json);
|
||||
}
|
||||
|
||||
public async ValueTask SetAsync<T>(string key, T value, CacheEntryOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
string json = JsonSerializer.Serialize(value);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
DistributedCacheEntryOptions distributedCacheOptions = new();
|
||||
|
||||
if (options.AbsoluteExpirationRelativeToNow != null)
|
||||
distributedCacheOptions.SetAbsoluteExpiration(options.AbsoluteExpirationRelativeToNow.Value);
|
||||
|
||||
if (options.AbsoluteExpiration != null)
|
||||
distributedCacheOptions.SetAbsoluteExpiration(options.AbsoluteExpiration.Value);
|
||||
|
||||
if (options.SlidingExpiration != null)
|
||||
distributedCacheOptions.SetSlidingExpiration(options.SlidingExpiration.Value);
|
||||
|
||||
await distributedCache.SetAsync(key, bytes, distributedCacheOptions, cancellationToken);
|
||||
}
|
||||
}
|
||||
32
JSMR.Infrastructure/Caching/MemoryCacheAdapter.cs
Normal file
32
JSMR.Infrastructure/Caching/MemoryCacheAdapter.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using JSMR.Application.Common.Caching;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace JSMR.Infrastructure.Caching;
|
||||
|
||||
public class MemoryCacheAdapter(IMemoryCache memoryCache) : ICache
|
||||
{
|
||||
public ValueTask<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
memoryCache.TryGetValue(key, out T? value);
|
||||
|
||||
return ValueTask.FromResult(value);
|
||||
}
|
||||
|
||||
public ValueTask SetAsync<T>(string key, T value, CacheEntryOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
MemoryCacheEntryOptions memoryCacheOptions = new();
|
||||
|
||||
if (options.AbsoluteExpirationRelativeToNow != null)
|
||||
memoryCacheOptions.SetAbsoluteExpiration(options.AbsoluteExpirationRelativeToNow.Value);
|
||||
|
||||
if (options.AbsoluteExpiration != null)
|
||||
memoryCacheOptions.SetAbsoluteExpiration(options.AbsoluteExpiration.Value);
|
||||
|
||||
if (options.SlidingExpiration != null)
|
||||
memoryCacheOptions.SetSlidingExpiration(options.SlidingExpiration.Value);
|
||||
|
||||
memoryCache.Set(key, value, memoryCacheOptions);
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
59
JSMR.Infrastructure/Common/Queries/SearchProvider.cs
Normal file
59
JSMR.Infrastructure/Common/Queries/SearchProvider.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using JSMR.Application.Common.Search;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace JSMR.Infrastructure.Common.Queries;
|
||||
|
||||
public abstract class SearchProvider<TItem, TCriteria, TSortField, TBaseQuery> : ISearchProvider<TItem, TCriteria, TSortField>
|
||||
where TCriteria : new()
|
||||
where TSortField : struct, Enum
|
||||
{
|
||||
public async Task<SearchResult<TItem>> SearchAsync(SearchOptions<TCriteria, TSortField> options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
IQueryable<TBaseQuery> baseQuery = GetBaseQuery();
|
||||
IQueryable<TBaseQuery> filteredQuery = ApplyFilters(baseQuery, options.Criteria);
|
||||
|
||||
int total = await filteredQuery.CountAsync(cancellationToken);
|
||||
|
||||
IOrderedQueryable<TBaseQuery> orderedQuery = ApplySorting(filteredQuery, options.SortOptions);
|
||||
IOrderedQueryable<TItem> selectQuery = GetSelectQuery(orderedQuery);
|
||||
|
||||
TItem[] items = await selectQuery
|
||||
.Skip((options.PageNumber - 1) * options.PageSize)
|
||||
.Take(options.PageSize)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
return new SearchResult<TItem>()
|
||||
{
|
||||
Items = items,
|
||||
TotalItems = total
|
||||
};
|
||||
}
|
||||
|
||||
protected abstract IQueryable<TBaseQuery> GetBaseQuery();
|
||||
protected abstract IQueryable<TBaseQuery> ApplyFilters(IQueryable<TBaseQuery> query, TCriteria criteria);
|
||||
|
||||
private IOrderedQueryable<TBaseQuery> ApplySorting(IQueryable<TBaseQuery> query, SortOption<TSortField>[] sortOptions)
|
||||
{
|
||||
IOrderedQueryable<TBaseQuery>? ordered = null;
|
||||
|
||||
for (int i = 0; i < sortOptions.Length; i++)
|
||||
{
|
||||
var (field, direction) = (sortOptions[i].Field, sortOptions[i].Direction);
|
||||
bool isDescending = direction == SortDirection.Descending;
|
||||
|
||||
IOrderedQueryable<TBaseQuery> applyFirst(Expression<Func<TBaseQuery, object>> selector) => isDescending ? query.OrderByDescending(selector) : query.OrderBy(selector);
|
||||
IOrderedQueryable<TBaseQuery> applyNext(Expression<Func<TBaseQuery, object>> selector) => isDescending ? ordered!.ThenByDescending(selector) : ordered!.ThenBy(selector);
|
||||
|
||||
Expression<Func<TBaseQuery, object>> selector = GetSortExpression(field);
|
||||
|
||||
ordered = (i == 0) ? applyFirst(selector) : applyNext(selector);
|
||||
}
|
||||
|
||||
return ordered ?? GetDefaultSortExpression(query);
|
||||
}
|
||||
|
||||
protected abstract Expression<Func<TBaseQuery, object>> GetSortExpression(TSortField field);
|
||||
protected abstract IOrderedQueryable<TBaseQuery> GetDefaultSortExpression(IQueryable<TBaseQuery> query);
|
||||
protected abstract IOrderedQueryable<TItem> GetSelectQuery(IOrderedQueryable<TBaseQuery> query);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using JSMR.Application.Circles.Queries.GetCreators;
|
||||
using JSMR.Application.Circles.Queries.GetTags;
|
||||
using JSMR.Application.Circles.Queries.Search;
|
||||
using JSMR.Infrastructure.Data.Repositories.Circles;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace JSMR.Infrastructure.DI;
|
||||
|
||||
public static class InfrastructureServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ICircleSearchProvider, CircleSearchProvider>();
|
||||
services.AddScoped<ICircleTagsProvider, CircleTagsProvider>();
|
||||
services.AddScoped<ICircleCreatorsProvider, CircleCreatorsProvider>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
18
JSMR.Infrastructure/Data/AppDbContext.cs
Normal file
18
JSMR.Infrastructure/Data/AppDbContext.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using JSMR.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JSMR.Infrastructure.Data;
|
||||
|
||||
public class AppDbContext : DbContext
|
||||
{
|
||||
public DbSet<VoiceWork> VoiceWorks { get; set; }
|
||||
public DbSet<EnglishVoiceWork> EnglishVoiceWorks { get; set; }
|
||||
public DbSet<Circle> Circles { get; set; }
|
||||
public DbSet<Tag> Tags { get; set; }
|
||||
public DbSet<EnglishTag> EnglishTags { get; set; }
|
||||
public DbSet<VoiceWorkTag> VoiceWorkTags { get; set; }
|
||||
public DbSet<Creator> Creators { get; set; }
|
||||
public DbSet<VoiceWorkCreator> VoiceWorkCreators { get; set; }
|
||||
public DbSet<Series> Series { get; set; }
|
||||
public DbSet<VoiceWorkSearch> VoiceWorkSearches { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using JSMR.Application.Circles.Queries.GetCreators;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JSMR.Infrastructure.Data.Repositories.Circles;
|
||||
|
||||
public class CircleCreatorsProvider(AppDbContext context) : ICircleCreatorsProvider
|
||||
{
|
||||
public async Task<GetCircleCreatorsResponse> HandleAsync(GetCircleCreatorsRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
int? circleId = await CircleLookup.ResolveCircleIdAsync(context, request.CircleId, request.NameOrMakerId, cancellationToken);
|
||||
|
||||
if (circleId is null)
|
||||
return new GetCircleCreatorsResponse([]);
|
||||
|
||||
IQueryable<CircleCreatorItem> query =
|
||||
from voiceWork in context.VoiceWorks.AsNoTracking()
|
||||
where voiceWork.CircleId == circleId.Value
|
||||
join voiceWorkCreator in context.VoiceWorkCreators.AsNoTracking() on voiceWork.VoiceWorkId equals voiceWorkCreator.VoiceWorkId
|
||||
join creator in context.Creators.AsNoTracking() on voiceWorkCreator.CreatorId equals creator.CreatorId
|
||||
group new { creator.Name, voiceWork.SalesDate, voiceWork.Downloads } by creator.Name into g
|
||||
select new CircleCreatorItem(
|
||||
g.Key,
|
||||
g.Min(x => x.SalesDate),
|
||||
g.Max(x => x.SalesDate),
|
||||
g.Sum(x => x.Downloads ?? 0),
|
||||
g.Count());
|
||||
|
||||
CircleCreatorItem[] items = await query
|
||||
.OrderByDescending(x => x.Count)
|
||||
.ThenByDescending(x => x.Downloads)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
return new GetCircleCreatorsResponse(items);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JSMR.Infrastructure.Data.Repositories.Circles;
|
||||
|
||||
internal static class CircleLookup
|
||||
{
|
||||
public static async Task<int?> ResolveCircleIdAsync(AppDbContext context, int? circleId, string? nameOrMakerId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (circleId.HasValue)
|
||||
return circleId.Value;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(nameOrMakerId))
|
||||
{
|
||||
return await context.Circles
|
||||
.Where(c => c.Name == nameOrMakerId || c.MakerId == nameOrMakerId)
|
||||
.Select(c => (int?)c.CircleId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using JSMR.Application.Circles.Queries.Search;
|
||||
using JSMR.Infrastructure.Common.Queries;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace JSMR.Infrastructure.Data.Repositories.Circles;
|
||||
|
||||
public class CircleSearchProvider(AppDbContext context) : SearchProvider<CircleSearchItem, CircleSearchCriteria, CircleSortField, CircleSearchItem>, ICircleSearchProvider
|
||||
{
|
||||
protected override IQueryable<CircleSearchItem> GetBaseQuery()
|
||||
{
|
||||
// Precompute LatestProductId per circle (by productId length, then value)
|
||||
var latestPerCircle =
|
||||
from vw in context.VoiceWorks.AsNoTracking()
|
||||
group vw by vw.CircleId into g
|
||||
let latest = g
|
||||
.OrderByDescending(x => x.ProductId.Length)
|
||||
.ThenByDescending(x => x.ProductId)
|
||||
.Select(x => x.ProductId)
|
||||
.FirstOrDefault()
|
||||
select new { CircleId = g.Key, LatestProductId = latest };
|
||||
|
||||
// Aggregates per circle
|
||||
var aggregates =
|
||||
from vw in context.VoiceWorks.AsNoTracking()
|
||||
group vw by vw.CircleId into g
|
||||
select new
|
||||
{
|
||||
CircleId = g.Key,
|
||||
Downloads = g.Sum(x => x.Downloads ?? 0),
|
||||
Releases = g.Count(x => x.SalesDate != null),
|
||||
Pending = g.Count(x => x.ExpectedDate != null),
|
||||
FirstReleaseDate = g.Min(x => x.SalesDate),
|
||||
LatestReleaseDate = g.Max(x => x.SalesDate)
|
||||
};
|
||||
|
||||
// Join circles with aggregates and latest product id
|
||||
var baseQuery =
|
||||
from c in context.Circles.AsNoTracking()
|
||||
join agg in aggregates on c.CircleId equals agg.CircleId into aggs
|
||||
from a in aggs.DefaultIfEmpty()
|
||||
join lp in latestPerCircle on c.CircleId equals lp.CircleId into lps
|
||||
from l in lps.DefaultIfEmpty()
|
||||
select new CircleSearchItem
|
||||
{
|
||||
CircleId = c.CircleId,
|
||||
Name = c.Name,
|
||||
MakerId = c.MakerId,
|
||||
Favorite = c.Favorite,
|
||||
Blacklisted = c.Blacklisted,
|
||||
Spam = c.Spam,
|
||||
Downloads = a != null ? a.Downloads : 0,
|
||||
Releases = a != null ? a.Releases : 0,
|
||||
Pending = a != null ? a.Pending : 0,
|
||||
FirstReleaseDate = a != null ? a.FirstReleaseDate : null,
|
||||
LatestReleaseDate = a != null ? a.LatestReleaseDate : null,
|
||||
LatestProductId = l != null ? l.LatestProductId : null,
|
||||
// these two get filled in during Select stage (below)
|
||||
LatestVoiceWorkHasImage = null,
|
||||
LatestVoiceWorkSalesDate = null
|
||||
};
|
||||
|
||||
return baseQuery;
|
||||
}
|
||||
|
||||
protected override IQueryable<CircleSearchItem> ApplyFilters(IQueryable<CircleSearchItem> query, CircleSearchCriteria criteria)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(criteria.Name))
|
||||
{
|
||||
var term = $"%{criteria.Name.Trim()}%";
|
||||
|
||||
query = query.Where(x =>
|
||||
EF.Functions.Like(x.Name, term) ||
|
||||
EF.Functions.Like(x.MakerId, term));
|
||||
}
|
||||
|
||||
//if (criteria.Status is CircleStatus.NotBlacklisted)
|
||||
// query = query.Where(x => !x.Blacklisted);
|
||||
//else if (criteria.Status is CircleStatus.Favorited)
|
||||
// query = query.Where(x => x.Favorite);
|
||||
//else if (criteria.Status is CircleStatus.Blacklisted)
|
||||
// query = query.Where(x => x.Blacklisted);
|
||||
//else if (criteria.Status is CircleStatus.Spam)
|
||||
// query = query.Where(x => x.Spam);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
protected override Expression<Func<CircleSearchItem, object>> GetSortExpression(CircleSortField field) => field switch
|
||||
{
|
||||
//CircleSortField.MakerId => x => x.MakerId,
|
||||
//CircleSortField.Downloads => x => x.Downloads,
|
||||
//CircleSortField.Releases => x => x.Releases,
|
||||
//CircleSortField.Pending => x => x.Pending,
|
||||
//CircleSortField.FirstReleaseDate => x => x.FirstReleaseDate ?? DateTime.MinValue,
|
||||
//CircleSortField.LatestReleaseDate => x => x.LatestReleaseDate ?? DateTime.MinValue,
|
||||
CircleSortField.Favorite => x => x.Favorite,
|
||||
CircleSortField.Blacklisted => x => x.Blacklisted,
|
||||
CircleSortField.Spam => x => x.Spam,
|
||||
_ => x => x.Name
|
||||
};
|
||||
|
||||
protected override IOrderedQueryable<CircleSearchItem> GetDefaultSortExpression(IQueryable<CircleSearchItem> query)
|
||||
=> query.OrderBy(x => x.Name).ThenBy(x => x.CircleId);
|
||||
|
||||
protected override IOrderedQueryable<CircleSearchItem> GetSelectQuery(IOrderedQueryable<CircleSearchItem> 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.CircleId,
|
||||
Name = item.Name,
|
||||
MakerId = item.MakerId,
|
||||
Favorite = item.Favorite,
|
||||
Blacklisted = item.Blacklisted,
|
||||
Spam = item.Spam,
|
||||
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
|
||||
};
|
||||
|
||||
// Preserve existing ordering; add stable tiebreaker
|
||||
return selected.OrderBy(x => 0).ThenBy(x => x.Name).ThenBy(x => x.CircleId);
|
||||
// NOTE: If your base class re-applies ordering after Select, you can just:
|
||||
// return selected.OrderBy(x => x.Name).ThenBy(x => x.CircleId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using JSMR.Application.Circles.Queries.GetTags;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JSMR.Infrastructure.Data.Repositories.Circles;
|
||||
|
||||
public class CircleTagsProvider(AppDbContext context) : ICircleTagsProvider
|
||||
{
|
||||
public async Task<GetCircleTagsResponse> HandleAsync(GetCircleTagsRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
int? circleId = await CircleLookup.ResolveCircleIdAsync(context, request.CircleId, request.NameOrMakerId, cancellationToken);
|
||||
|
||||
if (circleId is null)
|
||||
return new GetCircleTagsResponse([]);
|
||||
|
||||
IQueryable<CircleTagItem> query =
|
||||
from voiceWork in context.VoiceWorks.AsNoTracking()
|
||||
where voiceWork.CircleId == circleId.Value
|
||||
join voiceWorkTag in context.VoiceWorkTags.AsNoTracking() on voiceWork.VoiceWorkId equals voiceWorkTag.VoiceWorkId
|
||||
join tag in context.Tags.AsNoTracking() on voiceWorkTag.TagId equals tag.TagId
|
||||
join englishTag in context.EnglishTags.AsNoTracking() on tag.TagId equals englishTag.TagId into eng
|
||||
from e in eng.DefaultIfEmpty()
|
||||
group new { tag, e } by new { tag.Name, English = e != null ? e.Name : null } into g
|
||||
orderby g.Count() descending
|
||||
select new CircleTagItem(
|
||||
g.Key.Name,
|
||||
g.Key.English,
|
||||
g.Count());
|
||||
|
||||
CircleTagItem[] items = await query.ToArrayAsync(cancellationToken);
|
||||
|
||||
return new GetCircleTagsResponse(items);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using JSMR.Application.Circles.Commands.UpdateCircleStatus;
|
||||
using JSMR.Application.Circles.Contracts;
|
||||
using JSMR.Application.Circles.Ports;
|
||||
using JSMR.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JSMR.Infrastructure.Data.Repositories.Circles;
|
||||
|
||||
public class CircleWriter(AppDbContext context) : ICircleWriter
|
||||
{
|
||||
public async Task<UpdateCircleStatusResponse> UpdateStatusAsync(UpdateCircleStatusRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Circle circle = await GetCircleAsync(request.CircleId, cancellationToken);
|
||||
|
||||
switch (request.CircleStatus)
|
||||
{
|
||||
case CircleStatus.Neutral:
|
||||
circle.Favorite = false;
|
||||
circle.Blacklisted = false;
|
||||
circle.Spam = false;
|
||||
break;
|
||||
case CircleStatus.Favorite:
|
||||
circle.Favorite = true;
|
||||
circle.Blacklisted = false;
|
||||
circle.Spam = false;
|
||||
break;
|
||||
case CircleStatus.Blacklisted:
|
||||
circle.Favorite = false;
|
||||
circle.Blacklisted = true;
|
||||
circle.Spam = false;
|
||||
break;
|
||||
case CircleStatus.Spam:
|
||||
circle.Favorite = false;
|
||||
circle.Blacklisted = false;
|
||||
circle.Spam = true;
|
||||
break;
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new UpdateCircleStatusResponse(request.CircleId, request.CircleStatus);
|
||||
}
|
||||
|
||||
private async Task<Circle> GetCircleAsync(int circleId, CancellationToken cancellationToken)
|
||||
{
|
||||
return await context.Circles.FirstOrDefaultAsync(circle => circle.CircleId == circleId, cancellationToken)
|
||||
?? throw new KeyNotFoundException($"Circle {circleId} not found.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using JSMR.Application.Creators.Queries.Search.Contracts;
|
||||
using JSMR.Application.Creators.Queries.Search.Ports;
|
||||
using JSMR.Infrastructure.Common.Queries;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace JSMR.Infrastructure.Data.Repositories.Creators;
|
||||
|
||||
public class CreatorSearchProvider(AppDbContext context) : SearchProvider<CreatorSearchItem, CreatorSearchCriteria, CreatorSortField, CreatorSearchItem>, ICreatorSearchProvider
|
||||
{
|
||||
protected override IQueryable<CreatorSearchItem> GetBaseQuery()
|
||||
{
|
||||
return
|
||||
from creator in context.Creators
|
||||
join voiceWorkCreators in context.VoiceWorkCreators on creator.CreatorId equals voiceWorkCreators.CreatorId into vwcs
|
||||
select new CreatorSearchItem
|
||||
{
|
||||
CreatorId = creator.CreatorId,
|
||||
Name = creator.Name,
|
||||
Favorite = creator.Favorite,
|
||||
Blacklisted = creator.Blacklisted,
|
||||
VoiceWorkCount = vwcs.Count()
|
||||
};
|
||||
}
|
||||
|
||||
protected override IQueryable<CreatorSearchItem> ApplyFilters(IQueryable<CreatorSearchItem> query, CreatorSearchCriteria criteria)
|
||||
{
|
||||
IQueryable<CreatorSearchItem> filteredQuery = query;
|
||||
|
||||
if (string.IsNullOrEmpty(criteria.Name) == false)
|
||||
{
|
||||
string name = criteria.Name.Trim();
|
||||
|
||||
filteredQuery = filteredQuery.Where(x => x.Name.Contains(name));
|
||||
}
|
||||
|
||||
return filteredQuery;
|
||||
}
|
||||
|
||||
protected override Expression<Func<CreatorSearchItem, object>> GetSortExpression(CreatorSortField field)
|
||||
{
|
||||
Expression<Func<CreatorSearchItem, object>> selector = field switch
|
||||
{
|
||||
CreatorSortField.VoiceWorkCount => x => x.VoiceWorkCount,
|
||||
CreatorSortField.Favorite => x => x.Favorite,
|
||||
CreatorSortField.Blacklisted => x => x.Blacklisted,
|
||||
_ => x => x.Name
|
||||
};
|
||||
|
||||
return selector;
|
||||
}
|
||||
|
||||
protected override IOrderedQueryable<CreatorSearchItem> GetDefaultSortExpression(IQueryable<CreatorSearchItem> query)
|
||||
{
|
||||
return query.OrderBy(x => x.Name);
|
||||
}
|
||||
|
||||
protected override IOrderedQueryable<CreatorSearchItem> GetSelectQuery(IOrderedQueryable<CreatorSearchItem> query)
|
||||
{
|
||||
return query;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using JSMR.Application.Creators.Commands.UpdateCreatorStatus;
|
||||
using JSMR.Application.Creators.Contracts;
|
||||
using JSMR.Application.Creators.Ports;
|
||||
using JSMR.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JSMR.Infrastructure.Data.Repositories.Creators;
|
||||
|
||||
public class CreatorWriter(AppDbContext context) : ICreatorWriter
|
||||
{
|
||||
public async Task<UpdateCreatorStatusResponse> UpdateStatusAsync(UpdateCreatorStatusRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Creator creator = await GetCreatorAsync(request.CreatorId, cancellationToken);
|
||||
|
||||
switch (request.CreatorStatus)
|
||||
{
|
||||
case CreatorStatus.Neutral:
|
||||
creator.Favorite = false;
|
||||
creator.Blacklisted = false;
|
||||
break;
|
||||
case CreatorStatus.Favorite:
|
||||
creator.Favorite = true;
|
||||
creator.Blacklisted = false;
|
||||
break;
|
||||
case CreatorStatus.Blacklisted:
|
||||
creator.Favorite = false;
|
||||
creator.Blacklisted = true;
|
||||
break;
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new UpdateCreatorStatusResponse(request.CreatorId, request.CreatorStatus);
|
||||
}
|
||||
|
||||
private async Task<Creator> GetCreatorAsync(int creatorId, CancellationToken cancellationToken)
|
||||
{
|
||||
return await context.Creators.FirstOrDefaultAsync(creator => creator.CreatorId == creatorId, cancellationToken)
|
||||
?? throw new KeyNotFoundException($"Creator {creatorId} not found.");
|
||||
}
|
||||
}
|
||||
94
JSMR.Infrastructure/Data/Repositories/Tags/TagReader.cs
Normal file
94
JSMR.Infrastructure/Data/Repositories/Tags/TagReader.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using JSMR.Application.Tags.Queries.Search.Contracts;
|
||||
using JSMR.Application.Tags.Queries.Search.Ports;
|
||||
using JSMR.Infrastructure.Common.Queries;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace JSMR.Infrastructure.Data.Repositories.Tags;
|
||||
|
||||
|
||||
|
||||
//public class TagReader(AppDbContext context) : ITagReader
|
||||
//{
|
||||
// public async Task<TagSearchResults> SearchAsync(TagSearchOptions options, CancellationToken cancellationToken = default)
|
||||
// {
|
||||
// IQueryable<TagSearchItem> baseQuery = GetQuery();
|
||||
// IQueryable<TagSearchItem> filteredQuery = ApplyFilters(baseQuery, options);
|
||||
|
||||
// int total = await filteredQuery.CountAsync(cancellationToken);
|
||||
|
||||
// IOrderedQueryable<TagSearchItem> orderedQuery = ApplySorting(filteredQuery, options);
|
||||
|
||||
// TagSearchItem[] items = await orderedQuery
|
||||
// .Skip((options.PageNumber - 1) * options.PageSize)
|
||||
// .Take(options.PageSize)
|
||||
// .ToArrayAsync(cancellationToken);
|
||||
|
||||
// return new TagSearchResults()
|
||||
// {
|
||||
// Items = items,
|
||||
// TotalItems = total
|
||||
// };
|
||||
// }
|
||||
|
||||
// private IQueryable<TagSearchItem> GetQuery()
|
||||
// {
|
||||
// return
|
||||
// from tag in context.Tags
|
||||
// join englishTag in context.EnglishTags on tag.TagId equals englishTag.TagId into test
|
||||
// from subEnglishTag in test.DefaultIfEmpty()
|
||||
// join voiceWorkTags in context.VoiceWorkTags on tag.TagId equals voiceWorkTags.TagId into vwts
|
||||
// select new TagSearchItem
|
||||
// {
|
||||
// TagId = tag.TagId,
|
||||
// Name = tag.Name,
|
||||
// EnglishName = subEnglishTag.Name,
|
||||
// Favorite = tag.Favorite,
|
||||
// Blacklisted = tag.Blacklisted,
|
||||
// VoiceWorkCount = context.VoiceWorkTags.Count(x => x.TagId == tag.TagId)
|
||||
// };
|
||||
// }
|
||||
|
||||
// private static IQueryable<TagSearchItem> ApplyFilters(IQueryable<TagSearchItem> query, TagSearchOptions options)
|
||||
// {
|
||||
// IQueryable<TagSearchItem> filteredQuery = query;
|
||||
|
||||
// if (string.IsNullOrEmpty(options.Criteria.Name) == false)
|
||||
// {
|
||||
// string name = options.Criteria.Name.Trim();
|
||||
|
||||
// filteredQuery = filteredQuery.Where(x =>
|
||||
// x.Name.Contains(name) || (x.EnglishName != null && x.EnglishName.Contains(name)));
|
||||
// }
|
||||
|
||||
// return filteredQuery;
|
||||
// }
|
||||
|
||||
// private static IOrderedQueryable<TagSearchItem> ApplySorting(IQueryable<TagSearchItem> query, TagSearchOptions options)
|
||||
// {
|
||||
// IOrderedQueryable<TagSearchItem>? ordered = null;
|
||||
|
||||
// for (int i = 0; i < options.SortOptions.Length; i++)
|
||||
// {
|
||||
// var (field, direction) = (options.SortOptions[i].Field, options.SortOptions[i].Direction);
|
||||
// bool isDescending = direction == SortDirection.Descending;
|
||||
|
||||
// IOrderedQueryable<TagSearchItem> applyFirst(Expression<Func<TagSearchItem, object>> selector) => isDescending ? query.OrderByDescending(selector) : query.OrderBy(selector);
|
||||
// IOrderedQueryable<TagSearchItem> applyNext(Expression<Func<TagSearchItem, object>> selector) => isDescending ? ordered!.ThenByDescending(selector) : ordered!.ThenBy(selector);
|
||||
|
||||
// Expression<Func<TagSearchItem, object>> selector = field switch
|
||||
// {
|
||||
// TagSortField.EnglishName => x => x.EnglishName ?? "",
|
||||
// TagSortField.VoiceWorkCount => x => x.VoiceWorkCount,
|
||||
// TagSortField.Favorite => x => x.Favorite,
|
||||
// TagSortField.Blacklisted => x => x.Blacklisted,
|
||||
// _ => x => x.Name
|
||||
// };
|
||||
|
||||
// ordered = (i == 0) ? applyFirst(selector) : applyNext(selector);
|
||||
// }
|
||||
|
||||
// ordered ??= query.OrderBy(x => x.Name);
|
||||
|
||||
// return ordered;
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,66 @@
|
||||
using JSMR.Application.Tags.Queries.Search.Contracts;
|
||||
using JSMR.Application.Tags.Queries.Search.Ports;
|
||||
using JSMR.Infrastructure.Common.Queries;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace JSMR.Infrastructure.Data.Repositories.Tags;
|
||||
|
||||
public class TagSearchProvider(AppDbContext context) : SearchProvider<TagSearchItem, TagSearchCriteria, TagSortField, TagSearchItem>, ITagSearchProvider
|
||||
{
|
||||
protected override IQueryable<TagSearchItem> GetBaseQuery()
|
||||
{
|
||||
return
|
||||
from tag in context.Tags
|
||||
join englishTag in context.EnglishTags on tag.TagId equals englishTag.TagId into test
|
||||
from subEnglishTag in test.DefaultIfEmpty()
|
||||
join voiceWorkTags in context.VoiceWorkTags on tag.TagId equals voiceWorkTags.TagId into vwts
|
||||
select new TagSearchItem
|
||||
{
|
||||
TagId = tag.TagId,
|
||||
Name = tag.Name,
|
||||
EnglishName = subEnglishTag.Name,
|
||||
Favorite = tag.Favorite,
|
||||
Blacklisted = tag.Blacklisted,
|
||||
VoiceWorkCount = vwts.Count()
|
||||
};
|
||||
}
|
||||
|
||||
protected override IQueryable<TagSearchItem> ApplyFilters(IQueryable<TagSearchItem> query, TagSearchCriteria criteria)
|
||||
{
|
||||
IQueryable<TagSearchItem> filteredQuery = query;
|
||||
|
||||
if (string.IsNullOrEmpty(criteria.Name) == false)
|
||||
{
|
||||
string name = criteria.Name.Trim();
|
||||
|
||||
filteredQuery = filteredQuery.Where(x =>
|
||||
x.Name.Contains(name) || (x.EnglishName != null && x.EnglishName.Contains(name)));
|
||||
}
|
||||
|
||||
return filteredQuery;
|
||||
}
|
||||
|
||||
protected override Expression<Func<TagSearchItem, object>> GetSortExpression(TagSortField field)
|
||||
{
|
||||
Expression<Func<TagSearchItem, object>> selector = field switch
|
||||
{
|
||||
TagSortField.EnglishName => x => x.EnglishName ?? "",
|
||||
TagSortField.VoiceWorkCount => x => x.VoiceWorkCount,
|
||||
TagSortField.Favorite => x => x.Favorite,
|
||||
TagSortField.Blacklisted => x => x.Blacklisted,
|
||||
_ => x => x.Name
|
||||
};
|
||||
|
||||
return selector;
|
||||
}
|
||||
|
||||
protected override IOrderedQueryable<TagSearchItem> GetDefaultSortExpression(IQueryable<TagSearchItem> query)
|
||||
{
|
||||
return query.OrderBy(x => x.Name);
|
||||
}
|
||||
|
||||
protected override IOrderedQueryable<TagSearchItem> GetSelectQuery(IOrderedQueryable<TagSearchItem> query)
|
||||
{
|
||||
return query;
|
||||
}
|
||||
}
|
||||
67
JSMR.Infrastructure/Data/Repositories/Tags/TagWriter.cs
Normal file
67
JSMR.Infrastructure/Data/Repositories/Tags/TagWriter.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using JSMR.Application.Tags.Commands.SetEnglishName;
|
||||
using JSMR.Application.Tags.Commands.UpdateTagStatus;
|
||||
using JSMR.Application.Tags.Contracts;
|
||||
using JSMR.Application.Tags.Ports;
|
||||
using JSMR.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JSMR.Infrastructure.Data.Repositories.Tags;
|
||||
|
||||
public class TagWriter(AppDbContext context) : ITagWriter
|
||||
{
|
||||
public async Task<SetTagEnglishNameResponse> SetEnglishNameAsync(SetTagEnglishNameRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Tag tag = await GetTagAsync(request.TagId, cancellationToken);
|
||||
EnglishTag? englishTag = await context.EnglishTags.FirstOrDefaultAsync(e => e.TagId == request.TagId, cancellationToken);
|
||||
|
||||
if (englishTag is null)
|
||||
{
|
||||
englishTag = new EnglishTag
|
||||
{
|
||||
TagId = request.TagId,
|
||||
Name = request.EnglishName
|
||||
};
|
||||
|
||||
context.EnglishTags.Add(englishTag);
|
||||
}
|
||||
else
|
||||
{
|
||||
englishTag.Name = request.EnglishName;
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new SetTagEnglishNameResponse(request.TagId, request.EnglishName);
|
||||
}
|
||||
|
||||
public async Task<UpdateTagStatusResponse> UpdateStatusAsync(UpdateTagStatusRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Tag tag = await GetTagAsync(request.TagId, cancellationToken);
|
||||
|
||||
switch (request.TagStatus)
|
||||
{
|
||||
case TagStatus.Neutral:
|
||||
tag.Favorite = false;
|
||||
tag.Blacklisted = false;
|
||||
break;
|
||||
case TagStatus.Favorite:
|
||||
tag.Favorite = true;
|
||||
tag.Blacklisted = false;
|
||||
break;
|
||||
case TagStatus.Blacklisted:
|
||||
tag.Favorite = false;
|
||||
tag.Blacklisted = true;
|
||||
break;
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new UpdateTagStatusResponse(request.TagId, request.TagStatus);
|
||||
}
|
||||
|
||||
private async Task<Tag> GetTagAsync(int tagId, CancellationToken cancellationToken)
|
||||
{
|
||||
return await context.Tags.FirstOrDefaultAsync(tag => tag.TagId == tagId, cancellationToken)
|
||||
?? throw new KeyNotFoundException($"Tag {tagId} not found.");
|
||||
}
|
||||
}
|
||||
19
JSMR.Infrastructure/JSMR.Infrastructure.csproj
Normal file
19
JSMR.Infrastructure/JSMR.Infrastructure.csproj
Normal file
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\JSMR.Application\JSMR.Application.csproj" />
|
||||
<ProjectReference Include="..\JSMR.Domain\JSMR.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user