adding System.Text.Json serialiser with source generators

This commit is contained in:
andy 2021-12-24 20:59:31 +00:00
parent c0f514d2a4
commit 94f4435ad4
21 changed files with 437 additions and 22 deletions

View File

@ -15,7 +15,7 @@ namespace SpotifyAPI.Web
Assert.IsInstanceOf(typeof(SimplePaginator), defaultConfig.DefaultPaginator);
Assert.IsInstanceOf(typeof(NetHttpClient), defaultConfig.HTTPClient);
Assert.IsInstanceOf(typeof(NewtonsoftJSONSerializer), defaultConfig.JSONSerializer);
Assert.IsInstanceOf(typeof(TextJsonSerializer), defaultConfig.JSONSerializer);
Assert.AreEqual(SpotifyUrls.APIV1, defaultConfig.BaseAddress);
Assert.AreEqual(null, defaultConfig.Authenticator);
Assert.AreEqual(null, defaultConfig.HTTPLogger);
@ -32,7 +32,7 @@ namespace SpotifyAPI.Web
Assert.IsInstanceOf(typeof(SimplePaginator), defaultConfig.DefaultPaginator);
Assert.IsInstanceOf(typeof(NetHttpClient), defaultConfig.HTTPClient);
Assert.IsInstanceOf(typeof(NewtonsoftJSONSerializer), defaultConfig.JSONSerializer);
Assert.IsInstanceOf(typeof(TextJsonSerializer), defaultConfig.JSONSerializer);
Assert.AreEqual(SpotifyUrls.APIV1, defaultConfig.BaseAddress);
Assert.AreEqual(null, defaultConfig.HTTPLogger);
Assert.AreEqual(null, defaultConfig.RetryHandler);

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,8 @@ using Moq;
using NUnit.Framework;
using SpotifyAPI.Web.Http;
using System.Net.Http;
using System.Threading.Tasks;
using System.Linq;
namespace SpotifyAPI.Web.Tests
{
@ -82,9 +84,60 @@ namespace SpotifyAPI.Web.Tests
Assert.AreEqual(apiResonse.Response, response.Object);
}
public class TestResponseObject
[TestCase]
public async Task DeserializeResponse_TimeCurrentlyPlayingTestMessage()
{
public bool HelloWorld { get; set; }
var serializer = new NewtonsoftJSONSerializer();
var response = new Mock<IResponse>();
response.SetupGet(r => r.Body).Returns(ExampleResponses.CurrentlyPlayingContext);
response.SetupGet(r => r.ContentType).Returns("application/json");
var times = new List<long>();
foreach (var iter in Enumerable.Range(0, 50))
{
var watch = System.Diagnostics.Stopwatch.StartNew();
IAPIResponse<CurrentlyPlayingContext> apiResonse = serializer.DeserializeResponse<CurrentlyPlayingContext>(response.Object);
watch.Stop();
times.Add(watch.ElapsedMilliseconds);
Assert.AreEqual(apiResonse.Response, response.Object);
}
var mean = times.Sum() / 50;
using StreamWriter file = new("newtonsoft.json_test.txt", append: true);
await file.WriteLineAsync($"CurrentlyPlayingContext: {mean}ms");
}
[TestCase]
public async Task DeserializeResponse_TimeUserTestMessage()
{
var serializer = new NewtonsoftJSONSerializer();
var response = new Mock<IResponse>();
response.SetupGet(r => r.Body).Returns(ExampleResponses.PublicUser);
response.SetupGet(r => r.ContentType).Returns("application/json");
var times = new List<long>();
foreach (var iter in Enumerable.Range(0, 50))
{
var watch = System.Diagnostics.Stopwatch.StartNew();
IAPIResponse<PublicUser> apiResonse = serializer.DeserializeResponse<PublicUser>(response.Object);
watch.Stop();
times.Add(watch.ElapsedMilliseconds);
Assert.AreEqual(apiResonse.Response, response.Object);
}
var mean = times.Sum() / 50;
using StreamWriter file = new("newtonsoft.json_test.txt", append: true);
await file.WriteLineAsync($"User: {mean}ms");
}
}
}

View File

