From f9bf4fa919bd62fa6bfcdbe9a759a15291443653 Mon Sep 17 00:00:00 2001 From: niclastimle Date: Fri, 30 May 2025 00:47:47 +0200 Subject: [PATCH 1/6] memory-cashing --- Controller/ArtistController.cs | 11 -- Controller/ArtistInfoController.cs | 21 ++++ ...bumController.cs => CoverArtController.cs} | 6 +- Data/DbInitializer.cs | 80 ++++++++++++++ Endpoints/AlbumEndPoint.cs | 11 -- Endpoints/ArtistEndPoint.cs | 3 - Endpoints/CoverArtEndPoint.cs | 11 ++ Endpoints/IAlbumEndPoint.cs | 8 -- Endpoints/IArtistEndPoint.cs | 1 - Endpoints/ICoverArtEndPoint.cs | 8 ++ Handler/AlbumHandler.cs | 13 --- Handler/ArtistHandler.cs | 35 ++++-- Handler/ArtistInfoHandler.cs | 102 ++++++++++++++++++ Handler/CoverArtHandler.cs | 36 +++++++ HollyJukeBox.csproj | 7 +- Models/AlbumDto.cs | 7 -- Models/AlbumInfo.cs | 11 ++ Models/ArtistDto.cs | 25 +++-- Models/ArtistInfo.cs | 9 ++ Models/ArtistsDto.cs | 10 +- Models/CoverArtDto.cs | 11 ++ Models/ImageTypes.cs | 6 ++ Models/KeyTypes.cs | 8 ++ Models/WikiDataDto.cs | 17 +-- Models/WikipediaSummaryDto.cs | 10 +- Program.cs | 24 ++++- Querys/AlbumQuery.cs | 9 -- Querys/ArtistInfoQuery.cs | 9 ++ Querys/ArtistQuery.cs | 3 +- Querys/CoverArtQuery.cs | 9 ++ Repository/AlbumInfoRepository.cs | 40 +++++++ Repository/ArtistInfoRepository.cs | 33 ++++++ Repository/ArtistRepository.cs | 82 ++++++++++++++ Repository/CoverArtRepository.cs | 46 ++++++++ Repository/IAlbumInfoRepository.cs | 9 ++ Repository/IArtistInfoRepository.cs | 9 ++ Repository/IArtistRepository.cs | 9 ++ Repository/ICoverArtRepository.cs | 9 ++ Services/IMemoryCashingService.cs | 7 ++ Services/MemoryCashingService.cs | 16 +++ appsettings.json | 3 + 41 files changed, 667 insertions(+), 117 deletions(-) create mode 100644 Controller/ArtistInfoController.cs rename Controller/{AlbumController.cs => CoverArtController.cs} (74%) create mode 100644 Data/DbInitializer.cs delete mode 100644 Endpoints/AlbumEndPoint.cs create mode 100644 Endpoints/CoverArtEndPoint.cs delete mode 100644 Endpoints/IAlbumEndPoint.cs create mode 100644 Endpoints/ICoverArtEndPoint.cs delete mode 100644 Handler/AlbumHandler.cs create mode 100644 Handler/ArtistInfoHandler.cs create mode 100644 Handler/CoverArtHandler.cs delete mode 100644 Models/AlbumDto.cs create mode 100644 Models/AlbumInfo.cs create mode 100644 Models/ArtistInfo.cs create mode 100644 Models/CoverArtDto.cs create mode 100644 Models/ImageTypes.cs create mode 100644 Models/KeyTypes.cs delete mode 100644 Querys/AlbumQuery.cs create mode 100644 Querys/ArtistInfoQuery.cs create mode 100644 Querys/CoverArtQuery.cs create mode 100644 Repository/AlbumInfoRepository.cs create mode 100644 Repository/ArtistInfoRepository.cs create mode 100644 Repository/ArtistRepository.cs create mode 100644 Repository/CoverArtRepository.cs create mode 100644 Repository/IAlbumInfoRepository.cs create mode 100644 Repository/IArtistInfoRepository.cs create mode 100644 Repository/IArtistRepository.cs create mode 100644 Repository/ICoverArtRepository.cs create mode 100644 Services/IMemoryCashingService.cs create mode 100644 Services/MemoryCashingService.cs diff --git a/Controller/ArtistController.cs b/Controller/ArtistController.cs index 28e7db7..bdfe658 100644 --- a/Controller/ArtistController.cs +++ b/Controller/ArtistController.cs @@ -22,17 +22,6 @@ public async Task GetById([FromQuery] string? id) } return BadRequest("Provide either id for the artist"); } - - [HttpGet("name")] - public async Task GetByName([FromQuery] string? name) - { - if (!string.IsNullOrWhiteSpace(name)) - { - var result = await mediator.Send(new ArtistQuery.GetByName(name)); - return result is null ? NotFound() : Ok(result); - } - return BadRequest("Provide either name for the artist"); - } [HttpGet("wikidata")] public async Task GetWikidataById([FromQuery] string? id) diff --git a/Controller/ArtistInfoController.cs b/Controller/ArtistInfoController.cs new file mode 100644 index 0000000..244061b --- /dev/null +++ b/Controller/ArtistInfoController.cs @@ -0,0 +1,21 @@ +using HollyJukeBox.QueryModels; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace HollyJukeBox.Controller; + +[ApiController] +[Route("api/artistinfo")] +public class ArtistInfoController(IMediator mediator) : ControllerBase +{ + [HttpGet] + public async Task GetById([FromQuery] string? id) + { + if (!string.IsNullOrWhiteSpace(id)) + { + var result = await mediator.Send(new ArtistInfoQuery.GetById(id)); + return result is null ? NotFound() : Ok(result); + } + return BadRequest("Provide either id for the artist"); + } +} \ No newline at end of file diff --git a/Controller/AlbumController.cs b/Controller/CoverArtController.cs similarity index 74% rename from Controller/AlbumController.cs rename to Controller/CoverArtController.cs index ca46ccf..627e0da 100644 --- a/Controller/AlbumController.cs +++ b/Controller/CoverArtController.cs @@ -8,15 +8,15 @@ namespace HollyJukeBox.Controller; /// Handles album-related data access /// [ApiController] -[Route("api/album")] -public class AlbumController(IMediator mediator) : ControllerBase +[Route("api/coverart")] +public class CoverArtController(IMediator mediator) : ControllerBase { [HttpGet] public async Task GetById([FromQuery] string? id) { if (!string.IsNullOrWhiteSpace(id)) { - var result = await mediator.Send(new AlbumQuery.GetById(id)); + var result = await mediator.Send(new CoverArtQuery.GetById(id)); return result is null ? NotFound() : Ok(result); } diff --git a/Data/DbInitializer.cs b/Data/DbInitializer.cs new file mode 100644 index 0000000..0ddca17 --- /dev/null +++ b/Data/DbInitializer.cs @@ -0,0 +1,80 @@ +using Dapper; +using Microsoft.Data.Sqlite; + +namespace HollyJukeBox.Data; + +public class DbInitializer +{ + private SqliteConnection connection; + + public DbInitializer(IConfiguration configuration) + { + connection = new SqliteConnection(configuration.GetConnectionString("HollyJukeBoxDb")); + } + + public async Task EnsureTablesCreatedAsync() + { + await connection.ExecuteAsync(@" + CREATE TABLE IF NOT EXISTS Artist ( + Id TEXT PRIMARY KEY, + Name TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS Releases ( + Id TEXT PRIMARY KEY, + Title TEXT NOT NULL, + FirstReleaseDate TEXT, + ArtistId TEXT NOT NULL, + FOREIGN KEY (ArtistId) REFERENCES Artist(Id) + ); + + CREATE TABLE IF NOT EXISTS Relations ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + ArtistId TEXT NOT NULL, + Type TEXT NOT NULL, + UrlId TEXT NOT NULL, + FOREIGN KEY (ArtistId) REFERENCES Artist(Id), + FOREIGN KEY (UrlId) REFERENCES Urls(Id) + ); + + CREATE TABLE IF NOT EXISTS Urls ( + Id TEXT PRIMARY KEY, + Resource TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS CoverArt ( + Id TEXT PRIMARY KEY + ); + + CREATE TABLE IF NOT EXISTS Images ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + CoverArtId TEXT NOT NULL, + Image TEXT NOT NULL, + FOREIGN KEY (CoverArtId) REFERENCES CoverArt(Id) + ); + + CREATE TABLE IF NOT EXISTS ImageTypes ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + ImageId INTEGER NOT NULL, + Type TEXT NOT NULL, + FOREIGN KEY (ImageId) REFERENCES Images(Id) + ); + + CREATE TABLE IF NOT EXISTS ArtistInfo ( + Id TEXT PRIMARY KEY, + Name TEXT NOT NULL, + Description TEXT + ); + + CREATE TABLE IF NOT EXISTS AlbumInfo ( + Id TEXT PRIMARY KEY, + Title TEXT NOT NULL, + FirstReleaseDate TEXT, + ImageFront TEXT, + ImageBack TEXT, + ArtistInfoId TEXT NOT NULL, + FOREIGN KEY (ArtistInfoId) REFERENCES ArtistInfo(Id) + ); + "); + } +} \ No newline at end of file diff --git a/Endpoints/AlbumEndPoint.cs b/Endpoints/AlbumEndPoint.cs deleted file mode 100644 index da8b167..0000000 --- a/Endpoints/AlbumEndPoint.cs +++ /dev/null @@ -1,11 +0,0 @@ -using HollyJukeBox.Models; -using HollyJukeBox.Services; -using Microsoft.Extensions.Options; - -namespace HollyJukeBox.Endpoints; - -public class AlbumEndPoint(IOptions options, HttpClient client) : IAlbumEndPoint -{ - public async Task GetById(string id) => - await client.GetFromJsonAsync(options.Value.CoverArtArchiveUrl + $"release-group/{id}"); -} \ No newline at end of file diff --git a/Endpoints/ArtistEndPoint.cs b/Endpoints/ArtistEndPoint.cs index 00e3da6..8471f8c 100644 --- a/Endpoints/ArtistEndPoint.cs +++ b/Endpoints/ArtistEndPoint.cs @@ -9,9 +9,6 @@ public class ArtistEndPoint(IOptions options, HttpClient client) : public async Task GetById(string id) => await client.GetFromJsonAsync( options.Value.MusicBrainzUrl + $"artist/{id}?inc=release-groups+url-rels&fmt=json"); - public async Task GetByName(string name) => await client.GetFromJsonAsync( - options.Value.MusicBrainzUrl + $"artist?query={name}&fmt=json"); - public async Task GetWikiData(string id) => await client.GetFromJsonAsync( options.Value.WikiDataUrl + $"?action=wbgetentities&ids={id}&format=json&props=sitelinks"); diff --git a/Endpoints/CoverArtEndPoint.cs b/Endpoints/CoverArtEndPoint.cs new file mode 100644 index 0000000..8ef2875 --- /dev/null +++ b/Endpoints/CoverArtEndPoint.cs @@ -0,0 +1,11 @@ +using HollyJukeBox.Models; +using HollyJukeBox.Services; +using Microsoft.Extensions.Options; + +namespace HollyJukeBox.Endpoints; + +public class CoverArtEndPoint(IOptions options, HttpClient client) : ICoverArtEndPoint +{ + public async Task GetById(string id) => + await client.GetFromJsonAsync(options.Value.CoverArtArchiveUrl + $"release-group/{id}"); +} \ No newline at end of file diff --git a/Endpoints/IAlbumEndPoint.cs b/Endpoints/IAlbumEndPoint.cs deleted file mode 100644 index 8ebb51c..0000000 --- a/Endpoints/IAlbumEndPoint.cs +++ /dev/null @@ -1,8 +0,0 @@ -using HollyJukeBox.Models; - -namespace HollyJukeBox.Endpoints; - -public interface IAlbumEndPoint -{ - public Task GetById(string id); -} \ No newline at end of file diff --git a/Endpoints/IArtistEndPoint.cs b/Endpoints/IArtistEndPoint.cs index 7d3e1bb..7f469bc 100644 --- a/Endpoints/IArtistEndPoint.cs +++ b/Endpoints/IArtistEndPoint.cs @@ -5,7 +5,6 @@ namespace HollyJukeBox.Endpoints; public interface IArtistEndPoint { public Task GetById(string id); - public Task GetByName(string name); public Task GetWikiData(string id); public Task GetWikipediaSummary(string enwikiTitle); } \ No newline at end of file diff --git a/Endpoints/ICoverArtEndPoint.cs b/Endpoints/ICoverArtEndPoint.cs new file mode 100644 index 0000000..70299be --- /dev/null +++ b/Endpoints/ICoverArtEndPoint.cs @@ -0,0 +1,8 @@ +using HollyJukeBox.Models; + +namespace HollyJukeBox.Endpoints; + +public interface ICoverArtEndPoint +{ + public Task GetById(string id); +} \ No newline at end of file diff --git a/Handler/AlbumHandler.cs b/Handler/AlbumHandler.cs deleted file mode 100644 index 1ff6791..0000000 --- a/Handler/AlbumHandler.cs +++ /dev/null @@ -1,13 +0,0 @@ -using HollyJukeBox.Endpoints; -using HollyJukeBox.Models; -using HollyJukeBox.QueryModels; -using MediatR; - -namespace HollyJukeBox.Handler; - -public class AlbumHandler(IAlbumEndPoint albumEndPoint) : - IRequestHandler -{ - public async Task Handle(AlbumQuery.GetById query, CancellationToken cancellationToken) - => await albumEndPoint.GetById(query.Id); -} \ No newline at end of file diff --git a/Handler/ArtistHandler.cs b/Handler/ArtistHandler.cs index 3018e61..f2161d0 100644 --- a/Handler/ArtistHandler.cs +++ b/Handler/ArtistHandler.cs @@ -1,25 +1,40 @@ using HollyJukeBox.Endpoints; using HollyJukeBox.Models; using HollyJukeBox.QueryModels; +using HollyJukeBox.Repository; +using HollyJukeBox.Services; using MediatR; namespace HollyJukeBox.Handler; -public class ArtistHandler(IArtistEndPoint artistEndPoint) : +public class ArtistHandler( + IArtistEndPoint artistEndPoint, + IMemoryCashingService memoryCashingService, + IArtistRepository artistRepository) : IRequestHandler, - IRequestHandler, IRequestHandler, IRequestHandler { - public async Task Handle(ArtistQuery.GetById query, CancellationToken cancellationToken) - => await artistEndPoint.GetById(query.Id); + public async Task Handle(ArtistQuery.GetById request, CancellationToken cancellationToken) + { + var artist = memoryCashingService.Get(request.Id); + + if (artist is not null) + { + return artist; + } - public async Task Handle(ArtistQuery.GetByName query, CancellationToken cancellationToken) - { - var result = await artistEndPoint.GetByName(query.Name); - var artist = result.Artists.First(x => string.Equals(x.Name, query.Name, StringComparison.OrdinalIgnoreCase)); - artist = await artistEndPoint.GetById(artist.Id); - return artist; + artist = await artistRepository.GetByIdAsync(request.Id); + if (artist is not null) + { + memoryCashingService.Store(request.Id, artist); + return artist; + } + + artist = await artistEndPoint.GetById(request.Id); + memoryCashingService.Store(request.Id, artist); + await artistRepository.SaveAsync(artist); + return artist; } public async Task Handle(ArtistQuery.GetWikiData request, CancellationToken cancellationToken) diff --git a/Handler/ArtistInfoHandler.cs b/Handler/ArtistInfoHandler.cs new file mode 100644 index 0000000..01496f3 --- /dev/null +++ b/Handler/ArtistInfoHandler.cs @@ -0,0 +1,102 @@ +using HollyJukeBox.Endpoints; +using HollyJukeBox.Models; +using HollyJukeBox.QueryModels; +using HollyJukeBox.Repository; +using HollyJukeBox.Services; +using MediatR; + +namespace HollyJukeBox.Handler; + +public class ArtistInfoHandler( + IArtistEndPoint artistEndPoint, + IMemoryCashingService memoryCashingService, + IArtistRepository artistRepository, + IArtistInfoRepository artistInfoRepository, + IAlbumInfoRepository albumInfoRepository, + ICoverArtRepository coverArtRepository) + : IRequestHandler +{ + public async Task Handle(ArtistInfoQuery.GetById request, CancellationToken cancellationToken) + { + var artist = memoryCashingService.Get($"artist:{request.Id}"); + if (artist is not null) + { + return artist; + } + + artist = await artistInfoRepository.GetByIdAsync(request.Id); + if (artist is not null) + { + IEnumerable albumList = null; + if(artist.Albums is null && artist.Albums.Count == 0) + { + albumList = await albumInfoRepository.GetByArtistIdAsync(artist.Mbid); + } + if(artist.Albums is not null && artist.Albums.Count > 0) + { + artist.Albums = albumList.ToList(); + memoryCashingService.Store(request.Id, artist); + return artist; + } + } + + var summary = string.Empty; + var artistDto = memoryCashingService.Get(request.Id); + if (artistDto is null) + { + artistDto = await artistRepository.GetByIdAsync(request.Id); + if (artistDto is null) + { + artistDto = await artistEndPoint.GetById(request.Id); + var wikiSummaryId = string.Empty; + var relation = artistDto.Relations + .Find(x => Enum.TryParse(x.type, true, out var keyType) && keyType == KeyTypes.wikipedia); + if(relation is not null)//if relation has wikipedia + { + wikiSummaryId = relation.url.resource.Split('/').Last(); + } + else + { + relation = artistDto.Relations + .Find(x => Enum.TryParse(x.type, true, out var keyType) && keyType == KeyTypes.wikidata); + var wikiId = relation.url.resource.Split('/').Last(); + var wikiData = await artistEndPoint.GetWikiData(wikiId); + var sitelinks = wikiData.Entities.First().Value.Sitelinks; + wikiSummaryId = sitelinks.First(x => + Enum.TryParse(x.Key, true, out var keyType) && keyType == KeyTypes.enwiki).Value.Title; + } + + var wikiSummary = await artistEndPoint.GetWikipediaSummary(wikiSummaryId); + summary = wikiSummary.Extract; + } + } + var albums = new List(); + + foreach (var release in artistDto.ReleaseGroups) + { + //var coverArts = await coverArtRepository.GetByIdAsync(release.Id); + //var images = coverArts.Images; + albums.Add(new AlbumInfo + { + Id = release.Id, + ArtistInfoId = artistDto.Id, + Title = release.Title, + FirstReleaseDate = release.FirstReleaseDate, + //ImageFront = images.First(x => x.Types.Equals(ImageTypes.Front)).Image, + //ImageBack = images.First(x => x.Types.Equals(ImageTypes.Back)).Image + }); + } + artist = new ArtistInfo + { + Mbid = artistDto.Id, + Artist = artistDto.Name, + Description = summary, + Albums = albums + }; + + memoryCashingService.Store(request.Id, artist); + await artistInfoRepository.SaveAsync(artist); + await albumInfoRepository.SaveAsync(artist.Albums); + return artist; + } +} \ No newline at end of file diff --git a/Handler/CoverArtHandler.cs b/Handler/CoverArtHandler.cs new file mode 100644 index 0000000..2d2a00b --- /dev/null +++ b/Handler/CoverArtHandler.cs @@ -0,0 +1,36 @@ +using HollyJukeBox.Endpoints; +using HollyJukeBox.Models; +using HollyJukeBox.QueryModels; +using HollyJukeBox.Repository; +using HollyJukeBox.Services; +using MediatR; + +namespace HollyJukeBox.Handler; + +public class CoverArtHandler( + ICoverArtEndPoint coverArtEndPoint, + IMemoryCashingService memoryCashingService, + ICoverArtRepository coverArtRepository) : + IRequestHandler +{ + public async Task Handle(CoverArtQuery.GetById request, CancellationToken cancellationToken) + { + var coverArt = memoryCashingService.Get(request.Id); + if (coverArt is not null) + { + return coverArt; + } + + coverArt = await coverArtRepository.GetByIdAsync(request.Id); + if (coverArt is not null) + { + memoryCashingService.Store(request.Id, coverArt); + return coverArt; + } + + coverArt = await coverArtEndPoint.GetById(request.Id); + memoryCashingService.Store(request.Id, coverArt); + await coverArtRepository.SaveAsync(coverArt); + return coverArt; + } +} \ No newline at end of file diff --git a/HollyJukeBox.csproj b/HollyJukeBox.csproj index f862219..06f9d7f 100644 --- a/HollyJukeBox.csproj +++ b/HollyJukeBox.csproj @@ -7,9 +7,12 @@ + + - - + + + diff --git a/Models/AlbumDto.cs b/Models/AlbumDto.cs deleted file mode 100644 index 17e4b87..0000000 --- a/Models/AlbumDto.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace HollyJukeBox.Models; - -public class AlbumDto -{ - public List images { get; set; } -} -public record Images(string[] types, string image); \ No newline at end of file diff --git a/Models/AlbumInfo.cs b/Models/AlbumInfo.cs new file mode 100644 index 0000000..0bf9b4f --- /dev/null +++ b/Models/AlbumInfo.cs @@ -0,0 +1,11 @@ +namespace HollyJukeBox.Models; + +public class AlbumInfo +{ + public string Id { get; set; } + public string ArtistInfoId { get; set; } + public string Title { get; set; } + public string FirstReleaseDate { get; set; } + public string ImageFront { get; set; } + public string ImageBack { get; set; } +} \ No newline at end of file diff --git a/Models/ArtistDto.cs b/Models/ArtistDto.cs index ff6ba09..7f5a36c 100644 --- a/Models/ArtistDto.cs +++ b/Models/ArtistDto.cs @@ -1,25 +1,28 @@ using System.Text.Json.Serialization; - namespace HollyJukeBox.Models; public class ArtistDto { - [JsonPropertyName("id")] + [JsonPropertyName("id")] public string Id { get; set; } - [JsonPropertyName("name")] + [JsonPropertyName("name")] public string Name { get; set; } - - [JsonPropertyName("release-groups")] - public List ReleasesGroups { get; set; } + [JsonPropertyName("release-groups")] + public List ReleaseGroups { get; set; } = new(); [JsonPropertyName("relations")] - public List Relations { get; set; } + public List Relations { get; set; } = new(); } - -public record ReleasesGroups( +public record Relations( + [property: JsonPropertyName("type")]string type, + [property: JsonPropertyName("url")]Url url +); +public record ReleaseGroup( [property: JsonPropertyName("id")] string Id, [property: JsonPropertyName("title")] string Title, [property: JsonPropertyName("first-release-date")] string FirstReleaseDate ); -public record Relations(string type, Url url); -public record Url(string id, string resource); \ No newline at end of file +public record Url( + [property: JsonPropertyName("id")]string id, + [property: JsonPropertyName("resource")]string resource +); \ No newline at end of file diff --git a/Models/ArtistInfo.cs b/Models/ArtistInfo.cs new file mode 100644 index 0000000..9f952e8 --- /dev/null +++ b/Models/ArtistInfo.cs @@ -0,0 +1,9 @@ +namespace HollyJukeBox.Models; + +public class ArtistInfo +{ + public string Mbid { get; set; } + public string Artist { get; set; } + public List Albums { get; set; } + public string Description { get; set; } +} \ No newline at end of file diff --git a/Models/ArtistsDto.cs b/Models/ArtistsDto.cs index 837c3d0..eb3c0b4 100644 --- a/Models/ArtistsDto.cs +++ b/Models/ArtistsDto.cs @@ -1,9 +1,5 @@ using System.Text.Json.Serialization; - namespace HollyJukeBox.Models; - -public class ArtistsDto -{ - [JsonPropertyName("artists")] - public List Artists { get; set; } -} \ No newline at end of file +public record ArtistsDto( + [property: JsonPropertyName("artists")] List Artists +); \ No newline at end of file diff --git a/Models/CoverArtDto.cs b/Models/CoverArtDto.cs new file mode 100644 index 0000000..3bd48cb --- /dev/null +++ b/Models/CoverArtDto.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace HollyJukeBox.Models; +public record CoverArtDto( + string Id, + [property:JsonPropertyName("images")] List Images +); +public record Images( + [property: JsonPropertyName("types")] string[] Types, + [property: JsonPropertyName("image")] string Image +); diff --git a/Models/ImageTypes.cs b/Models/ImageTypes.cs new file mode 100644 index 0000000..f22b73d --- /dev/null +++ b/Models/ImageTypes.cs @@ -0,0 +1,6 @@ +namespace HollyJukeBox.Models; +public enum ImageTypes +{ + Front, + Back +} \ No newline at end of file diff --git a/Models/KeyTypes.cs b/Models/KeyTypes.cs new file mode 100644 index 0000000..0d206df --- /dev/null +++ b/Models/KeyTypes.cs @@ -0,0 +1,8 @@ +namespace HollyJukeBox.Models; + +public enum KeyTypes +{ + enwiki, + wikidata, + wikipedia +} \ No newline at end of file diff --git a/Models/WikiDataDto.cs b/Models/WikiDataDto.cs index c1bfbc5..abcef49 100644 --- a/Models/WikiDataDto.cs +++ b/Models/WikiDataDto.cs @@ -1,21 +1,14 @@ using System.Text.Json.Serialization; - namespace HollyJukeBox.Models; -public class WikiDataDto -{ - [JsonPropertyName("entities")] - public Dictionary Entities { get; set; } -} +public record WikiDataDto( + [property:JsonPropertyName("entities")] Dictionary Entities +); public record Entity( [property: JsonPropertyName("id")] string Id, [property: JsonPropertyName("sitelinks")] Dictionary Sitelinks ); public record SiteLink( - [property: JsonPropertyName("site")] string site, - [property: JsonPropertyName("title")] string title -); - -public record Enwiki( - [property: JsonPropertyName("site")] string enwiki + [property: JsonPropertyName("site")] string Site, + [property: JsonPropertyName("title")] string Title ); \ No newline at end of file diff --git a/Models/WikipediaSummaryDto.cs b/Models/WikipediaSummaryDto.cs index 4c715f7..d4b4233 100644 --- a/Models/WikipediaSummaryDto.cs +++ b/Models/WikipediaSummaryDto.cs @@ -2,8 +2,8 @@ namespace HollyJukeBox.Models; -public class WikipediaSummaryDto -{ - [JsonPropertyName("extract")] - public string Extract { get; set; } -} \ No newline at end of file +public record WikipediaSummaryDto +( + [property: JsonPropertyName("extract")] string Extract, + string ArtsistId +); \ No newline at end of file diff --git a/Program.cs b/Program.cs index 1c1915e..e00d76e 100644 --- a/Program.cs +++ b/Program.cs @@ -1,10 +1,24 @@ +using HollyJukeBox.Data; using HollyJukeBox.Endpoints; +using HollyJukeBox.Repository; using HollyJukeBox.Services; +using Microsoft.Data.Sqlite; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +builder.Services.AddMemoryCache(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(sp => + new SqliteConnection(builder.Configuration.GetConnectionString("HollyJukeBoxDb"))); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddSingleton(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -22,6 +36,12 @@ var app = builder.Build(); +using (var scope = app.Services.CreateScope()) +{ + var dbInit = scope.ServiceProvider.GetRequiredService(); + await dbInit.EnsureTablesCreatedAsync(); +} + app.UseSwagger(); app.UseSwaggerUI(); app.MapControllers(); diff --git a/Querys/AlbumQuery.cs b/Querys/AlbumQuery.cs deleted file mode 100644 index eb2fe78..0000000 --- a/Querys/AlbumQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -using HollyJukeBox.Models; -using MediatR; - -namespace HollyJukeBox.QueryModels; - -public class AlbumQuery -{ - public record GetById(string Id) : IRequest; -} \ No newline at end of file diff --git a/Querys/ArtistInfoQuery.cs b/Querys/ArtistInfoQuery.cs new file mode 100644 index 0000000..47b42ab --- /dev/null +++ b/Querys/ArtistInfoQuery.cs @@ -0,0 +1,9 @@ +using HollyJukeBox.Models; +using MediatR; + +namespace HollyJukeBox.QueryModels; + +public class ArtistInfoQuery +{ + public record GetById(string Id) : IRequest; +} \ No newline at end of file diff --git a/Querys/ArtistQuery.cs b/Querys/ArtistQuery.cs index 1de5680..c0329b5 100644 --- a/Querys/ArtistQuery.cs +++ b/Querys/ArtistQuery.cs @@ -3,10 +3,9 @@ namespace HollyJukeBox.QueryModels; -public class ArtistQuery : IRequest +public class ArtistQuery { public record GetById(string Id) : IRequest; - public record GetByName(string Name) : IRequest; public record GetWikiData(string id) : IRequest; public record GetWikipediaSummary(string enwikiTitle) : IRequest; } \ No newline at end of file diff --git a/Querys/CoverArtQuery.cs b/Querys/CoverArtQuery.cs new file mode 100644 index 0000000..868b5e3 --- /dev/null +++ b/Querys/CoverArtQuery.cs @@ -0,0 +1,9 @@ +using HollyJukeBox.Models; +using MediatR; + +namespace HollyJukeBox.QueryModels; + +public class CoverArtQuery +{ + public record GetById(string Id) : IRequest; +} \ No newline at end of file diff --git a/Repository/AlbumInfoRepository.cs b/Repository/AlbumInfoRepository.cs new file mode 100644 index 0000000..fb3097a --- /dev/null +++ b/Repository/AlbumInfoRepository.cs @@ -0,0 +1,40 @@ +using System.Data; +using Dapper; +using HollyJukeBox.Models; + +namespace HollyJukeBox.Repository; + +public class AlbumInfoRepository(IDbConnection connection) : IAlbumInfoRepository +{ + public async Task> GetByArtistIdAsync(string id) + { + var builder = new SqlBuilder(); + var template = builder.AddTemplate("SELECT * FROM AlbumInfo /**where**/"); + builder.Where("ArtistInfoId = @Id"); + return await connection.QueryAsync(template.RawSql, new { Id = id }); + } + + public async Task SaveAsync(List albums) + { + var rowsAffected = 0; + foreach (var album in albums) + { + var builder = new SqlBuilder(); + var artistInsert = + builder.AddTemplate("INSERT OR IGNORE INTO AlbumInfo (Id, Title,FirstReleaseDate, Description, Image, ArtistsInfoId) " + + "VALUES (@Id,ArtistsInfoId @Title, @FirstReleaseDate, @Description, @Image, @)"); + rowsAffected += await connection.ExecuteAsync(artistInsert.RawSql, + new + { + album.Id, + album.Title, + album.FirstReleaseDate, + album.ImageFront, + album.ImageBack, + album.ArtistInfoId + }); + } + + return rowsAffected; + } +} \ No newline at end of file diff --git a/Repository/ArtistInfoRepository.cs b/Repository/ArtistInfoRepository.cs new file mode 100644 index 0000000..fc0a2fb --- /dev/null +++ b/Repository/ArtistInfoRepository.cs @@ -0,0 +1,33 @@ +using System.Data; +using Dapper; +using HollyJukeBox.Models; + +namespace HollyJukeBox.Repository; + +public class ArtistInfoRepository(IDbConnection connection) : IArtistInfoRepository +{ + public async Task GetByIdAsync(string id) + { + var builder = new SqlBuilder(); + var template = builder.AddTemplate("SELECT * FROM ArtistInfo /**where**/ LIMIT 1"); + builder.Where("id = @Id", new { Id = id }); + return await connection.QueryFirstOrDefaultAsync(template.RawSql, + new { Id = id }); + } + + public async Task SaveAsync(ArtistInfo artist) + { + var builder = new SqlBuilder(); + var artistInsert = + builder.AddTemplate("INSERT OR IGNORE INTO ArtistInfo (Id, Name, Description) " + + "VALUES (@Id, @Name, @Description)"); + + return await connection.ExecuteAsync(artistInsert.RawSql, + new + { + artist.Mbid, + artist.Artist, + artist.Description + }); + } +} \ No newline at end of file diff --git a/Repository/ArtistRepository.cs b/Repository/ArtistRepository.cs new file mode 100644 index 0000000..144e437 --- /dev/null +++ b/Repository/ArtistRepository.cs @@ -0,0 +1,82 @@ +using System.Data; +using HollyJukeBox.Models; +using Dapper; + +namespace HollyJukeBox.Repository; + +public class ArtistRepository(IDbConnection connection) : IArtistRepository +{ + public async Task GetByIdAsync(string id) + { + var artist = await connection.QueryFirstOrDefaultAsync( + "SELECT Id, Name FROM Artist WHERE Id = @Id", new { Id = id }); + + if(artist is null) + { + return null; + } + + var releases = await connection.QueryAsync( + "SELECT Id, Title, FirstReleaseDate FROM Releases WHERE ArtistId = @Id", new { Id = id }); + + var rawData = await connection.QueryAsync<(string Type, string Id, string Resource)>( + @"SELECT rel.Type, u.Id, u.Resource + FROM Relations rel + JOIN Urls u ON u.Id = rel.UrlId + WHERE rel.ArtistId = @Id", new { Id = id }); + + var relations = rawData + .Select(r => new Relations(r.Type, new Url(r.Id, r.Resource))) + .ToList(); + + artist.ReleaseGroups = releases.ToList(); + artist.Relations = relations.ToList(); + return artist; + } + + public async Task SaveAsync(ArtistDto artist) + { + int affectedRows = 0; + + var builder = new SqlBuilder(); + var artistInsert = builder.AddTemplate("INSERT OR IGNORE INTO Artist (Id, Name) VALUES (@Id, @Name)"); + affectedRows += await connection.ExecuteAsync(artistInsert.RawSql, new { artist.Id, artist.Name }); + + foreach (var release in artist.ReleaseGroups) + { + var releaseBuilder = new SqlBuilder(); + var releaseInsert = releaseBuilder.AddTemplate( + "INSERT OR IGNORE INTO Releases (Id, Title, FirstReleaseDate, ArtistId) " + + "VALUES (@Id, @Title, @FirstReleaseDate, @ArtistId)"); + + affectedRows += await connection.ExecuteAsync(releaseInsert.RawSql, new + { + release.Id, + release.Title, + release.FirstReleaseDate, + ArtistId = artist.Id + }); + } + + foreach (var rel in artist.Relations) + { + var urlBuilder = new SqlBuilder(); + var insertUrl = urlBuilder.AddTemplate("INSERT OR IGNORE INTO Urls (Id, Resource) VALUES (@Id, @Resource)"); + affectedRows += await connection.ExecuteAsync(insertUrl.RawSql, new { Id = rel.url.id, Resource = rel.url.resource }); + + var relBuilder = new SqlBuilder(); + var insertRel = relBuilder.AddTemplate( + "INSERT OR IGNORE INTO Relations (ArtistId, Type, UrlId) VALUES (@ArtistId, @Type, @UrlId)"); + + affectedRows += await connection.ExecuteAsync(insertRel.RawSql, new + { + ArtistId = artist.Id, + Type = rel.type, + UrlId = rel.url.id + }); + } + + return affectedRows; + } + +} \ No newline at end of file diff --git a/Repository/CoverArtRepository.cs b/Repository/CoverArtRepository.cs new file mode 100644 index 0000000..612c437 --- /dev/null +++ b/Repository/CoverArtRepository.cs @@ -0,0 +1,46 @@ +using System.Data; +using HollyJukeBox.Models; +using Dapper; + +namespace HollyJukeBox.Repository; + +public class CoverArtRepository(IDbConnection connection) : ICoverArtRepository +{ + public async Task GetByIdAsync(string id) + { + var builder = new SqlBuilder(); + var template = builder.AddTemplate( + "SELECT c.* FROM CoverArt c /**join**/ /**leftjoin**/ /**where**/ LIMIT 1"); + builder.LeftJoin("Images i ON i.CoverArtId = c.Id"); + builder.LeftJoin("ImageTypes it ON it.ImageId = i.Id"); + builder.Where("c.Id = @Id", new { Id = id }); + + var covertArt = await connection.QueryFirstOrDefaultAsync(template.RawSql, template.Parameters); + return covertArt; + } + + public async Task SaveAsync(CoverArtDto coverArtDto) + { + int affectedRows = 0; + var builder = new SqlBuilder(); + var insertCoverArt = builder.AddTemplate("INSERT OR IGNORE INTO CoverArt (Id) VALUES (@Id)"); + affectedRows += await connection.ExecuteAsync(insertCoverArt.RawSql, new { Id = coverArtDto.Id }); + + foreach (var image in coverArtDto.Images) + { + var insertImage = "INSERT INTO Images (CoverArtId, Image) VALUES (@CoverArtId, @Image); SELECT last_insert_rowid();"; + var imageId = await connection.ExecuteScalarAsync(insertImage, new { CoverArtId = coverArtDto.Id, Image = image.Image }); + + foreach (var type in image.Types) + { + var builderType = new SqlBuilder(); + var insertType = builderType.AddTemplate("INSERT INTO ImageTypes (ImageId, Type) VALUES (@ImageId, @Type)"); + affectedRows += await connection.ExecuteAsync(insertType.RawSql, new { ImageId = imageId, Type = type }); + } + + affectedRows++; + } + + return affectedRows; + } +} \ No newline at end of file diff --git a/Repository/IAlbumInfoRepository.cs b/Repository/IAlbumInfoRepository.cs new file mode 100644 index 0000000..a288425 --- /dev/null +++ b/Repository/IAlbumInfoRepository.cs @@ -0,0 +1,9 @@ +using HollyJukeBox.Models; + +namespace HollyJukeBox.Repository; + +public interface IAlbumInfoRepository +{ + public Task> GetByArtistIdAsync(string id); + public Task SaveAsync(List album); +} \ No newline at end of file diff --git a/Repository/IArtistInfoRepository.cs b/Repository/IArtistInfoRepository.cs new file mode 100644 index 0000000..604579a --- /dev/null +++ b/Repository/IArtistInfoRepository.cs @@ -0,0 +1,9 @@ +using HollyJukeBox.Models; + +namespace HollyJukeBox.Repository; + +public interface IArtistInfoRepository +{ + public Task GetByIdAsync(string id); + public Task SaveAsync(ArtistInfo artist); +} \ No newline at end of file diff --git a/Repository/IArtistRepository.cs b/Repository/IArtistRepository.cs new file mode 100644 index 0000000..a5b6e26 --- /dev/null +++ b/Repository/IArtistRepository.cs @@ -0,0 +1,9 @@ +using HollyJukeBox.Models; + +namespace HollyJukeBox.Repository; + +public interface IArtistRepository +{ + public Task GetByIdAsync(string id); + public Task SaveAsync(ArtistDto artistDto); +} \ No newline at end of file diff --git a/Repository/ICoverArtRepository.cs b/Repository/ICoverArtRepository.cs new file mode 100644 index 0000000..5df7f03 --- /dev/null +++ b/Repository/ICoverArtRepository.cs @@ -0,0 +1,9 @@ +using HollyJukeBox.Models; + +namespace HollyJukeBox.Repository; + +public interface ICoverArtRepository +{ + public Task GetByIdAsync(string id); + public Task SaveAsync(CoverArtDto coverArtDto); +} \ No newline at end of file diff --git a/Services/IMemoryCashingService.cs b/Services/IMemoryCashingService.cs new file mode 100644 index 0000000..c957b70 --- /dev/null +++ b/Services/IMemoryCashingService.cs @@ -0,0 +1,7 @@ +namespace HollyJukeBox.Services; + +public interface IMemoryCashingService +{ + public T Get(string key) where T : class; + public bool Store(string key, T item) where T : class; +} \ No newline at end of file diff --git a/Services/MemoryCashingService.cs b/Services/MemoryCashingService.cs new file mode 100644 index 0000000..8b131e7 --- /dev/null +++ b/Services/MemoryCashingService.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.Caching.Memory; + +namespace HollyJukeBox.Services; + +public class MemoryCashingService(IMemoryCache memoryCache) : IMemoryCashingService +{ + public T Get(string key) where T : class => memoryCache.Get(key); + public bool Store(string key, T item) where T : class => + memoryCache.Set( + key, + item, + new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60) + }) is T; +} \ No newline at end of file diff --git a/appsettings.json b/appsettings.json index 0bde613..b88927a 100644 --- a/appsettings.json +++ b/appsettings.json @@ -11,5 +11,8 @@ "WikiDataUrl" : "https://www.wikidata.org/w/api.php", "WikipediaSummery" : "https://en.wikipedia.org/api/rest_v1/page/summary/" }, + "ConnectionStrings": { + "HollyJukeBoxDb": "Data Source=hollyjukebox.db" + }, "AllowedHosts": "*" } From f64ab40164a03a88ee71c9b813867055975c0732 Mon Sep 17 00:00:00 2001 From: niclastimle Date: Fri, 30 May 2025 01:20:18 +0200 Subject: [PATCH 2/6] memory cashing change, property on ArtistInfo/AlbumInfo required --- Handler/ArtistHandler.cs | 6 +++--- Handler/ArtistInfoHandler.cs | 7 ++++--- Handler/CoverArtHandler.cs | 6 +++--- Models/AlbumInfo.cs | 12 ++++++------ Models/ArtistInfo.cs | 8 ++++---- Repository/ArtistRepository.cs | 2 +- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/Handler/ArtistHandler.cs b/Handler/ArtistHandler.cs index f2161d0..04ee978 100644 --- a/Handler/ArtistHandler.cs +++ b/Handler/ArtistHandler.cs @@ -17,7 +17,7 @@ public class ArtistHandler( { public async Task Handle(ArtistQuery.GetById request, CancellationToken cancellationToken) { - var artist = memoryCashingService.Get(request.Id); + var artist = memoryCashingService.Get($"artistDto:{request.Id}"); if (artist is not null) { @@ -27,12 +27,12 @@ public async Task Handle(ArtistQuery.GetById request, CancellationTok artist = await artistRepository.GetByIdAsync(request.Id); if (artist is not null) { - memoryCashingService.Store(request.Id, artist); + memoryCashingService.Store($"artistDto:{request.Id}", artist); return artist; } artist = await artistEndPoint.GetById(request.Id); - memoryCashingService.Store(request.Id, artist); + memoryCashingService.Store($"artistDto:{request.Id}", artist); await artistRepository.SaveAsync(artist); return artist; } diff --git a/Handler/ArtistInfoHandler.cs b/Handler/ArtistInfoHandler.cs index 01496f3..06857e0 100644 --- a/Handler/ArtistInfoHandler.cs +++ b/Handler/ArtistInfoHandler.cs @@ -35,13 +35,13 @@ public async Task Handle(ArtistInfoQuery.GetById request, Cancellati if(artist.Albums is not null && artist.Albums.Count > 0) { artist.Albums = albumList.ToList(); - memoryCashingService.Store(request.Id, artist); + memoryCashingService.Store($"artist:{request.Id}", artist); return artist; } } var summary = string.Empty; - var artistDto = memoryCashingService.Get(request.Id); + var artistDto = memoryCashingService.Get($"artistDto:{request.Id}"); if (artistDto is null) { artistDto = await artistRepository.GetByIdAsync(request.Id); @@ -68,6 +68,7 @@ public async Task Handle(ArtistInfoQuery.GetById request, Cancellati var wikiSummary = await artistEndPoint.GetWikipediaSummary(wikiSummaryId); summary = wikiSummary.Extract; + memoryCashingService.Store($"artistDto:{request.Id}", artistDto); } } var albums = new List(); @@ -94,7 +95,7 @@ public async Task Handle(ArtistInfoQuery.GetById request, Cancellati Albums = albums }; - memoryCashingService.Store(request.Id, artist); + memoryCashingService.Store($"artist:{request.Id}", artist); await artistInfoRepository.SaveAsync(artist); await albumInfoRepository.SaveAsync(artist.Albums); return artist; diff --git a/Handler/CoverArtHandler.cs b/Handler/CoverArtHandler.cs index 2d2a00b..cb0222f 100644 --- a/Handler/CoverArtHandler.cs +++ b/Handler/CoverArtHandler.cs @@ -15,7 +15,7 @@ public class CoverArtHandler( { public async Task Handle(CoverArtQuery.GetById request, CancellationToken cancellationToken) { - var coverArt = memoryCashingService.Get(request.Id); + var coverArt = memoryCashingService.Get($"coverArt:{request.Id}"); if (coverArt is not null) { return coverArt; @@ -24,12 +24,12 @@ public async Task Handle(CoverArtQuery.GetById request, Cancellatio coverArt = await coverArtRepository.GetByIdAsync(request.Id); if (coverArt is not null) { - memoryCashingService.Store(request.Id, coverArt); + memoryCashingService.Store($"coverArt:{request.Id}", coverArt); return coverArt; } coverArt = await coverArtEndPoint.GetById(request.Id); - memoryCashingService.Store(request.Id, coverArt); + memoryCashingService.Store($"coverArt:{request.Id}", coverArt); await coverArtRepository.SaveAsync(coverArt); return coverArt; } diff --git a/Models/AlbumInfo.cs b/Models/AlbumInfo.cs index 0bf9b4f..f7dbc66 100644 --- a/Models/AlbumInfo.cs +++ b/Models/AlbumInfo.cs @@ -2,10 +2,10 @@ namespace HollyJukeBox.Models; public class AlbumInfo { - public string Id { get; set; } - public string ArtistInfoId { get; set; } - public string Title { get; set; } - public string FirstReleaseDate { get; set; } - public string ImageFront { get; set; } - public string ImageBack { get; set; } + public required string Id { get; set; } + public required string ArtistInfoId { get; set; } + public required string Title { get; set; } + public required string FirstReleaseDate { get; set; } + public required string ImageFront { get; set; } + public required string ImageBack { get; set; } } \ No newline at end of file diff --git a/Models/ArtistInfo.cs b/Models/ArtistInfo.cs index 9f952e8..4cfdb3d 100644 --- a/Models/ArtistInfo.cs +++ b/Models/ArtistInfo.cs @@ -2,8 +2,8 @@ namespace HollyJukeBox.Models; public class ArtistInfo { - public string Mbid { get; set; } - public string Artist { get; set; } - public List Albums { get; set; } - public string Description { get; set; } + public required string Mbid { get; set; } + public required string Artist { get; set; } + public required List? Albums { get; set; } + public required string Description { get; set; } } \ No newline at end of file diff --git a/Repository/ArtistRepository.cs b/Repository/ArtistRepository.cs index 144e437..55ac6d6 100644 --- a/Repository/ArtistRepository.cs +++ b/Repository/ArtistRepository.cs @@ -6,7 +6,7 @@ namespace HollyJukeBox.Repository; public class ArtistRepository(IDbConnection connection) : IArtistRepository { - public async Task GetByIdAsync(string id) + public async Task GetByIdAsync(string id) { var artist = await connection.QueryFirstOrDefaultAsync( "SELECT Id, Name FROM Artist WHERE Id = @Id", new { Id = id }); From 27d121bfd24ab216ea38c60ede1e96586a0d2b02 Mon Sep 17 00:00:00 2001 From: niclastimle Date: Fri, 30 May 2025 01:21:37 +0200 Subject: [PATCH 3/6] fix pipeline error --- Models/AlbumInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Models/AlbumInfo.cs b/Models/AlbumInfo.cs index f7dbc66..c58c580 100644 --- a/Models/AlbumInfo.cs +++ b/Models/AlbumInfo.cs @@ -6,6 +6,6 @@ public class AlbumInfo public required string ArtistInfoId { get; set; } public required string Title { get; set; } public required string FirstReleaseDate { get; set; } - public required string ImageFront { get; set; } - public required string ImageBack { get; set; } + public string ImageFront { get; set; } + public string ImageBack { get; set; } } \ No newline at end of file From dd76ad683df7550cd6625045ea4443c91e8950b2 Mon Sep 17 00:00:00 2001 From: niclastimle Date: Fri, 30 May 2025 01:23:41 +0200 Subject: [PATCH 4/6] fix nullable albums --- Models/ArtistInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Models/ArtistInfo.cs b/Models/ArtistInfo.cs index 4cfdb3d..a6e90ae 100644 --- a/Models/ArtistInfo.cs +++ b/Models/ArtistInfo.cs @@ -4,6 +4,6 @@ public class ArtistInfo { public required string Mbid { get; set; } public required string Artist { get; set; } - public required List? Albums { get; set; } + public required List Albums { get; set; } public required string Description { get; set; } } \ No newline at end of file From 45df8a67d09d19406fb9599d70423325e164c6c5 Mon Sep 17 00:00:00 2001 From: niclastimle Date: Fri, 30 May 2025 22:32:37 +0200 Subject: [PATCH 5/6] Added RetryPolicy fixed 404 error, query changes and endpoint changes. Logic in handlers --- Endpoints/CoverArtEndPoint.cs | 16 +++++++++-- Handler/ArtistInfoHandler.cs | 43 ++++++++++++++++++++--------- Handler/CoverArtHandler.cs | 8 ++++++ HollyJukeBox.csproj | 1 + Models/AlbumInfo.cs | 4 +-- Models/ArtistInfo.cs | 2 +- Models/ArtistsDto.cs | 5 ---- Models/CoverArtDto.cs | 12 +++++--- Program.cs | 1 + Repository/AlbumInfoRepository.cs | 8 +++--- Repository/ArtistInfoRepository.cs | 10 +++---- Repository/ArtistRepository.cs | 2 +- Repository/CoverArtRepository.cs | 2 +- Repository/IAlbumInfoRepository.cs | 4 +-- Repository/IArtistInfoRepository.cs | 2 +- Repository/IArtistRepository.cs | 2 +- Repository/ICoverArtRepository.cs | 2 +- Services/IMemoryCashingService.cs | 2 +- Services/IRetryPolicyService.cs | 6 ++++ Services/RetryPolicyService.cs | 16 +++++++++++ 20 files changed, 104 insertions(+), 44 deletions(-) delete mode 100644 Models/ArtistsDto.cs create mode 100644 Services/IRetryPolicyService.cs create mode 100644 Services/RetryPolicyService.cs diff --git a/Endpoints/CoverArtEndPoint.cs b/Endpoints/CoverArtEndPoint.cs index 8ef2875..447464b 100644 --- a/Endpoints/CoverArtEndPoint.cs +++ b/Endpoints/CoverArtEndPoint.cs @@ -1,3 +1,4 @@ +using System.Net; using HollyJukeBox.Models; using HollyJukeBox.Services; using Microsoft.Extensions.Options; @@ -6,6 +7,17 @@ namespace HollyJukeBox.Endpoints; public class CoverArtEndPoint(IOptions options, HttpClient client) : ICoverArtEndPoint { - public async Task GetById(string id) => - await client.GetFromJsonAsync(options.Value.CoverArtArchiveUrl + $"release-group/{id}"); + public async Task GetById(string id) + { + var response = await client.GetAsync(options.Value.CoverArtArchiveUrl + $"release-group/{id}"); + if (response.StatusCode != HttpStatusCode.OK) + { + return new CoverArtDto{ + Id = id, + Images = new List() + }; + } + + return await response.Content.ReadFromJsonAsync(); + } } \ No newline at end of file diff --git a/Handler/ArtistInfoHandler.cs b/Handler/ArtistInfoHandler.cs index 06857e0..10073eb 100644 --- a/Handler/ArtistInfoHandler.cs +++ b/Handler/ArtistInfoHandler.cs @@ -9,11 +9,13 @@ namespace HollyJukeBox.Handler; public class ArtistInfoHandler( IArtistEndPoint artistEndPoint, + ICoverArtEndPoint coverArtEndPoint, IMemoryCashingService memoryCashingService, IArtistRepository artistRepository, IArtistInfoRepository artistInfoRepository, IAlbumInfoRepository albumInfoRepository, - ICoverArtRepository coverArtRepository) + ICoverArtRepository coverArtRepository, + IRetryPolicyService retryPolicy) : IRequestHandler { public async Task Handle(ArtistInfoQuery.GetById request, CancellationToken cancellationToken) @@ -27,17 +29,15 @@ public async Task Handle(ArtistInfoQuery.GetById request, Cancellati artist = await artistInfoRepository.GetByIdAsync(request.Id); if (artist is not null) { - IEnumerable albumList = null; - if(artist.Albums is null && artist.Albums.Count == 0) - { - albumList = await albumInfoRepository.GetByArtistIdAsync(artist.Mbid); - } - if(artist.Albums is not null && artist.Albums.Count > 0) + var albumList = await albumInfoRepository.GetByArtistIdAsync(request.Id); + if(albumList is not null && albumList.ToList().Count > 0) { artist.Albums = albumList.ToList(); memoryCashingService.Store($"artist:{request.Id}", artist); return artist; - } + } + + return null; } var summary = string.Empty; @@ -75,22 +75,39 @@ public async Task Handle(ArtistInfoQuery.GetById request, Cancellati foreach (var release in artistDto.ReleaseGroups) { - //var coverArts = await coverArtRepository.GetByIdAsync(release.Id); - //var images = coverArts.Images; + var coverArt = memoryCashingService.Get($"coverArt:{release.Id}");//TODO fix + if(coverArt is null) coverArt = await coverArtRepository.GetByIdAsync(release.Id); + if(coverArt is null) + { + coverArt = await retryPolicy.RetryGet().ExecuteAsync(() => coverArtEndPoint.GetById(release.Id)); + if(coverArt is not null) + { + var sortedImages = coverArt.Images + .Where(x => + x.Types.Contains(nameof(ImageTypes.Front)) || + x.Types.Contains(nameof(ImageTypes.Back))); + + coverArt.Images = sortedImages.ToList(); + coverArt.Id = release.Id; + } + } + albums.Add(new AlbumInfo { Id = release.Id, ArtistInfoId = artistDto.Id, Title = release.Title, FirstReleaseDate = release.FirstReleaseDate, - //ImageFront = images.First(x => x.Types.Equals(ImageTypes.Front)).Image, - //ImageBack = images.First(x => x.Types.Equals(ImageTypes.Back)).Image + ImageFront = coverArt.Images.FirstOrDefault(x => x.Types.Contains(nameof(ImageTypes.Front)))?.Image ?? string.Empty, + ImageBack = coverArt.Images.FirstOrDefault(x => x.Types.Contains(nameof(ImageTypes.Back)))?.Image ?? string.Empty }); + memoryCashingService.Store($"coverArt:{release.Id}", coverArt); + await coverArtRepository.SaveAsync(coverArt); } artist = new ArtistInfo { Mbid = artistDto.Id, - Artist = artistDto.Name, + Name = artistDto.Name, Description = summary, Albums = albums }; diff --git a/Handler/CoverArtHandler.cs b/Handler/CoverArtHandler.cs index cb0222f..3d54efc 100644 --- a/Handler/CoverArtHandler.cs +++ b/Handler/CoverArtHandler.cs @@ -29,6 +29,14 @@ public async Task Handle(CoverArtQuery.GetById request, Cancellatio } coverArt = await coverArtEndPoint.GetById(request.Id); + coverArt.Id = request.Id; + var sortedImages = coverArt.Images + .Where(x => + x.Types.Contains(nameof(ImageTypes.Front)) || + x.Types.Contains(nameof(ImageTypes.Back))); + + coverArt.Images = sortedImages.ToList(); + memoryCashingService.Store($"coverArt:{request.Id}", coverArt); await coverArtRepository.SaveAsync(coverArt); return coverArt; diff --git a/HollyJukeBox.csproj b/HollyJukeBox.csproj index 06f9d7f..2345503 100644 --- a/HollyJukeBox.csproj +++ b/HollyJukeBox.csproj @@ -12,6 +12,7 @@ + diff --git a/Models/AlbumInfo.cs b/Models/AlbumInfo.cs index c58c580..7a5770a 100644 --- a/Models/AlbumInfo.cs +++ b/Models/AlbumInfo.cs @@ -6,6 +6,6 @@ public class AlbumInfo public required string ArtistInfoId { get; set; } public required string Title { get; set; } public required string FirstReleaseDate { get; set; } - public string ImageFront { get; set; } - public string ImageBack { get; set; } + public string? ImageFront { get; set; } + public string? ImageBack { get; set; } } \ No newline at end of file diff --git a/Models/ArtistInfo.cs b/Models/ArtistInfo.cs index a6e90ae..9f116d5 100644 --- a/Models/ArtistInfo.cs +++ b/Models/ArtistInfo.cs @@ -3,7 +3,7 @@ namespace HollyJukeBox.Models; public class ArtistInfo { public required string Mbid { get; set; } - public required string Artist { get; set; } + public required string Name { get; set; } public required List Albums { get; set; } public required string Description { get; set; } } \ No newline at end of file diff --git a/Models/ArtistsDto.cs b/Models/ArtistsDto.cs deleted file mode 100644 index eb3c0b4..0000000 --- a/Models/ArtistsDto.cs +++ /dev/null @@ -1,5 +0,0 @@ -using System.Text.Json.Serialization; -namespace HollyJukeBox.Models; -public record ArtistsDto( - [property: JsonPropertyName("artists")] List Artists -); \ No newline at end of file diff --git a/Models/CoverArtDto.cs b/Models/CoverArtDto.cs index 3bd48cb..e61395f 100644 --- a/Models/CoverArtDto.cs +++ b/Models/CoverArtDto.cs @@ -1,10 +1,14 @@ using System.Text.Json.Serialization; namespace HollyJukeBox.Models; -public record CoverArtDto( - string Id, - [property:JsonPropertyName("images")] List Images -); + +public class CoverArtDto +{ + public string Id { get; set; } + [JsonPropertyName("images")] + public List Images { get; set; } +} + public record Images( [property: JsonPropertyName("types")] string[] Types, [property: JsonPropertyName("image")] string Image diff --git a/Program.cs b/Program.cs index e00d76e..e3c2287 100644 --- a/Program.cs +++ b/Program.cs @@ -19,6 +19,7 @@ builder.Services.AddScoped(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); diff --git a/Repository/AlbumInfoRepository.cs b/Repository/AlbumInfoRepository.cs index fb3097a..4bb8704 100644 --- a/Repository/AlbumInfoRepository.cs +++ b/Repository/AlbumInfoRepository.cs @@ -6,7 +6,7 @@ namespace HollyJukeBox.Repository; public class AlbumInfoRepository(IDbConnection connection) : IAlbumInfoRepository { - public async Task> GetByArtistIdAsync(string id) + public async Task?> GetByArtistIdAsync(string id) { var builder = new SqlBuilder(); var template = builder.AddTemplate("SELECT * FROM AlbumInfo /**where**/"); @@ -14,15 +14,15 @@ public async Task> GetByArtistIdAsync(string id) return await connection.QueryAsync(template.RawSql, new { Id = id }); } - public async Task SaveAsync(List albums) + public async Task SaveAsync(List? albums) { var rowsAffected = 0; foreach (var album in albums) { var builder = new SqlBuilder(); var artistInsert = - builder.AddTemplate("INSERT OR IGNORE INTO AlbumInfo (Id, Title,FirstReleaseDate, Description, Image, ArtistsInfoId) " + - "VALUES (@Id,ArtistsInfoId @Title, @FirstReleaseDate, @Description, @Image, @)"); + builder.AddTemplate("INSERT OR IGNORE INTO AlbumInfo (Id, Title, FirstReleaseDate, ImageFront, ImageBack, ArtistInfoId) " + + "VALUES (@Id, @Title, @FirstReleaseDate, @ImageFront, @ImageBack, @ArtistInfoId)"); rowsAffected += await connection.ExecuteAsync(artistInsert.RawSql, new { diff --git a/Repository/ArtistInfoRepository.cs b/Repository/ArtistInfoRepository.cs index fc0a2fb..c21a278 100644 --- a/Repository/ArtistInfoRepository.cs +++ b/Repository/ArtistInfoRepository.cs @@ -6,10 +6,10 @@ namespace HollyJukeBox.Repository; public class ArtistInfoRepository(IDbConnection connection) : IArtistInfoRepository { - public async Task GetByIdAsync(string id) + public async Task GetByIdAsync(string id) { var builder = new SqlBuilder(); - var template = builder.AddTemplate("SELECT * FROM ArtistInfo /**where**/ LIMIT 1"); + var template = builder.AddTemplate("SELECT Id AS Mbid, Name, Description FROM ArtistInfo /**where**/ LIMIT 1"); builder.Where("id = @Id", new { Id = id }); return await connection.QueryFirstOrDefaultAsync(template.RawSql, new { Id = id }); @@ -24,9 +24,9 @@ public async Task SaveAsync(ArtistInfo artist) return await connection.ExecuteAsync(artistInsert.RawSql, new - { - artist.Mbid, - artist.Artist, + { + Id = artist.Mbid, + artist.Name, artist.Description }); } diff --git a/Repository/ArtistRepository.cs b/Repository/ArtistRepository.cs index 55ac6d6..144e437 100644 --- a/Repository/ArtistRepository.cs +++ b/Repository/ArtistRepository.cs @@ -6,7 +6,7 @@ namespace HollyJukeBox.Repository; public class ArtistRepository(IDbConnection connection) : IArtistRepository { - public async Task GetByIdAsync(string id) + public async Task GetByIdAsync(string id) { var artist = await connection.QueryFirstOrDefaultAsync( "SELECT Id, Name FROM Artist WHERE Id = @Id", new { Id = id }); diff --git a/Repository/CoverArtRepository.cs b/Repository/CoverArtRepository.cs index 612c437..d4049a3 100644 --- a/Repository/CoverArtRepository.cs +++ b/Repository/CoverArtRepository.cs @@ -6,7 +6,7 @@ namespace HollyJukeBox.Repository; public class CoverArtRepository(IDbConnection connection) : ICoverArtRepository { - public async Task GetByIdAsync(string id) + public async Task GetByIdAsync(string id) { var builder = new SqlBuilder(); var template = builder.AddTemplate( diff --git a/Repository/IAlbumInfoRepository.cs b/Repository/IAlbumInfoRepository.cs index a288425..1005182 100644 --- a/Repository/IAlbumInfoRepository.cs +++ b/Repository/IAlbumInfoRepository.cs @@ -4,6 +4,6 @@ namespace HollyJukeBox.Repository; public interface IAlbumInfoRepository { - public Task> GetByArtistIdAsync(string id); - public Task SaveAsync(List album); + public Task?> GetByArtistIdAsync(string id); + public Task SaveAsync(List? album); } \ No newline at end of file diff --git a/Repository/IArtistInfoRepository.cs b/Repository/IArtistInfoRepository.cs index 604579a..d685463 100644 --- a/Repository/IArtistInfoRepository.cs +++ b/Repository/IArtistInfoRepository.cs @@ -4,6 +4,6 @@ namespace HollyJukeBox.Repository; public interface IArtistInfoRepository { - public Task GetByIdAsync(string id); + public Task GetByIdAsync(string id); public Task SaveAsync(ArtistInfo artist); } \ No newline at end of file diff --git a/Repository/IArtistRepository.cs b/Repository/IArtistRepository.cs index a5b6e26..e4842d9 100644 --- a/Repository/IArtistRepository.cs +++ b/Repository/IArtistRepository.cs @@ -4,6 +4,6 @@ namespace HollyJukeBox.Repository; public interface IArtistRepository { - public Task GetByIdAsync(string id); + public Task GetByIdAsync(string id); public Task SaveAsync(ArtistDto artistDto); } \ No newline at end of file diff --git a/Repository/ICoverArtRepository.cs b/Repository/ICoverArtRepository.cs index 5df7f03..ed2ca97 100644 --- a/Repository/ICoverArtRepository.cs +++ b/Repository/ICoverArtRepository.cs @@ -4,6 +4,6 @@ namespace HollyJukeBox.Repository; public interface ICoverArtRepository { - public Task GetByIdAsync(string id); + public Task GetByIdAsync(string id); public Task SaveAsync(CoverArtDto coverArtDto); } \ No newline at end of file diff --git a/Services/IMemoryCashingService.cs b/Services/IMemoryCashingService.cs index c957b70..23c8536 100644 --- a/Services/IMemoryCashingService.cs +++ b/Services/IMemoryCashingService.cs @@ -2,6 +2,6 @@ namespace HollyJukeBox.Services; public interface IMemoryCashingService { - public T Get(string key) where T : class; + public T? Get(string key) where T : class; public bool Store(string key, T item) where T : class; } \ No newline at end of file diff --git a/Services/IRetryPolicyService.cs b/Services/IRetryPolicyService.cs new file mode 100644 index 0000000..724401d --- /dev/null +++ b/Services/IRetryPolicyService.cs @@ -0,0 +1,6 @@ +namespace HollyJukeBox.Services; + +public interface IRetryPolicyService +{ + public Polly.Retry.AsyncRetryPolicy RetryGet(); +} \ No newline at end of file diff --git a/Services/RetryPolicyService.cs b/Services/RetryPolicyService.cs new file mode 100644 index 0000000..0d1bd93 --- /dev/null +++ b/Services/RetryPolicyService.cs @@ -0,0 +1,16 @@ +using Polly; + +namespace HollyJukeBox.Services; + +public class RetryPolicyService : IRetryPolicyService +{ + public Polly.Retry.AsyncRetryPolicy RetryGet() + { + return Policy + .Handle() + .WaitAndRetryAsync( + retryCount: 3, + sleepDurationProvider: retryAttempt => + TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); + } +} \ No newline at end of file From d46c8e099dd61d956d38cdd7b13a1c31f31ee7e2 Mon Sep 17 00:00:00 2001 From: niclastimle Date: Fri, 6 Jun 2025 13:38:37 +0200 Subject: [PATCH 6/6] Small changes for ArtistInfoHandler.cs added README.md, swagger endpoint --- Handler/ArtistInfoHandler.cs | 2 +- Program.cs | 6 ++ README.md | 129 +++++++++++++++++++++++++++++++++++ launchSettings.json | 3 +- 4 files changed, 138 insertions(+), 2 deletions(-) diff --git a/Handler/ArtistInfoHandler.cs b/Handler/ArtistInfoHandler.cs index 10073eb..cafebb2 100644 --- a/Handler/ArtistInfoHandler.cs +++ b/Handler/ArtistInfoHandler.cs @@ -75,7 +75,7 @@ public async Task Handle(ArtistInfoQuery.GetById request, Cancellati foreach (var release in artistDto.ReleaseGroups) { - var coverArt = memoryCashingService.Get($"coverArt:{release.Id}");//TODO fix + var coverArt = memoryCashingService.Get($"coverArt:{release.Id}"); if(coverArt is null) coverArt = await coverArtRepository.GetByIdAsync(release.Id); if(coverArt is null) { diff --git a/Program.cs b/Program.cs index e3c2287..b3fcd47 100644 --- a/Program.cs +++ b/Program.cs @@ -43,6 +43,12 @@ await dbInit.EnsureTablesCreatedAsync(); } +app.MapGet("/", context => +{ + context.Response.Redirect("/swagger"); + return Task.CompletedTask; +}); + app.UseSwagger(); app.UseSwaggerUI(); app.MapControllers(); diff --git a/README.md b/README.md index 0c58eb4..f41d5d7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,131 @@ [![.NET](https://github.com/Carpenteri1/HollyJukeBox/actions/workflows/dotnet.yml/badge.svg)](https://github.com/Carpenteri1/HollyJukeBox/actions/workflows/dotnet.yml) [![CodeQL Advanced](https://github.com/Carpenteri1/HollyJukeBox/actions/workflows/codeql.yml/badge.svg)](https://github.com/Carpenteri1/HollyJukeBox/actions/workflows/codeql.yml) + +# Holly JukeBox + +### Starta +- git clone https://github.com/Carpenteri1/HollyJukeBox.git eller ladda ner som zip +- unzip (skip if git clone) +- cd HollyJukeBox +- dotnet restore +- dotnet build +- dotnet run +### Installera self contained +Om man vill köra HollyJukeBox helt self containd. +Efter att ha gjort någon av commandon nedanför hiitar du allt i HollyJukeBox/bin/Release/net8.0/osx-arm64/publish/ + +#### Windows (x64) +- dotnet publish -c Release -r win-x64 --self-contained true +#### Linux (x64) +- dotnet publish -c Release -r linux-x64 --self-contained true +#### macOS (x64) +- dotnet publish -c Release -r osx-x64 --self-contained true +#### macOS (Apple Silicon) +- dotnet publish -c Release -r osx-arm64 --self-contained true +### Ramverk +- NET Core 8 +- ADO.NET +- ASP.NET +- C# +- Swagger +- Dapper +- MediaR +- SQLlite +- Poppin +- Yaml +#### .NET Core +Jag har valt att använda .NET 8, som är den senaste LTS-versionen (Long Term Support). Det erbjuder hög stabilitet, långsiktigt stöd och god prestanda. + +.NET är mitt huvudsakliga utvecklingsspråk, vilket gör det till ett naturligt val. Plattformen är väletablerad, kraftfull och lätt att skala upp till större system. + +För denna lösning var det självklart att bygga ett REST API med ASP.NET Core, eftersom ramverket erbjuder en mängd funktionalitet “out of the box”, såsom: +- Inbyggd routing +- Swagger-stöd +- Dependency Injection +- Middleware för t.ex. logging och felhantering + +Valet av ADO.NET och Dapper som datalager följer .NET:s naturliga arkitekturella styrkor när man vill ha kontroll, prestanda och låg overhead. +#### Swagger +Swagger ingår som standard när man skapar ett API-projekt i .NET Core och är ett mycket smidigt verktyg för att testa, dokumentera och visualisera API:er. + +Det möjliggör en interaktiv dokumentation där man kan skicka anrop direkt via webbläsaren, vilket underlättar både utveckling och felsökning. Swagger är väl etablerat i branschen, enkelt att konfigurera, och ger snabbt värde i ett proof of concept. + +Jag är medveten om att Swagger byts ut i vissa sammanhang i nyare versioner av .NET, men valde ändå att använda det eftersom: +- Det är beprövat och välkänt +- Det finns god verktygssupport +- Jag har personlig erfarenhet och kännedom om det +#### Dapper +Jag har valt att använda Dapper som datalager istället för att arbeta direkt med ADO.NET. Dapper är ett så kallat micro ORM som kombinerar hög prestanda med låg komplexitet. Det erbjuder en mycket lättviktig och snabb datatillgång, utan att abstrahera bort SQL – vilket passar utmärkt för ett proof of concept där kontroll och läsbarhet är prioriterade. + +Fördelarna med Dapper i detta sammanhang: +- Minimal kodmängd +- Hög prestanda, nära rå ADO.NET +- Enkel syntax och snabb inlärningströskel +- Tydlig separation av queries i kodbasen + +Valet stödjer också en modulär struktur där varje query eller kommando är explicit och lätt att underhålla. +#### MediaR +För att strukturera applikationslogiken har jag valt att använda MediatR tillsammans med CQRS (Command Query Responsibility Segregation). + +MediatR fungerar som ett mediator pattern-bibliotek, vilket ger en tydlig separation mellan API-lagret (controllers) och affärslogiken (handlers). Detta möjliggör: +- Lös koppling mellan komponenter +- Enkel testbarhet +- Skalbar struktur vid växande kodbas +- Möjlighet att införa pipeline behaviors för t.ex. loggning, validering, caching + +Jag använder MediatR främst för queries, och strukturen gör det enkelt att skala vidare och bygga ut funktionalitet utan att controller-lagret växer okontrollerat. Jag är medveten om att det kan upplevas som överengineering i enklare lösningar, men ser det som en strategisk investering i struktur. +#### SQLite +För datalagring använder projektet SQLite, en lättviktig, filbaserad databas som kräver minimal konfiguration. Den lämpar sig utmärkt för ett proof of concept då den: +- Kräver ingen separat server +- Är portabel och fungerar direkt på alla plattformar +- Har fullständig SQL-stöd +- Passar väl för lokal utveckling och snabb testning + +SQLite är ett bra val i detta sammanhang eftersom det gör setup och distribution enkel, samtidigt som det tillåter ett tydligt schema och persistent lagring. +#### In-Memory Caching +Projektet använder Microsoft.Extensions.Caching.Memory för inbyggd in-memory caching i .NET. Det möjliggör temporär lagring av data direkt i applikationens minne, vilket är särskilt användbart för att: +- Minska onödiga externa anrop +- Snabba upp svarstider +- Förbättra resurseffektivitet + +Cachen används exempelvis för att spara artistinformation som hämtas via externa källor. Genom att använda IMemoryCache med konfigurerad expiration (t.ex. sliding eller absolute), undviker vi upprepade nätverksanrop och förbättrar användarupplevelsen i klienten. + +Denna typ av caching lämpar sig mycket väl för API-tjänster där datan är relativt statisk under en kortare tid, och den är enkel att konfigurera och underhålla. +#### Poppin +Poppin används som ett komplement till MediatR för att ge stöd för pipelines och policy-baserade behaviors. Det hjälper till att separera tvärgående aspekter såsom: +- Logging +- Retry +- Timeout-hantering +- Circuit breaker-mönster + +Detta följer principen om Separation of Concerns och gör systemet mer modulärt och underhållbart. Poppin passar särskilt bra i CQRS-arkitektur där man vill dekorera Handlers med extra funktionalitet utan att blanda in det i affärslogiken. +#### YAML +Projektet använder YAML som format för konfiguration och exempeldata. YAML är ett lättläst, human-friendly format som lämpar sig väl för: +- Enkel strukturering av konfigurationsfiler +- Överskådlig initdata till t.ex. API:er eller tester +- Miljöspecifika inställningar i DevOps + +I detta projekt används YAML t.ex. för att definiera seed-data eller inställningar på ett överskådligt sätt. Det ger bättre läsbarhet än JSON i många konfigurationssammanhang. +#### Arkitektur +Projektet följer en förenklad version av Clean Architecture för att uppnå tydlig separation av ansvar och god testbarhet: +- API-lager: Innehåller Controllers som exponerar endpoints via ASP.NET Core. +- Application-lager: Använder CQRS-mönstret med Commands, Queries och Handlers via MediatR för att kapsla in affärslogik. +- Infrastruktur-lager: Innehåller implementeringar för dataåtkomst med Dapper och SQLite som databasmotor. +#### Felhantering +Nuvarande implementation har grundläggande felhantering, men det finns förbättringspotential: +- Vissa null-fall hanteras inte uttryckligen. +- Endpoints saknar tydlig hantering av statuskoder — man förutsätter exempelvis 200 OK, vilket kan ge felaktig responslogik vid t.ex. 404 Not Found eller 400 Bad Request. +- Felhantering i t.ex. repository-lagret är minimal och bör förstärkas. +- Inga enhetstester har ännu skrivits, vilket var planerat men prioriterades bort på grund av tidsramen. + +I en vidareutveckling bör global felhantering via ExceptionMiddleware och användning av ProblemDetails enligt RFC7807 övervägas. +#### Kompletterande ändringar ej pushade +Den aktuella branchen är inte helt fullt committad då vissa ändringar gjordes efter att jag tillfälligt förlorade åtkomsten till GitHub. Lokala uppdateringar finns kvar men hann inte pushas innan deadline. Funktionaliteten påverkas inte i grunden, men några förbättringar och justeringar syns inte i det publika repot. Allt ska pushas remote när tillgången till github är tillbaka. +#### Vidareutvecklingsförslag +Det finns flera naturliga utvecklingsvägar för att bygga vidare på detta proof of concept: +- Tester: Lägg till enhetstester för både application-lagret (handlers) och API-lagret (controllers). +- Utökade endpoints: Exempelvis en sökfunktion baserad på artistnamn istället för endast ID. CQRS-strukturen gör det enkelt att införa fler queries med olika filter. +- Flera retry-policies: Just nu används en enkel retry-policy i ArtistInfoHandler.cs. Detta kan generaliseras med Polly eller liknande bibliotek för att hantera instabil extern datahämtning. +- Prestandaförbättringar: ArtistInfo-sökningen kan optimeras – just nu är svarstiden något lång innan all metadata laddats. +- UI-klient: Projektet skulle med fördel kunna kompletteras med en frontend (t.ex. React, Vue eller Blazor) för att visualisera artistdata och albumomslag på ett mer användarvänligt sätt. + diff --git a/launchSettings.json b/launchSettings.json index 023f15b..90349ad 100644 --- a/launchSettings.json +++ b/launchSettings.json @@ -4,7 +4,8 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "http://localhost:5088", + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5009", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }