feat(backend): add source, external id and artist to tracks

This commit is contained in:
2026-07-16 01:05:28 +02:00
parent 49b7d54e58
commit 87997d90a4
7 changed files with 829 additions and 2 deletions
@@ -1,10 +1,15 @@
using Musify.Domain.Entities; using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
namespace Musify.Application.Tracks.Responses namespace Musify.Application.Tracks.Responses
{ {
public record TrackApplicationResponse( public record TrackApplicationResponse(
Guid Id, Guid Id,
string Title, string Title,
string Artist,
TrackSource Source,
string ExternalId,
ProcessingStatus AudioStatus,
int Duration, int Duration,
int ListensCount, int ListensCount,
DateTime CreatedAt, DateTime CreatedAt,
@@ -14,6 +19,10 @@ namespace Musify.Application.Tracks.Responses
new( new(
track.Id, track.Id,
track.Title, track.Title,
track.Artist,
track.Source,
track.ExternalId,
track.AudioTranscodeProcessingStatus,
track.Duration, track.Duration,
listensCount, listensCount,
track.CreatedAt, track.CreatedAt,
+11
View File
@@ -23,6 +23,17 @@ namespace Musify.Domain.Entities
[Required] [Required]
public int Duration { get; set; } public int Duration { get; set; }
[Required]
[MaxLength(200)]
public string Artist { get; set; } = string.Empty;
[Required]
public TrackSource Source { get; set; } = TrackSource.Local;
[Required]
[MaxLength(16)]
public string ExternalId { get; set; } = string.Empty;
[Required] [Required]
[MaxLength(64)] [MaxLength(64)]
public required string OriginalPictureName { get; set; } public required string OriginalPictureName { get; set; }
@@ -0,0 +1,8 @@
namespace Musify.Domain.ValueObjects
{
public enum TrackSource
{
Local = 0,
YouTube = 1
}
}
@@ -30,6 +30,11 @@ namespace Musify.Infrastructure.Persistence
trackProcessing.Property(state => state.CorrelationId).ValueGeneratedNever(); trackProcessing.Property(state => state.CorrelationId).ValueGeneratedNever();
trackProcessing.Property(state => state.CurrentState).HasMaxLength(64); trackProcessing.Property(state => state.CurrentState).HasMaxLength(64);
modelBuilder.Entity<Track>()
.HasIndex(track => new { track.Source, track.ExternalId })
.IsUnique()
.HasFilter("\"ExternalId\" <> ''");
var playListProcessing = modelBuilder.Entity<PlayListProcessingState>(); var playListProcessing = modelBuilder.Entity<PlayListProcessingState>();
playListProcessing.HasKey(state => state.CorrelationId); playListProcessing.HasKey(state => state.CorrelationId);
playListProcessing.Property(state => state.CorrelationId).ValueGeneratedNever(); playListProcessing.Property(state => state.CorrelationId).ValueGeneratedNever();
@@ -0,0 +1,713 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Musify.Infrastructure.Persistence;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Musify.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(Database))]
[Migration("20260715193951_AddTrackSourceArtistExternalId")]
partial class AddTrackSourceArtistExternalId
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime?>("Consumed")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("ConsumerId")
.HasColumnType("uuid");
b.Property<DateTime?>("Delivered")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("timestamp with time zone");
b.Property<long?>("LastSequenceNumber")
.HasColumnType("bigint");
b.Property<Guid>("LockId")
.HasColumnType("uuid");
b.Property<Guid>("MessageId")
.HasColumnType("uuid");
b.Property<int>("ReceiveCount")
.HasColumnType("integer");
b.Property<DateTime>("Received")
.HasColumnType("timestamp with time zone");
b.Property<byte[]>("RowVersion")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("bytea");
b.HasKey("Id");
b.HasIndex("Delivered");
b.ToTable("InboxState");
});
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b =>
{
b.Property<long>("SequenceNumber")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("SequenceNumber"));
b.Property<string>("Body")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<Guid?>("ConversationId")
.HasColumnType("uuid");
b.Property<Guid?>("CorrelationId")
.HasColumnType("uuid");
b.Property<string>("DestinationAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("EnqueueTime")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("FaultAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Headers")
.HasColumnType("text");
b.Property<Guid?>("InboxConsumerId")
.HasColumnType("uuid");
b.Property<Guid?>("InboxMessageId")
.HasColumnType("uuid");
b.Property<Guid?>("InitiatorId")
.HasColumnType("uuid");
b.Property<Guid>("MessageId")
.HasColumnType("uuid");
b.Property<string>("MessageType")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("OutboxId")
.HasColumnType("uuid");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<Guid?>("RequestId")
.HasColumnType("uuid");
b.Property<string>("ResponseAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime>("SentTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("SourceAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("SequenceNumber");
b.HasIndex("EnqueueTime");
b.HasIndex("ExpirationTime");
b.HasIndex("OutboxId", "SequenceNumber")
.IsUnique();
b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber")
.IsUnique();
b.ToTable("OutboxMessage");
});
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b =>
{
b.Property<Guid>("OutboxId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("Delivered")
.HasColumnType("timestamp with time zone");
b.Property<long?>("LastSequenceNumber")
.HasColumnType("bigint");
b.Property<Guid>("LockId")
.HasColumnType("uuid");
b.Property<byte[]>("RowVersion")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("bytea");
b.HasKey("OutboxId");
b.HasIndex("Created");
b.ToTable("OutboxState");
});
modelBuilder.Entity("Musify.Domain.Entities.ListeningHistory", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("ListenedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("TrackId")
.HasColumnType("uuid");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("TrackId");
b.HasIndex("UserId");
b.ToTable("ListeningHistory");
});
modelBuilder.Entity("Musify.Domain.Entities.PlayList", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LargePictureName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<int>("LifeCycleStatus")
.HasColumnType("integer");
b.Property<string>("MediumPictureName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("OriginalPictureName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("SmallPictureName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("PlayLists");
});
modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("AddedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("PlayListId")
.HasColumnType("uuid");
b.Property<int>("Position")
.HasColumnType("integer");
b.Property<Guid>("TrackId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("PlayListId");
b.HasIndex("TrackId");
b.ToTable("PlayListHasTrack");
});
modelBuilder.Entity("Musify.Domain.Entities.Track", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Artist")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("AudioFolderName")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<int>("AudioTranscodeProcessingStatus")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("Duration")
.HasColumnType("integer");
b.Property<string>("ExternalId")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("character varying(16)");
b.Property<string>("LargePictureName")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime?>("LastRetryAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("LifeCycleStatus")
.HasColumnType("integer");
b.Property<string>("MediumPictureName")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("NormalizedTitle")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("OriginalAudioName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("OriginalPictureName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<int>("PicturesProcessingStatus")
.HasColumnType("integer");
b.Property<int>("RetryCount")
.HasColumnType("integer");
b.Property<string>("SmallPictureName")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<int>("Source")
.HasColumnType("integer");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("Source", "ExternalId")
.IsUnique()
.HasFilter("\"ExternalId\" <> ''");
b.ToTable("Tracks");
});
modelBuilder.Entity("Musify.Domain.Entities.Upload", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Bucket")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ContentType")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("text");
b.Property<int>("State")
.HasColumnType("integer");
b.HasKey("Id");
b.ToTable("Upload");
});
modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Bucket")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<long?>("ExpectedSizeBytes")
.HasColumnType("bigint");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("ObjectName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<int>("Purpose")
.HasColumnType("integer");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UploadIntents");
});
modelBuilder.Entity("Musify.Domain.Entities.User", b =>
{
b.Property<long>("Id")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("FirstName")
.HasMaxLength(48)
.HasColumnType("character varying(48)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(48)
.HasColumnType("character varying(48)");
b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(48)
.HasColumnType("character varying(48)");
b.Property<string>("ProfilePictureUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("SecondName")
.HasMaxLength(48)
.HasColumnType("character varying(48)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("User");
});
modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("TrackId")
.HasColumnType("uuid");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("TrackId");
b.HasIndex("UserId");
b.ToTable("UserHasTrack");
});
modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.PlayListProcessingState", b =>
{
b.Property<Guid>("CorrelationId")
.HasColumnType("uuid");
b.Property<string>("Bucket")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("CurrentState")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("PictureKey")
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("CorrelationId");
b.ToTable("PlayListProcessingState");
});
modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.TrackProcessingState", b =>
{
b.Property<Guid>("CorrelationId")
.HasColumnType("uuid");
b.Property<string>("AudioKey")
.HasColumnType("text");
b.Property<bool>("AudioProcessed")
.HasColumnType("boolean");
b.Property<string>("Bucket")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("CurrentState")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("PictureKey")
.HasColumnType("text");
b.Property<bool>("PictureProcessed")
.HasColumnType("boolean");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("CorrelationId");
b.ToTable("TrackProcessingState");
});
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b =>
{
b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null)
.WithMany()
.HasForeignKey("OutboxId");
b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null)
.WithMany()
.HasForeignKey("InboxMessageId", "InboxConsumerId")
.HasPrincipalKey("MessageId", "ConsumerId");
});
modelBuilder.Entity("Musify.Domain.Entities.ListeningHistory", b =>
{
b.HasOne("Musify.Domain.Entities.Track", "Track")
.WithMany("ListeningHistories")
.HasForeignKey("TrackId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Musify.Domain.Entities.User", "User")
.WithMany("ListeningHistories")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Track");
b.Navigation("User");
});
modelBuilder.Entity("Musify.Domain.Entities.PlayList", b =>
{
b.HasOne("Musify.Domain.Entities.User", "User")
.WithMany("PlayLists")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b =>
{
b.HasOne("Musify.Domain.Entities.PlayList", "PlayList")
.WithMany("PlayListTracks")
.HasForeignKey("PlayListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Musify.Domain.Entities.Track", "Track")
.WithMany("PlayListTracks")
.HasForeignKey("TrackId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("PlayList");
b.Navigation("Track");
});
modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b =>
{
b.HasOne("Musify.Domain.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b =>
{
b.HasOne("Musify.Domain.Entities.Track", "Track")
.WithMany("UserTracks")
.HasForeignKey("TrackId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Musify.Domain.Entities.User", "User")
.WithMany("UserTracks")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Track");
b.Navigation("User");
});
modelBuilder.Entity("Musify.Domain.Entities.PlayList", b =>
{
b.Navigation("PlayListTracks");
});
modelBuilder.Entity("Musify.Domain.Entities.Track", b =>
{
b.Navigation("ListeningHistories");
b.Navigation("PlayListTracks");
b.Navigation("UserTracks");
});
modelBuilder.Entity("Musify.Domain.Entities.User", b =>
{
b.Navigation("ListeningHistories");
b.Navigation("PlayLists");
b.Navigation("UserTracks");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,64 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddTrackSourceArtistExternalId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Artist",
table: "Tracks",
type: "character varying(200)",
maxLength: 200,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "ExternalId",
table: "Tracks",
type: "character varying(16)",
maxLength: 16,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<int>(
name: "Source",
table: "Tracks",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "IX_Tracks_Source_ExternalId",
table: "Tracks",
columns: new[] { "Source", "ExternalId" },
unique: true,
filter: "\"ExternalId\" <> ''");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Tracks_Source_ExternalId",
table: "Tracks");
migrationBuilder.DropColumn(
name: "Artist",
table: "Tracks");
migrationBuilder.DropColumn(
name: "ExternalId",
table: "Tracks");
migrationBuilder.DropColumn(
name: "Source",
table: "Tracks");
}
}
}
@@ -307,6 +307,11 @@ namespace Musify.Infrastructure.Persistence.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<string>("Artist")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("AudioFolderName") b.Property<string>("AudioFolderName")
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
@@ -320,6 +325,11 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.Property<int>("Duration") b.Property<int>("Duration")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("ExternalId")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("character varying(16)");
b.Property<string>("LargePictureName") b.Property<string>("LargePictureName")
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
@@ -359,6 +369,9 @@ namespace Musify.Infrastructure.Persistence.Migrations
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<int>("Source")
.HasColumnType("integer");
b.Property<string>("Title") b.Property<string>("Title")
.IsRequired() .IsRequired()
.HasMaxLength(50) .HasMaxLength(50)
@@ -369,6 +382,10 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("Source", "ExternalId")
.IsUnique()
.HasFilter("\"ExternalId\" <> ''");
b.ToTable("Tracks"); b.ToTable("Tracks");
}); });