refactor: swap MemoryCache for HybridCache

This commit is contained in:
Collin Barrett 2025-05-28 19:00:24 -05:00 committed by Collin Barrett
parent 836ce6751b
commit 6132173c40
13 changed files with 285 additions and 343 deletions

View file

@ -4,7 +4,6 @@
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using static FilterLists.Directory.Api.OpenApi.OpenApiTags;
using static FilterLists.Directory.Application.Queries.GetListDetails;
namespace FilterLists.Directory.Api;
@ -14,8 +13,9 @@ internal static void MapEndpoints(this WebApplication app)
{
app.MapGet("/languages",
(IMediator mediator, CancellationToken ct) =>
mediator.CreateStream(new GetLanguages.Request(), ct)
mediator.Send(new GetLanguages.Request(), ct)
)
.Produces<List<GetLanguages.Response>>()
.WithOpenApi(operation => new OpenApiOperation(operation)
{
Tags = [LanguagesTag],
@ -25,8 +25,9 @@ internal static void MapEndpoints(this WebApplication app)
app.MapGet("/licenses",
(IMediator mediator, CancellationToken ct) =>
mediator.CreateStream(new GetLicenses.Request(), ct)
mediator.Send(new GetLicenses.Request(), ct)
)
.Produces<List<GetLicenses.Response>>()
.WithOpenApi(operation => new OpenApiOperation(operation)
{
Tags = [LicensesTag],
@ -36,8 +37,9 @@ internal static void MapEndpoints(this WebApplication app)
app.MapGet("/lists",
(IMediator mediator, CancellationToken ct) =>
mediator.CreateStream(new GetLists.Request(), ct)
mediator.Send(new GetLists.Request(), ct)
)
.Produces<List<GetLists.Response>>()
.WithOpenApi(operation => new OpenApiOperation(operation)
{
Tags = [FilterListsTag],
@ -45,14 +47,17 @@ internal static void MapEndpoints(this WebApplication app)
OperationId = nameof(GetLists)
});
app.MapGet("/lists/{id:int}", async Task<Results<Ok<Response>, NotFound>>
(int id, IMediator mediator, CancellationToken ct) =>
{
var list = await mediator.Send(new Request(id), ct);
return list is not null
? TypedResults.Ok(list)
: TypedResults.NotFound();
})
app.MapGet("/lists/{id:int}",
async Task<Results<Ok<GetListDetails.Response>, NotFound>> (int id, IMediator mediator,
CancellationToken ct) =>
{
var list = await mediator.Send(new GetListDetails.Request(id), ct);
return list is not null
? TypedResults.Ok(list)
: TypedResults.NotFound();
})
.Produces<GetListDetails.Response>()
.Produces(StatusCodes.Status404NotFound)
.WithOpenApi(operation => new OpenApiOperation(operation)
{
Tags = [FilterListsTag],
@ -73,8 +78,9 @@ internal static void MapEndpoints(this WebApplication app)
app.MapGet("/maintainers",
(IMediator mediator, CancellationToken ct) =>
mediator.CreateStream(new GetMaintainers.Request(), ct)
mediator.Send(new GetMaintainers.Request(), ct)
)
.Produces<List<GetMaintainers.Response>>()
.WithOpenApi(operation => new OpenApiOperation(operation)
{
Tags = [MaintainersTag],
@ -84,8 +90,9 @@ internal static void MapEndpoints(this WebApplication app)
app.MapGet("/software",
(IMediator mediator, CancellationToken ct) =>
mediator.CreateStream(new GetSoftware.Request(), ct)
mediator.Send(new GetSoftware.Request(), ct)
)
.Produces<List<GetSoftware.Response>>()
.WithOpenApi(operation => new OpenApiOperation(operation)
{
Tags = [SoftwareTag],
@ -95,8 +102,9 @@ internal static void MapEndpoints(this WebApplication app)
app.MapGet("/syntaxes",
(IMediator mediator, CancellationToken ct) =>
mediator.CreateStream(new GetSyntaxes.Request(), ct)
mediator.Send(new GetSyntaxes.Request(), ct)
)
.Produces<List<GetSyntaxes.Response>>()
.WithOpenApi(operation => new OpenApiOperation(operation)
{
Tags = [SyntaxesTag],
@ -106,8 +114,9 @@ internal static void MapEndpoints(this WebApplication app)
app.MapGet("/tags",
(IMediator mediator, CancellationToken ct) =>
mediator.CreateStream(new GetTags.Request(), ct)
mediator.Send(new GetTags.Request(), ct)
)
.Produces<List<GetTags.Response>>()
.WithOpenApi(operation => new OpenApiOperation(operation)
{
Tags = [TagsTag],

View file

@ -9,9 +9,6 @@ public static class ConfigurationExtensions
public static void AddApplication(this IHostApplicationBuilder builder)
{
builder.AddInfrastructure();
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(ConfigurationExtensions).Assembly);
});
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(ConfigurationExtensions).Assembly));
}
}

View file

@ -1,41 +1,36 @@
using System.Runtime.CompilerServices;
using FilterLists.Directory.Infrastructure;
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Hybrid;
namespace FilterLists.Directory.Application.Queries;
public static class GetLanguages
{
private static readonly Func<QueryDbContext, IAsyncEnumerable<Response>> Query =
EF.CompileAsyncQuery((QueryDbContext ctx) =>
ctx.Languages
.Where(l => l.FilterListLanguages.Any())
.OrderBy(l => l.Iso6391)
.Select(l => new Response(
l.Id,
l.Iso6391,
l.Name,
l.FilterListLanguages
.OrderBy(fl => fl.FilterListId)
.Select(fl => fl.FilterListId)
))
.TagWith(nameof(GetLanguages))
);
public sealed record Request : IRequest<List<Response>>;
public sealed record Request : IStreamRequest<Response>;
private sealed class Handler(QueryDbContext ctx, IMemoryCache cache) : IStreamRequestHandler<Request, Response>
private sealed class Handler(QueryDbContext ctx, HybridCache cache) : IRequestHandler<Request, List<Response>>
{
public async IAsyncEnumerable<Response> Handle(Request request, [EnumeratorCancellation] CancellationToken ct)
public async Task<List<Response>> Handle(Request request, CancellationToken ct)
{
await foreach (var language in cache.GetOrCreateAsyncEnumerable(
nameof(GetLanguages),
Query(ctx).WithCancellation(ct))
.WithCancellation(ct))
yield return language;
const string key = nameof(GetLanguages);
return await cache.GetOrCreateAsync(
key,
async cancel =>
await ctx.Languages
.Where(l => l.FilterListLanguages.Any())
.OrderBy(l => l.Iso6391)
.Select(l => new Response(
l.Id,
l.Iso6391,
l.Name,
l.FilterListLanguages
.OrderBy(fl => fl.FilterListId)
.Select(fl => fl.FilterListId)
))
.TagWith(key)
.ToListAsync(cancel),
cancellationToken: ct);
}
}

View file

@ -1,44 +1,39 @@
using System.Runtime.CompilerServices;
using FilterLists.Directory.Infrastructure;
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Hybrid;
namespace FilterLists.Directory.Application.Queries;
public static class GetLicenses
{
private static readonly Func<QueryDbContext, IAsyncEnumerable<Response>> Query =
EF.CompileAsyncQuery((QueryDbContext ctx) =>
ctx.Licenses
.Where(l => l.FilterLists.Any())
.OrderBy(l => l.Id)
.Select(l => new Response(
l.Id,
l.Name,
l.Url,
l.PermitsModification,
l.PermitsDistribution,
l.PermitsCommercialUse,
l.FilterLists
.OrderBy(f => f.Id)
.Select(f => f.Id)
))
.TagWith(nameof(GetLicenses))
);
public sealed record Request : IRequest<List<Response>>;
public sealed record Request : IStreamRequest<Response>;
private sealed class Handler(QueryDbContext ctx, IMemoryCache cache) : IStreamRequestHandler<Request, Response>
private sealed class Handler(QueryDbContext ctx, HybridCache cache) : IRequestHandler<Request, List<Response>>
{
public async IAsyncEnumerable<Response> Handle(Request request, [EnumeratorCancellation] CancellationToken ct)
public async Task<List<Response>> Handle(Request request, CancellationToken ct)
{
await foreach (var license in cache.GetOrCreateAsyncEnumerable(
nameof(GetLicenses),
Query(ctx).WithCancellation(ct))
.WithCancellation(ct))
yield return license;
const string key = nameof(GetLicenses);
return await cache.GetOrCreateAsync(
key,
async cancel =>
await ctx.Licenses
.Where(l => l.FilterLists.Any())
.OrderBy(l => l.Id)
.Select(l => new Response(
l.Id,
l.Name,
l.Url,
l.PermitsModification,
l.PermitsDistribution,
l.PermitsCommercialUse,
l.FilterLists
.OrderBy(f => f.Id)
.Select(f => f.Id)
))
.TagWith(key)
.ToListAsync(cancel),
cancellationToken: ct);
}
}

View file

@ -1,80 +1,81 @@
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Hybrid;
namespace FilterLists.Directory.Application.Queries;
public static class GetListDetails
{
private static readonly Func<QueryDbContext, int, Task<Response?>> Query =
EF.CompileAsyncQuery((QueryDbContext ctx, int id) =>
ctx.FilterLists
.Select(f => new Response
{
Id = f.Id,
Name = f.Name,
Description = f.Description,
LicenseId = f.LicenseId,
SyntaxIds = f.FilterListSyntaxes
.OrderBy(fs => fs.SyntaxId)
.Select(fs => fs.SyntaxId),
LanguageIds = f.FilterListLanguages
.OrderBy(fl => fl.LanguageId)
.Select(fl => fl.LanguageId),
TagIds = f.FilterListTags
.OrderBy(ft => ft.TagId)
.Select(ft => ft.TagId),
ViewUrls = f.ViewUrls
.OrderBy(u => u.SegmentNumber)
.ThenBy(u => u.Primariness)
.Select(u => new ViewUrlResponse
(
u.SegmentNumber,
u.Primariness,
u.Url
)),
HomeUrl = f.HomeUrl,
OnionUrl = f.OnionUrl,
PolicyUrl = f.PolicyUrl,
SubmissionUrl = f.SubmissionUrl,
IssuesUrl = f.IssuesUrl,
ForumUrl = f.ForumUrl,
ChatUrl = f.ChatUrl,
EmailAddress = f.EmailAddress,
DonateUrl = f.DonateUrl,
MaintainerIds = f.FilterListMaintainers
.OrderBy(fm => fm.MaintainerId)
.Select(fm => fm.MaintainerId),
UpstreamFilterListIds = f.UpstreamFilterLists
.OrderBy(ff => ff.UpstreamFilterListId)
.Select(ff => ff.UpstreamFilterListId),
ForkFilterListIds = f.ForkFilterLists
.OrderBy(ff => ff.ForkFilterListId)
.Select(ff => ff.ForkFilterListId),
IncludedInFilterListIds = f.IncludedInFilterLists
.OrderBy(fm => fm.IncludedInFilterListId)
.Select(fm => fm.IncludedInFilterListId),
IncludesFilterListIds = f.IncludesFilterLists
.OrderBy(fm => fm.IncludesFilterListId)
.Select(fm => fm.IncludesFilterListId),
DependencyFilterListIds = f.DependencyFilterLists
.OrderBy(fd => fd.DependencyFilterListId)
.Select(fd => fd.DependencyFilterListId),
DependentFilterListIds = f.DependentFilterLists
.OrderBy(fd => fd.DependentFilterListId)
.Select(fd => fd.DependentFilterListId)
})
.TagWith(nameof(GetListDetails))
.SingleOrDefault(f => f.Id == id));
public sealed record Request(int Id) : IRequest<Response?>;
private sealed class Handler(QueryDbContext ctx, IMemoryCache cache) : IRequestHandler<Request, Response?>
private sealed class Handler(QueryDbContext ctx, HybridCache cache) : IRequestHandler<Request, Response?>
{
public Task<Response?> Handle(Request request, CancellationToken _)
public async Task<Response?> Handle(Request request, CancellationToken ct)
{
return cache.GetOrCreateAsync($"{nameof(GetListDetails)}_{request.Id}", _ => Query(ctx, request.Id));
var key = $"{nameof(GetListDetails)}_{request.Id}";
return await cache.GetOrCreateAsync(
key,
async cancel =>
await ctx.FilterLists
.Select(f => new Response
{
Id = f.Id,
Name = f.Name,
Description = f.Description,
LicenseId = f.LicenseId,
SyntaxIds = f.FilterListSyntaxes
.OrderBy(fs => fs.SyntaxId)
.Select(fs => fs.SyntaxId),
LanguageIds = f.FilterListLanguages
.OrderBy(fl => fl.LanguageId)
.Select(fl => fl.LanguageId),
TagIds = f.FilterListTags
.OrderBy(ft => ft.TagId)
.Select(ft => ft.TagId),
ViewUrls = f.ViewUrls
.OrderBy(u => u.SegmentNumber)
.ThenBy(u => u.Primariness)
.Select(u => new ViewUrlResponse
(
u.SegmentNumber,
u.Primariness,
u.Url
)),
HomeUrl = f.HomeUrl,
OnionUrl = f.OnionUrl,
PolicyUrl = f.PolicyUrl,
SubmissionUrl = f.SubmissionUrl,
IssuesUrl = f.IssuesUrl,
ForumUrl = f.ForumUrl,
ChatUrl = f.ChatUrl,
EmailAddress = f.EmailAddress,
DonateUrl = f.DonateUrl,
MaintainerIds = f.FilterListMaintainers
.OrderBy(fm => fm.MaintainerId)
.Select(fm => fm.MaintainerId),
UpstreamFilterListIds = f.UpstreamFilterLists
.OrderBy(ff => ff.UpstreamFilterListId)
.Select(ff => ff.UpstreamFilterListId),
ForkFilterListIds = f.ForkFilterLists
.OrderBy(ff => ff.ForkFilterListId)
.Select(ff => ff.ForkFilterListId),
IncludedInFilterListIds = f.IncludedInFilterLists
.OrderBy(fm => fm.IncludedInFilterListId)
.Select(fm => fm.IncludedInFilterListId),
IncludesFilterListIds = f.IncludesFilterLists
.OrderBy(fm => fm.IncludesFilterListId)
.Select(fm => fm.IncludesFilterListId),
DependencyFilterListIds = f.DependencyFilterLists
.OrderBy(fd => fd.DependencyFilterListId)
.Select(fd => fd.DependencyFilterListId),
DependentFilterListIds = f.DependentFilterLists
.OrderBy(fd => fd.DependentFilterListId)
.Select(fd => fd.DependentFilterListId)
})
.TagWith(key)
.SingleOrDefaultAsync(l => l.Id == request.Id, cancel),
cancellationToken: ct);
}
}

View file

@ -1,56 +1,51 @@
using System.Runtime.CompilerServices;
using FilterLists.Directory.Infrastructure;
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Hybrid;
namespace FilterLists.Directory.Application.Queries;
public static class GetLists
{
private static readonly Func<QueryDbContext, IAsyncEnumerable<Response>> Query =
EF.CompileAsyncQuery((QueryDbContext ctx) =>
ctx.FilterLists
.OrderBy(f => f.Id)
.Select(f => new Response
(
f.Id,
f.Name,
f.Description,
f.LicenseId,
f.FilterListSyntaxes
.OrderBy(fs => fs.SyntaxId)
.Select(fs => fs.SyntaxId),
f.FilterListLanguages
.OrderBy(fl => fl.LanguageId)
.Select(fl => fl.LanguageId),
f.FilterListTags
.OrderBy(ft => ft.TagId)
.Select(ft => ft.TagId),
f.ViewUrls
.OrderBy(u => u.SegmentNumber)
.ThenBy(u => u.Primariness)
.Select(u => u.Url)
.FirstOrDefault(),
f.FilterListMaintainers
.OrderBy(fm => fm.MaintainerId)
.Select(fm => fm.MaintainerId)
))
.TagWith(nameof(GetLists))
);
public sealed record Request : IRequest<List<Response>>;
public sealed record Request : IStreamRequest<Response>;
private sealed class Handler(QueryDbContext ctx, IMemoryCache cache) : IStreamRequestHandler<Request, Response>
private sealed class Handler(QueryDbContext ctx, HybridCache cache) : IRequestHandler<Request, List<Response>>
{
public async IAsyncEnumerable<Response> Handle(Request request, [EnumeratorCancellation] CancellationToken ct)
public async Task<List<Response>> Handle(Request request, CancellationToken ct)
{
await foreach (var list in cache.GetOrCreateAsyncEnumerable(
nameof(GetLists),
Query(ctx).WithCancellation(ct))
.WithCancellation(ct))
yield return list;
const string key = nameof(GetLists);
return await cache.GetOrCreateAsync(
key,
async cancel =>
await ctx.FilterLists
.OrderBy(f => f.Id)
.Select(f => new Response
(
f.Id,
f.Name,
f.Description,
f.LicenseId,
f.FilterListSyntaxes
.OrderBy(fs => fs.SyntaxId)
.Select(fs => fs.SyntaxId),
f.FilterListLanguages
.OrderBy(fl => fl.LanguageId)
.Select(fl => fl.LanguageId),
f.FilterListTags
.OrderBy(ft => ft.TagId)
.Select(ft => ft.TagId),
f.ViewUrls
.OrderBy(u => u.SegmentNumber)
.ThenBy(u => u.Primariness)
.Select(u => u.Url)
.FirstOrDefault(),
f.FilterListMaintainers
.OrderBy(fm => fm.MaintainerId)
.Select(fm => fm.MaintainerId)
))
.TagWith(key)
.ToListAsync(cancel),
cancellationToken: ct);
}
}

View file

@ -1,43 +1,38 @@
using System.Runtime.CompilerServices;
using FilterLists.Directory.Infrastructure;
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Hybrid;
namespace FilterLists.Directory.Application.Queries;
public static class GetMaintainers
{
private static readonly Func<QueryDbContext, IAsyncEnumerable<Response>> Query =
EF.CompileAsyncQuery((QueryDbContext ctx) =>
ctx.Maintainers
.Where(m => m.FilterListMaintainers.Any())
.OrderBy(m => m.Id)
.Select(m => new Response(
m.Id,
m.Name,
m.Url,
m.EmailAddress,
m.TwitterHandle,
m.FilterListMaintainers
.OrderBy(fm => fm.FilterListId)
.Select(fm => fm.FilterListId)
))
.TagWith(nameof(GetMaintainers))
);
public sealed record Request : IRequest<List<Response>>;
public sealed record Request : IStreamRequest<Response>;
private sealed class Handler(QueryDbContext ctx, IMemoryCache cache) : IStreamRequestHandler<Request, Response>
private sealed class Handler(QueryDbContext ctx, HybridCache cache) : IRequestHandler<Request, List<Response>>
{
public async IAsyncEnumerable<Response> Handle(Request request, [EnumeratorCancellation] CancellationToken ct)
public async Task<List<Response>> Handle(Request request, CancellationToken ct)
{
await foreach (var maintainer in cache.GetOrCreateAsyncEnumerable(
nameof(GetMaintainers),
Query(ctx).WithCancellation(ct))
.WithCancellation(ct))
yield return maintainer;
const string key = nameof(GetMaintainers);
return await cache.GetOrCreateAsync(
key,
async cancel =>
await ctx.Maintainers
.Where(m => m.FilterListMaintainers.Any())
.OrderBy(m => m.Id)
.Select(m => new Response(
m.Id,
m.Name,
m.Url,
m.EmailAddress,
m.TwitterHandle,
m.FilterListMaintainers
.OrderBy(fm => fm.FilterListId)
.Select(fm => fm.FilterListId)
))
.TagWith(key)
.ToListAsync(cancel),
cancellationToken: ct);
}
}

View file

@ -1,46 +1,40 @@
using System.Runtime.CompilerServices;
using FilterLists.Directory.Infrastructure;
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Hybrid;
namespace FilterLists.Directory.Application.Queries;
public static class GetSoftware
{
private static readonly Func<QueryDbContext, IAsyncEnumerable<Response>> Query =
EF.CompileAsyncQuery((QueryDbContext ctx) =>
ctx.Software
.Where(s => s.SoftwareSyntaxes
.Any(ss => ss.Syntax.FilterListSyntaxes.Any()))
.OrderBy(s => s.Id)
.Select(s => new Response
(
s.Id,
s.Name,
s.Description,
s.HomeUrl,
s.DownloadUrl,
s.SupportsAbpUrlScheme,
s.SoftwareSyntaxes
.OrderBy(ss => ss.SyntaxId)
.Select(ss => ss.SyntaxId)
))
.TagWith(nameof(GetSoftware))
);
public sealed record Request : IRequest<List<Response>>;
public sealed record Request : IStreamRequest<Response>;
private sealed class Handler(QueryDbContext ctx, IMemoryCache cache) : IStreamRequestHandler<Request, Response>
private sealed class Handler(QueryDbContext ctx, HybridCache cache) : IRequestHandler<Request, List<Response>>
{
public async IAsyncEnumerable<Response> Handle(Request request, [EnumeratorCancellation] CancellationToken ct)
public async Task<List<Response>> Handle(Request request, CancellationToken ct)
{
await foreach (var software in cache.GetOrCreateAsyncEnumerable(
nameof(GetSoftware),
Query(ctx).WithCancellation(ct))
.WithCancellation(ct))
yield return software;
const string key = nameof(GetSoftware);
return await cache.GetOrCreateAsync(
key,
async cancel =>
await ctx.Software
.Where(s => s.SoftwareSyntaxes.Any(ss => ss.Syntax.FilterListSyntaxes.Any()))
.OrderBy(s => s.Id)
.Select(s => new Response
(
s.Id,
s.Name,
s.Description,
s.HomeUrl,
s.DownloadUrl,
s.SupportsAbpUrlScheme,
s.SoftwareSyntaxes
.OrderBy(ss => ss.SyntaxId)
.Select(ss => ss.SyntaxId)
))
.TagWith(key)
.ToListAsync(cancel),
cancellationToken: ct);
}
}

View file

@ -1,46 +1,41 @@
using System.Runtime.CompilerServices;
using FilterLists.Directory.Infrastructure;
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Hybrid;
namespace FilterLists.Directory.Application.Queries;
public static class GetSyntaxes
{
private static readonly Func<QueryDbContext, IAsyncEnumerable<Response>> Query =
EF.CompileAsyncQuery((QueryDbContext ctx) =>
ctx.Syntaxes
.Where(s => s.FilterListSyntaxes.Any())
.OrderBy(s => s.Id)
.Select(s => new Response
(
s.Id,
s.Name,
s.Description,
s.Url,
s.FilterListSyntaxes
.OrderBy(fs => fs.FilterListId)
.Select(fs => fs.FilterListId),
s.SoftwareSyntaxes
.OrderBy(ss => ss.SoftwareId)
.Select(ss => ss.SoftwareId)
))
.TagWith(nameof(GetSyntaxes))
);
public sealed record Request : IRequest<List<Response>>;
public sealed record Request : IStreamRequest<Response>;
private sealed class Handler(QueryDbContext ctx, IMemoryCache cache) : IStreamRequestHandler<Request, Response>
private sealed class Handler(QueryDbContext ctx, HybridCache cache) : IRequestHandler<Request, List<Response>>
{
public async IAsyncEnumerable<Response> Handle(Request request, [EnumeratorCancellation] CancellationToken ct)
public async Task<List<Response>> Handle(Request request, CancellationToken ct)
{
await foreach (var syntax in cache.GetOrCreateAsyncEnumerable(
nameof(GetSyntaxes),
Query(ctx).WithCancellation(ct))
.WithCancellation(ct))
yield return syntax;
const string key = nameof(GetSyntaxes);
return await cache.GetOrCreateAsync(
key,
async cancel =>
await ctx.Syntaxes
.Where(s => s.FilterListSyntaxes.Any())
.OrderBy(s => s.Id)
.Select(s => new Response
(
s.Id,
s.Name,
s.Description,
s.Url,
s.FilterListSyntaxes
.OrderBy(fs => fs.FilterListId)
.Select(fs => fs.FilterListId),
s.SoftwareSyntaxes
.OrderBy(ss => ss.SoftwareId)
.Select(ss => ss.SoftwareId)
))
.TagWith(key)
.ToListAsync(cancel),
cancellationToken: ct);
}
}

View file

@ -1,42 +1,37 @@
using System.Runtime.CompilerServices;
using FilterLists.Directory.Infrastructure;
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Hybrid;
namespace FilterLists.Directory.Application.Queries;
public static class GetTags
{
private static readonly Func<QueryDbContext, IAsyncEnumerable<Response>> Query =
EF.CompileAsyncQuery((QueryDbContext ctx) =>
ctx.Tags
.Where(t => t.FilterListTags.Any())
.OrderBy(t => t.Id)
.Select(t => new Response
(
t.Id,
t.Name,
t.Description,
t.FilterListTags
.OrderBy(flt => flt.FilterListId)
.Select(flt => flt.FilterListId)
))
.TagWith(nameof(GetTags))
);
public sealed record Request : IRequest<List<Response>>;
public sealed record Request : IStreamRequest<Response>;
private sealed class Handler(QueryDbContext ctx, IMemoryCache cache) : IStreamRequestHandler<Request, Response>
private sealed class Handler(QueryDbContext ctx, HybridCache cache) : IRequestHandler<Request, List<Response>>
{
public async IAsyncEnumerable<Response> Handle(Request request, [EnumeratorCancellation] CancellationToken ct)
public async Task<List<Response>> Handle(Request request, CancellationToken ct)
{
await foreach (var tag in cache.GetOrCreateAsyncEnumerable(
nameof(GetTags),
Query(ctx).WithCancellation(ct))
.WithCancellation(ct))
yield return tag;
const string key = nameof(GetTags);
return await cache.GetOrCreateAsync(
key,
async cancel =>
await ctx.Tags
.Where(t => t.FilterListTags.Any())
.OrderBy(t => t.Id)
.Select(t => new Response
(
t.Id,
t.Name,
t.Description,
t.FilterListTags
.OrderBy(flt => flt.FilterListId)
.Select(flt => flt.FilterListId)
))
.TagWith(key)
.ToListAsync(cancel),
cancellationToken: ct);
}
}

View file

@ -25,6 +25,6 @@ public static void AddInfrastructure(this IHostApplicationBuilder builder)
StringComparison.OrdinalIgnoreCase)));
builder.Services.AddHostedService<MigrationService>();
builder.Services.AddMemoryCache();
builder.Services.AddHybridCache();
}
}

View file

@ -6,6 +6,7 @@
<ItemGroup>
<PackageReference Include="Aspire.Microsoft.EntityFrameworkCore.SqlServer" Version="8.2.2"/>
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.5.0" />
</ItemGroup>
</Project>

View file

@ -1,30 +0,0 @@
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Caching.Memory;
namespace FilterLists.Directory.Infrastructure;
public static class MemoryCacheExtension
{
public static async IAsyncEnumerable<TResponse> GetOrCreateAsyncEnumerable<TResponse>(
this IMemoryCache cache,
object key,
ConfiguredCancelableAsyncEnumerable<TResponse> query,
MemoryCacheEntryOptions? options = null) where TResponse : class
{
if (cache.TryGetValue(key, out List<TResponse>? cachedElements))
{
foreach (var element in cachedElements ?? []) yield return element;
}
else
{
var elementsToCache = new List<TResponse>();
await foreach (var element in query)
{
elementsToCache.Add(element);
yield return element;
}
cache.Set(key, elementsToCache, options);
}
}
}