@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Moq;
using NUnit.Framework;
using SpotifyAPI.Web.Http;
namespace SpotifyAPI.Web.Tests
{
public class SystemTextTests
{
public static IEnumerable<object> DontSerializeTestSource
{
get
{
yield return new TestCaseData(null);
yield return new TestCaseData("string");
yield return new TestCaseData(new MemoryStream(Encoding.UTF8.GetBytes("string")));
yield return new TestCaseData(new StringContent("string"));
}
}
[TestCaseSource(nameof(DontSerializeTestSource))]
public void SerializeRequest_SkipsAlreadySerialized(object input)
{
var serializer = new TextJsonSerializer();
var request = new Mock<IRequest>();
request.SetupGet(r => r.Body).Returns(input);
serializer.SerializeRequest(request.Object);
Assert.AreEqual(input, request.Object.Body);
}
[TestCase]
public void DeserializeResponse_SkipsNonJson()
{
var serializer = new TextJsonSerializer();
var response = new Mock<IResponse>();
response.SetupGet(r => r.Body).Returns("hello");
response.SetupGet(r => r.ContentType).Returns("media/mp4");
IAPIResponse<object> apiResonse = serializer.DeserializeResponse<object>(response.Object);
Assert.AreEqual(apiResonse.Body, null);
Assert.AreEqual(apiResonse.Response, response.Object);
}
[TestCase]
public void DeserializeResponse_HandlesJson()
{
var serializer = new TextJsonSerializer();
var response = new Mock<IResponse>();
response.SetupGet(r => r.Body).Returns("{\"hello_world\": false}");
response.SetupGet(r => r.ContentType).Returns("application/json");
IAPIResponse<TestResponseObject> apiResonse = serializer.DeserializeResponse<TestResponseObject>(response.Object);
Assert.AreEqual(apiResonse.Body?.HelloWorld, false);
Assert.AreEqual(apiResonse.Response, response.Object);
}
[TestCase]
public void DeserializeResponse_TestMessage()
{
var serializer = new TextJsonSerializer();
var response = new Mock<IResponse>();
response.SetupGet(r => r.Body).Returns(ExampleResponses.CurrentlyPlayingContext);
response.SetupGet(r => r.ContentType).Returns("application/json");
IAPIResponse<CurrentlyPlayingContext> apiResonse = serializer.DeserializeResponse<CurrentlyPlayingContext>(response.Object);
Assert.AreEqual(apiResonse.Response, response.Object);
}
[TestCase]
public async Task DeserializeResponse_TimeCurrentlyPlayingTestMessage()
{
var serializer = new TextJsonSerializer();
var response = new Mock<IResponse>();
response.SetupGet(r => r.Body).Returns(ExampleResponses.CurrentlyPlayingContext);
response.SetupGet(r => r.ContentType).Returns("application/json");
var times = new List<long>();
foreach(var iter in Enumerable.Range(0, 50))
{
var watch = System.Diagnostics.Stopwatch.StartNew();
IAPIResponse<CurrentlyPlayingContext> apiResonse = serializer.DeserializeResponse<CurrentlyPlayingContext>(response.Object);
watch.Stop();
times.Add(watch.ElapsedMilliseconds);
Assert.AreEqual(apiResonse.Response, response.Object);
}
var mean = times.Sum() / 50;
using StreamWriter file = new("system.text.json_test.txt", append: true);
await file.WriteLineAsync($"CurrentlyPlayingContext: {mean}ms");
}
[TestCase]
public async Task DeserializeResponse_TimeUserTestMessage()
{
var serializer = new TextJsonSerializer();
var response = new Mock<IResponse>();
response.SetupGet(r => r.Body).Returns(ExampleResponses.PublicUser);
response.SetupGet(r => r.ContentType).Returns("application/json");
var times = new List<long>();
foreach (var iter in Enumerable.Range(0, 50))
{
var watch = System.Diagnostics.Stopwatch.StartNew();
IAPIResponse<PublicUser> apiResonse = serializer.DeserializeResponse<PublicUser>(response.Object);
watch.Stop();
times.Add(watch.ElapsedMilliseconds);
Assert.AreEqual(apiResonse.Response, response.Object);
}
var mean = times.Sum() / 50;
using StreamWriter file = new("system.text.json_test.txt", append: true);
await file.WriteLineAsync($"User: {mean}ms");
}
}
}

View File

@ -12,6 +12,7 @@
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Include="NUnit.Console" Version="3.13.0" />
<PackageReference Include="System.IO" Version="4.3.0" />
</ItemGroup>
<ItemGroup>

View File

