diff --git a/src/Portfolio.Api/Controller/AboutController.cs b/src/Portfolio.Api/Controller/AboutController.cs index 8a8f2b4..886a595 100644 --- a/src/Portfolio.Api/Controller/AboutController.cs +++ b/src/Portfolio.Api/Controller/AboutController.cs @@ -1,9 +1,10 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; using Portfolio.Shared.Services; namespace Portfolio.Api.Controller; -[ApiController, Route("api/about")] +[ApiController, Route("api/about"), OutputCache(Tags = [DatabaseContext.CacheKey])] public class AboutController(IAboutRepository repository) : ControllerBase { [HttpGet] diff --git a/src/Portfolio.Api/Controller/ProjectController.cs b/src/Portfolio.Api/Controller/ProjectController.cs index 139228e..2a74271 100644 --- a/src/Portfolio.Api/Controller/ProjectController.cs +++ b/src/Portfolio.Api/Controller/ProjectController.cs @@ -1,9 +1,10 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; using Portfolio.Shared.Services; namespace Portfolio.Api.Controller; -[ApiController, Route("api/projects")] +[ApiController, Route("api/projects"), OutputCache(Tags = [DatabaseContext.CacheKey])] public class ProjectController(IProjectRepository repository) : ControllerBase { [HttpGet] diff --git a/src/Portfolio.Api/Controller/TechnologyController.cs b/src/Portfolio.Api/Controller/TechnologyController.cs index 40de333..33a997f 100644 --- a/src/Portfolio.Api/Controller/TechnologyController.cs +++ b/src/Portfolio.Api/Controller/TechnologyController.cs @@ -1,9 +1,10 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; using Portfolio.Shared.Services; namespace Portfolio.Api.Controller; -[ApiController, Route("api/technologies")] +[ApiController, Route("api/technologies"), OutputCache(Tags = [DatabaseContext.CacheKey])] public class TechnologyController(ITechnologyRepository repository) : ControllerBase { [HttpGet] diff --git a/src/Portfolio.Api/Controller/TimelineController.cs b/src/Portfolio.Api/Controller/TimelineController.cs index 4a405ce..c78d675 100644 --- a/src/Portfolio.Api/Controller/TimelineController.cs +++ b/src/Portfolio.Api/Controller/TimelineController.cs @@ -1,10 +1,11 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; using Portfolio.Shared.Models; using Portfolio.Shared.Services; namespace Portfolio.Api.Controller; -[ApiController, Route("api/timeline")] +[ApiController, Route("api/timeline"), OutputCache(Tags = [DatabaseContext.CacheKey])] public class TimelineController(ITimelineRepository repository) : ControllerBase { [HttpGet("{type}")] @@ -12,5 +13,11 @@ public class TimelineController(ITimelineRepository repository) : ControllerBase var timeline = await repository.GetTimeline(type, ct); return Ok(timeline); } + + [HttpGet("featured")] + public async Task GetFeaturedTimeline(CancellationToken ct) { + var timeline = await repository.GetFeaturedTimeline(ct); + return Ok(timeline); + } } \ No newline at end of file diff --git a/src/Portfolio.Api/DatabaseContext.cs b/src/Portfolio.Api/DatabaseContext.cs index 5a5ccc2..a5aa0cc 100644 --- a/src/Portfolio.Api/DatabaseContext.cs +++ b/src/Portfolio.Api/DatabaseContext.cs @@ -1,9 +1,12 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.OutputCaching; +using Microsoft.EntityFrameworkCore; using Portfolio.Shared.Models; namespace Portfolio.Api; -public class DatabaseContext(DbContextOptions options) : DbContext(options) { +public class DatabaseContext(DbContextOptions options, IOutputCacheStore cacheStore) : DbContext(options) { + + public const string CacheKey = "portfolio-data"; public DbSet Projects { get; set; } @@ -19,5 +22,9 @@ public class DatabaseContext(DbContextOptions options) : DbCont .WithMany() .UsingEntity("LanguageProject"); } - + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = new()) { + await cacheStore.EvictByTagAsync(CacheKey, cancellationToken); + return await base.SaveChangesAsync(cancellationToken); + } } \ No newline at end of file diff --git a/src/Portfolio.Api/Portfolio.Api.csproj b/src/Portfolio.Api/Portfolio.Api.csproj index f7751ce..8cdda73 100644 --- a/src/Portfolio.Api/Portfolio.Api.csproj +++ b/src/Portfolio.Api/Portfolio.Api.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Portfolio.Api/Program.cs b/src/Portfolio.Api/Program.cs index 7030e00..528c0e1 100644 --- a/src/Portfolio.Api/Program.cs +++ b/src/Portfolio.Api/Program.cs @@ -1,6 +1,5 @@ using HopFrame.Core.Config; using HopFrame.Web; -using Microsoft.EntityFrameworkCore; using Portfolio.Api; using Portfolio.Api.Services; using Portfolio.Shared.Models; @@ -23,6 +22,11 @@ builder.Services.AddScoped(); builder.Services.AddControllers(); +builder.AddRedisOutputCache("cache"); +builder.Services.AddOutputCache(options => { + options.DefaultExpirationTimeSpan = TimeSpan.FromDays(30); +}); + builder.Services.AddHopFrame(options => { options.DisplayUserInfo(false); options.AddDbContext(context => { @@ -94,6 +98,8 @@ await using (var scope = app.Services.CreateAsyncScope()) { app.MapDefaultEndpoints(); +app.UseOutputCache(); + app.MapControllers(); app.UseAntiforgery(); diff --git a/src/Portfolio.Api/Services/TimelineRepository.cs b/src/Portfolio.Api/Services/TimelineRepository.cs index 2a8428c..60d7b6f 100644 --- a/src/Portfolio.Api/Services/TimelineRepository.cs +++ b/src/Portfolio.Api/Services/TimelineRepository.cs @@ -11,5 +11,10 @@ internal sealed class TimelineRepository(DatabaseContext context) : ITimelineRep .Where(entry => entry.Type == type) .ToArrayAsync(ct); } - + + public async Task> GetFeaturedTimeline(CancellationToken ct) { + return await context.Timeline + .Where(entry => entry.Featured) + .ToArrayAsync(ct); + } } \ No newline at end of file diff --git a/src/Portfolio.Host/Portfolio.Host.csproj b/src/Portfolio.Host/Portfolio.Host.csproj index 083deb5..8868389 100644 --- a/src/Portfolio.Host/Portfolio.Host.csproj +++ b/src/Portfolio.Host/Portfolio.Host.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Portfolio.Host/Program.cs b/src/Portfolio.Host/Program.cs index ae405b9..d8492b2 100644 --- a/src/Portfolio.Host/Program.cs +++ b/src/Portfolio.Host/Program.cs @@ -5,13 +5,21 @@ var postgres = builder.AddPostgres("postgres") var db = postgres.AddDatabase("data"); +var cache = builder.AddRedis("cache"); + var api = builder.AddProject("api") .WithReference(db) - .WaitFor(db); + .WaitFor(db) + .WithReference(cache) + .WaitFor(cache); builder.AddProject("web") .WithReference(api) .WaitFor(api) .WithExternalHttpEndpoints(); +builder.AddContainer("cdn", "nginx") + .WithVolume("portfolio-cdn", "/usr/share/nginx/html", true) + .WithHttpEndpoint(80, 80); + builder.Build().Run(); \ No newline at end of file diff --git a/src/Portfolio.Shared/Services/ITimelineRepository.cs b/src/Portfolio.Shared/Services/ITimelineRepository.cs index 561f075..2c9ae1c 100644 --- a/src/Portfolio.Shared/Services/ITimelineRepository.cs +++ b/src/Portfolio.Shared/Services/ITimelineRepository.cs @@ -6,4 +6,6 @@ public interface ITimelineRepository { Task> GetTimeline(TimelineEntryType type, CancellationToken ct); + Task> GetFeaturedTimeline(CancellationToken ct); + } \ No newline at end of file diff --git a/src/Portfolio.Web/Components/Pages/Home.razor b/src/Portfolio.Web/Components/Pages/Home.razor index 67ba4bc..6bc41ca 100644 --- a/src/Portfolio.Web/Components/Pages/Home.razor +++ b/src/Portfolio.Web/Components/Pages/Home.razor @@ -104,12 +104,7 @@ var technologies = await TechnologyRepository.GetTechnologies(TokenSource.Token); _technologies = technologies.Where(t => t.Featured); - var carrierTimeline = await TimelineRepository.GetTimeline(TimelineEntryType.Carrier, 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); + _timeline = await TimelineRepository.GetFeaturedTimeline(TokenSource.Token); } } diff --git a/src/Portfolio.Web/Services/TimelineRepository.cs b/src/Portfolio.Web/Services/TimelineRepository.cs index 0057e61..1e178c1 100644 --- a/src/Portfolio.Web/Services/TimelineRepository.cs +++ b/src/Portfolio.Web/Services/TimelineRepository.cs @@ -12,4 +12,13 @@ internal sealed class TimelineRepository(IHttpClientFactory factory) : ITimeline var data = await result.Content.ReadFromJsonAsync>(ct); return data ?? []; } + + public async Task> 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>(ct); + return data ?? []; + } } \ No newline at end of file