The original playlist settings API required full object updates, even for a single change such as toggling the "favorite" flag. This caused overfetching, unnecessary payload size, and UX issues because the frontend had to maintain and submit the full state. I refactored the endpoint to accept modular partial updates based on setting instructions, allowing targeted updates without impacting unrelated fields.
public Playlist MapToPlaylist(Playlist playlist)
{
// Updates ALL properties every time
playlist.Name = Name;
playlist.Shuffle = Shuffle;
playlist.Delay = Delay;
playlist.Repeat = Repeat;
playlist.Favorite = Favorite;
playlist.BackgroundSoundId = BackgroundSoundId;
playlist.Archived = Archived;
playlist.ModifiedDate = DateTime.UtcNow;
return playlist;
}
}
using SelfTalk.Data.Schema;
using System.Text.Json;
namespace SelfTalk.Models.Requests;
public class UpdatePlaylistSettingsRequest
{
public record PlaylistSettingRequest(SettingTypeCode SettingTypeCode, object? SettingValue);
public required List<PlaylistSettingRequest> Settings { get; set; }
public Playlist MapToPlaylist(Playlist playlist)
{
SettingTypeCode typeCode = SettingTypeCode.NotSet;
JsonElement? jsonElement = null;
try
{
foreach (var setting in Settings)
{
typeCode = setting.SettingTypeCode;
jsonElement = setting.SettingValue as JsonElement?;
if (jsonElement is null)
{
UpdateNullableSetting(playlist, typeCode);
}
else
{
UpdateSetting(playlist, typeCode, jsonElement.Value);
}
}
playlist.ModifiedDate = DateTime.UtcNow;
return playlist;
}
catch (ArgumentException)
{
throw;
}
catch (Exception ex)
{
object? value = jsonElement is not null ? jsonElement.Value : null;
var message = FormatSettingError(typeCode, value, $"Unhandled exception: {ex.Message}");
var detailedEx = new Exception(message, ex);
throw detailedEx;
}
}
private static void UpdateSetting(Playlist playlist, SettingTypeCode typeCode, JsonElement value)
{
switch (typeCode)
{
case SettingTypeCode.Name:
playlist.Name = value.ToString();
break;
case SettingTypeCode.Shuffle:
playlist.Shuffle = value.GetBoolean();
break;
case SettingTypeCode.Delay:
int delay = value.GetInt32();
if (delay < 0) throw new ArgumentException(FormatSettingError(typeCode, value, $"{nameof(playlist.Delay)} cannot be negative"));
playlist.Delay = delay;
break;
case SettingTypeCode.Repeat:
int repeat = value.GetInt32();
if (repeat < 1) throw new ArgumentException(FormatSettingError(typeCode, value, $"{nameof(playlist.Repeat)} must be at least 1"));
playlist.Repeat = repeat;
break;
case SettingTypeCode.Favorite:
playlist.Favorite = value.GetBoolean();
break;
case SettingTypeCode.Archived:
playlist.Archived = value.GetBoolean();
break;
case SettingTypeCode.BackgroundSoundId:
playlist.BackgroundSoundId = value.GetGuid();
break;
default:
throw new ArgumentException(FormatSettingError(typeCode, value, $"{nameof(SettingTypeCode)} does not exist"));
}
}
private static void UpdateNullableSetting(Playlist playlist, SettingTypeCode typeCode)
{
switch (typeCode)
{
case SettingTypeCode.BackgroundSoundId:
playlist.BackgroundSoundId = null;
break;
default:
throw new ArgumentException(FormatSettingError(typeCode, null, $"{nameof(SettingTypeCode)} does not exist or does not allow nulls"));
}
return;
}
private static string FormatSettingError(SettingTypeCode typeCode, object? value, string reason)
{
var valueDisplay = value is null ? "null" : value.ToString();
return $"Invalid setting — Type: {typeCode}, Value: {valueDisplay}. Reason: {reason}";
}
}
public enum SettingTypeCode
{
Name,
Shuffle,
Delay,
Repeat,
Favorite,
Archived,
BackgroundSoundId,
NotSet
}
Must send ALL 7 properties every time
Send only the settings you want to change
Only modify the exact properties you intend to change, leaving everything else untouched.
Smaller network requests by sending only necessary data instead of the entire object.
Frontend doesn't need to maintain full state - can make targeted updates easily.
Less data transfer, more efficient database updates, faster response times.
Enhanced validation with specific error messages per setting type and value.
Easy to add new setting types without breaking existing API contracts.