@ -38,7 +38,7 @@ namespace SpotifyAPI.Web
/// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-playlists-tracks
/// </remarks>
/// <returns></returns>
Task<Paging<PlaylistTrack<IPlayableItem>>> GetItems(string playlistId);
Task<Paging<PlaylistTrack<BasePlayableItem>>> GetItems(string playlistId);
/// <summary>
/// Get full details of the items of a playlist owned by a Spotify user.
@ -49,7 +49,7 @@ namespace SpotifyAPI.Web
/// https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-playlists-tracks
/// </remarks>
/// <returns></returns>
Task<Paging<PlaylistTrack<IPlayableItem>>> GetItems(string playlistId, PlaylistGetItemsRequest request);
Task<Paging<PlaylistTrack<BasePlayableItem>>> GetItems(string playlistId, PlaylistGetItemsRequest request);
/// <summary>
/// Create a playlist for a Spotify user. (The playlist will be empty until you add tracks.)

View File

@ -26,19 +26,19 @@ namespace SpotifyAPI.Web
return API.Post<SnapshotResponse>(URLs.PlaylistTracks(playlistId), null, request.BuildBodyParams());
}
public Task<Paging<PlaylistTrack<IPlayableItem>>> GetItems(string playlistId)
public Task<Paging<PlaylistTrack<BasePlayableItem>>> GetItems(string playlistId)
{
var request = new PlaylistGetItemsRequest();
return GetItems(playlistId, request);
}
public Task<Paging<PlaylistTrack<IPlayableItem>>> GetItems(string playlistId, PlaylistGetItemsRequest request)
public Task<Paging<PlaylistTrack<BasePlayableItem>>> GetItems(string playlistId, PlaylistGetItemsRequest request)
{
Ensure.ArgumentNotNullOrEmptyString(playlistId, nameof(playlistId));
Ensure.ArgumentNotNull(request, nameof(request));
return API.Get<Paging<PlaylistTrack<IPlayableItem>>>(URLs.PlaylistTracks(playlistId), request.BuildQueryParams());
return API.Get<Paging<PlaylistTrack<BasePlayableItem>>>(URLs.PlaylistTracks(playlistId), request.BuildQueryParams());
}
public Task<FullPlaylist> Create(string userId, PlaylistCreateRequest request)

View File

@ -187,7 +187,7 @@ namespace SpotifyAPI.Web
return new SpotifyClientConfig(
SpotifyUrls.APIV1,
null,
new NewtonsoftJSONSerializer(),
new TextJsonSerializer(),
new NetHttpClient(),
null,
null,

View File

@ -18,7 +18,7 @@ namespace SpotifyAPI.Web.Http
public event EventHandler<IResponse>? ResponseReceived;
public APIConnector(Uri baseAddress, IAuthenticator authenticator) :
this(baseAddress, authenticator, new NewtonsoftJSONSerializer(), new NetHttpClient(), null, null)
this(baseAddress, authenticator, new TextJsonSerializer(), new NetHttpClient(), null, null)
{ }
public APIConnector(
Uri baseAddress,

View File

@ -0,0 +1,93 @@
using System;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.IO;
using System.Net.Http;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace SpotifyAPI.Web.Http
{
public class SnakeCase : JsonNamingPolicy
{
public override string ConvertName(string name)
{
if(string.IsNullOrWhiteSpace(name))
{
return name;
}else
{
return Regex.Replace(string.Concat(name[0].ToString().ToLowerInvariant(), name.Substring(1)), "[A-Z]", "_$0").ToLowerInvariant();
}
}
}
public class TextJsonSerializer : IJSONSerializer
{
private ModelJsonContext JsonContext;
public TextJsonSerializer()
{
JsonContext = ModelJsonContext.Get();
}
public IAPIResponse<T> DeserializeResponse<T>(IResponse response)
{
Ensure.ArgumentNotNull(response, nameof(response));
if (
(
response.ContentType?.Equals("application/json", StringComparison.Ordinal) is true || response.ContentType == null
))
{
if(response.Body is string bodyString && !string.IsNullOrWhiteSpace(bodyString))
{
var body = (T?)JsonSerializer.Deserialize(response.Body as string ?? "", typeof(T), JsonContext);
// In order to work out whether track or episode has been returned, first deserialise as BasePlayableItem
// which has enum of current playing type, then deserialise again with concrete playing type
if (body is CurrentlyPlaying currentlyPlaying)
{
if(currentlyPlaying.Item.Type is ItemType.Track)
{
body = (T?) JsonSerializer.Deserialize(response.Body as string ?? "", typeof(CurrentlyPlaying<FullTrack>), JsonContext);
}
else if (currentlyPlaying.Item.Type is ItemType.Episode)
{
body = (T?) JsonSerializer.Deserialize(response.Body as string ?? "", typeof(CurrentlyPlaying<FullEpisode>), JsonContext);
}
}
if (body is CurrentlyPlayingContext currentlyPlayingContext)
{
if (currentlyPlayingContext.Item.Type is ItemType.Track)
{
body = (T?)JsonSerializer.Deserialize(response.Body as string ?? "", typeof(CurrentlyPlayingContext<FullTrack>), JsonContext);
}
else if (currentlyPlayingContext.Item.Type is ItemType.Episode)
{
body = (T?)JsonSerializer.Deserialize(response.Body as string ?? "", typeof(CurrentlyPlayingContext<FullEpisode>), JsonContext);
}
}
return new APIResponse<T>(response, body!);
}
}
return new APIResponse<T>(response);
}
public void SerializeRequest(IRequest request)
{
Ensure.ArgumentNotNull(request, nameof(request));
if (request.Body is string || request.Body is Stream || request.Body is HttpContent || request.Body is null)
{
return;
}
request.Body = JsonSerializer.Serialize(request.Body, request.Body.GetType(), JsonContext);
}
}
}

View File

@ -0,0 +1,88 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using SpotifyAPI.Web.Http;
namespace SpotifyAPI.Web
{
[JsonSerializable(typeof(Actions))]
[JsonSerializable(typeof(AlbumsResponse))]
[JsonSerializable(typeof(ArtistsRelatedArtistsResponse))]
[JsonSerializable(typeof(ArtistsResponse))]
[JsonSerializable(typeof(ArtistsTopTracksResponse))]
[JsonSerializable(typeof(AuthorizationCodeRefreshResponse))]
[JsonSerializable(typeof(AuthorizationCodeTokenResponse))]
[JsonSerializable(typeof(CategoriesResponse))]
[JsonSerializable(typeof(Category))]
[JsonSerializable(typeof(CategoryPlaylistsResponse))]
[JsonSerializable(typeof(ClientCredentialsTokenResponse))]
[JsonSerializable(typeof(Context))]
[JsonSerializable(typeof(Copyright))]
[JsonSerializable(typeof(CurrentlyPlaying))]
[JsonSerializable(typeof(CurrentlyPlaying<FullEpisode>))]
[JsonSerializable(typeof(CurrentlyPlaying<FullTrack>))]
[JsonSerializable(typeof(CurrentlyPlayingContext))]
[JsonSerializable(typeof(CurrentlyPlayingContext<FullEpisode>))]
[JsonSerializable(typeof(CurrentlyPlayingContext<FullTrack>))]
[JsonSerializable(typeof(Cursor))]
[JsonSerializable(typeof(Device))]
[JsonSerializable(typeof(DeviceResponse))]
[JsonSerializable(typeof(EpisodesResponse))]
[JsonSerializable(typeof(FeaturedPlaylistsResponse))]
[JsonSerializable(typeof(FollowedArtistsResponse))]
[JsonSerializable(typeof(Followers))]
[JsonSerializable(typeof(FullAlbum))]
[JsonSerializable(typeof(FullEpisode))]
[JsonSerializable(typeof(FullPlaylist))]
[JsonSerializable(typeof(FullShow))]
[JsonSerializable(typeof(FullTrack))]
[JsonSerializable(typeof(Image))]
[JsonSerializable(typeof(LinkedTrack))]
[JsonSerializable(typeof(NewReleasesResponse))]
[JsonSerializable(typeof(PKCETokenResponse))]
[JsonSerializable(typeof(PlayHistoryItem))]
[JsonSerializable(typeof(PlaylistTrack<FullTrack>))]
[JsonSerializable(typeof(PlaylistTrack<FullEpisode>))]
[JsonSerializable(typeof(PrivateUser))]
[JsonSerializable(typeof(PublicUser))]
[JsonSerializable(typeof(RecommendationGenresResponse))]
[JsonSerializable(typeof(RecommendationSeed))]
[JsonSerializable(typeof(RecommendationsResponse))]
[JsonSerializable(typeof(ResumePoint))]
[JsonSerializable(typeof(SavedAlbum))]
[JsonSerializable(typeof(SavedEpisodes))]
[JsonSerializable(typeof(SavedShow))]
[JsonSerializable(typeof(SavedTrack))]
[JsonSerializable(typeof(SearchResponse))]
[JsonSerializable(typeof(Section))]
[JsonSerializable(typeof(Segment))]
[JsonSerializable(typeof(ShowsResponse))]
[JsonSerializable(typeof(SimpleAlbum))]
[JsonSerializable(typeof(SimpleArtist))]
[JsonSerializable(typeof(SimpleEpisode))]
[JsonSerializable(typeof(SimplePlaylist))]
[JsonSerializable(typeof(SimpleShow))]
[JsonSerializable(typeof(SimpleTrack))]
[JsonSerializable(typeof(SnapshotResponse))]
[JsonSerializable(typeof(TestResponseObject))]
[JsonSerializable(typeof(TimeInterval))]
[JsonSerializable(typeof(TrackAudio))]
[JsonSerializable(typeof(TrackAudioAnalysis))]
[JsonSerializable(typeof(TrackMeta))]
[JsonSerializable(typeof(TracksAudioFeaturesResponse))]
[JsonSerializable(typeof(TracksResponse))]
public partial class ModelJsonContext : JsonSerializerContext
{
public static ModelJsonContext Get()
{
return new ModelJsonContext(new JsonSerializerOptions()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = new SnakeCase(),
Converters =
{
new JsonStringEnumConverter()
}
});
}
}
}

View File

@ -9,10 +9,10 @@ namespace SpotifyAPI.Web
Episode
}
public interface IPlayableItem
public class BasePlayableItem
{
[JsonConverter(typeof(StringEnumConverter))]
ItemType Type { get; }
public ItemType Type { get; }
}
}

