Browse Source

Refactor & add more unit tests.

pull/5952/head
maliming 5 years ago
parent
commit
74affe2a74
  1. 2
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Json/AbpHybridJsonInputFormatter.cs
  2. 2
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Json/AbpHybridJsonOutputFormatter.cs
  3. 1
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Json/MvcCoreBuilderExtensions.cs
  4. 9
      framework/src/Volo.Abp.Json/Volo/Abp/Json/AbpJsonModule.cs
  5. 4
      framework/src/Volo.Abp.Json/Volo/Abp/Json/Newtonsoft/AbpNewtonsoftJsonSerializerProvider.cs
  6. 12
      framework/src/Volo.Abp.Json/Volo/Abp/Json/SystemTextJson/AbpSystemTextJsonSerializerProvider.cs
  7. 21
      framework/src/Volo.Abp.Json/Volo/Abp/Json/SystemTextJson/AbpSystemTextJsonSupportTypeMatcher.cs
  8. 14
      framework/src/Volo.Abp.Json/Volo/Abp/Json/SystemTextJson/AbpSystemTextJsonSupportTypeMatcherOptions.cs
  9. 182
      framework/src/Volo.Abp.Json/Volo/Abp/Json/SystemTextJson/SystemTextJsonSupportTypeMatcher.cs
  10. 18
      framework/src/Volo.Abp.Json/Volo/Abp/Json/SystemTextJson/SystemTextJsonSupportTypeMatcherOptions.cs
  11. 10
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController.cs
  12. 43
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController_Tests.cs
  13. 66
      framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpSystemTextJsonSupportTypeMatcher_Tests.cs
  14. 113
      framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/SystemTextJsonSupportTypeMatcher_Tests.cs

2
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Json/AbpHybridJsonInputFormatter.cs

