using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; using MassTransit; using Musify.Domain.Entities; using Musify.Domain.ValueObjects; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.MassTransit.Sagas; using AppIDatabase = Musify.Application.Contracts.IDatabase; using Musify.Application.Contracts; namespace Musify.Infrastructure.Persistence { public class Database(DatabaseConfiguration configuration) : DbContext, AppIDatabase { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseNpgsql(configuration.ConnectionString, o => o.MapEnum("genre")); } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.AddInboxStateEntity(); 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"); audio.Property(a => a.DownloadRequested).HasColumnName("DownloadRequested"); audio.Property(a => a.RetryCount).HasColumnName("RetryCount"); audio.Property(a => a.LastRetryAt).HasColumnName("LastRetryAt"); }); 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); pictures.Property(p => p.MediumName).HasColumnName("MediumPictureName").HasMaxLength(64); pictures.Property(p => p.LargeName).HasColumnName("LargePictureName").HasMaxLength(64); }); 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(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); } public DbSet Users => Set(); public DbSet Tracks => Set(); public DbSet PlayLists => Set(); public DbSet Albums => Set(); public DbSet AlbumHasTracks => Set(); public DbSet UserHasTracks => Set(); public DbSet PlayListHasTracks => Set(); public DbSet Mixes => Set(); public DbSet MixItems => Set(); public DbSet Uploads => Set(); public DbSet UploadIntents => Set(); public DbSet ListeningHistories => Set(); public DbSet TrackLikes => Set(); public DbSet TrackTags => Set(); public DbSet UserFollows => Set(); public async Task BeginTransactionAsync(System.Data.IsolationLevel isolationLevel, CancellationToken cancellationToken) { var transaction = await Database.BeginTransactionAsync(isolationLevel, cancellationToken); return new DatabaseTransaction(transaction); } private sealed class DatabaseTransaction(IDbContextTransaction inner) : IDatabaseTransaction { public Task CommitAsync(CancellationToken cancellationToken) => inner.CommitAsync(cancellationToken); public ValueTask DisposeAsync() => inner.DisposeAsync(); } } }