View File

@ -13,9 +13,17 @@ namespace SpotifyAPI.Web
/// </summary>
/// <value></value>
[JsonConverter(typeof(PlayableItemConverter))]
public IPlayableItem Item { get; set; } = default!;
public BasePlayableItem Item { get; set; } = default!;
public int? ProgressMs { get; set; }
public long Timestamp { get; set; }
}
public class CurrentlyPlaying<T>: CurrentlyPlaying where T : BasePlayableItem
{
public new T Item {
get => (T) base.Item;
set => base.Item = value;
}
}
}

View File

@ -17,10 +17,19 @@ namespace SpotifyAPI.Web
/// </summary>
/// <value></value>
[JsonConverter(typeof(PlayableItemConverter))]
public IPlayableItem Item { get; set; } = default!;
public BasePlayableItem Item { get; set; } = default!;
public string CurrentlyPlayingType { get; set; } = default!;
public Actions Actions { get; set; } = default!;
}
public class CurrentlyPlayingContext<T> : CurrentlyPlayingContext where T : BasePlayableItem
{
public new T Item
{
get => (T)base.Item;
set => base.Item = value;
}
}
}

View File

@ -4,7 +4,7 @@ using Newtonsoft.Json.Converters;
namespace SpotifyAPI.Web
{
public class FullEpisode : IPlayableItem
public class FullEpisode : BasePlayableItem
{
public string AudioPreviewUrl { get; set; } = default!;
public string Description { get; set; } = default!;

View File

@ -19,7 +19,7 @@ namespace SpotifyAPI.Web
/// A list of PlaylistTracks, which items can be a FullTrack or FullEpisode
/// </summary>
/// <value></value>
public Paging<PlaylistTrack<IPlayableItem>>? Tracks { get; set; } = default!;
public Paging<PlaylistTrack<BasePlayableItem>>? Tracks { get; set; } = default!;
public string? Type { get; set; } = default!;
public string? Uri { get; set; } = default!;
}

View File

@ -4,7 +4,7 @@ using Newtonsoft.Json.Converters;
namespace SpotifyAPI.Web
{
public class FullTrack : IPlayableItem
public class FullTrack : BasePlayableItem
{
public SimpleAlbum Album { get; set; } = default!;
public List<SimpleArtist> Artists { get; set; } = default!;

View File

@ -2,8 +2,8 @@ namespace SpotifyAPI.Web
{
public class Image
{
public int Height { get; set; }
public int Width { get; set; }
public int? Height { get; set; }
public int? Width { get; set; }
public string Url { get; set; } = default!;
}
}

View File

@ -21,7 +21,7 @@ namespace SpotifyAPI.Web
/// A list of PlaylistTracks, which items can be a FullTrack or FullEpisode
/// </summary>
/// <value></value>
public Paging<PlaylistTrack<IPlayableItem>> Tracks { get; set; } = default!;
public Paging<PlaylistTrack<BasePlayableItem>> Tracks { get; set; } = default!;
public string Type { get; set; } = default!;
public string Uri { get; set; } = default!;
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpotifyAPI.Web
{
public class TestResponseObject
{
public bool HelloWorld { get; set; }
}
}

View File

@ -31,10 +31,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1">
<PrivateAssets>None</PrivateAssets>
</PackageReference>
<PackageReference Include="System.Text.Json" Version="6.0.1" />
</ItemGroup>
<PropertyGroup>