@ -25,7 +25,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Json
protected virtual TextInputFormatter GetTextInputFormatter(InputFormatterContext context)
{
var typesMatcher = context.HttpContext.RequestServices.GetRequiredService<SystemTextJsonSupportTypeMatcher>();
var typesMatcher = context.HttpContext.RequestServices.GetRequiredService<AbpSystemTextJsonSupportTypeMatcher>();
if (typesMatcher.Match(context.ModelType))
{
return context.HttpContext.RequestServices.GetRequiredService<SystemTextJsonInputFormatter>();

2
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Json/AbpHybridJsonOutputFormatter.cs

@ -25,7 +25,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Json
protected virtual TextOutputFormatter GetTextInputFormatter(OutputFormatterWriteContext context)
{
var typesMatcher = context.HttpContext.RequestServices.GetRequiredService<SystemTextJsonSupportTypeMatcher>();
var typesMatcher = context.HttpContext.RequestServices.GetRequiredService<AbpSystemTextJsonSupportTypeMatcher>();
if (typesMatcher.Match(context.ObjectType))
{
return context.HttpContext.RequestServices.GetRequiredService<SystemTextJsonOutputFormatter>();

1
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Json/MvcCoreBuilderExtensions.cs

@ -23,6 +23,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Json
return new SystemTextJsonInputFormatter(jsonOptions.Value, logger);
});
builder.Services.AddTransient<DefaultObjectPoolProvider>();
//NewtonsoftJsonInputFormatter
builder.Services.AddTransient(provider =>
{

9
framework/src/Volo.Abp.Json/Volo/Abp/Json/AbpJsonModule.cs

@ -19,13 +19,8 @@ namespace Volo.Abp.Json
Configure<AbpJsonOptions>(options =>
{
options.Providers.Add<NewtonsoftJsonSerializerProvider>();
options.Providers.Add<SystemTextJsonSerializerProvider>();
});
Configure<SystemTextJsonSupportTypeMatcherOptions>(options =>
{
options.UnsupportedAttributes.Add<DisableDateTimeNormalizationAttribute>();
options.Providers.Add<AbpNewtonsoftJsonSerializerProvider>();
options.Providers.Add<AbpSystemTextJsonSerializerProvider>();
});
Configure<AbpNewtonsoftJsonSerializerOptions>(options =>

4
framework/src/Volo.Abp.Json/Volo/Abp/Json/Newtonsoft/NewtonsoftJsonSerializerProvider.cs → framework/src/Volo.Abp.Json/Volo/Abp/Json/Newtonsoft/AbpNewtonsoftJsonSerializerProvider.cs

@ -9,14 +9,14 @@ using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Json.Newtonsoft
{
public class NewtonsoftJsonSerializerProvider : IJsonSerializerProvider, ITransientDependency
public class AbpNewtonsoftJsonSerializerProvider : IJsonSerializerProvider, ITransientDependency
{
private static readonly CamelCaseExceptDictionaryKeysResolver SharedCamelCaseExceptDictionaryKeysResolver =
new CamelCaseExceptDictionaryKeysResolver();
protected List<JsonConverter> Converters { get; }
public NewtonsoftJsonSerializerProvider(
public AbpNewtonsoftJsonSerializerProvider(
IOptions<AbpNewtonsoftJsonSerializerOptions> options,
IServiceProvider serviceProvider)
{

12
framework/src/Volo.Abp.Json/Volo/Abp/Json/SystemTextJson/SystemTextJsonSerializerProvider.cs → framework/src/Volo.Abp.Json/Volo/Abp/Json/SystemTextJson/AbpSystemTextJsonSerializerProvider.cs

@ -5,23 +5,23 @@ using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Json.SystemTextJson
{
public class SystemTextJsonSerializerProvider : IJsonSerializerProvider, ITransientDependency
public class AbpSystemTextJsonSerializerProvider : IJsonSerializerProvider, ITransientDependency
{
protected AbpSystemTextJsonSerializerOptions Options { get; }
protected SystemTextJsonSupportTypeMatcher SystemTextJsonSupportTypeMatcher { get; }
protected AbpSystemTextJsonSupportTypeMatcher AbpSystemTextJsonSupportTypeMatcher { get; }
public SystemTextJsonSerializerProvider(
public AbpSystemTextJsonSerializerProvider(
IOptions<AbpSystemTextJsonSerializerOptions> options,
SystemTextJsonSupportTypeMatcher systemTextJsonSupportTypeMatcher)
AbpSystemTextJsonSupportTypeMatcher abpSystemTextJsonSupportTypeMatcher)
{
SystemTextJsonSupportTypeMatcher = systemTextJsonSupportTypeMatcher;
AbpSystemTextJsonSupportTypeMatcher = abpSystemTextJsonSupportTypeMatcher;
Options = options.Value;
}
public bool CanHandle(Type type)
{
return SystemTextJsonSupportTypeMatcher.Match(type);
return AbpSystemTextJsonSupportTypeMatcher.Match(type);
}
public string Serialize(object obj, bool camelCase = true, bool indented = false)

21
framework/src/Volo.Abp.Json/Volo/Abp/Json/SystemTextJson/AbpSystemTextJsonSupportTypeMatcher.cs

@ -0,0 +1,21 @@
using System;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Json.SystemTextJson
{
public class AbpSystemTextJsonSupportTypeMatcher : ITransientDependency
{
protected AbpSystemTextJsonSupportTypeMatcherOptions Options { get; }
public AbpSystemTextJsonSupportTypeMatcher(IOptions<AbpSystemTextJsonSupportTypeMatcherOptions> options)
{
Options = options.Value;
}
public virtual bool Match(Type type)
{
return !Options.UnsupportedTypes.Contains(type);
}
}
}

14
framework/src/Volo.Abp.Json/Volo/Abp/Json/SystemTextJson/AbpSystemTextJsonSupportTypeMatcherOptions.cs

@ -0,0 +1,14 @@
using Volo.Abp.Collections;
namespace Volo.Abp.Json.SystemTextJson
{
public class AbpSystemTextJsonSupportTypeMatcherOptions
{
public ITypeList UnsupportedTypes { get; }
public AbpSystemTextJsonSupportTypeMatcherOptions()
{
UnsupportedTypes = new TypeList();
}
}
}

182
framework/src/Volo.Abp.Json/Volo/Abp/Json/SystemTextJson/SystemTextJsonSupportTypeMatcher.cs

@ -1,182 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Reflection;
using Volo.Abp.Timing;
namespace Volo.Abp.Json.SystemTextJson
{
public class SystemTextJsonSupportTypeMatcher : ITransientDependency
{
private static readonly ConcurrentBag<Type> SupportedTypesCache = new ConcurrentBag<Type>();
private static readonly ConcurrentBag<Type> UnsupportedTypesCache = new ConcurrentBag<Type>();
private readonly SystemTextJsonSupportTypeMatcherOptions _options;
public SystemTextJsonSupportTypeMatcher(IOptions<SystemTextJsonSupportTypeMatcherOptions> options)
{
_options = options.Value;
}
public bool Match(Type type)
{
if (UnsupportedTypesCache.Contains(type))
{
return false;
}
if (SupportedTypesCache.Contains(type))
{
return true;
}
if (_options.UnsupportedTypes.Any(x => x == type))
{
UnsupportedTypesCache.Add(type);
return false;
}
if (type.IsGenericType)
{
foreach (var genericArgument in type.GetGenericArguments())
{
if (!TypeHelper.IsPrimitiveExtended(genericArgument, includeNullables: true, includeEnums: true))
{
if (!Match(genericArgument))
{
return false;
}
}
else
{
if (_options.UnsupportedTypes.Any(x => x == genericArgument))
{
UnsupportedTypesCache.Add(genericArgument);
return false;
}
}
}
return true;
}
if (type.IsArray)
{
var elementType = type.GetElementType();
if (!TypeHelper.IsPrimitiveExtended(elementType, includeNullables: true, includeEnums: true))
{
if (!Match(elementType))
{
return false;
}
}
else
{
if (_options.UnsupportedTypes.Any(x => x == elementType))
{
UnsupportedTypesCache.Add(elementType);
return false;
}
}
return true;
}
if (TypeHelper.IsPrimitiveExtended(type, includeNullables: true, includeEnums: true))
{
return true;
}
if (type.GetCustomAttributes(true).Any(x => _options.UnsupportedAttributes.Any(a => a == x.GetType())))
{
UnsupportedTypesCache.Add(type);
return false;
}
if (type.DeclaringType != null && type.DeclaringType.GetCustomAttributes(true).Any(x => _options.UnsupportedAttributes.Any(a => a == x.GetType())))
{
UnsupportedTypesCache.Add(type);
return false;
}
foreach (var propertyInfo in type.GetProperties())
{
if (propertyInfo.IsDefined(typeof(DisableDateTimeNormalizationAttribute), true))
{
UnsupportedTypesCache.Add(type);
return false;
}
if (_options.UnsupportedTypes.Any(x => x == propertyInfo.PropertyType))
{
UnsupportedTypesCache.Add(propertyInfo.PropertyType);
return false;
}
if (propertyInfo.PropertyType.IsGenericType)
{
foreach (var genericArgument in propertyInfo.PropertyType.GetGenericArguments())
{
if (!TypeHelper.IsPrimitiveExtended(genericArgument, includeNullables: true, includeEnums: true))
{
if (!Match(genericArgument))
{
return false;
}
}
else
{
if (_options.UnsupportedTypes.Any(x => x == genericArgument))
{
UnsupportedTypesCache.Add(genericArgument);
return false;
}
}
}
}
if (propertyInfo.PropertyType.IsArray)
{
var elementType = propertyInfo.PropertyType.GetElementType();
if (!TypeHelper.IsPrimitiveExtended(elementType, includeNullables: true, includeEnums: true))
{
if (!Match(elementType))
{
return false;
}
}
else
{
if (_options.UnsupportedTypes.Any(x => x == elementType))
{
UnsupportedTypesCache.Add(elementType);
return false;
}
}
}
if (!TypeHelper.IsPrimitiveExtended(propertyInfo.PropertyType, includeNullables: true, includeEnums: true))
{
if (!Match(propertyInfo.PropertyType))
{
return false;
}
}
else
{
if (_options.UnsupportedTypes.Any(x => x == propertyInfo.PropertyType))
{
UnsupportedTypesCache.Add(propertyInfo.PropertyType);
return false;
}
}
}
SupportedTypesCache.Add(type);
return true;
}
}
}

18
framework/src/Volo.Abp.Json/Volo/Abp/Json/SystemTextJson/SystemTextJsonSupportTypeMatcherOptions.cs

@ -1,18 +0,0 @@
using System;
using Volo.Abp.Collections;
namespace Volo.Abp.Json.SystemTextJson
{
public class SystemTextJsonSupportTypeMatcherOptions
{
public ITypeList<Attribute> UnsupportedAttributes { get; }
public ITypeList UnsupportedTypes { get; }
public SystemTextJsonSupportTypeMatcherOptions()
{
UnsupportedAttributes = new TypeList<Attribute>();
UnsupportedTypes = new TypeList();
}
}
}

10
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController.cs

@ -39,6 +39,16 @@ namespace Volo.Abp.AspNetCore.Mvc.ModelBinding
input.Time3.Value.Kind.ToString().ToLower() + "_" +
input.InnerModel.Time4.Kind.ToString().ToLower();
}
//JSON input and output.
[HttpPost("ComplexTypeDateTimeKind_JSON")]
public string ComplexTypeDateTimeKind_JSON([FromBody]GetDateTimeKindModel input)
{
return input.Time1.Kind.ToString().ToLower() + "_" +
input.Time2.Kind.ToString().ToLower() + "_" +
input.Time3.Value.Kind.ToString().ToLower() + "_" +
input.InnerModel.Time4.Kind.ToString().ToLower();
}
}
public class GetDateTimeKindModel

43
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController_Tests.cs

@ -1,9 +1,14 @@
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Shouldly;
using Volo.Abp.Http;
using Volo.Abp.Json.SystemTextJson;
using Volo.Abp.Timing;
using Xunit;
@ -13,6 +18,15 @@ namespace Volo.Abp.AspNetCore.Mvc.ModelBinding
{
protected DateTimeKind DateTimeKind { get; set; }
protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services)
{
services.Configure<AbpSystemTextJsonSupportTypeMatcherOptions>(options =>
{
options.UnsupportedTypes.Add<GetDateTimeKindModel>();
options.UnsupportedTypes.Add<GetDateTimeKindModel.GetDateTimeKindInnerModel>();
});
}
[Fact]
public async Task DateTimeKind_Test()
{
@ -75,8 +89,29 @@ namespace Volo.Abp.AspNetCore.Mvc.ModelBinding
var resultAsString = await response.Content.ReadAsStringAsync();
//Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is UTC
//https://docs.microsoft.com/en-us/aspnet/core/migration/31-to-50?view=aspnetcore-3.1&tabs=visual-studio#datetime-values-are-model-bound-as-utc-times
resultAsString.ShouldBe(
$"utc_{DateTimeKind.ToString().ToLower()}_{DateTimeKind.ToString().ToLower()}_utc");
resultAsString.ShouldBe($"utc_{DateTimeKind.ToString().ToLower()}_{DateTimeKind.ToString().ToLower()}_utc");
}
[Fact]
public async Task ComplexTypeDateTimeKind_JSON_Test()
{
var time = DateTime.Parse("2010-01-01T00:00:00Z");
var response = await Client.PostAsync("/api/model-Binding-test/ComplexTypeDateTimeKind_JSON",
new StringContent(JsonSerializer.Serialize(
new GetDateTimeKindModel {
Time1 = time,
Time2 = time,
Time3 = time,
InnerModel = new GetDateTimeKindModel.GetDateTimeKindInnerModel
{
Time4 = time
}
}
), Encoding.UTF8, MimeTypes.Application.Json));
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync();
resultAsString.ShouldBe($"local_{DateTimeKind.ToString().ToLower()}_{DateTimeKind.ToString().ToLower()}_local");
}
}
@ -86,6 +121,8 @@ namespace Volo.Abp.AspNetCore.Mvc.ModelBinding
{
DateTimeKind = DateTimeKind.Utc;
services.Configure<AbpClockOptions>(x => x.Kind = DateTimeKind);
base.ConfigureServices(context, services);
}
}
@ -95,6 +132,8 @@ namespace Volo.Abp.AspNetCore.Mvc.ModelBinding
{
DateTimeKind = DateTimeKind.Local;
services.Configure<AbpClockOptions>(x => x.Kind = DateTimeKind);
base.ConfigureServices(context, services);
}
}
}

66
framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpSystemTextJsonSupportTypeMatcher_Tests.cs

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Json.SystemTextJson;
using Xunit;
namespace Volo.Abp.Json
{
public class AbpSystemTextJsonSupportTypeMatcher_Tests : AbpJsonTestBase
{
private readonly AbpSystemTextJsonSupportTypeMatcher _abpSystemTextJsonSupportTypeMatcher;
public AbpSystemTextJsonSupportTypeMatcher_Tests()
{
_abpSystemTextJsonSupportTypeMatcher = GetRequiredService<AbpSystemTextJsonSupportTypeMatcher>();
}
protected override void AfterAddApplication(IServiceCollection services)
{
services.Configure<AbpSystemTextJsonSupportTypeMatcherOptions>(options =>
{
options.UnsupportedTypes.Add<MyClass>();
options.UnsupportedTypes.Add<byte[]>();
options.UnsupportedTypes.Add<Dictionary<string, MyClass4>>();
});
}
[Fact]
public void CanHandle_Test()
{
_abpSystemTextJsonSupportTypeMatcher.Match(typeof(MyClass)).ShouldBeFalse();
_abpSystemTextJsonSupportTypeMatcher.Match(typeof(byte[])).ShouldBeFalse();
_abpSystemTextJsonSupportTypeMatcher.Match(typeof(MyClass2)).ShouldBeTrue();
_abpSystemTextJsonSupportTypeMatcher.Match(typeof(MyClass3)).ShouldBeTrue();
_abpSystemTextJsonSupportTypeMatcher.Match(typeof(MyClass4)).ShouldBeTrue();
_abpSystemTextJsonSupportTypeMatcher.Match(typeof(string)).ShouldBeTrue();
_abpSystemTextJsonSupportTypeMatcher.Match(typeof(string[])).ShouldBeTrue();
_abpSystemTextJsonSupportTypeMatcher.Match(typeof(Dictionary<string, MyClass4>)).ShouldBeFalse();
_abpSystemTextJsonSupportTypeMatcher.Match(typeof(IDictionary<string, MyClass4>)).ShouldBeTrue();
}
class MyClass
{
public DateTime Prop1 { get; set; }
}
class MyClass2
{
public DateTime Prop1 { get; set; }
}
class MyClass3
{
public MyClass4 Prop1 { get; set; }
}
class MyClass4
{
public DateTime Prop1 { get; set; }
}
}
}

113
framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/SystemTextJsonSupportTypeMatcher_Tests.cs

@ -1,113 +0,0 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Json.SystemTextJson;
using Volo.Abp.Timing;
using Xunit;
namespace Volo.Abp.Json
{
public class SystemTextJsonSupportTypeMatcher_Tests : AbpJsonTestBase
{
private readonly SystemTextJsonSupportTypeMatcher _systemTextJsonSupportTypeMatcher;
protected override void AfterAddApplication(IServiceCollection services)
{
services.Configure<SystemTextJsonSupportTypeMatcherOptions>(options =>
{
options.UnsupportedTypes.Add<MyClass7>();
options.UnsupportedTypes.Add<byte>();
});
base.AfterAddApplication(services);
}
public SystemTextJsonSupportTypeMatcher_Tests()
{
_systemTextJsonSupportTypeMatcher = GetRequiredService<SystemTextJsonSupportTypeMatcher>();
}
[Fact]
public void CanHandle_Test()
{
_systemTextJsonSupportTypeMatcher.Match(typeof(MyClass)).ShouldBeFalse();
_systemTextJsonSupportTypeMatcher.Match(typeof(MyClass2)).ShouldBeFalse();
_systemTextJsonSupportTypeMatcher.Match(typeof(MyClass3)).ShouldBeFalse();
_systemTextJsonSupportTypeMatcher.Match(typeof(MyClass4)).ShouldBeFalse();
_systemTextJsonSupportTypeMatcher.Match(typeof(MyClass5)).ShouldBeTrue();
_systemTextJsonSupportTypeMatcher.Match(typeof(MyClass6)).ShouldBeTrue();
_systemTextJsonSupportTypeMatcher.Match(typeof(MyClass7)).ShouldBeFalse();
_systemTextJsonSupportTypeMatcher.Match(typeof(MyClass8)).ShouldBeFalse();
_systemTextJsonSupportTypeMatcher.Match(typeof(MyClass9)).ShouldBeFalse();
_systemTextJsonSupportTypeMatcher.Match(typeof(string)).ShouldBeTrue();
_systemTextJsonSupportTypeMatcher.Match(typeof(string[])).ShouldBeTrue();
_systemTextJsonSupportTypeMatcher.Match(typeof(int)).ShouldBeTrue();
_systemTextJsonSupportTypeMatcher.Match(typeof(byte)).ShouldBeFalse();
_systemTextJsonSupportTypeMatcher.Match(typeof(Dictionary<byte, byte>)).ShouldBeFalse();
_systemTextJsonSupportTypeMatcher.Match(typeof(Dictionary<string, MyClass10>)).ShouldBeFalse();
}
[DisableDateTimeNormalization]
class MyClass
{
public DateTime Prop1 { get; set; }
}
class MyClass2
{
[DisableDateTimeNormalization]
public DateTime Prop1 { get; set; }
}
class MyClass3
{
public MyClass4 Prop1 { get; set; }
}
class MyClass4
{
[DisableDateTimeNormalization]
public DateTime Prop1 { get; set; }
}
class MyClass5
{
public DateTime Prop1 { get; set; }
public MyClass6 Prop2 { get; set; }
}
class MyClass6
{
public DateTime Prop1 { get; set; }
}
class MyClass7
{
public DateTime Prop1 { get; set; }
}
class MyClass8
{
public MyClass10[] Prop1 { get; set; }
}
class MyClass9
{
public Dictionary<string, MyClass10> Prop1 { get; set; }
}
class MyClass10
{
[DisableDateTimeNormalization]
public DateTime Prop1 { get; set; }
}
}
}
Loading…
Cancel
Save