From bc6ca3651fa4f143df264640406261ae52e4d4aa Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 24 Sep 2026 12:41:05 +0200 Subject: [PATCH] Share the entity model between Database and the test database via ModelConfiguration --- .../Persistence/Database.cs | 140 +--------------- .../Persistence/ModelConfiguration.cs | 154 ++++++++++++++++++ .../Application.Tests.csproj | 1 + .../TestSupport/TestDatabase.cs | 99 +---------- 4 files changed, 158 insertions(+), 236 deletions(-) create mode 100644 backend/Core/Musify.Infrastructure/Persistence/ModelConfiguration.cs diff --git a/backend/Core/Musify.Infrastructure/Persistence/Database.cs b/backend/Core/Musify.Infrastructure/Persistence/Database.cs index 0814c5c..929c3db 100644 --- a/backend/Core/Musify.Infrastructure/Persistence/Database.cs +++ b/backend/Core/Musify.Infrastructure/Persistence/Database.cs @@ -6,7 +6,6 @@ using Musify.Application.Contracts; using Musify.Domain.Entities; using Musify.Domain.ValueObjects; using Musify.Infrastructure.Configuration; -using Musify.Infrastructure.MassTransit.Sagas; using Npgsql; namespace Musify.Infrastructure.Persistence; @@ -31,144 +30,7 @@ public class Database(DatabaseConfiguration configuration) modelBuilder.AddOutboxMessageEntity(); modelBuilder.AddOutboxStateEntity(); - var trackProcessing = modelBuilder.Entity(); - trackProcessing.HasKey(state => state.CorrelationId); - trackProcessing.Property(state => state.CorrelationId).ValueGeneratedNever(); - trackProcessing.Property(state => state.CurrentState).HasMaxLength(64); - - modelBuilder.Entity() - .OwnsOne(track => track.Pictures, pictures => - { - pictures.Property(p => p.OriginalName).HasColumnName("OriginalPictureName").HasMaxLength(64); - pictures.Property(p => p.SmallName).HasColumnName("SmallPictureName").HasMaxLength(64); - pictures.Property(p => p.MediumName).HasColumnName("MediumPictureName").HasMaxLength(64); - pictures.Property(p => p.LargeName).HasColumnName("LargePictureName").HasMaxLength(64); - pictures.Property(p => p.ProcessingStatus).HasColumnName("PicturesProcessingStatus"); - }); - modelBuilder.Entity().Navigation(track => track.Pictures).IsRequired(); - - modelBuilder.Entity() - .OwnsOne(track => track.Audio, audio => - { - audio.Property(a => a.OriginalName).HasColumnName("OriginalAudioName").HasMaxLength(64); - audio.Property(a => a.FolderName).HasColumnName("AudioFolderName").HasMaxLength(64); - audio.Property(a => a.TranscodeStatus).HasColumnName("AudioTranscodeProcessingStatus"); - }); - modelBuilder.Entity().Navigation(track => track.Audio).IsRequired(); - - modelBuilder.Entity() - .HasOne(track => track.Owner) - .WithMany() - .HasForeignKey(track => track.OwnerUserId) - .OnDelete(DeleteBehavior.Restrict); - - modelBuilder.Entity() - .Property(playList => playList.Visibility) - .HasDefaultValue(PlayListVisibility.Private); - - modelBuilder.Entity() - .OwnsOne(playList => playList.Pictures, pictures => - { - pictures.Property(p => p.OriginalName).HasColumnName("OriginalPictureName").HasMaxLength(64).IsRequired(false); - pictures.Property(p => p.SmallName).HasColumnName("SmallPictureName").HasMaxLength(64).IsRequired(false); - pictures.Property(p => p.MediumName).HasColumnName("MediumPictureName").HasMaxLength(64).IsRequired(false); - pictures.Property(p => p.LargeName).HasColumnName("LargePictureName").HasMaxLength(64).IsRequired(false); - }); - modelBuilder.Entity().Navigation(playList => playList.Pictures).IsRequired(false); - - modelBuilder.Entity() - .OwnsOne(album => album.Pictures, pictures => - { - pictures.Property(p => p.OriginalName).HasColumnName("OriginalPictureName").HasMaxLength(64); - pictures.Property(p => p.SmallName).HasColumnName("SmallPictureName").HasMaxLength(64).IsRequired(false); - pictures.Property(p => p.MediumName).HasColumnName("MediumPictureName").HasMaxLength(64).IsRequired(false); - pictures.Property(p => p.LargeName).HasColumnName("LargePictureName").HasMaxLength(64).IsRequired(false); - }); - modelBuilder.Entity().Navigation(album => album.Pictures).IsRequired(false); - - modelBuilder.Entity() - .HasOne(album => album.Owner) - .WithMany() - .HasForeignKey(album => album.OwnerUserId) - .OnDelete(DeleteBehavior.Restrict); - - modelBuilder.Entity() - .HasOne(albumTrack => albumTrack.Album) - .WithMany(album => album.AlbumTracks) - .HasForeignKey(albumTrack => albumTrack.AlbumId) - .OnDelete(DeleteBehavior.Cascade); - - modelBuilder.Entity() - .HasOne(albumTrack => albumTrack.Track) - .WithMany(track => track.AlbumTracks) - .HasForeignKey(albumTrack => albumTrack.TrackId) - .OnDelete(DeleteBehavior.Cascade); - - modelBuilder.Entity() - .HasIndex(albumTrack => new { albumTrack.AlbumId, albumTrack.TrackId }) - .IsUnique(); - - modelBuilder.Entity() - .HasIndex(playListTrack => new { playListTrack.PlayListId, playListTrack.TrackId }) - .IsUnique(); - - modelBuilder.Entity() - .HasIndex(mix => new { mix.UserId, mix.Position }); - - modelBuilder.Entity() - .HasOne(item => item.Mix) - .WithMany(mix => mix.Items) - .HasForeignKey(item => item.MixId) - .OnDelete(DeleteBehavior.Cascade); - - modelBuilder.Entity() - .HasOne(item => item.Track) - .WithMany() - .HasForeignKey(item => item.TrackId) - .OnDelete(DeleteBehavior.Cascade); - - modelBuilder.Entity() - .HasIndex(item => new { item.MixId, item.Position }); - - modelBuilder.Entity() - .HasIndex(like => new { like.UserId, like.TrackId }) - .IsUnique(); - - modelBuilder.Entity() - .HasOne(trackTag => trackTag.Track) - .WithMany(track => track.Tags) - .HasForeignKey(trackTag => trackTag.TrackId) - .OnDelete(DeleteBehavior.Cascade); - - modelBuilder.Entity() - .HasIndex(trackTag => new { trackTag.TrackId, trackTag.Tag }) - .IsUnique(); - - modelBuilder.Entity() - .HasOne(follow => follow.Follower) - .WithMany() - .HasForeignKey(follow => follow.FollowerId) - .OnDelete(DeleteBehavior.Restrict); - - modelBuilder.Entity() - .HasOne(follow => follow.Followed) - .WithMany() - .HasForeignKey(follow => follow.FollowedId) - .OnDelete(DeleteBehavior.Restrict); - - modelBuilder.Entity() - .HasIndex(follow => new { follow.FollowerId, follow.FollowedId }) - .IsUnique(); - - var playListProcessing = modelBuilder.Entity(); - playListProcessing.HasKey(state => state.CorrelationId); - playListProcessing.Property(state => state.CorrelationId).ValueGeneratedNever(); - playListProcessing.Property(state => state.CurrentState).HasMaxLength(64); - - var albumProcessing = modelBuilder.Entity(); - albumProcessing.HasKey(state => state.CorrelationId); - albumProcessing.Property(state => state.CorrelationId).ValueGeneratedNever(); - albumProcessing.Property(state => state.CurrentState).HasMaxLength(64); + ModelConfiguration.Apply(modelBuilder); } public DbSet Users => Set(); diff --git a/backend/Core/Musify.Infrastructure/Persistence/ModelConfiguration.cs b/backend/Core/Musify.Infrastructure/Persistence/ModelConfiguration.cs new file mode 100644 index 0000000..4e44ae1 --- /dev/null +++ b/backend/Core/Musify.Infrastructure/Persistence/ModelConfiguration.cs @@ -0,0 +1,154 @@ +using Microsoft.EntityFrameworkCore; +using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; +using Musify.Infrastructure.MassTransit.Sagas; + +namespace Musify.Infrastructure.Persistence; + +/// +/// Entity model shared by and the in-memory test database, so both always agree. +/// +public static class ModelConfiguration +{ + public static void Apply(ModelBuilder modelBuilder) + { + var trackProcessing = modelBuilder.Entity(); + trackProcessing.HasKey(state => state.CorrelationId); + trackProcessing.Property(state => state.CorrelationId).ValueGeneratedNever(); + trackProcessing.Property(state => state.CurrentState).HasMaxLength(64); + + modelBuilder.Entity() + .OwnsOne(track => track.Pictures, pictures => + { + pictures.Property(p => p.OriginalName).HasColumnName("OriginalPictureName").HasMaxLength(64); + pictures.Property(p => p.SmallName).HasColumnName("SmallPictureName").HasMaxLength(64); + pictures.Property(p => p.MediumName).HasColumnName("MediumPictureName").HasMaxLength(64); + pictures.Property(p => p.LargeName).HasColumnName("LargePictureName").HasMaxLength(64); + pictures.Property(p => p.ProcessingStatus).HasColumnName("PicturesProcessingStatus"); + }); + modelBuilder.Entity().Navigation(track => track.Pictures).IsRequired(); + + modelBuilder.Entity() + .OwnsOne(track => track.Audio, audio => + { + audio.Property(a => a.OriginalName).HasColumnName("OriginalAudioName").HasMaxLength(64); + audio.Property(a => a.FolderName).HasColumnName("AudioFolderName").HasMaxLength(64); + audio.Property(a => a.TranscodeStatus).HasColumnName("AudioTranscodeProcessingStatus"); + }); + modelBuilder.Entity().Navigation(track => track.Audio).IsRequired(); + + modelBuilder.Entity() + .HasOne(track => track.Owner) + .WithMany() + .HasForeignKey(track => track.OwnerUserId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .Property(playList => playList.Visibility) + .HasDefaultValue(PlayListVisibility.Private); + + modelBuilder.Entity() + .OwnsOne(playList => playList.Pictures, pictures => + { + pictures.Property(p => p.OriginalName).HasColumnName("OriginalPictureName").HasMaxLength(64).IsRequired(false); + pictures.Property(p => p.SmallName).HasColumnName("SmallPictureName").HasMaxLength(64).IsRequired(false); + pictures.Property(p => p.MediumName).HasColumnName("MediumPictureName").HasMaxLength(64).IsRequired(false); + pictures.Property(p => p.LargeName).HasColumnName("LargePictureName").HasMaxLength(64).IsRequired(false); + }); + modelBuilder.Entity().Navigation(playList => playList.Pictures).IsRequired(false); + + modelBuilder.Entity() + .OwnsOne(album => album.Pictures, pictures => + { + pictures.Property(p => p.OriginalName).HasColumnName("OriginalPictureName").HasMaxLength(64); + pictures.Property(p => p.SmallName).HasColumnName("SmallPictureName").HasMaxLength(64).IsRequired(false); + pictures.Property(p => p.MediumName).HasColumnName("MediumPictureName").HasMaxLength(64).IsRequired(false); + pictures.Property(p => p.LargeName).HasColumnName("LargePictureName").HasMaxLength(64).IsRequired(false); + }); + modelBuilder.Entity().Navigation(album => album.Pictures).IsRequired(false); + + modelBuilder.Entity() + .HasOne(album => album.Owner) + .WithMany() + .HasForeignKey(album => album.OwnerUserId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(albumTrack => albumTrack.Album) + .WithMany(album => album.AlbumTracks) + .HasForeignKey(albumTrack => albumTrack.AlbumId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasOne(albumTrack => albumTrack.Track) + .WithMany(track => track.AlbumTracks) + .HasForeignKey(albumTrack => albumTrack.TrackId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasIndex(albumTrack => new { albumTrack.AlbumId, albumTrack.TrackId }) + .IsUnique(); + + modelBuilder.Entity() + .HasIndex(playListTrack => new { playListTrack.PlayListId, playListTrack.TrackId }) + .IsUnique(); + + modelBuilder.Entity() + .HasIndex(mix => new { mix.UserId, mix.Position }); + + modelBuilder.Entity() + .HasOne(item => item.Mix) + .WithMany(mix => mix.Items) + .HasForeignKey(item => item.MixId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasOne(item => item.Track) + .WithMany() + .HasForeignKey(item => item.TrackId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasIndex(item => new { item.MixId, item.Position }); + + modelBuilder.Entity() + .HasIndex(like => new { like.UserId, like.TrackId }) + .IsUnique(); + + modelBuilder.Entity() + .HasOne(trackTag => trackTag.Track) + .WithMany(track => track.Tags) + .HasForeignKey(trackTag => trackTag.TrackId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasIndex(trackTag => new { trackTag.TrackId, trackTag.Tag }) + .IsUnique(); + + modelBuilder.Entity() + .HasOne(follow => follow.Follower) + .WithMany() + .HasForeignKey(follow => follow.FollowerId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(follow => follow.Followed) + .WithMany() + .HasForeignKey(follow => follow.FollowedId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasIndex(follow => new { follow.FollowerId, follow.FollowedId }) + .IsUnique(); + + var playListProcessing = modelBuilder.Entity(); + playListProcessing.HasKey(state => state.CorrelationId); + playListProcessing.Property(state => state.CorrelationId).ValueGeneratedNever(); + playListProcessing.Property(state => state.CurrentState).HasMaxLength(64); + + var albumProcessing = modelBuilder.Entity(); + albumProcessing.HasKey(state => state.CorrelationId); + albumProcessing.Property(state => state.CorrelationId).ValueGeneratedNever(); + albumProcessing.Property(state => state.CurrentState).HasMaxLength(64); + } +} diff --git a/backend/Tests/Musify.Application.Tests/Application.Tests.csproj b/backend/Tests/Musify.Application.Tests/Application.Tests.csproj index d6f7af5..9a4f183 100644 --- a/backend/Tests/Musify.Application.Tests/Application.Tests.csproj +++ b/backend/Tests/Musify.Application.Tests/Application.Tests.csproj @@ -27,6 +27,7 @@ + diff --git a/backend/Tests/Musify.Application.Tests/TestSupport/TestDatabase.cs b/backend/Tests/Musify.Application.Tests/TestSupport/TestDatabase.cs index 081ced1..b995a40 100644 --- a/backend/Tests/Musify.Application.Tests/TestSupport/TestDatabase.cs +++ b/backend/Tests/Musify.Application.Tests/TestSupport/TestDatabase.cs @@ -3,6 +3,7 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Musify.Application.Contracts; using Musify.Domain.Entities; +using Musify.Infrastructure.Persistence; namespace Musify.Application.Tests.TestSupport; @@ -34,103 +35,7 @@ public sealed class TestDatabase : DbContext, IDatabase { base.OnModelCreating(modelBuilder); - modelBuilder.Entity().OwnsOne(track => track.Pictures); - modelBuilder.Entity().Navigation(track => track.Pictures).IsRequired(); - - modelBuilder.Entity().OwnsOne(track => track.Audio); - modelBuilder.Entity().Navigation(track => track.Audio).IsRequired(); - - modelBuilder.Entity() - .HasOne(track => track.Owner) - .WithMany() - .HasForeignKey(track => track.OwnerUserId) - .OnDelete(DeleteBehavior.Restrict); - - modelBuilder.Entity().OwnsOne(playList => playList.Pictures, pictures => - { - pictures.Property(p => p.OriginalName).IsRequired(false); - pictures.Property(p => p.SmallName).IsRequired(false); - pictures.Property(p => p.MediumName).IsRequired(false); - pictures.Property(p => p.LargeName).IsRequired(false); - }); - modelBuilder.Entity().Navigation(playList => playList.Pictures).IsRequired(false); - - modelBuilder.Entity().OwnsOne(album => album.Pictures); - modelBuilder.Entity().Navigation(album => album.Pictures).IsRequired(false); - - modelBuilder.Entity() - .HasOne(album => album.Owner) - .WithMany() - .HasForeignKey(album => album.OwnerUserId) - .OnDelete(DeleteBehavior.Restrict); - - modelBuilder.Entity() - .HasOne(albumTrack => albumTrack.Album) - .WithMany(album => album.AlbumTracks) - .HasForeignKey(albumTrack => albumTrack.AlbumId) - .OnDelete(DeleteBehavior.Cascade); - - modelBuilder.Entity() - .HasOne(albumTrack => albumTrack.Track) - .WithMany(track => track.AlbumTracks) - .HasForeignKey(albumTrack => albumTrack.TrackId) - .OnDelete(DeleteBehavior.Cascade); - - modelBuilder.Entity() - .HasIndex(albumTrack => new { albumTrack.AlbumId, albumTrack.TrackId }) - .IsUnique(); - - modelBuilder.Entity() - .HasIndex(playListTrack => new { playListTrack.PlayListId, playListTrack.TrackId }) - .IsUnique(); - - modelBuilder.Entity() - .HasIndex(mix => new { mix.UserId, mix.Position }); - - modelBuilder.Entity() - .HasOne(item => item.Mix) - .WithMany(mix => mix.Items) - .HasForeignKey(item => item.MixId) - .OnDelete(DeleteBehavior.Cascade); - - modelBuilder.Entity() - .HasOne(item => item.Track) - .WithMany() - .HasForeignKey(item => item.TrackId) - .OnDelete(DeleteBehavior.Cascade); - - modelBuilder.Entity() - .HasIndex(item => new { item.MixId, item.Position }); - - modelBuilder.Entity() - .HasIndex(like => new { like.UserId, like.TrackId }) - .IsUnique(); - - modelBuilder.Entity() - .HasOne(trackTag => trackTag.Track) - .WithMany(track => track.Tags) - .HasForeignKey(trackTag => trackTag.TrackId) - .OnDelete(DeleteBehavior.Cascade); - - modelBuilder.Entity() - .HasIndex(trackTag => new { trackTag.TrackId, trackTag.Tag }) - .IsUnique(); - - modelBuilder.Entity() - .HasOne(follow => follow.Follower) - .WithMany() - .HasForeignKey(follow => follow.FollowerId) - .OnDelete(DeleteBehavior.Restrict); - - modelBuilder.Entity() - .HasOne(follow => follow.Followed) - .WithMany() - .HasForeignKey(follow => follow.FollowedId) - .OnDelete(DeleteBehavior.Restrict); - - modelBuilder.Entity() - .HasIndex(follow => new { follow.FollowerId, follow.FollowedId }) - .IsUnique(); + ModelConfiguration.Apply(modelBuilder); } public DbSet Users => Set();