Separate responsibilities: start-listening command, API-side JSON converters, bulk track endpoints, mix kinds

This commit is contained in:
2026-09-24 12:14:53 +02:00
parent 75d0c0817e
commit 0fa033faff
64 changed files with 2903 additions and 651 deletions
@@ -1,58 +0,0 @@
using ErrorOr;
using Mediator;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Musify.Application.Contracts;
using Musify.Application.Shared;
using Musify.Domain.Entities;
namespace Musify.Application.Albums;
public record AddTrackToAlbumCommand(long UserId, Guid AlbumId, Guid TrackId)
: ICommand<ErrorOr<Success>>;
public class AddTrackToAlbumCommandHandler(
IDatabase database,
ILogger<AddTrackToAlbumCommandHandler> logger)
: ICommandHandler<AddTrackToAlbumCommand, ErrorOr<Success>>
{
public async ValueTask<ErrorOr<Success>> Handle(AddTrackToAlbumCommand request, CancellationToken cancellationToken)
{
var album = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken);
if (album.IsError)
return album.Errors;
var track = await database.Tracks.FindOwnedAsync(request.TrackId, request.UserId, cancellationToken);
if (track.IsError)
return track.Errors;
var alreadyAdded = await database.AlbumHasTracks
.AsNoTracking()
.AnyAsync(albumTrack => albumTrack.AlbumId == request.AlbumId && albumTrack.TrackId == request.TrackId, cancellationToken);
if (alreadyAdded)
return AlreadyInAlbum;
// Album track numbers are user-visible, so they start at 1.
var nextTrackNumber = (await database.AlbumHasTracks
.Where(albumTrack => albumTrack.AlbumId == request.AlbumId)
.MaxAsync(albumTrack => (int?)albumTrack.TrackNumber, cancellationToken) ?? 0) + 1;
await database.AlbumHasTracks.AddAsync(new AlbumHasTrack
{
AlbumId = request.AlbumId,
TrackId = request.TrackId,
TrackNumber = nextTrackNumber
}, cancellationToken);
var saved = await database.TrySaveChangesAsync(AlreadyInAlbum, cancellationToken);
if (saved.IsError)
return saved;
logger.LogInformation("Added track {TrackId} to album {AlbumId} as number {TrackNumber}",
request.TrackId, request.AlbumId, nextTrackNumber);
return Result.Success;
}
private static Error AlreadyInAlbum => AppErrors.Conflict("Album.TrackAlreadyAdded", "Track is already in the album.");
}
@@ -0,0 +1,77 @@
using ErrorOr;
using Mediator;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Musify.Application.Contracts;
using Musify.Application.Shared;
using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
namespace Musify.Application.Albums;
/// <summary>Appends your own tracks to an album in the given order, skipping the ones it already has.</summary>
public record AddTracksToAlbumCommand(long UserId, Guid AlbumId, IReadOnlyList<Guid> TrackIds)
: ICommand<ErrorOr<Success>>;
public class AddTracksToAlbumCommandHandler(
IDatabase database,
ILogger<AddTracksToAlbumCommandHandler> logger)
: ICommandHandler<AddTracksToAlbumCommand, ErrorOr<Success>>
{
public async ValueTask<ErrorOr<Success>> Handle(AddTracksToAlbumCommand request, CancellationToken cancellationToken)
{
var album = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken);
if (album.IsError)
return album.Errors;
var requestedIds = request.TrackIds.Distinct().ToList();
var tracks = await database.Tracks
.AsNoTracking()
.Where(track => requestedIds.Contains(track.Id) && track.LifeCycleStatus == LifeCycleStatus.Active)
.Select(track => new { track.Id, track.OwnerUserId })
.ToListAsync(cancellationToken);
var missing = requestedIds.FirstOrDefault(id => tracks.All(track => track.Id != id));
if (missing != Guid.Empty)
return AppErrors.NotFound("Track", missing);
var foreign = tracks.FirstOrDefault(track => track.OwnerUserId != request.UserId);
if (foreign != null)
return AppErrors.Forbidden("Track", foreign.Id);
var alreadyInAlbum = await database.AlbumHasTracks
.AsNoTracking()
.Where(link => link.AlbumId == request.AlbumId && requestedIds.Contains(link.TrackId))
.Select(link => link.TrackId)
.ToListAsync(cancellationToken);
var toAdd = requestedIds.Where(id => !alreadyInAlbum.Contains(id)).ToList();
if (toAdd.Count == 0)
return Result.Success;
// Album track numbers are user-visible, so they start at 1.
var nextTrackNumber = (await database.AlbumHasTracks
.Where(link => link.AlbumId == request.AlbumId)
.MaxAsync(link => (int?)link.TrackNumber, cancellationToken) ?? 0) + 1;
foreach (var trackId in toAdd)
{
await database.AlbumHasTracks.AddAsync(new AlbumHasTrack
{
AlbumId = request.AlbumId,
TrackId = trackId,
TrackNumber = nextTrackNumber++
}, cancellationToken);
}
var saved = await database.TrySaveChangesAsync(
AppErrors.Conflict("Album.ConcurrentChange", "The album changed while adding tracks. Try again."), cancellationToken);
if (saved.IsError)
return saved;
logger.LogInformation("Added {Count} tracks to album {AlbumId}", toAdd.Count, request.AlbumId);
return Result.Success;
}
}
@@ -1,5 +1,3 @@
using System.Text.Json.Serialization;
using Musify.Application.Serialization;
using Musify.Domain.Entities;
namespace Musify.Application.Albums.Responses;
@@ -9,7 +7,7 @@ public record AlbumApplicationResponse(
string Title,
string? Description,
int? ReleaseYear,
[property: JsonConverter(typeof(LongAsStringConverter))] long OwnerUserId,
long OwnerUserId,
int TrackCount,
string? SmallImageKeyName,
string? MediumImageKeyName,
@@ -16,8 +16,6 @@ public interface IDatabase
public DbSet<Track> Tracks { get; }
public DbSet<UserHasTrack> UserHasTracks { get; }
public DbSet<PlayListHasTrack> PlayListHasTracks { get; }
public DbSet<Mix> Mixes { get; }
@@ -15,7 +15,7 @@ public class GetGenresQueryHandler(IDatabase database)
{
var rows = await database.TrackTags
.AsNoTracking()
.Where(tag => database.UserHasTracks.Any(userTrack => userTrack.TrackId == tag.TrackId && userTrack.Track.LifeCycleStatus == LifeCycleStatus.Active))
.Where(tag => tag.Track.LifeCycleStatus == LifeCycleStatus.Active)
.GroupBy(tag => tag.Tag)
.Select(group => new { Genre = group.Key, TrackCount = group.Select(tag => tag.TrackId).Distinct().Count() })
.ToListAsync(cancellationToken);
@@ -18,16 +18,16 @@ public class GenerateMixesForUserCommandHandler(
ILogger<GenerateMixesForUserCommandHandler> logger)
: ICommandHandler<GenerateMixesForUserCommand, ErrorOr<Success>>
{
private sealed record MixDraft(string Title, string? Subtitle, IReadOnlyList<Guid> TrackIds);
private sealed record MixDraft(MixKind Kind, IReadOnlyList<Guid> TrackIds);
public async ValueTask<ErrorOr<Success>> Handle(GenerateMixesForUserCommand request, CancellationToken cancellationToken)
{
var userId = request.UserId;
var libraryTrackIds = await database.UserHasTracks
var libraryTrackIds = await database.Tracks
.AsNoTracking()
.Where(userTrack => userTrack.UserId == userId && userTrack.Track.LifeCycleStatus == LifeCycleStatus.Active)
.Select(userTrack => userTrack.TrackId)
.Where(track => track.OwnerUserId == userId && track.LifeCycleStatus == LifeCycleStatus.Active)
.Select(track => track.Id)
.ToListAsync(cancellationToken);
if (libraryTrackIds.Count == 0)
@@ -51,19 +51,13 @@ public class GenerateMixesForUserCommandHandler(
var discovery = Take(Shuffle(unheardTrackIds));
if (discovery.Count > 0)
{
drafts.Add(new MixDraft(
"Descubrimiento",
"Canciones de tu biblioteca que todavía no has escuchado.",
discovery));
drafts.Add(new MixDraft(MixKind.Discovery, discovery));
}
var daily = Take(Interleave(Shuffle(historyTrackIds), Shuffle(unheardTrackIds)));
if (daily.Count > 0)
{
drafts.Add(new MixDraft(
"Tu mezcla diaria",
"Lo que más escuchas, con alguna sorpresa de tu biblioteca.",
daily));
drafts.Add(new MixDraft(MixKind.Daily, daily));
}
if (drafts.Count == 0)
@@ -108,12 +102,6 @@ public class GenerateMixesForUserCommandHandler(
if (previousMixes.Count > 0)
{
var previousIds = previousMixes.Select(mix => mix.Id).ToList();
var previousItems = await database.MixItems
.Where(item => previousIds.Contains(item.MixId))
.ToListAsync(cancellationToken);
database.MixItems.RemoveRange(previousItems);
database.Mixes.RemoveRange(previousMixes);
}
@@ -123,8 +111,7 @@ public class GenerateMixesForUserCommandHandler(
var mix = new Mix
{
UserId = userId,
Title = draft.Title,
Subtitle = draft.Subtitle,
Kind = draft.Kind,
Position = position
};
@@ -26,6 +26,6 @@ public class GetMixByIdQueryHandler(IDatabase database)
var orderedItems = mix.Items.OrderBy(item => item.Position).ToList();
var items = await MixItemMapper.ToResponsesAsync(database, orderedItems, cancellationToken);
return new MixApplicationResponse(mix.Id, mix.Title, mix.Subtitle, items.Count, items);
return new MixApplicationResponse(mix.Id, mix.Kind, items.Count, items);
}
}
@@ -42,7 +42,7 @@ public class GetMixesByUserIdQueryHandler(IDatabase database)
.Select(item => coverResponses[item.TrackId])
.ToList();
responses.Add(new MixApplicationResponse(mix.Id, mix.Title, mix.Subtitle, orderedItems.Count, covers));
responses.Add(new MixApplicationResponse(mix.Id, mix.Kind, orderedItems.Count, covers));
}
return responses;
@@ -1,9 +1,10 @@
using Musify.Domain.ValueObjects;
namespace Musify.Application.Mixes.Responses;
public record MixApplicationResponse(
Guid Id,
string Title,
string? Subtitle,
MixKind Kind,
int ItemCount,
IReadOnlyList<MixItemApplicationResponse> Items)
{
@@ -1,61 +0,0 @@
using ErrorOr;
using Mediator;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Musify.Application.Contracts;
using Musify.Application.Shared;
using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
namespace Musify.Application.PlayLists;
public record AddTrackToPlayListCommand(long UserId, Guid PlayListId, Guid TrackId)
: ICommand<ErrorOr<Success>>;
public class AddTrackToPlayListCommandHandler(
IDatabase database,
ILogger<AddTrackToPlayListCommandHandler> logger)
: ICommandHandler<AddTrackToPlayListCommand, ErrorOr<Success>>
{
public async ValueTask<ErrorOr<Success>> Handle(AddTrackToPlayListCommand request, CancellationToken cancellationToken)
{
var playList = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken);
if (playList.IsError)
return playList.Errors;
var trackExists = await database.Tracks
.AsNoTracking()
.AnyAsync(t => t.Id == request.TrackId && t.LifeCycleStatus == LifeCycleStatus.Active, cancellationToken);
if (!trackExists)
return AppErrors.NotFound("Track", request.TrackId);
var alreadyAdded = await database.PlayListHasTracks
.AsNoTracking()
.AnyAsync(plt => plt.PlayListId == request.PlayListId && plt.TrackId == request.TrackId, cancellationToken);
if (alreadyAdded)
return AlreadyInPlayList;
// Playlist positions are internal indexes, so they start at 0.
var nextPosition = (await database.PlayListHasTracks
.Where(plt => plt.PlayListId == request.PlayListId)
.MaxAsync(plt => (int?)plt.Position, cancellationToken) ?? -1) + 1;
await database.PlayListHasTracks.AddAsync(new PlayListHasTrack
{
PlayListId = request.PlayListId,
TrackId = request.TrackId,
Position = nextPosition
}, cancellationToken);
var saved = await database.TrySaveChangesAsync(AlreadyInPlayList, cancellationToken);
if (saved.IsError)
return saved;
logger.LogInformation("Added track {TrackId} to playlist {PlayListId} at position {Position}",
request.TrackId, request.PlayListId, nextPosition);
return Result.Success;
}
private static Error AlreadyInPlayList => AppErrors.Conflict("PlayList.TrackAlreadyAdded", "Track is already in the playlist.");
}
@@ -0,0 +1,73 @@
using ErrorOr;
using Mediator;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Musify.Application.Contracts;
using Musify.Application.Shared;
using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
namespace Musify.Application.PlayLists;
/// <summary>Appends tracks to a playlist in the given order, skipping the ones it already has.</summary>
public record AddTracksToPlayListCommand(long UserId, Guid PlayListId, IReadOnlyList<Guid> TrackIds)
: ICommand<ErrorOr<Success>>;
public class AddTracksToPlayListCommandHandler(
IDatabase database,
ILogger<AddTracksToPlayListCommandHandler> logger)
: ICommandHandler<AddTracksToPlayListCommand, ErrorOr<Success>>
{
public async ValueTask<ErrorOr<Success>> Handle(AddTracksToPlayListCommand request, CancellationToken cancellationToken)
{
var playList = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken);
if (playList.IsError)
return playList.Errors;
var requestedIds = request.TrackIds.Distinct().ToList();
var existingIds = await database.Tracks
.AsNoTracking()
.Where(track => requestedIds.Contains(track.Id) && track.LifeCycleStatus == LifeCycleStatus.Active)
.Select(track => track.Id)
.ToListAsync(cancellationToken);
var missing = requestedIds.FirstOrDefault(id => !existingIds.Contains(id));
if (missing != Guid.Empty)
return AppErrors.NotFound("Track", missing);
var alreadyInPlayList = await database.PlayListHasTracks
.AsNoTracking()
.Where(link => link.PlayListId == request.PlayListId && requestedIds.Contains(link.TrackId))
.Select(link => link.TrackId)
.ToListAsync(cancellationToken);
var toAdd = requestedIds.Where(id => !alreadyInPlayList.Contains(id)).ToList();
if (toAdd.Count == 0)
return Result.Success;
// Playlist positions are internal indexes, so they start at 0.
var nextPosition = (await database.PlayListHasTracks
.Where(link => link.PlayListId == request.PlayListId)
.MaxAsync(link => (int?)link.Position, cancellationToken) ?? -1) + 1;
foreach (var trackId in toAdd)
{
await database.PlayListHasTracks.AddAsync(new PlayListHasTrack
{
PlayListId = request.PlayListId,
TrackId = trackId,
Position = nextPosition++
}, cancellationToken);
}
var saved = await database.TrySaveChangesAsync(
AppErrors.Conflict("PlayList.ConcurrentChange", "The playlist changed while adding tracks. Try again."), cancellationToken);
if (saved.IsError)
return saved;
logger.LogInformation("Added {Count} tracks to playlist {PlayListId}", toAdd.Count, request.PlayListId);
return Result.Success;
}
}
@@ -1,8 +1,6 @@
using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
using System.Text.Json.Serialization;
using Musify.Application.Serialization;
namespace Musify.Application.PlayLists.Responses;
@@ -17,7 +15,7 @@ public record PlayListApplicationResponse(
DateTime CreatedAt,
DateTime UpdatedAt,
IReadOnlyList<Guid> CoverTrackIds,
[property: JsonConverter(typeof(LongAsStringConverter))] long OwnerUserId,
long OwnerUserId,
int TrackCount,
double DurationSeconds)
{
@@ -1,37 +0,0 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Musify.Application.Serialization;
public sealed class LongAsStringConverter : JsonConverter<long>
{
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
reader.TokenType == JsonTokenType.String
? long.Parse(reader.GetString()!, CultureInfo.InvariantCulture)
: reader.GetInt64();
public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options) =>
writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture));
}
public sealed class NullableLongAsStringConverter : JsonConverter<long?>
{
public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return null;
return reader.TokenType == JsonTokenType.String
? string.IsNullOrEmpty(reader.GetString()) ? null : long.Parse(reader.GetString()!, CultureInfo.InvariantCulture)
: reader.GetInt64();
}
public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options)
{
if (value.HasValue)
writer.WriteStringValue(value.Value.ToString(CultureInfo.InvariantCulture));
else
writer.WriteNullValue();
}
}
@@ -1,28 +1,20 @@
using Musify.Application.Configuration;
using Musify.Application.Contracts;
using Musify.Application.Tracks.Responses;
using Musify.Domain.Entities;
namespace Musify.Application.Services;
/// <summary>Builds the stream URL and signed ticket for a processed track. It never touches the database.</summary>
public sealed class TrackStreamIssuer(
IDatabase database,
IStreamTicketService ticketService,
TrackConfiguration trackConfiguration,
StreamGatewayConfiguration streamGatewayConfiguration,
PlaybackConfiguration playbackConfiguration)
{
public async Task<TrackStreamResponse> IssueAsync(
Guid trackId,
string audioFolderName,
long? userId,
CancellationToken cancellationToken)
public TrackStreamResponse Issue(string audioFolderName, long? userId, Guid? listenId)
{
var folderPath = trackConfiguration.Routes.BuildProcessedAudioPath(audioFolderName);
var keyPrefix = $"{folderPath}/";
var maxBytes = AnonymousFragmentBytes(userId);
var ticket = ticketService.IssueTicket(userId, keyPrefix, maxBytes);
var ticket = ticketService.IssueTicket(userId, $"{folderPath}/", AnonymousFragmentBytes(userId));
var manifestUrl = string.Join('/',
streamGatewayConfiguration.PublicBaseUrl.TrimEnd('/'),
@@ -30,19 +22,6 @@ public sealed class TrackStreamIssuer(
folderPath,
streamGatewayConfiguration.AudioFileName);
Guid? listenId = null;
if (userId != null)
{
var newListeningHistory = new ListeningHistory
{
UserId = userId.Value,
TrackId = trackId,
};
await database.ListeningHistories.AddAsync(newListeningHistory, cancellationToken);
await database.SaveChangesAsync(cancellationToken);
listenId = newListeningHistory.Id;
}
return new TrackStreamResponse(manifestUrl, ticket.Token, ticket.ExpiresInSeconds, listenId);
}
@@ -41,21 +41,6 @@ public class CreateTrackCommandHandler(
}
var distinctTags = request.Tags.Distinct().ToList();
if (distinctTags.Count == 0)
{
logger.LogWarning("Track {Title} was submitted without any tags", request.Title);
return Error.Validation(description: "At least one tag is required.");
}
var tagConflicts = GenreCompatibility.FindConflicts(distinctTags);
if (tagConflicts.Count > 0)
{
var conflict = tagConflicts.First();
logger.LogWarning(
"Track {Title} was submitted with incompatible tags {First} and {Second}",
request.Title, conflict.First, conflict.Second);
return Error.Validation(description: $"Tags '{conflict.First}' and '{conflict.Second}' are not compatible.");
}
if (request.PictureIntentId == request.AudioIntentId)
return Error.Validation(description: "Picture and audio must use different upload intents.");
@@ -105,11 +90,6 @@ public class CreateTrackCommandHandler(
await database.Tracks.AddAsync(trackEntity, cancellationToken);
await database.UserHasTracks.AddAsync(new UserHasTrack
{
UserId = request.UserId,
TrackId = trackEntity.Id
}, cancellationToken);
var sizes = trackConfiguration.PicturesSizes.ToImageSizes(trackConfiguration.Routes);
await eventBus.PublishAsync(
@@ -17,9 +17,6 @@ public class RecordListeningProgressCommandHandler(IDatabase database)
{
public async ValueTask<ErrorOr<Success>> Handle(RecordListeningProgressCommand request, CancellationToken cancellationToken)
{
if (!double.IsFinite(request.PlayedSeconds) || request.PlayedSeconds < 0)
return Error.Validation(description: "playedSeconds must be a non-negative number.");
var listen = await database.ListeningHistories
.Include(l => l.Track)
.SingleOrDefaultAsync(l => l.Id == request.ListenId && l.UserId == request.UserId, cancellationToken);
@@ -1,5 +1,3 @@
using System.Text.Json.Serialization;
using Musify.Application.Serialization;
using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
@@ -14,7 +12,7 @@ public record TrackApplicationResponse(
int ListensCount,
DateTime CreatedAt,
DateTime UpdatedAt,
[property: JsonConverter(typeof(LongAsStringConverter))] long OwnerUserId,
long OwnerUserId,
IReadOnlyCollection<Genre> Tags,
bool IsExplicit)
{
@@ -5,24 +5,25 @@ using Microsoft.Extensions.Logging;
using Musify.Application.Configuration;
using Musify.Application.Contracts;
using Musify.Application.Services;
using Musify.Application.Tracks.Responses;
using Musify.Domain.ValueObjects;
using Musify.Application.Shared;
using Musify.Application.Tracks.Responses;
using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
namespace Musify.Application.Tracks;
/// <summary>Starts a listen: records it for signed-in users and issues the stream ticket.</summary>
public record StartListeningCommand(Guid TrackId, long? UserId)
: ICommand<ErrorOr<TrackStreamResponse>>;
public record GetTrackStreamQuery(Guid TrackId, long? UserId)
: IQuery<ErrorOr<TrackStreamResponse>>;
public class GetTrackStreamQueryHandler(
public class StartListeningCommandHandler(
IDatabase database,
TrackStreamIssuer streamIssuer,
PlaybackConfiguration playbackConfiguration,
ILogger<GetTrackStreamQueryHandler> logger)
: IQueryHandler<GetTrackStreamQuery, ErrorOr<TrackStreamResponse>>
ILogger<StartListeningCommandHandler> logger)
: ICommandHandler<StartListeningCommand, ErrorOr<TrackStreamResponse>>
{
public async ValueTask<ErrorOr<TrackStreamResponse>> Handle(GetTrackStreamQuery request, CancellationToken cancellationToken)
public async ValueTask<ErrorOr<TrackStreamResponse>> Handle(StartListeningCommand request, CancellationToken cancellationToken)
{
if (request.UserId == null && !playbackConfiguration.AllowAnonymousListening)
return Error.Unauthorized(description: "Sign in to stream music, or ask an administrator to enable anonymous listening.");
@@ -38,10 +39,17 @@ public class GetTrackStreamQueryHandler(
if (!track.Audio.IsProcessed)
return AppErrors.Conflict("Track.AudioNotReady", "Track audio is not available for streaming yet.");
var response = await streamIssuer.IssueAsync(track.Id, track.Audio.FolderName, request.UserId, cancellationToken);
Guid? listenId = null;
if (request.UserId is { } userId)
{
var listen = new ListeningHistory { UserId = userId, TrackId = track.Id };
await database.ListeningHistories.AddAsync(listen, cancellationToken);
await database.SaveChangesAsync(cancellationToken);
listenId = listen.Id;
}
logger.LogInformation("Issued stream ticket for track {TrackId} to user {UserId}", request.TrackId, request.UserId?.ToString() ?? "(anonymous)");
return response;
return streamIssuer.Issue(track.Audio.FolderName, request.UserId, listenId);
}
}
@@ -1,11 +1,9 @@
using System.Text.Json.Serialization;
using Musify.Application.Serialization;
using Musify.Domain.Entities;
namespace Musify.Application.Users.Responses;
public record UserApplicationResponse(
[property: JsonConverter(typeof(LongAsStringConverter))] long Id,
long Id,
string Name,
string? FirstName,
string? SecondName,
@@ -1,10 +1,8 @@
using System.Text.Json.Serialization;
using Musify.Application.Serialization;
namespace Musify.Application.Users.Responses;
public record UserProfileResponse(
[property: JsonConverter(typeof(LongAsStringConverter))] long Id,
long Id,
string Name,
string? FirstName,
string? SecondName,
@@ -1,10 +1,8 @@
using System.Text.Json.Serialization;
using Musify.Application.Serialization;
namespace Musify.Application.Users.Responses;
public record UserSummaryResponse(
[property: JsonConverter(typeof(LongAsStringConverter))] long Id,
long Id,
string Name,
string? FirstName,
string? SecondName,