mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-19 04:59:35 -07:00
New: Use System.Text.Json for Nancy and SignalR
(cherry picked from commit d3e8c7e0c94a3d2987329d278dc0d00ae3d76c8f)
This commit is contained in:
parent
9d265ef9b2
commit
b0ea6550d7
38 changed files with 375 additions and 83 deletions
|
@ -1,8 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Lidarr.Http.REST;
|
using Lidarr.Http.REST;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Dynamic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Lidarr.Http.REST;
|
using Lidarr.Http.REST;
|
||||||
using NzbDrone.Common.Serializer;
|
using NzbDrone.Common.Serializer;
|
||||||
|
@ -10,7 +11,7 @@ namespace Lidarr.Api.V1.CustomFilters
|
||||||
{
|
{
|
||||||
public string Type { get; set; }
|
public string Type { get; set; }
|
||||||
public string Label { get; set; }
|
public string Label { get; set; }
|
||||||
public List<dynamic> Filters { get; set; }
|
public List<ExpandoObject> Filters { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class CustomFilterResourceMapper
|
public static class CustomFilterResourceMapper
|
||||||
|
@ -27,7 +28,7 @@ namespace Lidarr.Api.V1.CustomFilters
|
||||||
Id = model.Id,
|
Id = model.Id,
|
||||||
Type = model.Type,
|
Type = model.Type,
|
||||||
Label = model.Label,
|
Label = model.Label,
|
||||||
Filters = Json.Deserialize<List<dynamic>>(model.Filters)
|
Filters = STJson.Deserialize<List<ExpandoObject>>(model.Filters)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +44,7 @@ namespace Lidarr.Api.V1.CustomFilters
|
||||||
Id = resource.Id,
|
Id = resource.Id,
|
||||||
Type = resource.Type,
|
Type = resource.Type,
|
||||||
Label = resource.Label,
|
Label = resource.Label,
|
||||||
Filters = Json.ToJson(resource.Filters)
|
Filters = STJson.ToJson(resource.Filters)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Lidarr.Http.REST;
|
using Lidarr.Http.REST;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using NzbDrone.Core.DecisionEngine;
|
using NzbDrone.Core.DecisionEngine;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
@ -49,12 +49,12 @@ namespace Lidarr.Api.V1.Indexers
|
||||||
public DownloadProtocol Protocol { get; set; }
|
public DownloadProtocol Protocol { get; set; }
|
||||||
|
|
||||||
// Sent when queuing an unknown release
|
// Sent when queuing an unknown release
|
||||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||||
|
|
||||||
// [JsonIgnore]
|
// [JsonIgnore]
|
||||||
public int? ArtistId { get; set; }
|
public int? ArtistId { get; set; }
|
||||||
|
|
||||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||||
|
|
||||||
// [JsonIgnore]
|
// [JsonIgnore]
|
||||||
public int? AlbumId { get; set; }
|
public int? AlbumId { get; set; }
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
<PackageReference Include="Nancy" Version="2.0.0" />
|
<PackageReference Include="Nancy" Version="2.0.0" />
|
||||||
<PackageReference Include="Nancy.Authentication.Basic" Version="2.0.0" />
|
<PackageReference Include="Nancy.Authentication.Basic" Version="2.0.0" />
|
||||||
<PackageReference Include="Nancy.Authentication.Forms" Version="2.0.0" />
|
<PackageReference Include="Nancy.Authentication.Forms" Version="2.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
|
||||||
<PackageReference Include="NLog" Version="4.7.9" />
|
<PackageReference Include="NLog" Version="4.7.9" />
|
||||||
<PackageReference Include="System.IO.Abstractions" Version="7.0.15" />
|
<PackageReference Include="System.IO.Abstractions" Version="7.0.15" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Lidarr.Http.REST;
|
using Lidarr.Http.REST;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using NzbDrone.Core.Update;
|
using NzbDrone.Core.Update;
|
||||||
|
|
||||||
namespace Lidarr.Api.V1.Update
|
namespace Lidarr.Api.V1.Update
|
||||||
{
|
{
|
||||||
public class UpdateResource : RestResource
|
public class UpdateResource : RestResource
|
||||||
{
|
{
|
||||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.VersionConverter))]
|
|
||||||
public Version Version { get; set; }
|
public Version Version { get; set; }
|
||||||
|
|
||||||
public string Branch { get; set; }
|
public string Branch { get; set; }
|
||||||
|
|
|
@ -2,10 +2,11 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Newtonsoft.Json.Linq;
|
using System.Text.Json;
|
||||||
using NzbDrone.Common.EnsureThat;
|
using NzbDrone.Common.EnsureThat;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Reflection;
|
using NzbDrone.Common.Reflection;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
using NzbDrone.Core.Annotations;
|
using NzbDrone.Core.Annotations;
|
||||||
|
|
||||||
namespace Lidarr.Http.ClientSchema
|
namespace Lidarr.Http.ClientSchema
|
||||||
|
@ -212,9 +213,9 @@ namespace Lidarr.Http.ClientSchema
|
||||||
{
|
{
|
||||||
return Enumerable.Empty<int>();
|
return Enumerable.Empty<int>();
|
||||||
}
|
}
|
||||||
else if (fieldValue.GetType() == typeof(JArray))
|
else if (fieldValue is JsonElement e && e.ValueKind == JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
return ((JArray)fieldValue).Select(s => s.Value<int>());
|
return e.EnumerateArray().Select(s => s.GetInt32());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -230,9 +231,9 @@ namespace Lidarr.Http.ClientSchema
|
||||||
{
|
{
|
||||||
return Enumerable.Empty<string>();
|
return Enumerable.Empty<string>();
|
||||||
}
|
}
|
||||||
else if (fieldValue.GetType() == typeof(JArray))
|
else if (fieldValue is JsonElement e && e.ValueKind == JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
return ((JArray)fieldValue).Select(s => s.Value<string>());
|
return e.EnumerateArray().Select(s => s.GetString());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -242,7 +243,18 @@ namespace Lidarr.Http.ClientSchema
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return fieldValue => fieldValue;
|
return fieldValue =>
|
||||||
|
{
|
||||||
|
var element = fieldValue as JsonElement?;
|
||||||
|
|
||||||
|
if (element == null || !element.HasValue)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = element.Value.GetRawText();
|
||||||
|
return STJson.Deserialize(json, propertyType);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
using Nancy;
|
using Nancy;
|
||||||
using Nancy.Responses.Negotiation;
|
using Nancy.Responses.Negotiation;
|
||||||
using NzbDrone.Common.Serializer;
|
using NzbDrone.Common.Serializer;
|
||||||
|
@ -8,6 +9,13 @@ namespace Lidarr.Http.Extensions
|
||||||
{
|
{
|
||||||
public class NancyJsonSerializer : ISerializer
|
public class NancyJsonSerializer : ISerializer
|
||||||
{
|
{
|
||||||
|
protected readonly JsonSerializerOptions _serializerSettings;
|
||||||
|
|
||||||
|
public NancyJsonSerializer()
|
||||||
|
{
|
||||||
|
_serializerSettings = STJson.GetSerializerSettings();
|
||||||
|
}
|
||||||
|
|
||||||
public bool CanSerialize(MediaRange contentType)
|
public bool CanSerialize(MediaRange contentType)
|
||||||
{
|
{
|
||||||
return contentType == "application/json";
|
return contentType == "application/json";
|
||||||
|
@ -15,7 +23,7 @@ namespace Lidarr.Http.Extensions
|
||||||
|
|
||||||
public void Serialize<TModel>(MediaRange contentType, TModel model, Stream outputStream)
|
public void Serialize<TModel>(MediaRange contentType, TModel model, Stream outputStream)
|
||||||
{
|
{
|
||||||
Json.Serialize(model, outputStream);
|
STJson.Serialize(model, outputStream, _serializerSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<string> Extensions { get; private set; }
|
public IEnumerable<string> Extensions { get; private set; }
|
||||||
|
|
|
@ -28,10 +28,8 @@ namespace Lidarr.Http.Extensions
|
||||||
|
|
||||||
public static object FromJson(this Stream body, Type type)
|
public static object FromJson(this Stream body, Type type)
|
||||||
{
|
{
|
||||||
var reader = new StreamReader(body, true);
|
|
||||||
body.Position = 0;
|
body.Position = 0;
|
||||||
var value = reader.ReadToEnd();
|
return STJson.Deserialize(body, type);
|
||||||
return Json.Deserialize(value, type);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static JsonResponse<TModel> AsResponse<TModel>(this TModel model, NancyContext context, HttpStatusCode statusCode = HttpStatusCode.OK)
|
public static JsonResponse<TModel> AsResponse<TModel>(this TModel model, NancyContext context, HttpStatusCode statusCode = HttpStatusCode.OK)
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
<PackageReference Include="Nancy" Version="2.0.0" />
|
<PackageReference Include="Nancy" Version="2.0.0" />
|
||||||
<PackageReference Include="Nancy.Authentication.Basic" Version="2.0.0" />
|
<PackageReference Include="Nancy.Authentication.Basic" Version="2.0.0" />
|
||||||
<PackageReference Include="Nancy.Authentication.Forms" Version="2.0.0" />
|
<PackageReference Include="Nancy.Authentication.Forms" Version="2.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
|
||||||
<PackageReference Include="NLog" Version="4.7.9" />
|
<PackageReference Include="NLog" Version="4.7.9" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
using Lidarr.Http.Extensions;
|
using Lidarr.Http.Extensions;
|
||||||
using Nancy;
|
using Nancy;
|
||||||
using Nancy.Responses.Negotiation;
|
using Nancy.Responses.Negotiation;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
|
|
||||||
namespace Lidarr.Http.REST
|
namespace Lidarr.Http.REST
|
||||||
|
@ -248,9 +248,9 @@ namespace Lidarr.Http.REST
|
||||||
{
|
{
|
||||||
resource = Request.Body.FromJson<TResource>();
|
resource = Request.Body.FromJson<TResource>();
|
||||||
}
|
}
|
||||||
catch (JsonReaderException ex)
|
catch (JsonException e)
|
||||||
{
|
{
|
||||||
throw new BadRequestException(ex.Message);
|
throw new BadRequestException(e.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resource == null)
|
if (resource == null)
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
using Newtonsoft.Json;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Lidarr.Http.REST
|
namespace Lidarr.Http.REST
|
||||||
{
|
{
|
||||||
public abstract class RestResource
|
public abstract class RestResource
|
||||||
{
|
{
|
||||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
<PackageReference Include="Sentry" Version="2.1.1" />
|
<PackageReference Include="Sentry" Version="2.1.1" />
|
||||||
<PackageReference Include="SharpZipLib" Version="1.2.0" />
|
<PackageReference Include="SharpZipLib" Version="1.2.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions" Version="7.0.15" />
|
<PackageReference Include="System.IO.Abstractions" Version="7.0.15" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="5.0.2" />
|
||||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
|
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -77,10 +77,5 @@ namespace NzbDrone.Common.Serializer
|
||||||
Serializer.Serialize(jsonTextWriter, model);
|
Serializer.Serialize(jsonTextWriter, model);
|
||||||
jsonTextWriter.Flush();
|
jsonTextWriter.Flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Serialize<TModel>(TModel model, Stream outputStream)
|
|
||||||
{
|
|
||||||
Serialize(model, new StreamWriter(outputStream));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Serializer
|
||||||
|
{
|
||||||
|
public class PolymorphicWriteOnlyJsonConverter<T> : JsonConverter<T>
|
||||||
|
{
|
||||||
|
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
JsonSerializer.Serialize(writer, value, value.GetType(), options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Serializer
|
||||||
|
{
|
||||||
|
public class STJHttpUriConverter : JsonConverter<HttpUri>
|
||||||
|
{
|
||||||
|
public override HttpUri Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
return new HttpUri(reader.GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, HttpUri value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
writer.WriteNullValue();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(value.FullUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,9 +2,9 @@
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Converters
|
namespace NzbDrone.Common.Serializer
|
||||||
{
|
{
|
||||||
public class TimeSpanConverter : JsonConverter<TimeSpan>
|
public class STJTimeSpanConverter : JsonConverter<TimeSpan>
|
||||||
{
|
{
|
||||||
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
|
@ -0,0 +1,19 @@
|
||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Serializer
|
||||||
|
{
|
||||||
|
public class STJUtcConverter : JsonConverter<DateTime>
|
||||||
|
{
|
||||||
|
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
return DateTime.Parse(reader.GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(value.ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Serializer
|
||||||
|
{
|
||||||
|
public class STJVersionConverter : JsonConverter<Version>
|
||||||
|
{
|
||||||
|
public override Version Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Version v = new Version(reader.GetString());
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw new JsonException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new JsonException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
writer.WriteNullValue();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(value.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
88
src/NzbDrone.Common/Serializer/System.Text.Json/STJson.cs
Normal file
88
src/NzbDrone.Common/Serializer/System.Text.Json/STJson.cs
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Serializer
|
||||||
|
{
|
||||||
|
public static class STJson
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions SerializerSettings = GetSerializerSettings();
|
||||||
|
private static readonly JsonWriterOptions WriterOptions = new JsonWriterOptions
|
||||||
|
{
|
||||||
|
Indented = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public static JsonSerializerOptions GetSerializerSettings()
|
||||||
|
{
|
||||||
|
var serializerSettings = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
AllowTrailingCommas = true,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = true
|
||||||
|
};
|
||||||
|
|
||||||
|
serializerSettings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true));
|
||||||
|
serializerSettings.Converters.Add(new STJVersionConverter());
|
||||||
|
serializerSettings.Converters.Add(new STJHttpUriConverter());
|
||||||
|
serializerSettings.Converters.Add(new STJTimeSpanConverter());
|
||||||
|
serializerSettings.Converters.Add(new STJUtcConverter());
|
||||||
|
|
||||||
|
return serializerSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T Deserialize<T>(string json)
|
||||||
|
where T : new()
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<T>(json, SerializerSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static object Deserialize(string json, Type type)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize(json, type, SerializerSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static object Deserialize(Stream input, Type type)
|
||||||
|
{
|
||||||
|
return JsonSerializer.DeserializeAsync(input, type, SerializerSettings).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryDeserialize<T>(string json, out T result)
|
||||||
|
where T : new()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result = Deserialize<T>(json);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
result = default(T);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToJson(object obj)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(obj, SerializerSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Serialize<TModel>(TModel model, Stream outputStream, JsonSerializerOptions options = null)
|
||||||
|
{
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
options = SerializerSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cast to object to get all properties written out
|
||||||
|
// https://github.com/dotnet/corefx/issues/38650
|
||||||
|
using (var writer = new Utf8JsonWriter(outputStream, options: WriterOptions))
|
||||||
|
{
|
||||||
|
JsonSerializer.Serialize(writer, (object)model, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ using System.Data;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Converters
|
namespace NzbDrone.Core.Datastore.Converters
|
||||||
{
|
{
|
||||||
|
@ -22,9 +23,8 @@ namespace NzbDrone.Core.Datastore.Converters
|
||||||
};
|
};
|
||||||
|
|
||||||
serializerSettings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true));
|
serializerSettings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true));
|
||||||
serializerSettings.Converters.Add(new KeyValuePairConverter()); /* Remove in .NET 5 */
|
serializerSettings.Converters.Add(new STJTimeSpanConverter());
|
||||||
serializerSettings.Converters.Add(new TimeSpanConverter());
|
serializerSettings.Converters.Add(new STJUtcConverter());
|
||||||
serializerSettings.Converters.Add(new UtcConverter());
|
|
||||||
|
|
||||||
SerializerSettings = serializerSettings;
|
SerializerSettings = serializerSettings;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using Dapper;
|
using Dapper;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Converters
|
namespace NzbDrone.Core.Datastore.Converters
|
||||||
|
@ -18,17 +16,4 @@ namespace NzbDrone.Core.Datastore.Converters
|
||||||
return DateTime.SpecifyKind((DateTime)value, DateTimeKind.Utc);
|
return DateTime.SpecifyKind((DateTime)value, DateTimeKind.Utc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UtcConverter : JsonConverter<DateTime>
|
|
||||||
{
|
|
||||||
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
return DateTime.Parse(reader.GetString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
writer.WriteStringValue(value.ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore
|
namespace NzbDrone.Core.Datastore
|
||||||
{
|
{
|
||||||
|
@ -13,6 +14,7 @@ namespace NzbDrone.Core.Datastore
|
||||||
/// Allows a field to be lazy loaded.
|
/// Allows a field to be lazy loaded.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TChild"></typeparam>
|
/// <typeparam name="TChild"></typeparam>
|
||||||
|
[JsonConverter(typeof(LazyLoadedConverterFactory))]
|
||||||
public class LazyLoaded<TChild> : ILazyLoaded
|
public class LazyLoaded<TChild> : ILazyLoaded
|
||||||
{
|
{
|
||||||
protected TChild _value;
|
protected TChild _value;
|
||||||
|
@ -60,11 +62,6 @@ namespace NzbDrone.Core.Datastore
|
||||||
{
|
{
|
||||||
return MemberwiseClone();
|
return MemberwiseClone();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ShouldSerializeValue()
|
|
||||||
{
|
|
||||||
return IsLoaded;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
90
src/NzbDrone.Core/Datastore/LazyLoadedConverterFactory.cs
Normal file
90
src/NzbDrone.Core/Datastore/LazyLoadedConverterFactory.cs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore
|
||||||
|
{
|
||||||
|
public class LazyLoadedConverterFactory : JsonConverterFactory
|
||||||
|
{
|
||||||
|
public override bool CanConvert(Type typeToConvert)
|
||||||
|
{
|
||||||
|
if (!typeToConvert.IsGenericType)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeToConvert.GetGenericTypeDefinition() == typeof(LazyLoaded<>);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var childType = type.GetGenericArguments()[0];
|
||||||
|
|
||||||
|
return (JsonConverter)Activator.CreateInstance(
|
||||||
|
typeof(LazyLoadedConverter<>).MakeGenericType(childType),
|
||||||
|
BindingFlags.Instance | BindingFlags.Public,
|
||||||
|
binder: null,
|
||||||
|
args: new object[] { options },
|
||||||
|
culture: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LazyLoadedConverter<TChild> : JsonConverter<LazyLoaded<TChild>>
|
||||||
|
{
|
||||||
|
private readonly JsonConverter<TChild> _childConverter;
|
||||||
|
private readonly Type _childType;
|
||||||
|
|
||||||
|
public LazyLoadedConverter(JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
// For performance, use the existing converter if available.
|
||||||
|
_childConverter = (JsonConverter<TChild>)options
|
||||||
|
.GetConverter(typeof(TChild));
|
||||||
|
|
||||||
|
// Cache the type.
|
||||||
|
_childType = typeof(TChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override LazyLoaded<TChild> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
TChild value;
|
||||||
|
if (_childConverter != null)
|
||||||
|
{
|
||||||
|
reader.Read();
|
||||||
|
value = _childConverter.Read(ref reader, _childType, options);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
value = JsonSerializer.Deserialize<TChild>(ref reader, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
return new LazyLoaded<TChild>(value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, LazyLoaded<TChild> value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value.IsLoaded)
|
||||||
|
{
|
||||||
|
if (_childConverter != null)
|
||||||
|
{
|
||||||
|
_childConverter.Write(writer, value.Value, options);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
JsonSerializer.Serialize(writer, value.Value, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
writer.WriteNullValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@
|
||||||
<PackageReference Include="MailKit" Version="2.10.1" />
|
<PackageReference Include="MailKit" Version="2.10.1" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="NLog" Version="4.7.9" />
|
<PackageReference Include="NLog" Version="4.7.9" />
|
||||||
<PackageReference Include="RestSharp" Version="106.10.1" />
|
<PackageReference Include="RestSharp" Version="106.11.7" />
|
||||||
<PackageReference Include="System.IO.Abstractions" Version="7.0.15" />
|
<PackageReference Include="System.IO.Abstractions" Version="7.0.15" />
|
||||||
<PackageReference Include="TagLibSharp-Lidarr" Version="2.2.0.25" />
|
<PackageReference Include="TagLibSharp-Lidarr" Version="2.2.0.25" />
|
||||||
<PackageReference Include="Kveer.XmlRPC" Version="1.1.1" />
|
<PackageReference Include="Kveer.XmlRPC" Version="1.1.1" />
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Messaging.Commands
|
namespace NzbDrone.Core.Messaging.Commands
|
||||||
{
|
{
|
||||||
|
[JsonConverter(typeof(PolymorphicWriteOnlyJsonConverter<Command>))]
|
||||||
public abstract class Command
|
public abstract class Command
|
||||||
{
|
{
|
||||||
private bool _sendUpdatesToClient;
|
private bool _sendUpdatesToClient;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Newtonsoft.Json;
|
using System.Text.Json.Serialization;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
|
@ -9,7 +9,7 @@ namespace NzbDrone.Core.Profiles.Qualities
|
||||||
{
|
{
|
||||||
public class QualityProfileQualityItem : IEmbeddedDocument
|
public class QualityProfileQualityItem : IEmbeddedDocument
|
||||||
{
|
{
|
||||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Newtonsoft.Json;
|
using System.Text.Json.Serialization;
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Qualities
|
namespace NzbDrone.Core.Qualities
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Reflection;
|
using NzbDrone.Common.Reflection;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.Datastore.Converters;
|
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
|
||||||
namespace NzbDrone.Core.ThingiProvider
|
namespace NzbDrone.Core.ThingiProvider
|
||||||
|
@ -30,8 +30,8 @@ namespace NzbDrone.Core.ThingiProvider
|
||||||
};
|
};
|
||||||
|
|
||||||
serializerSettings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true));
|
serializerSettings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true));
|
||||||
serializerSettings.Converters.Add(new TimeSpanConverter());
|
serializerSettings.Converters.Add(new STJTimeSpanConverter());
|
||||||
serializerSettings.Converters.Add(new UtcConverter());
|
serializerSettings.Converters.Add(new STJUtcConverter());
|
||||||
|
|
||||||
_serializerSettings = serializerSettings;
|
_serializerSettings = serializerSettings;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Owin" Version="5.0.5" />
|
<PackageReference Include="Microsoft.AspNetCore.Owin" Version="5.0.5" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.5" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.2" />
|
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.2" />
|
||||||
|
|
|
@ -106,17 +106,15 @@ namespace NzbDrone.Host
|
||||||
services
|
services
|
||||||
.AddSignalR()
|
.AddSignalR()
|
||||||
#if !NETCOREAPP
|
#if !NETCOREAPP
|
||||||
.AddJsonProtocol(
|
.AddJsonProtocol(options =>
|
||||||
options =>
|
{
|
||||||
{
|
options.PayloadSerializerSettings = Json.GetSerializerSettings();
|
||||||
options.PayloadSerializerSettings = Json.GetSerializerSettings();
|
});
|
||||||
});
|
|
||||||
#else
|
#else
|
||||||
.AddNewtonsoftJsonProtocol(
|
.AddJsonProtocol(options =>
|
||||||
options =>
|
{
|
||||||
{
|
options.PayloadSerializerOptions = STJson.GetSerializerSettings();
|
||||||
options.PayloadSerializerSettings = Json.GetSerializerSettings();
|
});
|
||||||
});
|
|
||||||
#endif
|
#endif
|
||||||
})
|
})
|
||||||
.Configure(app =>
|
.Configure(app =>
|
||||||
|
|
|
@ -51,7 +51,7 @@ namespace NzbDrone.Integration.Test.Client
|
||||||
throw response.ErrorException;
|
throw response.ErrorException;
|
||||||
}
|
}
|
||||||
|
|
||||||
AssertDisableCache(response.Headers);
|
AssertDisableCache(response);
|
||||||
|
|
||||||
response.ErrorMessage.Should().BeNullOrWhiteSpace();
|
response.ErrorMessage.Should().BeNullOrWhiteSpace();
|
||||||
|
|
||||||
|
@ -68,9 +68,10 @@ namespace NzbDrone.Integration.Test.Client
|
||||||
return Json.Deserialize<T>(content);
|
return Json.Deserialize<T>(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AssertDisableCache(IList<Parameter> headers)
|
private static void AssertDisableCache(IRestResponse response)
|
||||||
{
|
{
|
||||||
// cache control header gets reordered on net core
|
// cache control header gets reordered on net core
|
||||||
|
var headers = response.Headers;
|
||||||
((string)headers.Single(c => c.Name == "Cache-Control").Value).Split(',').Select(x => x.Trim())
|
((string)headers.Single(c => c.Name == "Cache-Control").Value).Split(',').Select(x => x.Trim())
|
||||||
.Should().BeEquivalentTo("no-store, must-revalidate, no-cache, max-age=0".Split(',').Select(x => x.Trim()));
|
.Should().BeEquivalentTo("no-store, must-revalidate, no-cache, max-age=0".Split(',').Select(x => x.Trim()));
|
||||||
headers.Single(c => c.Name == "Pragma").Value.Should().Be("no-cache");
|
headers.Single(c => c.Name == "Pragma").Value.Should().Be("no-cache");
|
||||||
|
|
|
@ -27,6 +27,7 @@ using NzbDrone.SignalR;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
using NzbDrone.Test.Common.Categories;
|
using NzbDrone.Test.Common.Categories;
|
||||||
using RestSharp;
|
using RestSharp;
|
||||||
|
using RestSharp.Serializers.SystemTextJson;
|
||||||
|
|
||||||
namespace NzbDrone.Integration.Test
|
namespace NzbDrone.Integration.Test
|
||||||
{
|
{
|
||||||
|
@ -98,6 +99,7 @@ namespace NzbDrone.Integration.Test
|
||||||
RestClient = new RestClient(RootUrl + "api/v1/");
|
RestClient = new RestClient(RootUrl + "api/v1/");
|
||||||
RestClient.AddDefaultHeader("Authentication", ApiKey);
|
RestClient.AddDefaultHeader("Authentication", ApiKey);
|
||||||
RestClient.AddDefaultHeader("X-Api-Key", ApiKey);
|
RestClient.AddDefaultHeader("X-Api-Key", ApiKey);
|
||||||
|
RestClient.UseSystemTextJson();
|
||||||
|
|
||||||
Blacklist = new ClientBase<BlacklistResource>(RestClient, ApiKey);
|
Blacklist = new ClientBase<BlacklistResource>(RestClient, ApiKey);
|
||||||
Commands = new CommandClient(RestClient, ApiKey);
|
Commands = new CommandClient(RestClient, ApiKey);
|
||||||
|
@ -291,6 +293,7 @@ namespace NzbDrone.Integration.Test
|
||||||
|
|
||||||
if (changed)
|
if (changed)
|
||||||
{
|
{
|
||||||
|
result.NextAlbum = result.LastAlbum = null;
|
||||||
Artist.Put(result);
|
Artist.Put(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
using Newtonsoft.Json;
|
|
||||||
using NzbDrone.Core.Datastore.Events;
|
using NzbDrone.Core.Datastore.Events;
|
||||||
|
|
||||||
namespace NzbDrone.SignalR
|
namespace NzbDrone.SignalR
|
||||||
|
@ -8,7 +7,11 @@ namespace NzbDrone.SignalR
|
||||||
public object Body { get; set; }
|
public object Body { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
#if !NETCOREAPP
|
||||||
|
[Newtonsoft.Json.JsonIgnore]
|
||||||
|
#else
|
||||||
|
[System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
#endif
|
||||||
public ModelAction Action { get; set; }
|
public ModelAction Action { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="NLog" Version="4.7.9" />
|
<PackageReference Include="NLog" Version="4.7.9" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||||
<PackageReference Include="RestSharp" Version="106.10.1" />
|
<PackageReference Include="RestSharp" Version="106.11.7" />
|
||||||
|
<PackageReference Include="RestSharp.Serializers.SystemTextJson" Version="106.11.7" />
|
||||||
<PackageReference Include="System.IO.Abstractions" Version="7.0.15" />
|
<PackageReference Include="System.IO.Abstractions" Version="7.0.15" />
|
||||||
<PackageReference Include="Unity" Version="5.11.2" />
|
<PackageReference Include="Unity" Version="5.11.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue