New: Use System.Text.Json for Nancy and SignalR

pull/5870/head
ta264 4 years ago committed by Qstick
parent e623efefd3
commit f03dfda2f0

@ -1,7 +1,7 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
using NzbDrone.Core.Messaging.Commands;
using Radarr.Http.REST;

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Languages;

@ -1,11 +1,11 @@
using Newtonsoft.Json;
using System.Text.Json.Serialization;
using Radarr.Http.REST;
namespace NzbDrone.Api.Profiles.Languages
{
public class LanguageResource : RestResource
{
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public new int Id { get; set; }
public string Name { get; set; }
public string NameLower => Name.ToLowerInvariant();

@ -3,8 +3,8 @@ using System.Linq;
using FluentValidation;
using FluentValidation.Results;
using Nancy;
using Newtonsoft.Json;
using NzbDrone.Common.Reflection;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
using Radarr.Http;
@ -188,7 +188,7 @@ namespace NzbDrone.Api
var query = ((IDictionary<string, object>)Request.Query.ToDictionary()).ToDictionary(k => k.Key, k => k.Value.ToString());
var data = _providerFactory.RequestAction(providerDefinition, action, query);
Response resp = JsonConvert.SerializeObject(data);
Response resp = data.ToJson();
resp.ContentType = "application/json";
return resp;
}

@ -8,7 +8,6 @@
<PackageReference Include="Nancy" Version="2.0.0" />
<PackageReference Include="Nancy.Authentication.Basic" Version="2.0.0" />
<PackageReference Include="Nancy.Authentication.Forms" Version="2.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Core\Radarr.Core.csproj" />

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using NzbDrone.Core.Update;
using Radarr.Http.REST;
@ -9,7 +8,6 @@ namespace NzbDrone.Api.Update
{
public class UpdateResource : RestResource
{
[JsonConverter(typeof(Newtonsoft.Json.Converters.VersionConverter))]
public Version Version { get; set; }
public string Branch { get; set; }

@ -9,6 +9,7 @@
<PackageReference Include="NLog" Version="4.7.0" />
<PackageReference Include="Sentry" Version="2.1.8" />
<PackageReference Include="SharpZipLib" Version="1.2.0" />
<PackageReference Include="System.Text.Json" Version="5.0.1" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
</ItemGroup>

@ -127,10 +127,5 @@ namespace NzbDrone.Common.Serializer
Serializer.Serialize(jsonTextWriter, model);
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.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)
{

@ -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());
}
}
}
}

@ -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.Serialization;
using Dapper;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Datastore.Converters
{
@ -22,8 +23,8 @@ namespace NzbDrone.Core.Datastore.Converters
};
serializerSettings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true));
serializerSettings.Converters.Add(new TimeSpanConverter());
serializerSettings.Converters.Add(new UtcConverter());
serializerSettings.Converters.Add(new STJTimeSpanConverter());
serializerSettings.Converters.Add(new STJUtcConverter());
SerializerSettings = serializerSettings;
}

@ -1,7 +1,5 @@
using System;
using System.Data;
using System.Text.Json;
using System.Text.Json.Serialization;
using Dapper;
namespace NzbDrone.Core.Datastore.Converters
@ -18,17 +16,4 @@ namespace NzbDrone.Core.Datastore.Converters
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,7 +1,10 @@
using System;
using System.Text.Json.Serialization;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Messaging.Commands
{
[JsonConverter(typeof(PolymorphicWriteOnlyJsonConverter<Command>))]
public abstract class Command
{
private bool _sendUpdatesToClient;

@ -1,4 +1,4 @@
using Newtonsoft.Json;
using System.Text.Json.Serialization;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Datastore;
@ -6,7 +6,7 @@ namespace NzbDrone.Core.Profiles
{
public class ProfileFormatItem : IEmbeddedDocument
{
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public int Id { get; set; }
public CustomFormat Format { get; set; }
public int Score { get; set; }

@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Qualities;
@ -9,7 +9,7 @@ namespace NzbDrone.Core.Profiles
{
public class ProfileQualityItem : IEmbeddedDocument
{
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public int Id { get; set; }
public string Name { get; set; }

@ -1,5 +1,5 @@
using System;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Qualities

@ -1,11 +1,11 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Dapper;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Reflection;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Converters;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.ThingiProvider
@ -29,8 +29,8 @@ namespace NzbDrone.Core.ThingiProvider
};
serializerSettings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true));
serializerSettings.Converters.Add(new TimeSpanConverter());
serializerSettings.Converters.Add(new UtcConverter());
serializerSettings.Converters.Add(new STJTimeSpanConverter());
serializerSettings.Converters.Add(new STJUtcConverter());
_serializerSettings = serializerSettings;
}

@ -11,7 +11,6 @@
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
<PackageReference Include="Microsoft.AspNetCore.Owin" Version="5.0.2" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NLog.Extensions.Logging" Version="1.6.2" />

@ -114,9 +114,9 @@ namespace Radarr.Host
options.PayloadSerializerSettings = Json.GetSerializerSettings();
});
#else
.AddNewtonsoftJsonProtocol(options =>
.AddJsonProtocol(options =>
{
options.PayloadSerializerSettings = Json.GetSerializerSettings();
options.PayloadSerializerOptions = STJson.GetSerializerSettings();
});
#endif

@ -1,4 +1,3 @@
using Newtonsoft.Json;
using NzbDrone.Core.Datastore.Events;
namespace NzbDrone.SignalR
@ -8,7 +7,11 @@ namespace NzbDrone.SignalR
public object Body { 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; }
}
}

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Messaging.Commands;
using Radarr.Http.REST;

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.CustomFilters;
@ -10,7 +11,7 @@ namespace Radarr.Api.V3.CustomFilters
{
public string Type { get; set; }
public string Label { get; set; }
public List<dynamic> Filters { get; set; }
public List<ExpandoObject> Filters { get; set; }
}
public static class CustomFilterResourceMapper
@ -27,7 +28,7 @@ namespace Radarr.Api.V3.CustomFilters
Id = model.Id,
Type = model.Type,
Label = model.Label,
Filters = Json.Deserialize<List<dynamic>>(model.Filters)
Filters = STJson.Deserialize<List<ExpandoObject>>(model.Filters)
};
}
@ -43,7 +44,7 @@ namespace Radarr.Api.V3.CustomFilters
Id = resource.Id,
Type = resource.Type,
Label = resource.Label,
Filters = Json.ToJson(resource.Filters)
Filters = STJson.ToJson(resource.Filters)
};
}

@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
using NzbDrone.Core.CustomFormats;
using Radarr.Http.ClientSchema;
using Radarr.Http.REST;
@ -9,7 +9,7 @@ namespace Radarr.Api.V3.CustomFormats
{
public class CustomFormatResource : RestResource
{
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override int Id { get; set; }
public string Name { get; set; }
public bool IncludeCustomFormatWhenRenaming { get; set; }

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Languages;
@ -53,7 +53,7 @@ namespace Radarr.Api.V3.Indexers
public DownloadProtocol Protocol { get; set; }
// Sent when queuing an unknown release
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public int? MovieId { get; set; }
}

@ -1,4 +1,5 @@
using Newtonsoft.Json;
using System.Text.Json;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Localization;
using Radarr.Http;
@ -7,26 +8,21 @@ namespace Radarr.Api.V3.Localization
public class LocalizationModule : RadarrRestModule<LocalizationResource>
{
private readonly ILocalizationService _localizationService;
private readonly JsonSerializerOptions _serializerSettings;
public LocalizationModule(ILocalizationService localizationService)
{
_localizationService = localizationService;
_serializerSettings = STJson.GetSerializerSettings();
_serializerSettings.DictionaryKeyPolicy = null;
_serializerSettings.PropertyNamingPolicy = null;
Get("/", x => GetLocalizationDictionary());
}
private string GetLocalizationDictionary()
{
// We don't want camel case for transation strings, create new serializer settings
var serializerSettings = new JsonSerializerSettings
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
NullValueHandling = NullValueHandling.Ignore,
Formatting = Formatting.Indented,
DefaultValueHandling = DefaultValueHandling.Include
};
return JsonConvert.SerializeObject(_localizationService.GetLocalizationDictionary().ToResource(), serializerSettings);
return JsonSerializer.Serialize(_localizationService.GetLocalizationDictionary().ToResource(), _serializerSettings);
}
}
}

@ -1,11 +1,11 @@
using Newtonsoft.Json;
using System.Text.Json.Serialization;
using Radarr.Http.REST;
namespace Radarr.Api.V3.Profiles.Languages
{
public class LanguageResource : RestResource
{
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public new int Id { get; set; }
public string Name { get; set; }
public string NameLower => Name.ToLowerInvariant();

@ -8,7 +8,6 @@
<PackageReference Include="Nancy" Version="2.0.0" />
<PackageReference Include="Nancy.Authentication.Basic" 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.0" />
</ItemGroup>
<ItemGroup>

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using NzbDrone.Core.Update;
using Radarr.Http.REST;
@ -9,7 +8,6 @@ namespace Radarr.Api.V3.Update
{
public class UpdateResource : RestResource
{
[JsonConverter(typeof(Newtonsoft.Json.Converters.VersionConverter))]
public Version Version { get; set; }
public string Branch { get; set; }

@ -2,10 +2,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json.Linq;
using System.Text.Json;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Reflection;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Annotations;
namespace Radarr.Http.ClientSchema
@ -225,9 +226,9 @@ namespace Radarr.Http.ClientSchema
{
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
{
@ -243,9 +244,9 @@ namespace Radarr.Http.ClientSchema
{
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
{
@ -255,7 +256,18 @@ namespace Radarr.Http.ClientSchema
}
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.Text.Json;
using Nancy;
using Nancy.Responses.Negotiation;
using NzbDrone.Common.Serializer;
@ -8,6 +9,13 @@ namespace Radarr.Http.Extensions
{
public class NancyJsonSerializer : ISerializer
{
protected readonly JsonSerializerOptions _serializerSettings;
public NancyJsonSerializer()
{
_serializerSettings = STJson.GetSerializerSettings();
}
public bool CanSerialize(MediaRange contentType)
{
return contentType == "application/json";
@ -15,7 +23,7 @@ namespace Radarr.Http.Extensions
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; }

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using Nancy;
@ -27,10 +27,8 @@ namespace Radarr.Http.Extensions
public static object FromJson(this Stream body, Type type)
{
var reader = new StreamReader(body, true);
body.Position = 0;
var value = reader.ReadToEnd();
return Json.Deserialize(value, type);
return STJson.Deserialize(body, type);
}
public static JsonResponse<TModel> AsResponse<TModel>(this TModel model, NancyContext context, HttpStatusCode statusCode = HttpStatusCode.OK)

@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using FluentValidation;
using FluentValidation.Results;
using Nancy;
using Nancy.Responses.Negotiation;
using Newtonsoft.Json;
using NzbDrone.Core.Datastore;
using Radarr.Http.Extensions;
@ -248,7 +248,7 @@ namespace Radarr.Http.REST
{
resource = Request.Body.FromJson<TResource>();
}
catch (JsonReaderException e)
catch (JsonException e)
{
throw new BadRequestException($"Invalid request body. {e.Message}");
}

@ -1,10 +1,10 @@
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace Radarr.Http.REST
{
public abstract class RestResource
{
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public virtual int Id { get; set; }
[JsonIgnore]

@ -7,7 +7,6 @@
<PackageReference Include="Nancy" Version="2.0.0" />
<PackageReference Include="Nancy.Authentication.Basic" 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.0" />
</ItemGroup>
<ItemGroup>

Loading…
Cancel
Save