diff --git a/src/DotVVM.Stryker.sln b/src/DotVVM.Stryker.sln deleted file mode 100644 index ebd4ace11c..0000000000 --- a/src/DotVVM.Stryker.sln +++ /dev/null @@ -1,70 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Framework", "Framework", "{57E0C0AE-FC80-4A15-A574-B51686AE50D8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotVVM.Core", "Framework\Core\DotVVM.Core.csproj", "{E266F025-4398-4443-8043-996FD3244C4E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotVVM.Framework", "Framework\Framework\DotVVM.Framework.csproj", "{96A44EA8-05FC-4013-9533-EA474B900268}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotVVM.Framework.Hosting.AspNetCore", "Framework\Hosting.AspNetCore\DotVVM.Framework.Hosting.AspNetCore.csproj", "{AD599F24-5994-40AA-983A-E523E4B49BCF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotVVM.Framework.Testing", "Framework\Testing\DotVVM.Framework.Testing.csproj", "{2C65B2E0-C88E-4E4A-94BC-D03F292EB867}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotVVM.Framework.Tests", "Tests\DotVVM.Framework.Tests.csproj", "{E9DBE18B-113E-40DE-816E-9C9374DBF78F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AutoUI", "AutoUI", "{7F0236F5-4759-4DAF-A7D8-52394AF78455}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotVVM.AutoUI.Annotations", "AutoUI\Annotations\DotVVM.AutoUI.Annotations.csproj", "{376CBC39-447B-4E13-B167-6DF99FB90E12}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotVVM.AutoUI", "AutoUI\Core\DotVVM.AutoUI.csproj", "{653F84D2-5598-4C68-89CA-C0C7D99944BB}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {E266F025-4398-4443-8043-996FD3244C4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E266F025-4398-4443-8043-996FD3244C4E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E266F025-4398-4443-8043-996FD3244C4E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E266F025-4398-4443-8043-996FD3244C4E}.Release|Any CPU.Build.0 = Release|Any CPU - {96A44EA8-05FC-4013-9533-EA474B900268}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {96A44EA8-05FC-4013-9533-EA474B900268}.Debug|Any CPU.Build.0 = Debug|Any CPU - {96A44EA8-05FC-4013-9533-EA474B900268}.Release|Any CPU.ActiveCfg = Release|Any CPU - {96A44EA8-05FC-4013-9533-EA474B900268}.Release|Any CPU.Build.0 = Release|Any CPU - {AD599F24-5994-40AA-983A-E523E4B49BCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AD599F24-5994-40AA-983A-E523E4B49BCF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AD599F24-5994-40AA-983A-E523E4B49BCF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AD599F24-5994-40AA-983A-E523E4B49BCF}.Release|Any CPU.Build.0 = Release|Any CPU - {2C65B2E0-C88E-4E4A-94BC-D03F292EB867}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2C65B2E0-C88E-4E4A-94BC-D03F292EB867}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2C65B2E0-C88E-4E4A-94BC-D03F292EB867}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2C65B2E0-C88E-4E4A-94BC-D03F292EB867}.Release|Any CPU.Build.0 = Release|Any CPU - {E9DBE18B-113E-40DE-816E-9C9374DBF78F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E9DBE18B-113E-40DE-816E-9C9374DBF78F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E9DBE18B-113E-40DE-816E-9C9374DBF78F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E9DBE18B-113E-40DE-816E-9C9374DBF78F}.Release|Any CPU.Build.0 = Release|Any CPU - {376CBC39-447B-4E13-B167-6DF99FB90E12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {376CBC39-447B-4E13-B167-6DF99FB90E12}.Debug|Any CPU.Build.0 = Debug|Any CPU - {376CBC39-447B-4E13-B167-6DF99FB90E12}.Release|Any CPU.ActiveCfg = Release|Any CPU - {376CBC39-447B-4E13-B167-6DF99FB90E12}.Release|Any CPU.Build.0 = Release|Any CPU - {653F84D2-5598-4C68-89CA-C0C7D99944BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {653F84D2-5598-4C68-89CA-C0C7D99944BB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {653F84D2-5598-4C68-89CA-C0C7D99944BB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {653F84D2-5598-4C68-89CA-C0C7D99944BB}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {E266F025-4398-4443-8043-996FD3244C4E} = {57E0C0AE-FC80-4A15-A574-B51686AE50D8} - {96A44EA8-05FC-4013-9533-EA474B900268} = {57E0C0AE-FC80-4A15-A574-B51686AE50D8} - {AD599F24-5994-40AA-983A-E523E4B49BCF} = {57E0C0AE-FC80-4A15-A574-B51686AE50D8} - {2C65B2E0-C88E-4E4A-94BC-D03F292EB867} = {57E0C0AE-FC80-4A15-A574-B51686AE50D8} - {376CBC39-447B-4E13-B167-6DF99FB90E12} = {7F0236F5-4759-4DAF-A7D8-52394AF78455} - {653F84D2-5598-4C68-89CA-C0C7D99944BB} = {7F0236F5-4759-4DAF-A7D8-52394AF78455} - EndGlobalSection -EndGlobal diff --git a/src/DynamicData/DynamicData/Metadata/PropertyDisplayMetadata.cs b/src/DynamicData/DynamicData/Metadata/PropertyDisplayMetadata.cs index cfc276f9cc..d4b8113fb1 100644 --- a/src/DynamicData/DynamicData/Metadata/PropertyDisplayMetadata.cs +++ b/src/DynamicData/DynamicData/Metadata/PropertyDisplayMetadata.cs @@ -7,7 +7,7 @@ namespace DotVVM.Framework.Controls.DynamicData.Metadata public class PropertyDisplayMetadata { - public MemberInfo PropertyInfo { get; set; } + public PropertyInfo PropertyInfo { get; set; } public string DisplayName { get; set; } diff --git a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs index f560451e36..9f426fa708 100644 --- a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs +++ b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs @@ -58,6 +58,7 @@ public static IServiceCollection RegisterDotVVMServices(IServiceCollection servi services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/src/Framework/Framework/Hosting/StaticCommandExecutor.cs b/src/Framework/Framework/Hosting/StaticCommandExecutor.cs index 8ae0d4d90b..a8fd04c97c 100644 --- a/src/Framework/Framework/Hosting/StaticCommandExecutor.cs +++ b/src/Framework/Framework/Hosting/StaticCommandExecutor.cs @@ -23,7 +23,7 @@ public class StaticCommandExecutor private readonly DotvvmConfiguration configuration; private readonly JsonSerializerOptions jsonOptions; - public StaticCommandExecutor(IStaticCommandServiceLoader serviceLoader, IViewModelSerializer serializer, IViewModelProtector viewModelProtector, IStaticCommandArgumentValidator validator, DotvvmConfiguration configuration) + public StaticCommandExecutor(IStaticCommandServiceLoader serviceLoader, IDotvvmJsonOptionsProvider jsonOptions, IViewModelProtector viewModelProtector, IStaticCommandArgumentValidator validator, DotvvmConfiguration configuration) { this.serviceLoader = serviceLoader; this.viewModelProtector = viewModelProtector; @@ -31,7 +31,7 @@ public StaticCommandExecutor(IStaticCommandServiceLoader serviceLoader, IViewMod this.configuration = configuration; if (configuration.ExperimentalFeatures.UseDotvvmSerializationForStaticCommandArguments.Enabled) { - this.jsonOptions = serializer.ViewModelJsonOptions; + this.jsonOptions = jsonOptions.ViewModelJsonOptions; } else { diff --git a/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs index cdd87f3239..72258c88cf 100644 --- a/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs @@ -20,8 +20,7 @@ public override bool CanConvert(Type typeToConvert) => class InnerConverter: JsonConverter where T: IDotvvmPrimitiveType { - private CustomPrimitiveTypeRegistration registration = ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(typeof(T))!; - // TODO: make this converter factory? + private CustomPrimitiveTypeRegistration registration = ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(typeof(T)) ?? throw new InvalidOperationException($"The type {typeof(T)} is not a custom primitive type!"); public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType is JsonTokenType.String @@ -30,7 +29,6 @@ or JsonTokenType.False or JsonTokenType.Number) { // TODO: utf8 parsing? - var registration = ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(typeToConvert)!; var str = reader.TokenType is JsonTokenType.String ? reader.GetString() : reader.HasValueSequence ? StringUtils.Utf8Decode(reader.ValueSequence.ToArray()) : StringUtils.Utf8Decode(reader.ValueSpan); diff --git a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs index b3e1292f82..5a89ac829a 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs @@ -40,33 +40,22 @@ public record SerializationException(bool Serialize, Type? ViewModelType, string private readonly IViewModelSerializationMapper viewModelMapper; private readonly IViewModelServerCache viewModelServerCache; private readonly IViewModelTypeMetadataSerializer viewModelTypeMetadataSerializer; - private readonly ViewModelJsonConverter viewModelConverter; + private readonly IDotvvmJsonOptionsProvider jsonOptions; private readonly ILogger? logger; public bool SendDiff { get; set; } = true; - public JsonSerializerOptions ViewModelJsonOptions { get; } - /// JsonOptions without the - public JsonSerializerOptions PlainJsonOptions { get; } - /// /// Initializes a new instance of the class. /// - public DefaultViewModelSerializer(DotvvmConfiguration configuration, IViewModelProtector protector, IViewModelSerializationMapper serializationMapper, IViewModelServerCache viewModelServerCache, IViewModelTypeMetadataSerializer viewModelTypeMetadataSerializer, ViewModelJsonConverter viewModelConverter, ILogger? logger) + public DefaultViewModelSerializer(DotvvmConfiguration configuration, IViewModelProtector protector, IViewModelSerializationMapper serializationMapper, IViewModelServerCache viewModelServerCache, IViewModelTypeMetadataSerializer viewModelTypeMetadataSerializer, IDotvvmJsonOptionsProvider jsonOptions, ILogger? logger) { this.viewModelProtector = protector; this.viewModelMapper = serializationMapper; this.viewModelServerCache = viewModelServerCache; this.viewModelTypeMetadataSerializer = viewModelTypeMetadataSerializer; - this.viewModelConverter = viewModelConverter; + this.jsonOptions = jsonOptions; this.logger = logger; - this.ViewModelJsonOptions = new JsonSerializerOptions(DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe) { - Converters = { viewModelConverter }, - WriteIndented = configuration.Debug, - }; - this.PlainJsonOptions = new JsonSerializerOptions(DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe) { - WriteIndented = configuration.Debug, - }; } /// @@ -119,13 +108,13 @@ public string SerializeViewModel(IDotvvmRequestContext context, object? commandR (int vmStart, int vmEnd) WriteViewModelJson(Utf8JsonWriter writer, IDotvvmRequestContext context, DotvvmSerializationState state) { - var converter = this.viewModelConverter.GetConverterCached(context.ViewModel!.GetType()); + var converter = jsonOptions.GetRootViewModelConverter(context.ViewModel!.GetType()); writer.WriteStartObject(); writer.Flush(); var vmStart = (int)writer.BytesCommitted; // needed for server side VM cache - we only store the object body, without $csrfToken and $encryptedValues - converter.WriteUntyped(writer, context.ViewModel, ViewModelJsonOptions, state, wrapObject: false); + converter.WriteUntyped(writer, context.ViewModel, jsonOptions.ViewModelJsonOptions, state, wrapObject: false); writer.Flush(); var vmEnd = (int)writer.BytesCommitted; @@ -163,8 +152,8 @@ public MemoryStream BuildViewModel(IDotvvmRequestContext context, object? comman var buffer = new MemoryStream(); using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions { - Indented = this.ViewModelJsonOptions.WriteIndented, - Encoder = ViewModelJsonOptions.Encoder, + Indented = jsonOptions.ViewModelJsonOptions.WriteIndented, + Encoder = jsonOptions.ViewModelJsonOptions.Encoder, //SkipValidation = true, // for the hack with WriteRawValue })) { @@ -293,7 +282,7 @@ public ReadOnlyMemory BuildStaticCommandResponse(IDotvvmRequestContext con using var state = DotvvmSerializationState.Create(isPostback: true, context.Services); var outputBuffer = new MemoryStream(); - using (var writer = new Utf8JsonWriter(outputBuffer, new JsonWriterOptions { Indented = this.ViewModelJsonOptions.WriteIndented, Encoder = ViewModelJsonOptions.Encoder })) + using (var writer = new Utf8JsonWriter(outputBuffer, new JsonWriterOptions { Indented = jsonOptions.ViewModelJsonOptions.WriteIndented, Encoder = jsonOptions.ViewModelJsonOptions.Encoder })) { writer.WriteStartObject(); writer.WritePropertyName("result"u8); @@ -329,7 +318,7 @@ private void WriteCommandData(object? data, Utf8JsonWriter writer, MemoryStream Debug.Assert(DotvvmSerializationState.Current is {}); try { - JsonSerializer.Serialize(writer, data, this.ViewModelJsonOptions); + JsonSerializer.Serialize(writer, data, jsonOptions.ViewModelJsonOptions); } catch (Exception ex) { @@ -395,7 +384,7 @@ public byte[] SerializeModelState(IDotvvmRequestContext context) { modelState = context.ModelState.Errors, action = "validationErrors" - }, this.PlainJsonOptions); + }, jsonOptions.PlainJsonOptions); } @@ -472,8 +461,8 @@ public void PopulateViewModel(IDotvvmRequestContext context, ReadOnlyMemory (Func)(t => { using var state = DotvvmSerializationState.Create(isPostback: true, context.Services, readEncryptedValues: new JsonObject()); - return JsonSerializer.Deserialize(a, t, ViewModelJsonOptions); + return JsonSerializer.Deserialize(a, t, jsonOptions.ViewModelJsonOptions); })).ToArray() : new Func[0]; diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmJsonOptionsProvider.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmJsonOptionsProvider.cs new file mode 100644 index 0000000000..550b27ab85 --- /dev/null +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmJsonOptionsProvider.cs @@ -0,0 +1,47 @@ +using System; +using System.Text.Json; +using DotVVM.Framework.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace DotVVM.Framework.ViewModel.Serialization; + +/// Creates and provides System.Text.Json serialization options for ViewModel serialization +public interface IDotvvmJsonOptionsProvider +{ + /// Options used for view model serialization, includes the + JsonSerializerOptions ViewModelJsonOptions { get; } + /// Options used for serialization of other objects like the ModelState in the invalid VM response. + JsonSerializerOptions PlainJsonOptions { get; } + + /// The the main converter used for viewmodel serialization and deserialization (in initial requests and commands) + IDotvvmJsonConverter GetRootViewModelConverter(Type type); +} + + +public class DotvvmJsonOptionsProvider : IDotvvmJsonOptionsProvider +{ + private Lazy _viewModelOptions; + public JsonSerializerOptions ViewModelJsonOptions => _viewModelOptions.Value; + private Lazy _plainJsonOptions; + public JsonSerializerOptions PlainJsonOptions => _plainJsonOptions.Value; + + private Lazy _viewModelConverter; + + public DotvvmJsonOptionsProvider(DotvvmConfiguration configuration) + { + var debug = configuration.Debug; + _viewModelConverter = new Lazy(() => configuration.ServiceProvider.GetRequiredService()); + _viewModelOptions = new Lazy(() => + new JsonSerializerOptions(DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe) { + Converters = { _viewModelConverter.Value }, + WriteIndented = debug + } + ); + _plainJsonOptions = new Lazy(() => + !debug ? DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe + : new JsonSerializerOptions(DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe) { WriteIndented = true } + ); + } + + public IDotvvmJsonConverter GetRootViewModelConverter(Type type) => _viewModelConverter.Value.GetDotvvmConverter(type); +} diff --git a/src/Framework/Framework/ViewModel/Serialization/IDotvvmJsonConverter.cs b/src/Framework/Framework/ViewModel/Serialization/IDotvvmJsonConverter.cs new file mode 100644 index 0000000000..6cea477348 --- /dev/null +++ b/src/Framework/Framework/ViewModel/Serialization/IDotvvmJsonConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DotVVM.Framework.ViewModel.Serialization; + +/// System.Text.Json converter which supports population of existing objects. Implementations of this interface are also expected to implement and inherit from +public interface IDotvvmJsonConverter +{ + public object? ReadUntyped(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, DotvvmSerializationState state); + public object? PopulateUntyped(ref Utf8JsonReader reader, Type typeToConvert, object? value, JsonSerializerOptions options, DotvvmSerializationState state); + public void WriteUntyped(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, DotvvmSerializationState state, bool requireTypeField = true, bool wrapObject = true); +} + +/// System.Text.Json converter which supports population of existing objects. +public interface IDotvvmJsonConverter: IDotvvmJsonConverter +{ + public T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, DotvvmSerializationState state); + public T Populate(ref Utf8JsonReader reader, Type typeToConvert, T value, JsonSerializerOptions options, DotvvmSerializationState state); + public void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options, DotvvmSerializationState state, bool requireTypeField = true, bool wrapObject = true); +} + diff --git a/src/Framework/Framework/ViewModel/Serialization/IViewModelSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/IViewModelSerializer.cs index 460e7ca386..ebd5c1d3dd 100644 --- a/src/Framework/Framework/ViewModel/Serialization/IViewModelSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/IViewModelSerializer.cs @@ -9,7 +9,6 @@ namespace DotVVM.Framework.ViewModel.Serialization { public interface IViewModelSerializer { - JsonSerializerOptions ViewModelJsonOptions { get; } ReadOnlyMemory BuildStaticCommandResponse(IDotvvmRequestContext context, object? commandResult, string[]? knownTypeMetadata = null); string SerializeViewModel(IDotvvmRequestContext context, object? commandResult = null, IEnumerable<(string name, string html)>? postbackUpdatedControls = null, bool serializeNewResources = false); diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs index dabbf5e1d6..58185679ee 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs @@ -15,7 +15,7 @@ namespace DotVVM.Framework.ViewModel.Serialization { /// - /// A JSON.NET converter that handles special features of DotVVM ViewModel serialization. + /// A System.Text.Json converter that handles special features of DotVVM ViewModel serialization. /// public class ViewModelJsonConverter : JsonConverterFactory { @@ -29,6 +29,7 @@ public static bool CanConvertType(Type type) => !ReflectionUtils.IsEnumerable(type) && ReflectionUtils.IsComplexType(type) && !ReflectionUtils.IsJsonDom(type) && + !type.IsDefined(typeof(JsonConverterAttribute), true) && type != typeof(object); /// @@ -39,29 +40,25 @@ public override bool CanConvert(Type objectType) return CanConvertType(objectType); } - public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => CreateConverter(typeToConvert); - public JsonConverter CreateConverter(Type typeToConvert) => + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => (JsonConverter)GetDotvvmConverter(typeToConvert); + private JsonConverter CreateConverterReally(Type typeToConvert) => (JsonConverter)Activator.CreateInstance(typeof(VMConverter<>).MakeGenericType(typeToConvert), this)!; public VMConverter CreateConverter() => new VMConverter(this); - private ConcurrentDictionary converterCache = new(); - internal IVMConverter GetConverterCached(Type type) => - converterCache.GetOrAdd(type, t => (IVMConverter)CreateConverter(t)); + private ConcurrentDictionary converterCache = new(); + internal IDotvvmJsonConverter GetDotvvmConverter(Type type) => + converterCache.GetOrAdd(type, t => (IDotvvmJsonConverter)CreateConverterReally(t)); + internal JsonConverter GetConverter(Type type) => + (JsonConverter)GetDotvvmConverter(type); - internal interface IVMConverter - { - public object? ReadUntyped(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, DotvvmSerializationState state); - public object? PopulateUntyped(ref Utf8JsonReader reader, Type typeToConvert, object? value, JsonSerializerOptions options, DotvvmSerializationState state); - public void WriteUntyped(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, DotvvmSerializationState state, bool requireTypeField = true, bool wrapObject = true); - } - public class VMConverter(ViewModelJsonConverter factory): JsonConverter, IVMConverter + public class VMConverter(ViewModelJsonConverter factory): JsonConverter, IDotvvmJsonConverter { ViewModelSerializationMap SerializationMap { get; } = factory.viewModelSerializationMapper.GetMap(); public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => this.Read(ref reader, typeToConvert, options, DotvvmSerializationState.Current!); - public T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, DotvvmSerializationState state) + public T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, DotvvmSerializationState state) { if (state is null) throw new ArgumentNullException(nameof(state), "DotvvmSerializationState must be created before calling the ViewModelJsonConverter."); @@ -71,7 +68,7 @@ public class VMConverter(ViewModelJsonConverter factory): JsonConverter, I if (reader.TokenType == JsonTokenType.Null) { Debug.Assert(!typeof(T).IsValueType); - return default; + return default!; } ReadObjectStart(ref reader); @@ -158,8 +155,8 @@ public void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options, /// Populates the specified JObject. /// public T? Populate(ref Utf8JsonReader reader, JsonSerializerOptions options, T value) => - this.Populate(ref reader, options, value, DotvvmSerializationState.Current!); - public T? Populate(ref Utf8JsonReader reader, JsonSerializerOptions options, T? value, DotvvmSerializationState state) + this.Populate(ref reader, typeof(T), value, options, DotvvmSerializationState.Current!); + public T Populate(ref Utf8JsonReader reader, Type typeToConvert, T value, JsonSerializerOptions options, DotvvmSerializationState state) { if (state is null) throw new ArgumentNullException(nameof(state), "DotvvmSerializationState must be created before calling the ViewModelJsonConverter."); @@ -167,7 +164,7 @@ public void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options, if (reader.TokenType == JsonTokenType.Null) { Debug.Assert(!typeof(T).IsValueType); - return default; + return default!; } ReadObjectStart(ref reader); var evSuppressed = state.EVReader!.Suppressed; @@ -193,7 +190,7 @@ public void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options, public object? ReadUntyped(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, DotvvmSerializationState state) => this.Read(ref reader, typeToConvert, options, state); public object? PopulateUntyped(ref Utf8JsonReader reader, Type typeToConvert, object? value, JsonSerializerOptions options, DotvvmSerializationState state) => - this.Populate(ref reader, options, (T)value!, state); + this.Populate(ref reader, typeof(T), (T)value!, options, state); public void WriteUntyped(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, DotvvmSerializationState state, bool requireTypeField = true, bool wrapObject = true) => this.Write(writer, (T)value!, options, state, requireTypeField, wrapObject); } diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs index 36f9acc504..baf49ad7dc 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs @@ -24,6 +24,7 @@ namespace DotVVM.Framework.ViewModel.Serialization /// public abstract class ViewModelSerializationMap { + protected readonly JsonSerializerOptions jsonOptions; protected readonly DotvvmConfiguration configuration; protected readonly ViewModelJsonConverter viewModelJsonConverter; @@ -45,8 +46,9 @@ public abstract class ViewModelSerializationMap /// /// Initializes a new instance of the class. /// - internal ViewModelSerializationMap(Type type, IEnumerable properties, MethodBase? constructor, DotvvmConfiguration configuration) + internal ViewModelSerializationMap(Type type, IEnumerable properties, MethodBase? constructor, JsonSerializerOptions jsonOptions, DotvvmConfiguration configuration) { + this.jsonOptions = jsonOptions; this.configuration = configuration; this.viewModelJsonConverter = configuration.ServiceProvider.GetRequiredService(); Type = type; @@ -62,10 +64,14 @@ public static ViewModelSerializationMap Create(Type type, IEnumerable(); + var dict = new Dictionary(capacity: Properties.Length); foreach (var propertyMap in Properties) { - if (!dict.TryAdd(propertyMap.Name, propertyMap)) + if (!dict.ContainsKey(propertyMap.Name)) + { + dict.Add(propertyMap.Name, propertyMap); + } + else { var other = dict[propertyMap.Name]; throw new InvalidOperationException($"Serialization map for '{Type.ToCode()}' has a name conflict between a {(propertyMap.PropertyInfo is FieldInfo ? "field" : "property")} '{propertyMap.PropertyInfo.Name}' and {(other.PropertyInfo is FieldInfo ? "field" : "property")} '{other.PropertyInfo.Name}' — both are named '{propertyMap.Name}' in JSON."); @@ -79,8 +85,8 @@ private void ValidatePropertyMap() } public sealed class ViewModelSerializationMap : ViewModelSerializationMap { - public ViewModelSerializationMap(IEnumerable properties, MethodBase? constructor, DotvvmConfiguration configuration): - base(typeof(T), properties, constructor, configuration) + public ViewModelSerializationMap(IEnumerable properties, MethodBase? constructor, JsonSerializerOptions jsonOptions, DotvvmConfiguration configuration): + base(typeof(T), properties, constructor, jsonOptions, configuration) { } public override void ResetFunctions() @@ -375,6 +381,7 @@ p.ConstructorParameter is {} || Block(typeof(T), [ currentProperty, readerTmp, ..propertyVars.Values ], block).OptimizeConstants(), reader, jsonOptions, value, allowPopulate, encryptedValuesReader, state); return ex.CompileFast(flags: CompilerFlags.ThrowOnNotSupportedExpression | CompilerFlags.EnableDelegateDebugInfo); + // return ex.Compile(); } Expression MemberAccess(Expression obj, ViewModelPropertyMap property) @@ -510,6 +517,7 @@ public WriterDelegate CreateWriterFactory() var ex = Lambda>( Block(new[] { value }, block).OptimizeConstants(), writer, value, jsonOptions, requireTypeField, encryptedValuesWriter, dotvvmState); return ex.CompileFast(flags: CompilerFlags.ThrowOnNotSupportedExpression | CompilerFlags.EnableDelegateDebugInfo); + // return ex.Compile(); } /// @@ -530,29 +538,29 @@ private bool CanContainEncryptedValues(Type type) if (property.JsonConverter is null) return null; if (property.JsonConverter is JsonConverterFactory factory) - return factory.CreateConverter(type, DefaultSerializerSettingsProvider.Instance.Settings); + return factory.CreateConverter(type, jsonOptions); return property.JsonConverter; } - private Expression CallPropertyConverterRead(JsonConverter converter, Expression reader, Expression jsonOptions, Expression dotvvmState, Expression? existingValue) + private Expression CallPropertyConverterRead(JsonConverter converter, Type type, Expression reader, Expression jsonOptions, Expression dotvvmState, Expression? existingValue) { Debug.Assert(reader.Type == typeof(Utf8JsonReader).MakeByRefType() || reader.Type == typeof(Utf8JsonReader), $"{reader.Type} != {typeof(Utf8JsonReader).MakeByRefType()}"); Debug.Assert(jsonOptions.Type == typeof(JsonSerializerOptions)); Debug.Assert(dotvvmState.Type == typeof(DotvvmSerializationState)); - if (converter is ViewModelJsonConverter.IVMConverter) + if (converter is IDotvvmJsonConverter) { - // T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, DotvvmSerializationState state) - // T Populate(ref Utf8JsonReader reader, JsonSerializerOptions options, T value, DotvvmSerializationState state) + // T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, DotvvmSerializationState state) + // T Populate(ref Utf8JsonReader reader, Type typeToConvert, T value, JsonSerializerOptions options, DotvvmSerializationState state); if (existingValue is null) - return Call(Constant(converter), "Read", Type.EmptyTypes, reader, Expression.Constant(Type), jsonOptions, dotvvmState); + return Call(Constant(converter), "Read", Type.EmptyTypes, reader, Constant(type), jsonOptions, dotvvmState); else - return Call(Constant(converter), "Populate", Type.EmptyTypes, reader, jsonOptions, existingValue, dotvvmState); + return Call(Constant(converter), "Populate", Type.EmptyTypes, reader, Constant(type), existingValue, jsonOptions, dotvvmState); } else { - var read = Call(Constant(converter), "Read", Type.EmptyTypes, reader, Expression.Constant(Type), jsonOptions); + var read = Call(Constant(converter), "Read", Type.EmptyTypes, reader, Constant(type), jsonOptions); if (read.Type.IsValueType) return read; else @@ -570,14 +578,14 @@ private Expression CallPropertyConverterWrite(JsonConverter converter, Expressio Debug.Assert(jsonOptions.Type == typeof(JsonSerializerOptions)); Debug.Assert(dotvvmState.Type == typeof(DotvvmSerializationState)); - // void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options, DotvvmSerializationState state) - if (converter is ViewModelJsonConverter.IVMConverter) + // void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options, DotvvmSerializationState state, bool requireTypeField = true, bool wrapObject = true) + if (converter is IDotvvmJsonConverter) { - return Call(Constant(converter), nameof(ViewModelJsonConverter.VMConverter.Write), Type.EmptyTypes, writer, value, jsonOptions, dotvvmState, Constant(true), Constant(true)); + return Call(Constant(converter), nameof(IDotvvmJsonConverter.Write), Type.EmptyTypes, writer, value, jsonOptions, dotvvmState, Constant(true), Constant(true)); } else { - return Call(Constant(converter), nameof(ViewModelJsonConverter.VMConverter.Write), Type.EmptyTypes, writer, value, jsonOptions); + return Call(Constant(converter), nameof(IDotvvmJsonConverter.Write), Type.EmptyTypes, writer, value, jsonOptions); } } @@ -662,7 +670,7 @@ private Expression DeserializePropertyValue(ViewModelPropertyMap property, Expre } if (GetPropertyConverter(property, type) is {} customConverter) { - return CallPropertyConverterRead(customConverter, reader, jsonOptions, dotvvmState, property.Populate ? existingValue : null); + return CallPropertyConverterRead(customConverter, type, reader, jsonOptions, dotvvmState, property.Populate ? existingValue : null); } if (TryDeserializePrimitive(reader, type) is {} primitive) @@ -670,36 +678,35 @@ private Expression DeserializePropertyValue(ViewModelPropertyMap property, Expre return primitive; } - if (this.viewModelJsonConverter.CanConvert(type)) + var converter = this.jsonOptions.GetConverter(type); + if (!converter.CanConvert(type)) { - var defaultConverter = this.viewModelJsonConverter.CreateConverter(type); - if (property.AllowDynamicDispatch && !type.IsSealed) + throw new Exception($"JsonOptions returned an invalid converter {converter} for type {type}."); + } + if (property.AllowDynamicDispatch && !type.IsSealed) + { + if (converter is IDotvvmJsonConverter) { return Call( JsonSerializationCodegenFragments.DeserializeViewModelDynamicMethod.MakeGenericMethod(type), reader, jsonOptions, existingValue, Constant(property.Populate), // ref Utf8JsonReader reader, JsonSerializerOptions options, TVM? existingValue, bool populate Constant(this.viewModelJsonConverter), // ViewModelJsonConverter factory - Constant(defaultConverter), // ViewModelJsonConverter.VMConverter? defaultConverter + Constant(converter), // ViewModelJsonConverter.VMConverter? defaultConverter dotvvmState); // DotvvmSerializationState state } else { - return CallPropertyConverterRead(defaultConverter, reader, jsonOptions, dotvvmState, property.Populate ? existingValue : null); + return Call( + JsonSerializationCodegenFragments.DeserializeValueDynamicMethod.MakeGenericMethod(property.Type), + reader, jsonOptions, existingValue, // ref Utf8JsonReader reader, JsonSerializerOptions options, TValue? existingValue + Constant(property.Populate), // bool populate + Constant(this.viewModelJsonConverter), // ViewModelJsonConverter factory + dotvvmState); // DotvvmSerializationState state } } - - if (property.AllowDynamicDispatch && !type.IsSealed && !type.IsValueType) - { - return Call( - JsonSerializationCodegenFragments.DeserializeValueDynamicMethod.MakeGenericMethod(property.Type), - reader, jsonOptions, existingValue, // ref Utf8JsonReader reader, JsonSerializerOptions options, TValue? existingValue - Constant(property.Populate), // bool populate - Constant(this.viewModelJsonConverter), // ViewModelJsonConverter factory - dotvvmState); // DotvvmSerializationState state - } else { - return Call(JsonSerializationCodegenFragments.DeserializeValueStaticMethod.MakeGenericMethod(property.Type), reader, jsonOptions); + return CallPropertyConverterRead(converter, type, reader, jsonOptions, dotvvmState, property.Populate ? existingValue : null); } } @@ -740,7 +747,7 @@ private Expression GetSerializeExpression(ViewModelPropertyMap property, Express } else { - var defaultConverter = this.viewModelJsonConverter.CreateConverter(value.Type); + var defaultConverter = this.viewModelJsonConverter.GetConverter(value.Type); return CallPropertyConverterWrite(defaultConverter, writer, value, jsonOptions, dotvvmState); } } @@ -804,7 +811,7 @@ private static void SerializeValue(Utf8JsonWriter writer, JsonSerializer // otherwise, just JsonSerializer.Deserialize with the specific type if (factory.CanConvert(type)) { - var converter = factory.GetConverterCached(type); + var converter = factory.GetDotvvmConverter(type); return populate ? (TValue?)converter.PopulateUntyped(ref reader, type, existingValue, options, state) : (TValue?)converter.ReadUntyped(ref reader, type, options, state); } @@ -813,7 +820,7 @@ private static void SerializeValue(Utf8JsonWriter writer, JsonSerializer } public static readonly MethodInfo DeserializeViewModelDynamicMethod = typeof(JsonSerializationCodegenFragments).GetMethod(nameof(DeserializeViewModelDynamic), BindingFlags.NonPublic | BindingFlags.Static).NotNull(); - private static TVM? DeserializeViewModelDynamic(ref Utf8JsonReader reader, JsonSerializerOptions options, TVM? existingValue, bool populate, ViewModelJsonConverter factory, ViewModelJsonConverter.VMConverter defaultConverter, DotvvmSerializationState state) + private static TVM? DeserializeViewModelDynamic(ref Utf8JsonReader reader, JsonSerializerOptions options, TVM? existingValue, bool populate, ViewModelJsonConverter factory, IDotvvmJsonConverter defaultConverter, DotvvmSerializationState state) where TVM: class { if (reader.TokenType == JsonTokenType.Null) @@ -828,11 +835,11 @@ private static void SerializeValue(Utf8JsonWriter writer, JsonSerializer if (defaultConverter is {} && realType == typeof(TVM)) { return populate && existingValue is {} - ? defaultConverter.Populate(ref reader, options, existingValue, state) + ? defaultConverter.Populate(ref reader, typeof(TVM), existingValue, options, state) : defaultConverter.Read(ref reader, typeof(TVM), options, state); } - var converter = factory.GetConverterCached(realType); + var converter = factory.GetDotvvmConverter(realType); return populate ? (TVM?)converter.PopulateUntyped(ref reader, realType, existingValue, options, state) : (TVM?)converter.ReadUntyped(ref reader, realType, options, state); } diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs index cfc5a8cce7..63e8483e0e 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs @@ -24,15 +24,17 @@ public class ViewModelSerializationMapper : IViewModelSerializationMapper private readonly IViewModelValidationMetadataProvider validationMetadataProvider; private readonly IPropertySerialization propertySerialization; private readonly DotvvmConfiguration configuration; + private readonly IDotvvmJsonOptionsProvider jsonOptions; private readonly ILogger? logger; public ViewModelSerializationMapper(IValidationRuleTranslator validationRuleTranslator, IViewModelValidationMetadataProvider validationMetadataProvider, - IPropertySerialization propertySerialization, DotvvmConfiguration configuration, ILogger? logger) + IPropertySerialization propertySerialization, DotvvmConfiguration configuration, IDotvvmJsonOptionsProvider jsonOptions, ILogger? logger) { this.validationRuleTranslator = validationRuleTranslator; this.validationMetadataProvider = validationMetadataProvider; this.propertySerialization = propertySerialization; this.configuration = configuration; + this.jsonOptions = jsonOptions; this.logger = logger; HotReloadMetadataUpdateHandler.SerializationMappers.Add(new(this)); @@ -57,7 +59,7 @@ protected virtual ViewModelSerializationMap CreateMap() // constructor which takes properties as parameters // if it exists, we always need to recreate the viewmodel var valueConstructor = GetConstructor(type); - return new ViewModelSerializationMap(GetProperties(type, valueConstructor), valueConstructor, configuration); + return new ViewModelSerializationMap(GetProperties(type, valueConstructor), valueConstructor, jsonOptions.ViewModelJsonOptions, configuration); } protected virtual MethodBase? GetConstructor(Type type) @@ -123,8 +125,11 @@ protected virtual MemberInfo[] ResolveShadowing(Type type, MemberInfo[] members) var shadowed = new Dictionary(); foreach (var member in members) { - if (shadowed.TryAdd(member.Name, member)) + if (!shadowed.ContainsKey(member.Name)) + { + shadowed.Add(member.Name, member); continue; + } var previous = shadowed[member.Name]; if (member.DeclaringType == previous.DeclaringType) throw new InvalidOperationException($"Two or more members named '{member.Name}' on type '{member.DeclaringType!.ToCode()}' are not allowed."); @@ -174,7 +179,7 @@ protected virtual IEnumerable GetProperties(Type type, Met include = include || !(bindAttribute is null or { Direction: Direction.None }) || property.IsDefined(typeof(JsonIncludeAttribute)) || - (type.IsGenericType && type.FullName.StartsWith("System.ValueTuple`")); + (type.IsGenericType && type.FullName!.StartsWith("System.ValueTuple`")); } if (!include) continue; @@ -198,7 +203,7 @@ protected virtual IEnumerable GetProperties(Type type, Met ); propertyMap.ConstructorParameter = ctorParam; propertyMap.JsonConverter = GetJsonConverter(property); - propertyMap.AllowDynamicDispatch = propertyType.IsAbstract || propertyType == typeof(object); + propertyMap.AllowDynamicDispatch = propertyMap.JsonConverter is null && (propertyType.IsAbstract || propertyType == typeof(object)); foreach (ISerializationInfoAttribute attr in property.GetCustomAttributes().OfType()) { @@ -209,6 +214,9 @@ protected virtual IEnumerable GetProperties(Type type, Met { propertyMap.Bind(bindAttribute.Direction); propertyMap.AllowDynamicDispatch = bindAttribute.AllowsDynamicDispatch(propertyMap.AllowDynamicDispatch); + + if (propertyMap.AllowDynamicDispatch && propertyMap.JsonConverter is {}) + throw new NotSupportedException($"Property '{property.DeclaringType?.ToCode()}.{property.Name}' cannot use dynamic dispatch, because it has an explicit JsonConverter."); } var viewModelProtectionAttribute = property.GetCustomAttribute(); diff --git a/src/Framework/Framework/ViewModel/Validation/ValidationErrorFactory.cs b/src/Framework/Framework/ViewModel/Validation/ValidationErrorFactory.cs index 43fed11397..855d217183 100644 --- a/src/Framework/Framework/ViewModel/Validation/ValidationErrorFactory.cs +++ b/src/Framework/Framework/ViewModel/Validation/ValidationErrorFactory.cs @@ -57,9 +57,11 @@ public static ValidationResult CreateValidationResult(this T vm, string error CreateValidationResult(vm.Context.Configuration, error, expressions); - private static JavascriptTranslator defaultJavaScriptTranslator = new JavascriptTranslator( - new JavascriptTranslatorConfiguration(), - new ViewModelSerializationMapper(new ViewModelValidationRuleTranslator(), new AttributeViewModelValidationMetadataProvider(), new DefaultPropertySerialization(), DotvvmConfiguration.CreateDefault(), null)); + private static Lazy defaultJavaScriptTranslator = new Lazy(() => { + var config = DotvvmConfiguration.CreateDefault(); + var mapper = config.ServiceProvider.GetRequiredService(); + return new JavascriptTranslator(new JavascriptTranslatorConfiguration(), mapper); + }); public static ValidationResult CreateValidationResult(ValidationContext validationContext, string error, params Expression>[] expressions) { @@ -69,7 +71,7 @@ public static ValidationResult CreateValidationResult(ValidationContext valid } // Fallback to default version of JavaScriptTranslator - return new ValidationResult ( error, expressions.Select(expr => GetPathFromExpression(defaultJavaScriptTranslator, expr)) ); + return new ValidationResult ( error, expressions.Select(expr => GetPathFromExpression(defaultJavaScriptTranslator.Value, expr)) ); } public static ViewModelValidationError CreateModelError(DotvvmConfiguration config, object? obj, Expression> expr, string error) => diff --git a/src/Tests/ViewModel/SerializerTests.cs b/src/Tests/ViewModel/SerializerTests.cs index e17e41bdac..f0056a812b 100644 --- a/src/Tests/ViewModel/SerializerTests.cs +++ b/src/Tests/ViewModel/SerializerTests.cs @@ -29,7 +29,7 @@ public class SerializerTests Converters = { jsonConverter }, WriteIndented = true }; - static DotvvmSerializationState CreateState(bool isPostback, JsonObject? readEncryptedValues = null) + static DotvvmSerializationState CreateState(bool isPostback, JsonObject readEncryptedValues = null) { var config = DotvvmTestHelper.DefaultConfig; return DotvvmSerializationState.Create( @@ -61,7 +61,7 @@ static T PopulateViewModel(string json, T existingValue, JsonObject encrypted using var state = CreateState(true, encryptedValues ?? new JsonObject()); var specificConverter = jsonConverter.CreateConverter(); var jsonReader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); - return (T)specificConverter.Populate(ref jsonReader, jsonOptions, existingValue, state); + return (T)specificConverter.Populate(ref jsonReader, typeof(T), existingValue, jsonOptions, state); } internal static (T vm, JsonObject json) SerializeAndDeserialize(T viewModel, bool isPostback = false) @@ -747,6 +747,43 @@ interface IVMInterface2 string Property2 { get; set; } } + + [TestMethod] + public void SupportCustomConverters() + { + var obj = new TestViewModelWithCustomConverter() { Property1 = "A", Property2 = "B" }; + var (obj2, json) = SerializeAndDeserialize(new StaticDispatchVMContainer { Value = obj }); + Console.WriteLine(json); + json = json["Value"].AsObject(); + Assert.AreEqual(obj.Property1, obj2.Value.Property1); + Assert.AreEqual(obj.Property2, obj2.Value.Property2); + Assert.AreEqual("A", (string)json["Property1"]); + Assert.AreEqual(null, json["Property2"]); + Assert.AreEqual("A,B", (string)json["Properties"]); + + var obj3 = Deserialize("""{"Properties":"C,D"}"""); + Assert.AreEqual("C", obj3.Property1); + Assert.AreEqual("D", obj3.Property2); + } + + [TestMethod] + public void SupportCustomConverters_DynamicDispatch() + { + var obj = new TestViewModelWithCustomConverter() { Property1 = "A", Property2 = "B" }; + var jsonStr = Serialize(new DefaultDispatchVMContainer { Value = obj }, out var _, false); + Console.WriteLine(jsonStr); + var obj2 = new DefaultDispatchVMContainer { Value = new TestViewModelWithCustomConverter() }; + var obj2Populated = PopulateViewModel(jsonStr, obj2); + Assert.AreSame(obj2, obj2Populated); + Assert.AreEqual(obj.Property1, ((TestViewModelWithCustomConverter)obj2.Value).Property1); + Assert.AreEqual(obj.Property2, ((TestViewModelWithCustomConverter)obj2.Value).Property2); + + var json = JsonNode.Parse(jsonStr).AsObject()["Value"].AsObject(); + Assert.AreEqual("A", (string)json["Property1"]); + Assert.AreEqual(null, json["Property2"]); + Assert.AreEqual("A,B", (string)json["Properties"]); + } + } public class DataNode @@ -962,6 +999,47 @@ public class Inner: TestViewModelWithPropertyShadowing } } + [JsonConverter(typeof(TestViewModelWithCustomConverter.Converter))] + class TestViewModelWithCustomConverter + { + public string Property1 { get; set; } + public string Property2 { get; set; } + + public class Converter : JsonConverter + { + public override TestViewModelWithCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var result = new TestViewModelWithCustomConverter(); + while (reader.TokenType != JsonTokenType.EndObject && reader.Read()) + { + if (reader.ValueTextEquals("Properties")) + { + reader.Read(); + var val = reader.GetString().Split(','); + result.Property1 = val[0]; + result.Property2 = val[1]; + reader.Read(); + } + else + { + reader.Read(); + reader.Skip(); + reader.Read(); + } + } + return result; + } + + public override void Write(Utf8JsonWriter writer, TestViewModelWithCustomConverter value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString("Properties"u8, $"{value.Property1},{value.Property2}"); + writer.WriteString("Property1"u8, value.Property1); + writer.WriteEndObject(); + } + } + } + class DynamicDispatchVMContainer { [Bind(AllowDynamicDispatch = true)] diff --git a/src/stryker-config.json b/src/stryker-config.json deleted file mode 100644 index d6b98b4484..0000000000 --- a/src/stryker-config.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "stryker-config": { - "mutation-level": "Advanced", - "mutate": [ - "**/*" - ], - "target-framework": "net8.0", - "coverage-analysis": "off", - "disable-bail": false, - "disable-mix-mutants": false, - "verbosity": "info", - "reporters": [ "progress", "html", "json" ], - "since": { - "enabled": false, - "target": "main" - }, - "test-projects": [ - "Tests/DotVVM.Framework.Tests.csproj" - ], - // stryker needs to be able to build the solution, but DotVVM.sln is not buildable on Linux and DotVVM.Crossplatform.slnf is not a solution file - "solution": "DotVVM.Stryker.sln", - "report-file-name": "strykker-report" - } -}