From 63b0992343e4eda67e3d6a44ef2d1060d325d5ac Mon Sep 17 00:00:00 2001 From: Natalia Portillo Date: Sun, 7 Jun 2026 13:38:16 +0100 Subject: [PATCH] feat: implement SitemapController for lightweight ID retrieval for sitemap generation --- .../Controllers/SitemapController.cs | 195 ++++++++++++++++++ Marechai/Services/SitemapService.cs | 15 +- 2 files changed, 201 insertions(+), 9 deletions(-) create mode 100644 Marechai.Server/Controllers/SitemapController.cs diff --git a/Marechai.Server/Controllers/SitemapController.cs b/Marechai.Server/Controllers/SitemapController.cs new file mode 100644 index 00000000..e1b55ae1 --- /dev/null +++ b/Marechai.Server/Controllers/SitemapController.cs @@ -0,0 +1,195 @@ +/****************************************************************************** +// MARECHAI: Master repository of computing history artifacts information +// ---------------------------------------------------------------------------- +// +// Author(s) : Natalia Portillo +// +// --[ License ] -------------------------------------------------------------- +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2003-2026 Natalia Portillo +*******************************************************************************/ + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Marechai.Database.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Marechai.Server.Controllers; + +/// +/// Lightweight endpoint that returns only entity IDs for sitemap generation. +/// Avoids the overhead of full DTO projections, joins, and serialization. +/// +[Route("/sitemap")] +[ApiController] +public class SitemapController(MarechaiContext context) : ControllerBase +{ + [HttpGet("machines/ids")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task> GetMachineIdsAsync([FromQuery] int? skip = null, + [FromQuery] int? take = null, + CancellationToken ct = default) + { + IQueryable query = context.Machines.AsNoTracking().Select(m => m.Id).OrderBy(id => id); + + if(skip.HasValue) query = query.Skip(skip.Value); + if(take.HasValue) query = query.Take(take.Value); + + return query.ToListAsync(ct); + } + + [HttpGet("companies/ids")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task> GetCompanyIdsAsync([FromQuery] int? skip = null, + [FromQuery] int? take = null, + CancellationToken ct = default) + { + IQueryable query = context.Companies.AsNoTracking().Select(c => c.Id).OrderBy(id => id); + + if(skip.HasValue) query = query.Skip(skip.Value); + if(take.HasValue) query = query.Take(take.Value); + + return query.ToListAsync(ct); + } + + [HttpGet("software/ids")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task> GetSoftwareIdsAsync([FromQuery] int? skip = null, + [FromQuery] int? take = null, + CancellationToken ct = default) + { + IQueryable query = context.Softwares.AsNoTracking().Select(s => s.Id).OrderBy(id => id); + + if(skip.HasValue) query = query.Skip(skip.Value); + if(take.HasValue) query = query.Take(take.Value); + + return query.ToListAsync(ct); + } + + [HttpGet("books/ids")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task> GetBookIdsAsync([FromQuery] int? skip = null, + [FromQuery] int? take = null, + CancellationToken ct = default) + { + IQueryable query = context.Books.AsNoTracking().Select(b => b.Id).OrderBy(id => id); + + if(skip.HasValue) query = query.Skip(skip.Value); + if(take.HasValue) query = query.Take(take.Value); + + return query.ToListAsync(ct); + } + + [HttpGet("documents/ids")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task> GetDocumentIdsAsync([FromQuery] int? skip = null, + [FromQuery] int? take = null, + CancellationToken ct = default) + { + IQueryable query = context.Documents.AsNoTracking().Select(d => d.Id).OrderBy(id => id); + + if(skip.HasValue) query = query.Skip(skip.Value); + if(take.HasValue) query = query.Take(take.Value); + + return query.ToListAsync(ct); + } + + [HttpGet("magazines/ids")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task> GetMagazineIdsAsync([FromQuery] int? skip = null, + [FromQuery] int? take = null, + CancellationToken ct = default) + { + IQueryable query = context.Magazines.AsNoTracking().Select(m => m.Id).OrderBy(id => id); + + if(skip.HasValue) query = query.Skip(skip.Value); + if(take.HasValue) query = query.Take(take.Value); + + return query.ToListAsync(ct); + } + + [HttpGet("gpus/ids")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task> GetGpuIdsAsync([FromQuery] int? skip = null, + [FromQuery] int? take = null, + CancellationToken ct = default) + { + IQueryable query = context.Gpus.AsNoTracking().Select(g => g.Id).OrderBy(id => id); + + if(skip.HasValue) query = query.Skip(skip.Value); + if(take.HasValue) query = query.Take(take.Value); + + return query.ToListAsync(ct); + } + + [HttpGet("sound-synths/ids")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task> GetSoundSynthIdsAsync([FromQuery] int? skip = null, + [FromQuery] int? take = null, + CancellationToken ct = default) + { + IQueryable query = context.SoundSynths.AsNoTracking().Select(s => s.Id).OrderBy(id => id); + + if(skip.HasValue) query = query.Skip(skip.Value); + if(take.HasValue) query = query.Take(take.Value); + + return query.ToListAsync(ct); + } + + [HttpGet("processors/ids")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task> GetProcessorIdsAsync([FromQuery] int? skip = null, + [FromQuery] int? take = null, + CancellationToken ct = default) + { + IQueryable query = context.Processors.AsNoTracking().Select(p => p.Id).OrderBy(id => id); + + if(skip.HasValue) query = query.Skip(skip.Value); + if(take.HasValue) query = query.Take(take.Value); + + return query.ToListAsync(ct); + } + + [HttpGet("people/ids")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + public Task> GetPersonIdsAsync([FromQuery] int? skip = null, + [FromQuery] int? take = null, + CancellationToken ct = default) + { + IQueryable query = context.People.AsNoTracking().Select(p => p.Id).OrderBy(id => id); + + if(skip.HasValue) query = query.Skip(skip.Value); + if(take.HasValue) query = query.Take(take.Value); + + return query.ToListAsync(ct); + } +} diff --git a/Marechai/Services/SitemapService.cs b/Marechai/Services/SitemapService.cs index 912cd062..f02e3b4e 100644 --- a/Marechai/Services/SitemapService.cs +++ b/Marechai/Services/SitemapService.cs @@ -250,18 +250,18 @@ public sealed class SitemapService(IHttpClientFactory httpClientFactory, IMemory int skip = (page - 1) * UrlsPerSitemap; - var entities = await client.GetFromJsonAsync>( - $"/{apiEndpoint}?skip={skip}&take={UrlsPerSitemap}"); + var ids = await client.GetFromJsonAsync>( + $"/sitemap/{apiEndpoint}/ids?skip={skip}&take={UrlsPerSitemap}"); - if(entities is not null) + if(ids is not null) { - foreach(SitemapEntity entity in entities) - urls.Add($"{urlPrefix}{entity.Id}"); + foreach(long id in ids) + urls.Add($"{urlPrefix}{id}"); } } catch(Exception ex) { - logger.LogWarning(ex, "Failed to fetch entities from /{Endpoint} page {Page} for sitemap", + logger.LogWarning(ex, "Failed to fetch IDs from /sitemap/{Endpoint}/ids page {Page} for sitemap", apiEndpoint, page); } @@ -332,7 +332,4 @@ public sealed class SitemapService(IHttpClientFactory httpClientFactory, IMemory Indent = true, OmitXmlDeclaration = false }; - - /// Minimal DTO for deserializing only the id field from the API response. - sealed record SitemapEntity(long Id); }