diff --git a/JSMR.Api/JSMR.Api.csproj b/JSMR.Api/JSMR.Api.csproj
index 03310f9..e72e0ef 100644
--- a/JSMR.Api/JSMR.Api.csproj
+++ b/JSMR.Api/JSMR.Api.csproj
@@ -9,6 +9,10 @@
+
+
+
+
diff --git a/JSMR.Api/Program.cs b/JSMR.Api/Program.cs
index d044abb..611210d 100644
--- a/JSMR.Api/Program.cs
+++ b/JSMR.Api/Program.cs
@@ -7,6 +7,9 @@ using JSMR.Infrastructure.Data;
using JSMR.Infrastructure.DI;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.EntityFrameworkCore;
+using Serilog;
+using Serilog.Events;
+using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -37,6 +40,15 @@ builder.Services.Configure(options =>
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); // or null for exact names
});
+// Serilog bootstrap (before Build)
+Log.Logger = new LoggerConfiguration()
+ .ReadFrom.Configuration(builder.Configuration)
+ .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
+ .Enrich.WithProperty("Service", "JSMR.Api")
+ .Enrich.WithProperty("Environment", builder.Environment.EnvironmentName)
+ .CreateLogger();
+builder.Host.UseSerilog();
+
var app = builder.Build();
// Configure the HTTP request pipeline.
@@ -51,6 +63,25 @@ app.UseAuthorization();
app.MapControllers();
+// Request logging with latency, status, path
+app.UseSerilogRequestLogging(opts =>
+{
+ opts.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms";
+});
+
+// Correlation: ensure every log has traceId and return it to clients
+app.Use(async (ctx, next) =>
+{
+ // Use current Activity if present (W3C trace context), else fall back
+ var traceId = Activity.Current?.TraceId.ToString() ?? ctx.TraceIdentifier;
+ using (Serilog.Context.LogContext.PushProperty("TraceId", traceId))
+ {
+ ctx.Response.Headers["x-trace-id"] = traceId;
+ await next();
+ }
+});
+
+
// Health check
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
@@ -62,9 +93,16 @@ app.MapPost("/api/circles/search", async (
SearchCirclesHandler handler,
CancellationToken cancallationToken) =>
{
- var result = await handler.HandleAsync(request, cancallationToken);
+ try
+ {
+ SearchCirclesResponse result = await handler.HandleAsync(request, cancallationToken);
- return Results.Ok(result);
+ return Results.Ok(result);
+ }
+ catch (OperationCanceledException) when (cancallationToken.IsCancellationRequested)
+ {
+ return Results.StatusCode(StatusCodes.Status499ClientClosedRequest);
+ }
});
// Voice Works Search
diff --git a/JSMR.Api/Properties/launchSettings.json b/JSMR.Api/Properties/launchSettings.json
index 63877de..b43b0c9 100644
--- a/JSMR.Api/Properties/launchSettings.json
+++ b/JSMR.Api/Properties/launchSettings.json
@@ -7,7 +7,8 @@
"launchBrowser": false,
"applicationUrl": "http://localhost:5226",
"environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "SEQ_URL": "http://localhost:5341"
}
},
"https": {
@@ -16,7 +17,8 @@
"launchBrowser": false,
"applicationUrl": "https://localhost:7277;http://localhost:5226",
"environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "SEQ_URL": "http://localhost:5341"
}
}
}
diff --git a/JSMR.Api/appsettings.json b/JSMR.Api/appsettings.json
index 10f68b8..d7310f9 100644
--- a/JSMR.Api/appsettings.json
+++ b/JSMR.Api/appsettings.json
@@ -5,5 +5,22 @@
"Microsoft.AspNetCore": "Warning"
}
},
- "AllowedHosts": "*"
+ "AllowedHosts": "*",
+ "Serilog": {
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "Microsoft": "Warning",
+ "System": "Warning"
+ }
+ },
+ "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ],
+ "WriteTo": [
+ { "Name": "Console" },
+ {
+ "Name": "Seq",
+ "Args": { "serverUrl": "%SEQ_URL%" }
+ }
+ ]
+ }
}
diff --git a/JSMR.Application/Circles/Queries/Search/SearchCirclesHandler.cs b/JSMR.Application/Circles/Queries/Search/SearchCirclesHandler.cs
index d20daaa..eb43340 100644
--- a/JSMR.Application/Circles/Queries/Search/SearchCirclesHandler.cs
+++ b/JSMR.Application/Circles/Queries/Search/SearchCirclesHandler.cs
@@ -1,14 +1,33 @@
using JSMR.Application.Common.Search;
+using JSMR.Application.Creators.Queries.Search;
+using JSMR.Application.Logging;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
namespace JSMR.Application.Circles.Queries.Search;
-public sealed class SearchCirclesHandler(ICircleSearchProvider searchProvider)
+public sealed class SearchCirclesHandler(ICircleSearchProvider searchProvider, ILogger logger)
{
public async Task HandleAsync(SearchCirclesRequest request, CancellationToken cancellationToken)
{
SearchOptions searchOptions = request.Options;
+ Stopwatch stopWatch = Stopwatch.StartNew();
+ LogEvents.SearchStart(logger, searchOptions.PageNumber, searchOptions.PageSize, searchOptions.Criteria);
+
SearchResult results = await searchProvider.SearchAsync(searchOptions, cancellationToken);
+ long elapsedMilliseconds = stopWatch.ElapsedMilliseconds;
+
+ LogEvents.SearchCompleted(
+ logger,
+ Elapsed: elapsedMilliseconds,
+ Items: results.Items.Length,
+ Total: results.TotalItems,
+ Page: searchOptions.PageNumber,
+ Size: searchOptions.PageSize,
+ Sort: searchOptions.SortOptions.ToLogObject(),
+ Criteria: searchOptions.Criteria.ToLogObject()
+ );
return new SearchCirclesResponse(results);
}
diff --git a/JSMR.Application/Creators/Queries/Search/SearchCreatorsHandler.cs b/JSMR.Application/Creators/Queries/Search/SearchCreatorsHandler.cs
index f9bc5bc..5dcaec9 100644
--- a/JSMR.Application/Creators/Queries/Search/SearchCreatorsHandler.cs
+++ b/JSMR.Application/Creators/Queries/Search/SearchCreatorsHandler.cs
@@ -1,31 +1,32 @@
-using JSMR.Application.Common.Caching;
-using JSMR.Application.Common.Search;
+using JSMR.Application.Common.Search;
using JSMR.Application.Creators.Queries.Search.Contracts;
using JSMR.Application.Creators.Queries.Search.Ports;
+using JSMR.Application.Logging;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
namespace JSMR.Application.Creators.Queries.Search;
-public sealed class SearchCreatorsHandler(ICreatorSearchProvider searchProvider, ICache cache)
+public sealed class SearchCreatorsHandler(ICreatorSearchProvider searchProvider, ILogger logger)
{
public async Task HandleAsync(SearchCreatorsRequest request, CancellationToken cancellationToken)
{
SearchOptions searchOptions = request.Options;
-
- string cacheKey = $"creator:{searchOptions.GetHashCode()}";
-
- CreatorSearchResults? cachedResults = await cache.GetAsync(cacheKey, cancellationToken);
-
- if (cachedResults != null)
- return new SearchCreatorsResponse(cachedResults);
+ Stopwatch stopWatch = Stopwatch.StartNew();
SearchResult results = await searchProvider.SearchAsync(searchOptions, cancellationToken);
+ long elapsedMilliseconds = stopWatch.ElapsedMilliseconds;
- CacheEntryOptions cacheEntryOptions = new()
- {
- SlidingExpiration = TimeSpan.FromMinutes(10)
- };
-
- await cache.SetAsync(cacheKey, results, cacheEntryOptions, cancellationToken);
+ LogEvents.SearchCompleted(
+ logger,
+ Elapsed: elapsedMilliseconds,
+ Items: results.Items.Length,
+ Total: results.TotalItems,
+ Page: searchOptions.PageNumber,
+ Size: searchOptions.PageSize,
+ Sort: searchOptions.SortOptions.ToLogObject(),
+ Criteria: searchOptions.Criteria.ToLogObject()
+ );
return new SearchCreatorsResponse(results);
}
diff --git a/JSMR.Application/JSMR.Application.csproj b/JSMR.Application/JSMR.Application.csproj
index 4136680..1ee20ff 100644
--- a/JSMR.Application/JSMR.Application.csproj
+++ b/JSMR.Application/JSMR.Application.csproj
@@ -12,6 +12,7 @@
+
diff --git a/JSMR.Application/Logging/CriteriaLoggingExtensions.cs b/JSMR.Application/Logging/CriteriaLoggingExtensions.cs
new file mode 100644
index 0000000..2b15b25
--- /dev/null
+++ b/JSMR.Application/Logging/CriteriaLoggingExtensions.cs
@@ -0,0 +1,51 @@
+using JSMR.Application.Circles.Queries.Search;
+using JSMR.Application.Creators.Queries.Search.Contracts;
+using JSMR.Application.Tags.Queries.Search.Contracts;
+using JSMR.Application.VoiceWorks.Queries.Search;
+
+namespace JSMR.Application.Logging;
+
+public static class CriteriaLoggingExtensions
+{
+ //public static object ToLogObject(this VoiceWorkSearchCriteria criteria)
+ //{
+ // return new LogObjectBuilder()
+ // .AddIfNotEmpty("Keywords", criteria.Keywords)
+ // .AddIfNotEmpty("Title", criteria.Title)
+ // .AddIfNotEmpty("Circle", criteria.Circle)
+ // .Add("Locale", criteria.Locale)
+ // .AddIfNotEmpty("AgeRatings", criteria.AgeRatings)
+ // .AddIfNotEmpty("Languages", criteria.SupportedLanguages)
+ // .AddIfNotEmpty("TagIds", criteria.TagIds, preview: 5)
+ // .AddIfNotEmpty("CreatorIds", criteria.CreatorIds, preview: 5)
+ // .Add("IncludeAllTags", criteria.IncludeAllTags ? true : null)
+ // .Add("IncludeAllCreators", criteria.IncludeAllCreators ? true : null)
+ // .Add("MinDownloads", criteria.MinDownloads)
+ // .Add("MaxDownloads", criteria.MaxDownloads)
+ // .Add("ReleaseDateStart", criteria.ReleaseDateStart)
+ // .Add("ReleaseDateEnd", criteria.ReleaseDateEnd)
+ // .Build();
+ //}
+
+ //public static object ToLogObject(this CircleSearchCriteria criteria)
+ //{
+ // return new LogObjectBuilder()
+ // .AddIfNotEmpty("Name", criteria.Name)
+ // .AddIfNotEmpty("Status", criteria.Status?.ToString())
+ // .Build();
+ //}
+
+ //public static object ToLogObject(this TagSearchCriteria criteria)
+ //{
+ // return new LogObjectBuilder()
+ // .AddIfNotEmpty("Name", criteria.Name)
+ // .Build();
+ //}
+
+ //public static object ToLogObject(this CreatorSearchCriteria criteria)
+ //{
+ // return new LogObjectBuilder()
+ // .AddIfNotEmpty("Name", criteria.Name)
+ // .Build();
+ //}
+}
\ No newline at end of file
diff --git a/JSMR.Application/Logging/LogEvents.cs b/JSMR.Application/Logging/LogEvents.cs
new file mode 100644
index 0000000..68604f5
--- /dev/null
+++ b/JSMR.Application/Logging/LogEvents.cs
@@ -0,0 +1,59 @@
+using Microsoft.Extensions.Logging;
+
+namespace JSMR.Application.Logging;
+
+public static partial class LogEvents
+{
+ public static void SearchCompleted(ILogger logger, long Elapsed, int Items, int Total, int Page, int Size, object Sort, object Criteria)
+ {
+ logger.LogInformation(
+ new EventId(1000, "SearchCompleted"),
+ "Search returned {Items} items (out of {Total} total) results in {Elapsed} ms on page {Page} with Size {Size}, sorted by {@Sort}, based on the following criteria: {@Criteria}",
+ Items,
+ Total,
+ Elapsed,
+ Page,
+ Size,
+ Sort,
+ Criteria
+ );
+ }
+
+ // Search
+ [LoggerMessage(EventId = 1000, Level = LogLevel.Information,
+ Message = "Search started Page={Page} Size={Size} Criteria={@Criteria}")]
+ public static partial void SearchStart(ILogger logger, int Page, int Size, object Criteria);
+
+ //[LoggerMessage(EventId = 1001, Level = LogLevel.Information,
+ // Message = "Search completed in {ElapsedMs} ms, Total={Total}")]
+ //public static partial void SearchComplete(ILogger logger, long ElapsedMs, int Total);
+
+ [LoggerMessage(EventId = 1001, Level = LogLevel.Information,
+ Message = "Search returned {Items} items (out of {Total} total) results in {Elapsed} ms on page {Page} with Size {Size}, sorted by {@Sort}, based on the following criteria: {@Criteria}")]
+ public static partial void SearchComplete(ILogger logger, long Elapsed, int Items, int Total, int Page, int Size, object Sort, object Criteria);
+
+ [LoggerMessage(EventId = 1002, Level = LogLevel.Warning,
+ Message = "External search provider timeout after {ElapsedMs} ms")]
+ public static partial void ExternalSearchTimeout(ILogger logger, long ElapsedMs);
+
+ [LoggerMessage(EventId = 1003, Level = LogLevel.Warning,
+ Message = "Slow Search Detected: Elapsed {ElapsedMs} ms (threshold {ThresholdMs} ms) Path={Path}")]
+ public static partial void SlowSearchDetected(ILogger logger, long ElapsedMs, long ThresholdMs, string Path);
+
+ [LoggerMessage(EventId = 1004, Level = LogLevel.Error,
+ Message = "Very Slow Search Detected: Elapsed {ElapsedMs} ms (threshold {ThresholdMs} ms) Path={Path}")]
+ public static partial void VerySlowSearchDetected(ILogger logger, long ElapsedMs, long ThresholdMs, string Path);
+
+ // Worker scan batch
+ [LoggerMessage(EventId = 2000, Level = LogLevel.Information,
+ Message = "Scan batch {BatchId} started")]
+ public static partial void ScanBatchStart(ILogger logger, string BatchId);
+
+ [LoggerMessage(EventId = 2001, Level = LogLevel.Information,
+ Message = "Scan batch {BatchId} completed in {ElapsedMs} ms. Processed={Processed} Failures={Failures}")]
+ public static partial void ScanBatchComplete(ILogger logger, string BatchId, int Processed, int Failures, long ElapsedMs);
+
+ [LoggerMessage(EventId = 2002, Level = LogLevel.Error,
+ Message = "Scan batch {BatchId} failed")]
+ public static partial void ScanBatchFailed(ILogger logger, string BatchId, Exception ex);
+}
\ No newline at end of file
diff --git a/JSMR.Application/Logging/LogObjectBuilder.cs b/JSMR.Application/Logging/LogObjectBuilder.cs
new file mode 100644
index 0000000..96afa48
--- /dev/null
+++ b/JSMR.Application/Logging/LogObjectBuilder.cs
@@ -0,0 +1,36 @@
+namespace JSMR.Application.Logging;
+
+public sealed class LogObjectBuilder
+{
+ private readonly Dictionary _d = [];
+
+ public LogObjectBuilder Add(string key, object? value)
+ {
+ if (value is null)
+ return this;
+
+ _d[key] = value;
+
+ return this;
+ }
+
+ public LogObjectBuilder AddIfNotEmpty(string key, string? value)
+ => string.IsNullOrWhiteSpace(value) ? this : Add(key, value);
+
+ public LogObjectBuilder AddIfNotEmpty(string key, IReadOnlyCollection? value, int? preview = null)
+ {
+ if (value is null || value.Count == 0)
+ return this;
+
+ if (preview is null || value.Count <= preview)
+ return Add(key, value);
+
+ // Store small preview + count so logs stay compact
+ return Add(key, new { Preview = value.Take(preview.Value), value.Count });
+ }
+
+ public LogObjectBuilder AddIfNotDefault(string key, T value) where T : struct, IEquatable
+ => value.Equals(default) ? this : Add(key, value);
+
+ public object Build() => _d;
+}
\ No newline at end of file
diff --git a/JSMR.Application/Logging/LoggingExtensions.cs b/JSMR.Application/Logging/LoggingExtensions.cs
new file mode 100644
index 0000000..26e0338
--- /dev/null
+++ b/JSMR.Application/Logging/LoggingExtensions.cs
@@ -0,0 +1,87 @@
+using JSMR.Application.Circles.Queries.Search;
+using JSMR.Application.Common.Search;
+using JSMR.Application.Creators.Queries.Search.Contracts;
+using JSMR.Application.Tags.Queries.Search.Contracts;
+using JSMR.Application.VoiceWorks.Queries.Search;
+
+namespace JSMR.Application.Logging;
+
+public static class LoggingExtensions
+{
+ public static object ToLogObject(this SearchOptions searchOptions)
+ where TCriteria : new()
+ where TSortField : struct, Enum
+ {
+ return new LogObjectBuilder()
+ .Add("PageNumber", searchOptions.PageNumber)
+ .Add("PageSize", searchOptions.PageSize)
+ .Add("Criteria", MapCriteriaToLogObject(searchOptions.Criteria))
+ //.AddIfNotEmpty("Sort", searchOptions.SortOptions.ToLogObject())
+ .Build();
+ }
+
+ private static object MapCriteriaToLogObject(TCriteria criteria) => criteria switch
+ {
+ VoiceWorkSearchCriteria voiceWorkSearchCriteria => voiceWorkSearchCriteria.ToLogObject(),
+ CircleSearchCriteria circleSearchCriteria => circleSearchCriteria.ToLogObject(),
+ TagSearchCriteria tagSearchCriteria => tagSearchCriteria.ToLogObject(),
+ CreatorSearchCriteria creatorSearchCriteria => creatorSearchCriteria.ToLogObject(),
+ _ => criteria!
+ };
+
+ public record SortLog(int Index, string Field, string Direction);
+
+ public static object ToLogObject(this IEnumerable> sort)
+ where TSortField : struct, Enum
+ {
+ return sort.Select((sortOption, index) =>
+ new {
+ Index = index,
+ Field = sortOption.Field.ToString(),
+ Direction = sortOption.Direction.ToString()
+ }
+ ).ToList();
+ }
+
+ public static object ToLogObject(this VoiceWorkSearchCriteria criteria)
+ {
+ return new LogObjectBuilder()
+ .AddIfNotEmpty("Keywords", criteria.Keywords)
+ .AddIfNotEmpty("Title", criteria.Title)
+ .AddIfNotEmpty("Circle", criteria.Circle)
+ .Add("Locale", criteria.Locale)
+ .AddIfNotEmpty("AgeRatings", criteria.AgeRatings)
+ .AddIfNotEmpty("Languages", criteria.SupportedLanguages)
+ .AddIfNotEmpty("TagIds", criteria.TagIds, preview: 5)
+ .AddIfNotEmpty("CreatorIds", criteria.CreatorIds, preview: 5)
+ .Add("IncludeAllTags", criteria.IncludeAllTags ? true : null)
+ .Add("IncludeAllCreators", criteria.IncludeAllCreators ? true : null)
+ .Add("MinDownloads", criteria.MinDownloads)
+ .Add("MaxDownloads", criteria.MaxDownloads)
+ .Add("ReleaseDateStart", criteria.ReleaseDateStart)
+ .Add("ReleaseDateEnd", criteria.ReleaseDateEnd)
+ .Build();
+ }
+
+ public static object ToLogObject(this CircleSearchCriteria criteria)
+ {
+ return new LogObjectBuilder()
+ .AddIfNotEmpty("Name", criteria.Name)
+ .AddIfNotEmpty("Status", criteria.Status?.ToString())
+ .Build();
+ }
+
+ public static object ToLogObject(this TagSearchCriteria criteria)
+ {
+ return new LogObjectBuilder()
+ .AddIfNotEmpty("Name", criteria.Name)
+ .Build();
+ }
+
+ public static object ToLogObject(this CreatorSearchCriteria criteria)
+ {
+ return new LogObjectBuilder()
+ .AddIfNotEmpty("Name", criteria.Name)
+ .Build();
+ }
+}
\ No newline at end of file
diff --git a/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs b/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs
index f72d385..582cb7f 100644
--- a/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs
+++ b/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs
@@ -38,7 +38,7 @@ public sealed class ScanVoiceWorksHandler(
string[] productIds = [.. works.Where(x => !string.IsNullOrWhiteSpace(x.ProductId)).Select(x => x.ProductId!)];
VoiceWorkDetailCollection voiceWorkDetails = await dlsiteClient.GetVoiceWorkDetailsAsync(productIds, cancellationToken);
- List ingests = [.. works.Select(work =>
+ VoiceWorkIngest[] ingests = [.. works.Select(work =>
{
voiceWorkDetails.TryGetValue(work.ProductId!, out VoiceWorkDetails? value);
return new VoiceWorkIngest(work, value);
diff --git a/JSMR.Application/Tags/Queries/Search/SearchTagsHandler.cs b/JSMR.Application/Tags/Queries/Search/SearchTagsHandler.cs
index 9e209b2..fde8a63 100644
--- a/JSMR.Application/Tags/Queries/Search/SearchTagsHandler.cs
+++ b/JSMR.Application/Tags/Queries/Search/SearchTagsHandler.cs
@@ -1,31 +1,32 @@
-using JSMR.Application.Common.Caching;
-using JSMR.Application.Common.Search;
+using JSMR.Application.Common.Search;
+using JSMR.Application.Logging;
using JSMR.Application.Tags.Queries.Search.Contracts;
using JSMR.Application.Tags.Queries.Search.Ports;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
namespace JSMR.Application.Tags.Queries.Search;
-public sealed class SearchTagsHandler(ITagSearchProvider searchProvider)
+public sealed class SearchTagsHandler(ITagSearchProvider searchProvider, ILogger logger)
{
public async Task HandleAsync(SearchTagsRequest request, CancellationToken cancellationToken)
{
SearchOptions searchOptions = request.Options;
-
- //string cacheKey = $"tag:{searchOptions.GetHashCode()}";
-
- //TagSearchResults? cachedResults = await cache.GetAsync(cacheKey, cancellationToken);
-
- //if (cachedResults != null)
- // return new SearchTagsResponse(cachedResults);
+ Stopwatch stopWatch = Stopwatch.StartNew();
SearchResult results = await searchProvider.SearchAsync(searchOptions, cancellationToken);
+ long elapsedMilliseconds = stopWatch.ElapsedMilliseconds;
- //CacheEntryOptions cacheEntryOptions = new()
- //{
- // SlidingExpiration = TimeSpan.FromMinutes(10)
- //};
-
- //await cache.SetAsync(cacheKey, results, cacheEntryOptions, cancellationToken);
+ LogEvents.SearchCompleted(
+ logger,
+ Elapsed: elapsedMilliseconds,
+ Items: results.Items.Length,
+ Total: results.TotalItems,
+ Page: searchOptions.PageNumber,
+ Size: searchOptions.PageSize,
+ Sort: searchOptions.SortOptions.ToLogObject(),
+ Criteria: searchOptions.Criteria.ToLogObject()
+ );
return new SearchTagsResponse(results);
}
diff --git a/JSMR.Application/VoiceWorks/Queries/Search/SearchVoiceWorksHandler.cs b/JSMR.Application/VoiceWorks/Queries/Search/SearchVoiceWorksHandler.cs
index 7ce332a..1b2e8aa 100644
--- a/JSMR.Application/VoiceWorks/Queries/Search/SearchVoiceWorksHandler.cs
+++ b/JSMR.Application/VoiceWorks/Queries/Search/SearchVoiceWorksHandler.cs
@@ -1,30 +1,31 @@
-using JSMR.Application.Common.Caching;
-using JSMR.Application.VoiceWorks.Ports;
+using JSMR.Application.Common.Search;
+using JSMR.Application.Logging;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
namespace JSMR.Application.VoiceWorks.Queries.Search;
// TODO: Caching?
-public sealed class SearchVoiceWorksHandler(IVoiceWorkSearchProvider provider)
+public sealed class SearchVoiceWorksHandler(IVoiceWorkSearchProvider provider, ILogger logger)
{
public async Task HandleAsync(SearchVoiceWorksRequest request, CancellationToken cancellationToken)
{
var searchOptions = request.Options;
- //string cacheKey = $"vw:{searchOptions.GetHashCode()}";
+ Stopwatch stopWatch = Stopwatch.StartNew();
+ SearchResult results = await provider.SearchAsync(searchOptions, cancellationToken);
+ long elapsedMilliseconds = stopWatch.ElapsedMilliseconds;
- //VoiceWorkSearchResults? cachedResults = await cache.GetAsync(cacheKey, cancellationToken);
-
- //if (cachedResults != null)
- // return new SearchVoiceWorksResponse(cachedResults);
-
- var results = await provider.SearchAsync(searchOptions, cancellationToken);
-
- //CacheEntryOptions cacheEntryOptions = new()
- //{
- // SlidingExpiration = TimeSpan.FromMinutes(10)
- //};
-
- //await cache.SetAsync(cacheKey, results, cacheEntryOptions, cancellationToken);
+ LogEvents.SearchCompleted(
+ logger,
+ Elapsed: elapsedMilliseconds,
+ Items: results.Items.Length,
+ Total: results.TotalItems,
+ Page: searchOptions.PageNumber,
+ Size: searchOptions.PageSize,
+ Sort: searchOptions.SortOptions.ToLogObject(),
+ Criteria: searchOptions.Criteria.ToLogObject()
+ );
return new SearchVoiceWorksResponse(results);
}
diff --git a/JSMR.Application/VoiceWorks/Queries/Search/VoiceWorkSearchCriteria.cs b/JSMR.Application/VoiceWorks/Queries/Search/VoiceWorkSearchCriteria.cs
index 75be8b5..14c83f5 100644
--- a/JSMR.Application/VoiceWorks/Queries/Search/VoiceWorkSearchCriteria.cs
+++ b/JSMR.Application/VoiceWorks/Queries/Search/VoiceWorkSearchCriteria.cs
@@ -16,8 +16,8 @@ public class VoiceWorkSearchCriteria
public int[] CreatorIds { get; init; } = [];
public bool IncludeAllCreators { get; init; }
public Locale Locale { get; init; } = Locale.Japanese;
- public DateTime? ReleaseDateStart { get; init; }
- public DateTime? ReleaseDateEnd { get; init; }
+ public DateOnly? ReleaseDateStart { get; init; }
+ public DateOnly? ReleaseDateEnd { get; init; }
public AgeRating[] AgeRatings { get; init; } = [];
public Language[] SupportedLanguages { get; init; } = [];
public AIGeneration[] AIGenerationOptions { get; init; } = [];
diff --git a/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkSearchProvider.cs b/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkSearchProvider.cs
index 9344d18..6ebf13d 100644
--- a/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkSearchProvider.cs
+++ b/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkSearchProvider.cs
@@ -62,10 +62,10 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea
}
if (criteria.ReleaseDateStart is not null)
- filteredQuery = filteredQuery.Where(x => x.VoiceWork.SalesDate >= criteria.ReleaseDateStart.Value);
+ filteredQuery = filteredQuery.Where(x => x.VoiceWork.SalesDate >= criteria.ReleaseDateStart.Value.ToDateTime(TimeOnly.MinValue));
if (criteria.ReleaseDateEnd is not null)
- filteredQuery = filteredQuery.Where(x => x.VoiceWork.SalesDate <= criteria.ReleaseDateEnd.Value);
+ filteredQuery = filteredQuery.Where(x => x.VoiceWork.SalesDate <= criteria.ReleaseDateEnd.Value.ToDateTime(TimeOnly.MinValue));
if (criteria.AgeRatings.Length > 0)
filteredQuery = filteredQuery.Where(x => criteria.AgeRatings.Contains((AgeRating)x.VoiceWork.Rating));
diff --git a/JSMR.Tests/Integration/VoiceWorkSearchProviderTests.cs b/JSMR.Tests/Integration/VoiceWorkSearchProviderTests.cs
index 0c7490f..f135702 100644
--- a/JSMR.Tests/Integration/VoiceWorkSearchProviderTests.cs
+++ b/JSMR.Tests/Integration/VoiceWorkSearchProviderTests.cs
@@ -657,8 +657,8 @@ public class VoiceWorkSearchProviderTests(VoiceWorkSearchProviderFixture fixture
{
Criteria = new()
{
- ReleaseDateStart = new DateTime(2025, 1, 1),
- ReleaseDateEnd = new DateTime(2025, 1, 2)
+ ReleaseDateStart = new DateOnly(2025, 1, 1),
+ ReleaseDateEnd = new DateOnly(2025, 1, 2)
}
};
diff --git a/JSMR.Tests/JSMR.Tests.csproj b/JSMR.Tests/JSMR.Tests.csproj
index 74dd7a5..70ee19a 100644
--- a/JSMR.Tests/JSMR.Tests.csproj
+++ b/JSMR.Tests/JSMR.Tests.csproj
@@ -28,8 +28,8 @@
-
-
+
+
all