Merge branch 'feature/caching' into 'dev'

Resolve "Caching"

Closes #13

See merge request leon.hoppe/Portfolio!9
This commit was merged in pull request #25.
This commit is contained in:
2025-01-26 15:27:50 +00:00
13 changed files with 60 additions and 16 deletions

View File

@@ -1,9 +1,10 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OutputCaching;
using Portfolio.Shared.Services; using Portfolio.Shared.Services;
namespace Portfolio.Api.Controller; namespace Portfolio.Api.Controller;
[ApiController, Route("api/about")] [ApiController, Route("api/about"), OutputCache(Tags = [DatabaseContext.CacheKey])]
public class AboutController(IAboutRepository repository) : ControllerBase { public class AboutController(IAboutRepository repository) : ControllerBase {
[HttpGet] [HttpGet]

View File

@@ -1,9 +1,10 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OutputCaching;
using Portfolio.Shared.Services; using Portfolio.Shared.Services;
namespace Portfolio.Api.Controller; namespace Portfolio.Api.Controller;
[ApiController, Route("api/projects")] [ApiController, Route("api/projects"), OutputCache(Tags = [DatabaseContext.CacheKey])]
public class ProjectController(IProjectRepository repository) : ControllerBase { public class ProjectController(IProjectRepository repository) : ControllerBase {
[HttpGet] [HttpGet]

View File

@@ -1,9 +1,10 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OutputCaching;
using Portfolio.Shared.Services; using Portfolio.Shared.Services;
namespace Portfolio.Api.Controller; namespace Portfolio.Api.Controller;
[ApiController, Route("api/technologies")] [ApiController, Route("api/technologies"), OutputCache(Tags = [DatabaseContext.CacheKey])]
public class TechnologyController(ITechnologyRepository repository) : ControllerBase { public class TechnologyController(ITechnologyRepository repository) : ControllerBase {
[HttpGet] [HttpGet]

View File

@@ -1,10 +1,11 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OutputCaching;
using Portfolio.Shared.Models; using Portfolio.Shared.Models;
using Portfolio.Shared.Services; using Portfolio.Shared.Services;
namespace Portfolio.Api.Controller; namespace Portfolio.Api.Controller;
[ApiController, Route("api/timeline")] [ApiController, Route("api/timeline"), OutputCache(Tags = [DatabaseContext.CacheKey])]
public class TimelineController(ITimelineRepository repository) : ControllerBase { public class TimelineController(ITimelineRepository repository) : ControllerBase {
[HttpGet("{type}")] [HttpGet("{type}")]
@@ -12,5 +13,11 @@ public class TimelineController(ITimelineRepository repository) : ControllerBase
var timeline = await repository.GetTimeline(type, ct); var timeline = await repository.GetTimeline(type, ct);
return Ok(timeline); return Ok(timeline);
} }
[HttpGet("featured")]
public async Task<IActionResult> GetFeaturedTimeline(CancellationToken ct) {
var timeline = await repository.GetFeaturedTimeline(ct);
return Ok(timeline);
}
} }

View File

@@ -1,9 +1,12 @@
using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.OutputCaching;
using Microsoft.EntityFrameworkCore;
using Portfolio.Shared.Models; using Portfolio.Shared.Models;
namespace Portfolio.Api; namespace Portfolio.Api;
public class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbContext(options) { public class DatabaseContext(DbContextOptions<DatabaseContext> options, IOutputCacheStore cacheStore) : DbContext(options) {
public const string CacheKey = "portfolio-data";
public DbSet<Project> Projects { get; set; } public DbSet<Project> Projects { get; set; }
@@ -19,5 +22,9 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbCont
.WithMany() .WithMany()
.UsingEntity("LanguageProject"); .UsingEntity("LanguageProject");
} }
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new()) {
await cacheStore.EvictByTagAsync(CacheKey, cancellationToken);
return await base.SaveChangesAsync(cancellationToken);
}
} }

View File

@@ -9,6 +9,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" /> <PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
<PackageReference Include="Aspire.StackExchange.Redis.OutputCaching" Version="9.0.0" />
<PackageReference Include="HopFrame.Web" Version="3.0.0" /> <PackageReference Include="HopFrame.Web" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0"/> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0"/>
</ItemGroup> </ItemGroup>

View File

@@ -1,6 +1,5 @@
using HopFrame.Core.Config; using HopFrame.Core.Config;
using HopFrame.Web; using HopFrame.Web;
using Microsoft.EntityFrameworkCore;
using Portfolio.Api; using Portfolio.Api;
using Portfolio.Api.Services; using Portfolio.Api.Services;
using Portfolio.Shared.Models; using Portfolio.Shared.Models;
@@ -23,6 +22,11 @@ builder.Services.AddScoped<IAboutRepository, AboutRepository>();
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.AddRedisOutputCache("cache");
builder.Services.AddOutputCache(options => {
options.DefaultExpirationTimeSpan = TimeSpan.FromDays(30);
});
builder.Services.AddHopFrame(options => { builder.Services.AddHopFrame(options => {
options.DisplayUserInfo(false); options.DisplayUserInfo(false);
options.AddDbContext<DatabaseContext>(context => { options.AddDbContext<DatabaseContext>(context => {
@@ -94,6 +98,8 @@ await using (var scope = app.Services.CreateAsyncScope()) {
app.MapDefaultEndpoints(); app.MapDefaultEndpoints();
app.UseOutputCache();
app.MapControllers(); app.MapControllers();
app.UseAntiforgery(); app.UseAntiforgery();

View File

@@ -11,5 +11,10 @@ internal sealed class TimelineRepository(DatabaseContext context) : ITimelineRep
.Where(entry => entry.Type == type) .Where(entry => entry.Type == type)
.ToArrayAsync(ct); .ToArrayAsync(ct);
} }
public async Task<IEnumerable<TimelineEntry>> GetFeaturedTimeline(CancellationToken ct) {
return await context.Timeline
.Where(entry => entry.Featured)
.ToArrayAsync(ct);
}
} }

View File

@@ -14,6 +14,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.0.0"/> <PackageReference Include="Aspire.Hosting.AppHost" Version="9.0.0"/>
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="9.0.0" /> <PackageReference Include="Aspire.Hosting.PostgreSQL" Version="9.0.0" />
<PackageReference Include="Aspire.Hosting.Redis" Version="9.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -5,13 +5,21 @@ var postgres = builder.AddPostgres("postgres")
var db = postgres.AddDatabase("data"); var db = postgres.AddDatabase("data");
var cache = builder.AddRedis("cache");
var api = builder.AddProject<Projects.Portfolio_Api>("api") var api = builder.AddProject<Projects.Portfolio_Api>("api")
.WithReference(db) .WithReference(db)
.WaitFor(db); .WaitFor(db)
.WithReference(cache)
.WaitFor(cache);
builder.AddProject<Projects.Portfolio_Web>("web") builder.AddProject<Projects.Portfolio_Web>("web")
.WithReference(api) .WithReference(api)
.WaitFor(api) .WaitFor(api)
.WithExternalHttpEndpoints(); .WithExternalHttpEndpoints();
builder.AddContainer("cdn", "nginx")
.WithVolume("portfolio-cdn", "/usr/share/nginx/html", true)
.WithHttpEndpoint(80, 80);
builder.Build().Run(); builder.Build().Run();

View File

@@ -6,4 +6,6 @@ public interface ITimelineRepository {
Task<IEnumerable<TimelineEntry>> GetTimeline(TimelineEntryType type, CancellationToken ct); Task<IEnumerable<TimelineEntry>> GetTimeline(TimelineEntryType type, CancellationToken ct);
Task<IEnumerable<TimelineEntry>> GetFeaturedTimeline(CancellationToken ct);
} }

View File

@@ -104,12 +104,7 @@
var technologies = await TechnologyRepository.GetTechnologies(TokenSource.Token); var technologies = await TechnologyRepository.GetTechnologies(TokenSource.Token);
_technologies = technologies.Where(t => t.Featured); _technologies = technologies.Where(t => t.Featured);
var carrierTimeline = await TimelineRepository.GetTimeline(TimelineEntryType.Carrier, TokenSource.Token); _timeline = await TimelineRepository.GetFeaturedTimeline(TokenSource.Token);
var experienceTimeline = await TimelineRepository.GetTimeline(TimelineEntryType.Experience, TokenSource.Token);
_timeline = experienceTimeline
.Aggregate(carrierTimeline, (current, entry) => current.Append(entry))
.Where(t => t.Featured)
.OrderBy(t => t.Date);
} }
} }

View File

@@ -12,4 +12,13 @@ internal sealed class TimelineRepository(IHttpClientFactory factory) : ITimeline
var data = await result.Content.ReadFromJsonAsync<IEnumerable<TimelineEntry>>(ct); var data = await result.Content.ReadFromJsonAsync<IEnumerable<TimelineEntry>>(ct);
return data ?? []; return data ?? [];
} }
public async Task<IEnumerable<TimelineEntry>> GetFeaturedTimeline(CancellationToken ct) {
var client = factory.CreateClient("api");
var result = await client.GetAsync("api/timeline/featured", ct);
if (!result.IsSuccessStatusCode) return [];
var data = await result.Content.ReadFromJsonAsync<IEnumerable<TimelineEntry>>(ct);
return data ?? [];
}
} }