From f88e21d215d9da37beb9771d6aed3f03d14c750e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Thu, 14 Mar 2024 18:09:34 +0100 Subject: [PATCH 01/20] Initial migration to System.Text.Json --- .github/workflows/main.yml | 3 +- src/Directory.Build.props | 2 +- src/Framework/Core/DotVVM.Core.csproj | 1 + src/Framework/Core/ViewModel/BindAttribute.cs | 5 + .../ViewModel/DefaultPropertySerialization.cs | 8 +- .../Binding/BindingPropertyException.cs | 2 +- .../Framework/Binding/DotvvmProperty.cs | 2 +- .../Expressions/BindingDebugJsonConverter.cs | 15 +- .../Binding/Expressions/BindingExpression.cs | 2 +- .../Expressions/CommandBindingExpression.cs | 4 +- .../Binding/HelperNamespace/BindingApi.cs | 1 - .../Framework/Binding/ValueOrBinding.cs | 6 +- .../Binding/ValueOrBindingExtensions.cs | 1 - .../Compilation/Binding/ExpressionHelper.cs | 6 +- .../StaticCommandExecutionPlanSerializer.cs | 4 +- .../ControlTree/BindingExtensionParameter.cs | 16 + .../ControlTree/ControlResolverMetadata.cs | 2 +- .../ControlResolverMetadataBase.cs | 2 +- .../Resolved/ResolvedTypeDescriptor.cs | 4 +- .../DotvvmCompilationDiagnostic.cs | 2 +- .../Compilation/DotvvmCompilationException.cs | 2 +- .../Compilation/Javascript/Ast/JsLiteral.cs | 6 +- .../Compilation/Javascript/Ast/JsNode.cs | 2 +- .../Javascript/JavascriptCompilationHelper.cs | 13 +- .../Javascript/JsViewModelPropertyAdjuster.cs | 3 +- .../Javascript/ParametrizedCode.cs | 4 +- .../Framework/Compilation/NamespaceImport.cs | 12 +- .../DefaultSerializerSettingsProvider.cs | 45 +- .../Configuration/Dotvvm3StateFeatureFlag.cs | 20 +- .../DotvvmCompilationPageConfiguration.cs | 18 +- .../Configuration/DotvvmConfiguration.cs | 29 +- .../DotvvmConfigurationException.cs | 6 +- ...otvvmConfigurationSerializationResolver.cs | 102 ++- .../DotvvmControlConfiguration.cs | 13 +- .../DotvvmDiagnosticsConfiguration.cs | 6 +- ...DotvvmExperimentalFeaturesConfiguration.cs | 12 +- .../Configuration/DotvvmFeatureFlag.cs | 20 +- .../DotvvmGlobal3StateFeatureFlag.cs | 14 +- .../Configuration/DotvvmGlobalFeatureFlag.cs | 12 +- .../DotvvmMarkupConfiguration.cs | 20 +- .../DotvvmPerfWarningsConfiguration.cs | 8 +- .../DotvvmRuntimeConfiguration.cs | 8 +- .../DotvvmSecurityConfiguration.cs | 20 +- .../HtmlAttributeTransformConfiguration.cs | 16 +- .../Configuration/HtmlTagAttributePair.cs | 32 +- .../RestApiRegistrationHelpers.cs | 2 +- .../ViewCompilationConfiguration.cs | 10 +- src/Framework/Framework/Controls/Content.cs | 6 +- .../Framework/Controls/ContentPlaceHolder.cs | 1 - .../Controls/DotvvmBindableObject.cs | 2 +- .../Framework/Controls/DotvvmControl.cs | 1 - .../DotvvmControlDebugJsonConverter.cs | 53 +- .../Framework/Controls/DotvvmMarkupControl.cs | 13 +- .../Framework/Controls/FileUpload.cs | 5 +- src/Framework/Framework/Controls/GridView.cs | 4 +- .../Infrastructure/BodyResourceLinks.cs | 5 +- .../Controls/KnockoutBindingGroup.cs | 6 +- .../Framework/Controls/KnockoutHelper.cs | 33 +- src/Framework/Framework/Controls/Literal.cs | 3 +- .../Framework/Controls/ModalDialog.cs | 1 - src/Framework/Framework/Controls/Repeater.cs | 4 +- .../Framework/Controls/RouteLinkHelpers.cs | 10 +- src/Framework/Framework/Controls/TextBox.cs | 1 - src/Framework/Framework/Controls/Validator.cs | 11 +- .../DotVVMServiceCollectionExtensions.cs | 1 + .../CompilationPageApiPresenter.cs | 6 +- .../DiagnosticsInformationSender.cs | 9 +- .../Diagnostics/DiagnosticsRenderer.cs | 7 +- .../Diagnostics/DiagnosticsRequestTracer.cs | 24 +- .../DiagnosticsServerConfiguration.cs | 21 +- .../Diagnostics/DiagnosticsStartupTracer.cs | 2 +- .../Framework/Diagnostics/JsonSizeAnalyzer.cs | 128 ++-- .../Diagnostics/Models/RequestDiagnostics.cs | 13 +- .../Diagnostics/PerformanceWarningTracer.cs | 67 +- .../Framework/DotVVM.Framework.csproj | 2 +- .../Framework/Hosting/DotvvmPresenter.cs | 50 +- .../Hosting/DotvvmPropertySerializableList.cs | 16 +- .../Framework/Hosting/DotvvmRequestContext.cs | 5 +- .../Hosting/DotvvmRequestContextExtensions.cs | 5 +- .../Hosting/ErrorPages/ErrorPageTemplate.cs | 98 ++- .../Framework/Hosting/HttpRedirectService.cs | 1 - .../Hosting/IDotvvmRequestContext.cs | 11 +- .../Framework/Hosting/IHttpResponse.cs | 12 +- .../Middlewares/DotvvmFileUploadMiddleware.cs | 1 - .../Hosting/StaticCommandExecutor.cs | 24 +- .../Framework/Hosting/VisualStudioHelper.cs | 41 +- .../JQueryGlobalizeScriptCreator.cs | 223 +++--- .../DotvvmResourceRepository.cs | 2 +- .../EmbeddedResourceLocation.cs | 2 +- .../InlineScriptResource.cs | 9 +- .../InlineStylesheetResource.cs | 11 +- .../ResourceManagement/LinkResourceBase.cs | 18 +- .../LocalResourceLocation.cs | 1 - .../ReflectionAssemblyJsonConverter.cs | 118 ++-- .../ResourceManagement/ResourceBase.cs | 1 - .../ResourceRepositoryJsonConverter.cs | 136 ++-- .../ResourceManagement/ScriptResource.cs | 3 +- .../ViewModuleImportResource.cs | 5 +- .../ViewModuleInitResource.cs | 1 - .../Resources/Scripts/postback/updater.ts | 4 +- .../Routing/RouteTableJsonConverter.cs | 76 +- .../Runtime/DefaultOutputRenderer.cs | 13 +- .../Framework/Runtime/IOutputRenderer.cs | 5 +- .../Security/FakeViewModelProtector.cs | 13 +- .../Framework/Security/IViewModelProtector.cs | 4 +- .../Storage/FileSystemReturnedFileStorage.cs | 23 +- .../Testing/TestDotvvmRequestContext.cs | 6 +- .../Framework/Testing/TestHttpResponse.cs | 16 +- .../Framework/Utils/ExpressionUtils.cs | 47 ++ .../Framework/Utils/FunctionalExtensions.cs | 4 + .../Framework/Utils/JsonDiffWriter.cs | 273 +++++++ src/Framework/Framework/Utils/JsonUtils.cs | 158 ++--- src/Framework/Framework/Utils/MemoryUtils.cs | 30 + .../Framework/Utils/ReflectionUtils.cs | 6 +- src/Framework/Framework/Utils/StringUtils.cs | 2 + .../Framework/Utils/SystemTextJsonHacks.cs | 69 ++ .../Framework/Utils/SystemTextJsonUtils.cs | 115 +++ .../ViewModel/DotvvmViewModelBase.cs | 2 +- .../Serialization/ClientExtenderInfo.cs | 6 +- .../ViewModel/Serialization/ClientTypeId.cs | 94 +++ .../CustomPrimitiveTypeJsonConverter.cs | 75 +- .../DefaultViewModelSerializer.cs | 427 +++++++---- .../DefaultViewModelServerCache.cs | 42 +- .../Serialization/DotvvmByteArrayConverter.cs | 38 +- .../Serialization/DotvvmDateOnlyConverter.cs | 46 +- .../Serialization/DotvvmDateTimeConverter.cs | 48 +- .../DotvvmDictionaryConverter.cs | 122 ++-- .../Serialization/DotvvmEnumConverter.cs | 381 ++++++++++ .../Serialization/DotvvmTimeOnlyConverter.cs | 46 +- .../Serialization/EncryptedValuesReader.cs | 40 +- .../Serialization/EncryptedValuesWriter.cs | 24 +- .../IViewModelSerializationMapper.cs | 1 + .../Serialization/IViewModelSerializer.cs | 18 +- .../Serialization/IViewModelServerCache.cs | 7 +- .../IViewModelTypeMetadataSerializer.cs | 7 +- .../SerialiationMapperAttributeHelper.cs | 31 + .../Serialization/ViewModelJsonConverter.cs | 285 +++++--- .../Serialization/ViewModelMapperHelper.cs | 15 +- .../Serialization/ViewModelPropertyMap.cs | 6 +- .../ViewModelSerializationMap.cs | 668 ++++++++++++------ .../ViewModelSerializationMapper.cs | 31 +- .../ViewModelTypeMetadataSerializer.cs | 175 +++-- .../StaticCommandValidationError.cs | 8 +- .../Validation/ValidationErrorFactory.cs | 2 +- .../ViewModelPropertyValidationRule.cs | 8 +- .../Validation/ViewModelValidationError.cs | 6 +- .../Hosting/DotvvmHttpResponse.cs | 31 +- .../Security/DefaultViewModelProtector.cs | 16 +- src/Framework/Testing/ControlTestHelper.cs | 54 +- .../Testing/DotVVM.Framework.Testing.csproj | 2 + src/Framework/Testing/DotvvmTestHelper.cs | 30 +- src/Samples/Api.AspNetCore/Startup.cs | 1 - src/Samples/Api.AspNetCoreLatest/Startup.cs | 1 - ...VVM.Samples.BasicSamples.Api.Common.csproj | 2 +- ...VVM.Samples.BasicSamples.AspNetCore.csproj | 4 +- .../DateTimeSerialization.cs | 3 - .../Binding/JavascriptCompilationTests.cs | 4 +- src/Tests/Binding/NullPropagationTests.cs | 4 +- .../Binding/StaticCommandExecutorTests.cs | 3 +- .../ControlTests/AutoUIResourcesTests.cs | 4 +- src/Tests/ControlTests/CommandTests.cs | 10 +- .../ControlTests/CompositeControlTests.cs | 4 +- .../ControlTests/HierarchyRepeaterTests.cs | 4 +- src/Tests/ControlTests/MarkupControlTests.cs | 22 +- src/Tests/ControlTests/SimpleControlTests.cs | 6 +- ...ulesServerSideTests.IncludeViewModule.html | 5 +- ...rSideTests.IncludeViewModuleInControl.html | 16 +- src/Tests/DotVVM.Framework.Tests.csproj | 2 + src/Tests/Routing/RouteSerializationTests.cs | 7 +- .../ConfigurationSerializationTests.cs | 13 +- ...mCompilationExceptionSerializationTests.cs | 6 +- src/Tests/Runtime/ResourceManagerTests.cs | 12 +- ...gurationSerializationTests.AuxOptions.json | 38 +- ...rializationTests.ExperimentalFeatures.json | 42 +- ...onfigurationSerializationTests.Markup.json | 33 +- ...nfigurationSerializationTests.RestAPI.json | 44 +- ...alizationTests.SerializeDefaultConfig.json | 302 +++----- ...rializationTests.SerializeEmptyConfig.json | 41 +- ...SerializationTests.SerializeResources.json | 84 +-- ...ionSerializationTests.SerializeRoutes.json | 41 +- .../DefaultViewModelSerializerTests.cs | 95 ++- src/Tests/ViewModel/JsonDiffTests.cs | 69 +- src/Tests/ViewModel/JsonPatchTests.cs | 84 ++- src/Tests/ViewModel/SerializerErrorTests.cs | 3 +- src/Tests/ViewModel/SerializerTests.cs | 267 +++++-- .../ViewModelSerializationMapperTests.cs | 29 +- .../ViewModelTypeMetadataSerializerTests.cs | 53 +- .../DotVVM.HotReload.AspNetCore.csproj | 2 +- 188 files changed, 4146 insertions(+), 2646 deletions(-) create mode 100644 src/Framework/Framework/Utils/JsonDiffWriter.cs create mode 100644 src/Framework/Framework/Utils/MemoryUtils.cs create mode 100644 src/Framework/Framework/Utils/SystemTextJsonHacks.cs create mode 100644 src/Framework/Framework/Utils/SystemTextJsonUtils.cs create mode 100644 src/Framework/Framework/ViewModel/Serialization/ClientTypeId.cs create mode 100644 src/Framework/Framework/ViewModel/Serialization/DotvvmEnumConverter.cs create mode 100644 src/Framework/Framework/ViewModel/Serialization/SerialiationMapperAttributeHelper.cs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 054358ef42..8fb15ea459 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -117,7 +117,7 @@ jobs: # target-framework: net7.0 - name: Tests (net472) uses: ./.github/unittest - if: matrix.os == 'windows-2022' + if: ${{ (matrix.os == 'windows-2022') && (success() || failure()) }} with: project: src/Tests name: framework-tests @@ -126,6 +126,7 @@ jobs: target-framework: net472 - name: Analyzers.Tests (net6.0) uses: ./.github/unittest + if: ${{ success() || failure() }} with: project: src/Analyzers/Analyzers.Tests name: analyzers-tests diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 56b7b42208..cc0bba6d99 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -20,7 +20,7 @@ - 11.0 + 12.0 $(NoWarn);CS1591;CS1573 true diff --git a/src/Framework/Core/DotVVM.Core.csproj b/src/Framework/Core/DotVVM.Core.csproj index 59d4af116a..738f8d37ed 100644 --- a/src/Framework/Core/DotVVM.Core.csproj +++ b/src/Framework/Core/DotVVM.Core.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Framework/Core/ViewModel/BindAttribute.cs b/src/Framework/Core/ViewModel/BindAttribute.cs index 354416c024..9b485cf556 100644 --- a/src/Framework/Core/ViewModel/BindAttribute.cs +++ b/src/Framework/Core/ViewModel/BindAttribute.cs @@ -21,6 +21,11 @@ public class BindAttribute : Attribute /// public string? Name { get; set; } + public bool? _allowDynamicDispatch; + public bool AllowDynamicDispatch { get => _allowDynamicDispatch ?? false; set => _allowDynamicDispatch = value; } + + public bool AllowsDynamicDispatch(bool defaultValue) => _allowDynamicDispatch ?? defaultValue; + /// /// Initializes a new instance of the class. diff --git a/src/Framework/Core/ViewModel/DefaultPropertySerialization.cs b/src/Framework/Core/ViewModel/DefaultPropertySerialization.cs index 12def499c6..ef97c13eae 100644 --- a/src/Framework/Core/ViewModel/DefaultPropertySerialization.cs +++ b/src/Framework/Core/ViewModel/DefaultPropertySerialization.cs @@ -1,5 +1,5 @@ using System.Reflection; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace DotVVM.Framework.ViewModel { @@ -19,10 +19,10 @@ public string ResolveName(PropertyInfo propertyInfo) if (string.IsNullOrEmpty(bindAttribute?.Name)) { // use JsonProperty name if Bind attribute is not present or doesn't specify it - var jsonPropertyAttribute = propertyInfo.GetCustomAttribute(); - if (!string.IsNullOrEmpty(jsonPropertyAttribute?.PropertyName)) + var jsonPropertyAttribute = propertyInfo.GetCustomAttribute(); + if (!string.IsNullOrEmpty(jsonPropertyAttribute?.Name)) { - return jsonPropertyAttribute!.PropertyName!; + return jsonPropertyAttribute!.Name!; } } diff --git a/src/Framework/Framework/Binding/BindingPropertyException.cs b/src/Framework/Framework/Binding/BindingPropertyException.cs index 07d847dae7..ba813c89f4 100644 --- a/src/Framework/Framework/Binding/BindingPropertyException.cs +++ b/src/Framework/Framework/Binding/BindingPropertyException.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Compilation; using DotVVM.Framework.Runtime; using FastExpressionCompiler; -using Newtonsoft.Json; namespace DotVVM.Framework.Binding { diff --git a/src/Framework/Framework/Binding/DotvvmProperty.cs b/src/Framework/Framework/Binding/DotvvmProperty.cs index 8eb7261768..79aed966bf 100644 --- a/src/Framework/Framework/Binding/DotvvmProperty.cs +++ b/src/Framework/Framework/Binding/DotvvmProperty.cs @@ -11,11 +11,11 @@ using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Compilation.ControlTree.Resolved; -using Newtonsoft.Json; using System.Diagnostics.CodeAnalysis; using System.Collections.Immutable; using DotVVM.Framework.Runtime; using System.Threading; +using System.Text.Json.Serialization; namespace DotVVM.Framework.Binding { diff --git a/src/Framework/Framework/Binding/Expressions/BindingDebugJsonConverter.cs b/src/Framework/Framework/Binding/Expressions/BindingDebugJsonConverter.cs index 753865b170..c236ae50d1 100644 --- a/src/Framework/Framework/Binding/Expressions/BindingDebugJsonConverter.cs +++ b/src/Framework/Framework/Binding/Expressions/BindingDebugJsonConverter.cs @@ -1,20 +1,17 @@ using System; using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; namespace DotVVM.Framework.Binding.Expressions { - internal class BindingDebugJsonConverter: JsonConverter + internal class BindingDebugJsonConverter: JsonConverter { - public override bool CanConvert(Type objectType) => - typeof(IBinding).IsAssignableFrom(objectType); - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => + public override IBinding Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException("Deserializing dotvvm bindings from JSON is not supported."); - public override void WriteJson(JsonWriter w, object? valueObj, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, IBinding obj, JsonSerializerOptions options) { - var obj = valueObj; - w.WriteValue(obj?.ToString()); + writer.WriteStringValue(obj?.ToString()); // w.WriteStartObject(); // w.WritePropertyName("ToString"); diff --git a/src/Framework/Framework/Binding/Expressions/BindingExpression.cs b/src/Framework/Framework/Binding/Expressions/BindingExpression.cs index c84e512c8b..58dfd3504a 100644 --- a/src/Framework/Framework/Binding/Expressions/BindingExpression.cs +++ b/src/Framework/Framework/Binding/Expressions/BindingExpression.cs @@ -22,7 +22,7 @@ namespace DotVVM.Framework.Binding.Expressions /// This is a base class for all bindings, BindingExpression in general does not guarantee that the binding will have any property. /// This class only contains the glue code which automatically calls resolvers and caches the results when is invoked. [BindingCompilationRequirements(optional: new[] { typeof(BindingResolverCollection) })] - [Newtonsoft.Json.JsonConverter(typeof(BindingDebugJsonConverter))] + [System.Text.Json.Serialization.JsonConverter(typeof(BindingDebugJsonConverter))] public abstract class BindingExpression : IBinding, ICloneableBinding { private protected struct PropValue where TValue : class diff --git a/src/Framework/Framework/Binding/Expressions/CommandBindingExpression.cs b/src/Framework/Framework/Binding/Expressions/CommandBindingExpression.cs index ee99725ee8..6e5f4cc477 100644 --- a/src/Framework/Framework/Binding/Expressions/CommandBindingExpression.cs +++ b/src/Framework/Framework/Binding/Expressions/CommandBindingExpression.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Linq; using System.Linq.Expressions; +using System.Net; using System.Reflection; using System.Threading.Tasks; using DotVVM.Framework.Binding.Properties; @@ -14,7 +15,6 @@ using DotVVM.Framework.Runtime.Filters; using DotVVM.Framework.Utils; using FastExpressionCompiler; -using Newtonsoft.Json; namespace DotVVM.Framework.Binding.Expressions { @@ -149,7 +149,7 @@ public ExpectedTypeBindingProperty GetExpectedType(AssignedPropertyBindingProper needsCommandArgs == false ? javascriptPostbackInvocation_noCommandArgs : javascriptPostbackInvocation) .AssignParameters(p => - p == CommandIdParameter ? CodeParameterAssignment.FromLiteral(id) : + p == CommandIdParameter ? new(KnockoutHelper.MakeStringLiteral(id, htmlSafe: false), OperatorPrecedence.Max) : default); public CommandBindingExpression(BindingCompilationService service, Action command, string id) diff --git a/src/Framework/Framework/Binding/HelperNamespace/BindingApi.cs b/src/Framework/Framework/Binding/HelperNamespace/BindingApi.cs index dc10ec1d39..73d1f4764f 100644 --- a/src/Framework/Framework/Binding/HelperNamespace/BindingApi.cs +++ b/src/Framework/Framework/Binding/HelperNamespace/BindingApi.cs @@ -4,7 +4,6 @@ using System.Text.RegularExpressions; using DotVVM.Framework.Compilation.Javascript; using DotVVM.Framework.Compilation.Javascript.Ast; -using Newtonsoft.Json; using Generic = DotVVM.Framework.Compilation.Javascript.MethodFindingHelper.Generic; namespace DotVVM.Framework.Binding.HelperNamespace diff --git a/src/Framework/Framework/Binding/ValueOrBinding.cs b/src/Framework/Framework/Binding/ValueOrBinding.cs index 82814c2805..aa5db0bf3c 100644 --- a/src/Framework/Framework/Binding/ValueOrBinding.cs +++ b/src/Framework/Framework/Binding/ValueOrBinding.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Text.Json; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Binding.Properties; using DotVVM.Framework.Compilation.Javascript; using DotVVM.Framework.Configuration; using DotVVM.Framework.Controls; using DotVVM.Framework.Utils; -using Newtonsoft.Json; namespace DotVVM.Framework.Binding { @@ -111,14 +111,14 @@ public ValueOrBinding UpCast() /// Returns a Javascript (knockout) expression representing this value or this binding. public ParametrizedCode GetParametrizedJsExpression(DotvvmBindableObject control, bool unwrapped = false) => ProcessValueBinding(control, - value => new ParametrizedCode(JsonConvert.SerializeObject(value, DefaultSerializerSettingsProvider.Instance.Settings), OperatorPrecedence.Max), + value => new ParametrizedCode(JsonSerializer.Serialize(value, DefaultSerializerSettingsProvider.Instance.Settings), OperatorPrecedence.Max), binding => binding.GetParametrizedKnockoutExpression(control, unwrapped) ); /// Returns a Javascript (knockout) expression representing this value or this binding. The parameters are set to defaults, so knockout context is $context, view model is $data and both are available as global. public string GetJsExpression(DotvvmBindableObject control, bool unwrapped = false) => ProcessValueBinding(control, - value => JsonConvert.SerializeObject(value, DefaultSerializerSettingsProvider.Instance.Settings), + value => JsonSerializer.Serialize(value, DefaultSerializerSettingsProvider.Instance.Settings), binding => binding.GetKnockoutBindingExpression(control, unwrapped) ); diff --git a/src/Framework/Framework/Binding/ValueOrBindingExtensions.cs b/src/Framework/Framework/Binding/ValueOrBindingExtensions.cs index 2b684a250a..a822e42cdf 100644 --- a/src/Framework/Framework/Binding/ValueOrBindingExtensions.cs +++ b/src/Framework/Framework/Binding/ValueOrBindingExtensions.cs @@ -12,7 +12,6 @@ using DotVVM.Framework.Compilation.Javascript; using DotVVM.Framework.Controls; using DotVVM.Framework.Utils; -using Newtonsoft.Json; public static class ValueOrBindingExtensions { diff --git a/src/Framework/Framework/Compilation/Binding/ExpressionHelper.cs b/src/Framework/Framework/Compilation/Binding/ExpressionHelper.cs index 3ff2baca8e..9611a28763 100644 --- a/src/Framework/Framework/Compilation/Binding/ExpressionHelper.cs +++ b/src/Framework/Framework/Compilation/Binding/ExpressionHelper.cs @@ -50,8 +50,10 @@ public static Expression WrapAsTask(Expression expr) } - public static Expression UnwrapNullable(this Expression expression) => - expression.Type.IsNullable() ? Expression.Property(expression, "Value") : expression; + public static Expression UnwrapNullable(this Expression expression, bool throwOnNull = true) => + !expression.Type.IsNullable() ? expression : + throwOnNull ? Expression.Property(expression, "Value") : + Expression.Call(expression, "GetValueOrDefault", Type.EmptyTypes); public static Expression GetIndexer(Expression expr, Expression index) { diff --git a/src/Framework/Framework/Compilation/Binding/StaticCommandExecutionPlanSerializer.cs b/src/Framework/Framework/Compilation/Binding/StaticCommandExecutionPlanSerializer.cs index 76b48dab73..1ad70fa819 100644 --- a/src/Framework/Framework/Compilation/Binding/StaticCommandExecutionPlanSerializer.cs +++ b/src/Framework/Framework/Compilation/Binding/StaticCommandExecutionPlanSerializer.cs @@ -77,9 +77,7 @@ public static byte[] EncryptJson(JToken json, IViewModelProtector protector) } public static string[] GetEncryptionPurposes() { - return new[] { - "StaticCommand", - }; + return [ "StaticCommand" ]; } public static StaticCommandInvocationPlan DeserializePlan(JToken planInJson) { diff --git a/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs b/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs index c6ff6b4663..bbf6f640cf 100644 --- a/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs +++ b/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Security.Claims; using System.Text; +using System.Text.Json.Serialization; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.HelperNamespace; using DotVVM.Framework.Compilation.ControlTree.Resolved; @@ -19,8 +20,23 @@ namespace DotVVM.Framework.Compilation.ControlTree { /// Base class for defining an extension parameter. + [JsonPolymorphic(TypeDiscriminatorPropertyName = "$typeSerialized", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)] + [JsonDerivedType(typeof(CurrentMarkupControlExtensionParameter))] + [JsonDerivedType(typeof(CurrentCollectionIndexExtensionParameter))] + [JsonDerivedType(typeof(BindingPageInfoExtensionParameter))] + [JsonDerivedType(typeof(BindingCollectionInfoExtensionParameter))] + [JsonDerivedType(typeof(InjectedServiceExtensionParameter))] + [JsonDerivedType(typeof(BindingApiExtensionParameter))] + [JsonDerivedType(typeof(JsExtensionParameter))] + [JsonDerivedType(typeof(CurrentUserExtensionParameter))] + [JsonDerivedType(typeof(JavascriptTranslationVisitor.FakeExtensionParameter))] + [JsonDerivedType(typeof(Configuration.RestApiRegistrationHelpers.ApiExtensionParameter))] public abstract class BindingExtensionParameter { + [JsonInclude] + [JsonPropertyName("$type")] + internal Type JsonHackThisType => this.GetType(); + /// A name that will be used in binding expressions to reference this parameter public string Identifier { get; } /// Type of the parameter. When used in a binding, the expression will have this type. diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlResolverMetadata.cs b/src/Framework/Framework/Compilation/ControlTree/ControlResolverMetadata.cs index 06f8a616c3..8aa201d8c5 100644 --- a/src/Framework/Framework/Compilation/ControlTree/ControlResolverMetadata.cs +++ b/src/Framework/Framework/Compilation/ControlTree/ControlResolverMetadata.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text.Json.Serialization; using DotVVM.Framework.Binding; using DotVVM.Framework.Runtime; -using Newtonsoft.Json; namespace DotVVM.Framework.Compilation.ControlTree { diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlResolverMetadataBase.cs b/src/Framework/Framework/Compilation/ControlTree/ControlResolverMetadataBase.cs index 51a0d315e0..db2f3185c7 100644 --- a/src/Framework/Framework/Compilation/ControlTree/ControlResolverMetadataBase.cs +++ b/src/Framework/Framework/Compilation/ControlTree/ControlResolverMetadataBase.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text.Json.Serialization; using DotVVM.Framework.Binding; using DotVVM.Framework.Compilation.ControlTree.Resolved; using DotVVM.Framework.Controls; -using Newtonsoft.Json; namespace DotVVM.Framework.Compilation.ControlTree { diff --git a/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedTypeDescriptor.cs b/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedTypeDescriptor.cs index 05b84f1b0c..df9b6ba2e6 100644 --- a/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedTypeDescriptor.cs +++ b/src/Framework/Framework/Compilation/ControlTree/Resolved/ResolvedTypeDescriptor.cs @@ -6,14 +6,14 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using System.Text.Json.Serialization; using DotVVM.Framework.Controls; using DotVVM.Framework.Utils; using FastExpressionCompiler; -using Newtonsoft.Json; namespace DotVVM.Framework.Compilation.ControlTree.Resolved { - [JsonConverter(typeof(ResourceManagement.DotvvmTypeDescriptorJsonConverter))] + [JsonConverter(typeof(ResourceManagement.DotvvmTypeDescriptorJsonConverter))] public sealed class ResolvedTypeDescriptor : ITypeDescriptor { private static ConcurrentDictionary<(Type, string), ResolvedTypeDescriptor?> cache = new ConcurrentDictionary<(Type, string), ResolvedTypeDescriptor?>(); diff --git a/src/Framework/Framework/Compilation/DotvvmCompilationDiagnostic.cs b/src/Framework/Framework/Compilation/DotvvmCompilationDiagnostic.cs index dcb07ebd1e..9ea2264772 100644 --- a/src/Framework/Framework/Compilation/DotvvmCompilationDiagnostic.cs +++ b/src/Framework/Framework/Compilation/DotvvmCompilationDiagnostic.cs @@ -8,8 +8,8 @@ using System; using DotVVM.Framework.Compilation.ControlTree.Resolved; using DotVVM.Framework.Binding; -using Newtonsoft.Json; using DotVVM.Framework.Binding.Expressions; +using System.Text.Json.Serialization; namespace DotVVM.Framework.Compilation { diff --git a/src/Framework/Framework/Compilation/DotvvmCompilationException.cs b/src/Framework/Framework/Compilation/DotvvmCompilationException.cs index 00eb544bc3..edf6a949fa 100644 --- a/src/Framework/Framework/Compilation/DotvvmCompilationException.cs +++ b/src/Framework/Framework/Compilation/DotvvmCompilationException.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Runtime.Serialization; +using System.Text.Json.Serialization; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Binding.Properties; @@ -14,7 +15,6 @@ using DotVVM.Framework.Hosting; using DotVVM.Framework.ResourceManagement; using DotVVM.Framework.Runtime; -using Newtonsoft.Json; namespace DotVVM.Framework.Compilation { diff --git a/src/Framework/Framework/Compilation/Javascript/Ast/JsLiteral.cs b/src/Framework/Framework/Compilation/Javascript/Ast/JsLiteral.cs index 20c0c9f4a6..d58495de28 100644 --- a/src/Framework/Framework/Compilation/Javascript/Ast/JsLiteral.cs +++ b/src/Framework/Framework/Compilation/Javascript/Ast/JsLiteral.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Text; +using System.Text.Json; using DotVVM.Framework.Configuration; using DotVVM.Framework.Utils; -using Newtonsoft.Json; namespace DotVVM.Framework.Compilation.Javascript.Ast { @@ -22,8 +22,8 @@ public sealed class JsLiteral : JsExpression /// public string LiteralValue { - get => JavascriptCompilationHelper.CompileConstant(Value); - set => Value = JsonConvert.DeserializeObject(value, DefaultSerializerSettingsProvider.Instance.Settings); + get => JavascriptCompilationHelper.CompileConstant(Value, htmlSafe: false).Replace("<", "\\u003C"); + set => Value = JsonDocument.Parse(value).RootElement; } public JsLiteral() { } diff --git a/src/Framework/Framework/Compilation/Javascript/Ast/JsNode.cs b/src/Framework/Framework/Compilation/Javascript/Ast/JsNode.cs index 1d474d0094..227ca06a2c 100644 --- a/src/Framework/Framework/Compilation/Javascript/Ast/JsNode.cs +++ b/src/Framework/Framework/Compilation/Javascript/Ast/JsNode.cs @@ -3,8 +3,8 @@ using System.Diagnostics; using System.Linq; using System.Text; +using System.Text.Json.Serialization; using DotVVM.Framework.Configuration; -using Newtonsoft.Json; using RecordExceptions; // Tree architecture is inspired by NRefactory, large pieces of code are copy-pasted, see https://github.com/icsharpcode/NRefactory for source diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptCompilationHelper.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptCompilationHelper.cs index 1e65359cf9..70e23552e9 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptCompilationHelper.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptCompilationHelper.cs @@ -1,24 +1,25 @@ using System; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; using DotVVM.Framework.Compilation.Javascript.Ast; using DotVVM.Framework.Configuration; +using DotVVM.Framework.Controls; using DotVVM.Framework.Utils; using DotVVM.Framework.ViewModel.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; namespace DotVVM.Framework.Compilation.Javascript { public static class JavascriptCompilationHelper { - public static string CompileConstant(object? obj) => + public static string CompileConstant(object? obj, bool htmlSafe = true) => obj switch { null => "null", true => "true", false => "false", - string s => JsonConvert.ToString(s), - int i => JsonConvert.ToString(i), - _ => JsonConvert.SerializeObject(obj, DefaultSerializerSettingsProvider.Instance.Settings) + string s => KnockoutHelper.MakeStringLiteral(s, htmlSafe), + int i => i.ToString(), + _ => JsonSerializer.Serialize(obj, htmlSafe ? DefaultSerializerSettingsProvider.Instance.Settings : DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe) }; public static ViewModelInfoAnnotation? GetResultType(this JsExpression expr) diff --git a/src/Framework/Framework/Compilation/Javascript/JsViewModelPropertyAdjuster.cs b/src/Framework/Framework/Compilation/Javascript/JsViewModelPropertyAdjuster.cs index 1cc6a40fc1..08ebf725c2 100644 --- a/src/Framework/Framework/Compilation/Javascript/JsViewModelPropertyAdjuster.cs +++ b/src/Framework/Framework/Compilation/Javascript/JsViewModelPropertyAdjuster.cs @@ -117,7 +117,8 @@ protected override void DefaultVisit(JsNode node) if (node.Annotation() is { Type: {}, SerializationMap: null } vmAnnotation) { - vmAnnotation.SerializationMap = mapper.GetMap(vmAnnotation.Type); + if (!vmAnnotation.Type.IsPrimitive && vmAnnotation.Type != typeof(void)) + vmAnnotation.SerializationMap = mapper.GetMap(vmAnnotation.Type); } } diff --git a/src/Framework/Framework/Compilation/Javascript/ParametrizedCode.cs b/src/Framework/Framework/Compilation/Javascript/ParametrizedCode.cs index 95a0941035..79f7c3932c 100644 --- a/src/Framework/Framework/Compilation/Javascript/ParametrizedCode.cs +++ b/src/Framework/Framework/Compilation/Javascript/ParametrizedCode.cs @@ -6,9 +6,9 @@ using System.Collections.Immutable; using DotVVM.Framework.Binding.Properties; using DotVVM.Framework.Binding.Expressions; -using Newtonsoft.Json; using System.Diagnostics; using DotVVM.Framework.Utils; +using DotVVM.Framework.Controls; namespace DotVVM.Framework.Compilation.Javascript { @@ -345,7 +345,7 @@ public static CodeParameterAssignment FromExpression(JsExpression expression, bo public static CodeParameterAssignment FromIdentifier(string identifier, bool isGlobalContext = false) => new CodeParameterAssignment(identifier, OperatorPrecedence.Max, isGlobalContext); public static CodeParameterAssignment FromLiteral(string value, bool isGlobalContext = false) => - new CodeParameterAssignment(JsonConvert.ToString(value), OperatorPrecedence.Max, isGlobalContext); + new CodeParameterAssignment(KnockoutHelper.MakeStringLiteral(value), OperatorPrecedence.Max, isGlobalContext); public static implicit operator CodeParameterAssignment(ParametrizedCode? val) => new CodeParameterAssignment(val); } diff --git a/src/Framework/Framework/Compilation/NamespaceImport.cs b/src/Framework/Framework/Compilation/NamespaceImport.cs index 27246e4824..3cae311696 100644 --- a/src/Framework/Framework/Compilation/NamespaceImport.cs +++ b/src/Framework/Framework/Compilation/NamespaceImport.cs @@ -1,19 +1,19 @@ -using Newtonsoft.Json; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Text; +using System.Text.Json.Serialization; using System.Threading.Tasks; namespace DotVVM.Framework.Compilation { public struct NamespaceImport: IEquatable { - [JsonProperty("namespace")] - public readonly string Namespace; - [JsonProperty("alias")] - public readonly string? Alias; + [JsonPropertyName("namespace")] + public readonly string Namespace { get; } + [JsonPropertyName("alias")] + public readonly string? Alias { get; } [JsonIgnore] public bool HasAlias => Alias is not null; diff --git a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs index d66ed10b8e..e99321c68a 100644 --- a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs +++ b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs @@ -4,36 +4,43 @@ using System.Text; using System.Threading.Tasks; using DotVVM.Framework.ViewModel.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Encodings.Web; +using System.Text.Unicode; namespace DotVVM.Framework.Configuration { public sealed class DefaultSerializerSettingsProvider { private const int defaultMaxSerializationDepth = 64; - internal readonly JsonSerializerSettings Settings; + internal readonly JsonSerializerOptions SettingsHtmlUnsafe; + internal readonly JsonSerializerOptions Settings; - public JsonSerializerSettings GetSettingsCopy() - { - return CreateSettings(); - } + // We need to encode for script tags (i.e. either < or / has to go) and for HTML comments (< and > have to go - https://html.spec.whatwg.org/#comments) + // The default JavaScriptEncoder is just annoyingly paranoid, I'm not interested in having all non-ASCII characters escaped to unicode codepoints + // Newtonsoft's EscapeHtml setting just escapes HTML (<, >, &, ', ") and control characters (e.g. newline); and it doesn't have any CVEs open + // + // then they claim it isn't safe to use in JavaScript context because the ECMA-262 standard doesn't allow something in string literals + // https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#prod-DoubleStringCharacter + // - it says « SourceCharacter [=any Unicode code point] but not one of " or \ or LineTerminator » + // ...which isn't allowed in JSON in any context anyway + internal readonly JavaScriptEncoder HtmlSafeLessParaoidEncoder; - private JsonSerializerSettings CreateSettings() + private JsonSerializerOptions CreateSettings() { - return new JsonSerializerSettings() + return new JsonSerializerOptions() { - DateTimeZoneHandling = DateTimeZoneHandling.Unspecified, - Converters = new List - { + Converters = { new DotvvmDateTimeConverter(), new DotvvmDateOnlyConverter(), new DotvvmTimeOnlyConverter(), - new StringEnumConverter(), + new DotvvmEnumConverter(), new DotvvmDictionaryConverter(), new DotvvmByteArrayConverter(), new DotvvmCustomPrimitiveTypeConverter() }, + Encoder = HtmlSafeLessParaoidEncoder, MaxDepth = defaultMaxSerializationDepth }; } @@ -51,10 +58,18 @@ public static DefaultSerializerSettingsProvider Instance private DefaultSerializerSettingsProvider() { - JsonConvert.DefaultSettings = () => new JsonSerializerSettings() { MaxDepth = defaultMaxSerializationDepth }; + var encoderSettings = new TextEncoderSettings(); + encoderSettings.AllowRange(UnicodeRanges.All); + encoderSettings.ForbidCharacters('>', '<'); + HtmlSafeLessParaoidEncoder = JavaScriptEncoder.Create(encoderSettings); + // JsonConvert.DefaultSettings = () => new JsonSerializerSettings() { MaxDepth = defaultMaxSerializationDepth }; Settings = CreateSettings(); + SettingsHtmlUnsafe = new JsonSerializerOptions(Settings) + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; } - public static JsonSerializer CreateJsonSerializer() => JsonSerializer.Create(Instance.Settings); + // public static JsonSerializer CreateJsonSerializer() => JsonSerializer.Serialize( } } diff --git a/src/Framework/Framework/Configuration/Dotvvm3StateFeatureFlag.cs b/src/Framework/Framework/Configuration/Dotvvm3StateFeatureFlag.cs index 70e51ed3d3..26ee92486a 100644 --- a/src/Framework/Framework/Configuration/Dotvvm3StateFeatureFlag.cs +++ b/src/Framework/Framework/Configuration/Dotvvm3StateFeatureFlag.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace DotVVM.Framework.Configuration { /// Overrides an automatically enabled feature by always enabling or disabling it for the entire application or only for certain routes. - public class Dotvvm3StateFeatureFlag: IDotvvmFeatureFlagAdditiveConfiguration + public class Dotvvm3StateFeatureFlag: IDotvvmFeatureFlagAdditiveConfiguration, IEquatable { [JsonIgnore] public string FlagName { get; } @@ -17,7 +17,8 @@ public Dotvvm3StateFeatureFlag(string flagName) } /// Default state of this feature flag. true = enabled, false = disabled, null = enabled automatically based on other conditions (usually running in Development/Production environment) - [JsonProperty("enabled")] + [JsonPropertyName("enabled")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] public bool? Enabled { get => _enabled; @@ -30,7 +31,7 @@ public Dotvvm3StateFeatureFlag(string flagName) private bool? _enabled = null; /// List of routes where the feature flag is always enabled. - [JsonProperty("includedRoutes")] + [JsonPropertyName("includedRoutes")] public ISet IncludedRoutes { get => _includedRoutes; @@ -43,7 +44,7 @@ public ISet IncludedRoutes private ISet _includedRoutes = new FreezableSet(comparer: StringComparer.OrdinalIgnoreCase); /// List of routes where the feature flag is always disabled. - [JsonProperty("excludedRoutes")] + [JsonPropertyName("excludedRoutes")] public ISet ExcludedRoutes { get => _excludedRoutes; @@ -202,5 +203,14 @@ public override string ToString() var disabledStr = ExcludedRoutes.Count > 0 ? $", disabled for routes: [{string.Join(", ", ExcludedRoutes)}]" : null; return $"Feature flag {this.FlagName}: {defaultStr}{enabledStr}{disabledStr}"; } + + public bool Equals(Dotvvm3StateFeatureFlag? other) => + other is not null && + this.Enabled == other.Enabled && + this.IncludedRoutes.SetEquals(other.IncludedRoutes) && + this.ExcludedRoutes.SetEquals(other.ExcludedRoutes); + + public override bool Equals(object? obj) => obj is Dotvvm3StateFeatureFlag other && Equals(other); + public override int GetHashCode() => throw new NotSupportedException("Use ReferenceEqualityComparer"); } } diff --git a/src/Framework/Framework/Configuration/DotvvmCompilationPageConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmCompilationPageConfiguration.cs index a08070bc37..67c6bb6e06 100644 --- a/src/Framework/Framework/Configuration/DotvvmCompilationPageConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmCompilationPageConfiguration.cs @@ -3,10 +3,10 @@ using System.ComponentModel; using System.Linq; using System.Text; +using System.Text.Json.Serialization; using System.Threading.Tasks; using DotVVM.Framework.Diagnostics; using DotVVM.Framework.Hosting; -using Newtonsoft.Json; namespace DotVVM.Framework.Configuration { @@ -22,8 +22,8 @@ public class DotvvmCompilationPageConfiguration /// When null, the compilation page is automatically enabled if /// is true. /// - [JsonProperty("isEnabled", DefaultValueHandling = DefaultValueHandling.Ignore)] - [DefaultValue(null)] + [JsonPropertyName("isEnabled")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? IsEnabled { get { return _isEnabled; } @@ -40,7 +40,8 @@ public class DotvvmCompilationPageConfiguration /// if all pages and controls can be compiled successfully, otherwise an HTTP 500 status code /// is sent back. /// - [JsonProperty("isApiEnabled", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("isApiEnabled")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [DefaultValue(false)] public bool IsApiEnabled { @@ -52,7 +53,8 @@ public bool IsApiEnabled /// /// Gets or sets the URL where the compilation page will be accessible from. /// - [JsonProperty("url", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("url")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [DefaultValue(DefaultUrl)] public string Url { @@ -64,7 +66,8 @@ public string Url /// /// Gets or sets the name of the route that the compilation page will be registered as. /// - [JsonProperty("routeName", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("routeName")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [DefaultValue(DefaultRouteName)] public string RouteName { @@ -78,7 +81,8 @@ public string RouteName /// Gets or sets whether the compilation page should attempt to compile all registered /// pages and markup controls when it is loaded. /// - [JsonProperty("shouldCompileAllOnLoad", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("shouldCompileAllOnLoad")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [DefaultValue(true)] public bool ShouldCompileAllOnLoad { diff --git a/src/Framework/Framework/Configuration/DotvvmConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmConfiguration.cs index 76f3043031..021c05b125 100644 --- a/src/Framework/Framework/Configuration/DotvvmConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmConfiguration.cs @@ -9,7 +9,6 @@ using DotVVM.Framework.Compilation.Parser; using DotVVM.Framework.Compilation.Styles; using DotVVM.Framework.Compilation.Validation; -using Newtonsoft.Json; using DotVVM.Framework.Hosting; using DotVVM.Framework.Routing; using DotVVM.Framework.ResourceManagement; @@ -30,6 +29,7 @@ using DotVVM.Framework.Compilation.Javascript; using System.ComponentModel; using Microsoft.Extensions.DependencyInjection.Extensions; +using System.Text.Json.Serialization; namespace DotVVM.Framework.Configuration { @@ -41,7 +41,7 @@ public sealed class DotvvmConfiguration /// /// Gets or sets the application physical path. /// - [JsonProperty("applicationPhysicalPath")] + [JsonPropertyName("applicationPhysicalPath")] [DefaultValue(".")] public string ApplicationPhysicalPath { @@ -53,39 +53,40 @@ public string ApplicationPhysicalPath /// /// Gets the settings of the markup. /// - [JsonProperty("markup")] + [JsonPropertyName("markup")] + [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] public DotvvmMarkupConfiguration Markup { get; private set; } /// /// Gets the route table. /// - [JsonProperty("routes")] + [JsonPropertyName("routes")] [JsonConverter(typeof(RouteTableJsonConverter))] public DotvvmRouteTable RouteTable { get; private set; } /// /// Gets the configuration of resources. /// - [JsonProperty("resources")] + [JsonPropertyName("resources")] [JsonConverter(typeof(ResourceRepositoryJsonConverter))] public DotvvmResourceRepository Resources { get; private set; } = new DotvvmResourceRepository(); /// /// Gets the security configuration. /// - [JsonProperty("security")] + [JsonPropertyName("security")] public DotvvmSecurityConfiguration Security { get; private set; } = new DotvvmSecurityConfiguration(); /// /// Gets the runtime configuration. /// - [JsonProperty("runtime")] + [JsonPropertyName("runtime")] public DotvvmRuntimeConfiguration Runtime { get; private set; } = new DotvvmRuntimeConfiguration(); /// /// Gets or sets the default culture. /// - [JsonProperty("defaultCulture")] + [JsonPropertyName("defaultCulture")] public string DefaultCulture { get { return _defaultCulture; } @@ -96,7 +97,7 @@ public string DefaultCulture /// /// Gets or sets whether the client side validation rules should be enabled. /// - [JsonProperty("clientSideValidation", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("clientSideValidation")] [DefaultValue(true)] public bool ClientSideValidation { @@ -115,7 +116,7 @@ public bool ClientSideValidation /// /// Gets or sets the configuration for experimental features. /// - [JsonProperty("experimentalFeatures")] + [JsonPropertyName("experimentalFeatures")] public DotvvmExperimentalFeaturesConfiguration ExperimentalFeatures { get => _experimentalFeatures; @@ -127,7 +128,8 @@ public DotvvmExperimentalFeaturesConfiguration ExperimentalFeatures /// Gets or sets whether the application should run in debug mode. /// For ASP.NET Core check out /// - [JsonProperty("debug", DefaultValueHandling = DefaultValueHandling.Include)] + [JsonPropertyName("debug")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] public bool Debug { get => _debug; @@ -138,7 +140,7 @@ public bool Debug /// /// Gets or sets the configuration for diagnostic features useful during the development of an application. /// - [JsonProperty("diagnostics")] + [JsonPropertyName("diagnostics")] public DotvvmDiagnosticsConfiguration Diagnostics { get { return _diagnostics; } @@ -203,7 +205,7 @@ public StyleRepository Styles } private StyleRepository _styles; - [JsonProperty("compiledViewsAssemblies")] + [JsonPropertyName("compiledViewsAssemblies")] public IList CompiledViewsAssemblies { get { return _compiledViewsAssemblies; } @@ -212,6 +214,7 @@ public IList CompiledViewsAssemblies private IList _compiledViewsAssemblies = new FreezableList() { "CompiledViews.dll" }; /// must be there for serialization + [JsonConstructor] internal DotvvmConfiguration(): this(new ServiceLocator(CreateDefaultServiceCollection().BuildServiceProvider()).GetServiceProvider()) { } /// diff --git a/src/Framework/Framework/Configuration/DotvvmConfigurationException.cs b/src/Framework/Framework/Configuration/DotvvmConfigurationException.cs index 5c6e020e0c..04575fccf9 100644 --- a/src/Framework/Framework/Configuration/DotvvmConfigurationException.cs +++ b/src/Framework/Framework/Configuration/DotvvmConfigurationException.cs @@ -3,8 +3,8 @@ using System.Linq; using System.Runtime.Serialization; using System.Text; +using System.Text.Json; using DotVVM.Framework.Routing; -using Newtonsoft.Json; namespace DotVVM.Framework.Configuration { @@ -68,14 +68,14 @@ private static void BuildControlsMessage(List - !(prop.GetValue(o) is DotvvmFeatureFlag flag) || flag.IsEnabledForAnyRoute(); + defaults = Activator.CreateInstance(type, nonPublic: true); } - if (property.PropertyType == typeof(DotvvmGlobalFeatureFlag) && prop is object) + else if (type == typeof(DotvvmConfiguration)) { - // ignore defaults for brevity - property.ShouldSerialize = o => - !(prop.GetValue(o) is DotvvmGlobalFeatureFlag flag) || flag.Enabled; + defaults = new DotvvmConfiguration(new EmptyServiceProvider()) { DefaultCulture = null! }; } - - if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType) && property.PropertyType != typeof(string) && prop is object) + else if (type == typeof(DotvvmPropertySerializableList.DotvvmPropertyInfo)) + defaults = new DotvvmPropertySerializableList.DotvvmPropertyInfo(null!, null, null); + else if (type == typeof(DotvvmPropertySerializableList.DotvvmPropertyGroupInfo)) + defaults = new DotvvmPropertySerializableList.DotvvmPropertyGroupInfo(null!, null, null!, null, null); + else if (type == typeof(DotvvmPropertySerializableList.DotvvmControlInfo)) + defaults = new DotvvmPropertySerializableList.DotvvmControlInfo(typeof(DotvvmControl).Assembly.GetName().Name, null, null, false, null, false, null); + foreach (var property in info.Properties) { - property.TypeNameHandling = TypeNameHandling.None; - var originalCondition = property.ShouldSerialize; - property.ShouldSerialize = o => - originalCondition?.Invoke(o) != false && - (!(prop.GetValue(o) is IEnumerable c) || c.Cast().Any()); - } + var originalCondition = property.ShouldSerialize ?? ((_, _) => true); + if (defaults is {} && (property.Get is {} || property.AttributeProvider is PropertyInfo prop)) + { + var def = property.Get is {} ? property.Get(defaults!) : ((PropertyInfo)property.AttributeProvider!).GetValue(defaults); + if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType) && property.PropertyType != typeof(string)) + { + property.ShouldSerialize = (obj, value) => + originalCondition(obj, value) && + !object.Equals(value, def) && // null + (value is not IEnumerable valE || def is not IEnumerable defE || !valE.Cast().SequenceEqual(defE.Cast())); + } + // else if (property.Name == "compiledViewsAssemblies" && info.Type == typeof(DotvvmConfiguration)) + // { + // property.ShouldSerialize = (obj, value) => + // originalCondition(obj, value) && + // (value is not IEnumerable c || !new [] { "CompiledViews.dll" }.SequenceEqual(c)); + // } + else + { + property.ShouldSerialize = (obj, value) => + originalCondition(obj, value) && + !object.Equals(def, value); + } + } + else if ( + property.Name is "includedRoutes" or "excludedRoutes" || + property.Name is "Dependencies" && typeof(IResource).IsAssignableFrom(type)) + { + property.ShouldSerialize = (obj, value) => + originalCondition(obj, value) && !(value is IEnumerable e && !e.Cast().Any()); + } - if (prop is object && prop.Name == "CompiledViewsAssemblies" && prop.DeclaringType == typeof(DotvvmConfiguration)) - { - property.ShouldSerialize = o => - !(prop.GetValue(o) is IEnumerable c) || !new [] { "CompiledViews.dll" }.SequenceEqual(c); + // if (type.GetMethod("ShouldSerialize" + property.Name, 0, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, Array.Empty()) is {} shouldSerializeMethod) + // { + // property.ShouldSerialize = (obj, value) => + // originalCondition(obj, value) && (bool)shouldSerializeMethod.Invoke(obj, Array.Empty()); + // } } + return info; + } - return property; + class EmptyServiceProvider : IServiceProvider + { + public object? GetService(Type serviceType) => null; } } } diff --git a/src/Framework/Framework/Configuration/DotvvmControlConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmControlConfiguration.cs index 5ab3607ddb..4bf6d8b98c 100644 --- a/src/Framework/Framework/Configuration/DotvvmControlConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmControlConfiguration.cs @@ -1,15 +1,16 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Text.RegularExpressions; -using Newtonsoft.Json; namespace DotVVM.Framework.Configuration { public class DotvvmControlConfiguration { - [JsonProperty("tagPrefix", Required = Required.Always)] + [JsonPropertyName("tagPrefix")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] public string? TagPrefix { get => _tagPrefix; @@ -17,7 +18,7 @@ public class DotvvmControlConfiguration } private string? _tagPrefix; - [JsonProperty("tagName", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("tagName")] public string? TagName { get => _tagName; @@ -25,7 +26,7 @@ public class DotvvmControlConfiguration } private string? _tagName; - [JsonProperty("namespace", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("namespace")] public string? Namespace { get => _namespace; @@ -33,7 +34,7 @@ public class DotvvmControlConfiguration } private string? _namespace; - [JsonProperty("assembly", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("assembly")] public string? Assembly { get => _assembly; @@ -41,7 +42,7 @@ public class DotvvmControlConfiguration } private string? _assembly; - [JsonProperty("src", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("src")] public string? Src { get => _src; diff --git a/src/Framework/Framework/Configuration/DotvvmDiagnosticsConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmDiagnosticsConfiguration.cs index 60b7741189..40f25ad421 100644 --- a/src/Framework/Framework/Configuration/DotvvmDiagnosticsConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmDiagnosticsConfiguration.cs @@ -3,8 +3,8 @@ using System.ComponentModel; using System.Linq; using System.Text; +using System.Text.Json.Serialization; using System.Threading.Tasks; -using Newtonsoft.Json; namespace DotVVM.Framework.Configuration { @@ -13,7 +13,7 @@ public class DotvvmDiagnosticsConfiguration /// /// Gets or sets the options of the compilation status page. /// - [JsonProperty("compilationPage")] + [JsonPropertyName("compilationPage")] public DotvvmCompilationPageConfiguration CompilationPage { get { return _compilationPage; } @@ -24,7 +24,7 @@ public DotvvmCompilationPageConfiguration CompilationPage /// /// Gets or sets the options for runtime warning about slow requests, too big viewmodels, ... /// - [JsonProperty("perfWarnings")] + [JsonPropertyName("perfWarnings")] public DotvvmPerfWarningsConfiguration PerfWarnings { get { return _perfWarnings; } diff --git a/src/Framework/Framework/Configuration/DotvvmExperimentalFeaturesConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmExperimentalFeaturesConfiguration.cs index cc292d6547..be99e9deb4 100644 --- a/src/Framework/Framework/Configuration/DotvvmExperimentalFeaturesConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmExperimentalFeaturesConfiguration.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace DotVVM.Framework.Configuration { @@ -13,7 +13,7 @@ public class DotvvmExperimentalFeaturesConfiguration /// When enabled, the CSRF token is not generated in each document, but lazy loaded when the first postback is performed. This may help with caching DotVVM pages. /// See DotVVM 2.4 release blog post for more information /// - [JsonProperty("lazyCsrfToken")] + [JsonPropertyName("lazyCsrfToken")] public DotvvmFeatureFlag LazyCsrfToken { get; private set; } = new DotvvmFeatureFlag("LazyCsrfToken"); @@ -22,14 +22,14 @@ public class DotvvmExperimentalFeaturesConfiguration /// The view models are stored using the service. /// See documentation page for more information. /// - [JsonProperty("serverSideViewModelCache")] + [JsonPropertyName("serverSideViewModelCache")] public DotvvmFeatureFlag ServerSideViewModelCache { get; private set; } = new DotvvmFeatureFlag("ServerSideViewModelCache"); /// /// When enabled, the DotVVM runtime only automatically load assemblies listed in . This may prevent failures during startup and reduce startup time. /// See documentation page for more information /// - [JsonProperty("explicitAssemblyLoading")] + [JsonPropertyName("explicitAssemblyLoading")] public DotvvmGlobalFeatureFlag ExplicitAssemblyLoading { get; private set; } = new DotvvmGlobalFeatureFlag("ExplicitAssemblyLoading"); @@ -37,10 +37,10 @@ public class DotvvmExperimentalFeaturesConfiguration /// When enabled, knockout subscriptions are evaluated asynchronously. This may significantly improve client-side performance, but some component might not be compatible with the setting. /// /// - [JsonProperty("knockoutDeferUpdates")] + [JsonPropertyName("knockoutDeferUpdates")] public DotvvmFeatureFlag KnockoutDeferUpdates { get; private set; } = new DotvvmFeatureFlag("KnockoutDeferUpdates"); - [JsonProperty("useDotvvmSerializationForStaticCommandArguments")] + [JsonPropertyName("useDotvvmSerializationForStaticCommandArguments")] public DotvvmGlobalFeatureFlag UseDotvvmSerializationForStaticCommandArguments { get; private set; } = new DotvvmGlobalFeatureFlag("UseDotvvmSerializationForStaticCommandArguments"); public void Freeze() diff --git a/src/Framework/Framework/Configuration/DotvvmFeatureFlag.cs b/src/Framework/Framework/Configuration/DotvvmFeatureFlag.cs index ee44f61e44..90ece9447e 100644 --- a/src/Framework/Framework/Configuration/DotvvmFeatureFlag.cs +++ b/src/Framework/Framework/Configuration/DotvvmFeatureFlag.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace DotVVM.Framework.Configuration { /// Enables or disables certain DotVVM feature for the entire application or only for certain routes. - public class DotvvmFeatureFlag: IDotvvmFeatureFlagAdditiveConfiguration + public sealed class DotvvmFeatureFlag: IDotvvmFeatureFlagAdditiveConfiguration, IEquatable { [JsonIgnore] public string FlagName { get; } @@ -23,7 +23,8 @@ public DotvvmFeatureFlag(): this("Unknown") } /// Gets or set the default state of this feature flag. If the current route doesn't match any or , it will - [JsonProperty("enabled")] + [JsonPropertyName("enabled")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] public bool Enabled { get => _enabled; @@ -36,7 +37,7 @@ public bool Enabled private bool _enabled = false; /// List of routes where the feature flag is always enabled. - [JsonProperty("includedRoutes")] + [JsonPropertyName("includedRoutes")] public ISet IncludedRoutes { get => _includedRoutes; @@ -49,7 +50,7 @@ public ISet IncludedRoutes private ISet _includedRoutes = new FreezableSet(comparer: StringComparer.OrdinalIgnoreCase); /// List of routes where the feature flag is always disabled. - [JsonProperty("excludedRoutes")] + [JsonPropertyName("excludedRoutes")] public ISet ExcludedRoutes { get => _excludedRoutes; @@ -181,6 +182,15 @@ public override string ToString() var exceptInStr = exceptIn.Count > 0 ? $", except in {string.Join(", ", exceptIn)}" : ""; return $"Feature flag {this.FlagName}: {(Enabled ? "Enabled" : "Disabled")}{exceptInStr}"; } + + public bool Equals(DotvvmFeatureFlag? other) => + other is not null && + this.Enabled == other.Enabled && + this.IncludedRoutes.SetEquals(other.IncludedRoutes) && + this.ExcludedRoutes.SetEquals(other.ExcludedRoutes); + + public override bool Equals(object? obj) => obj is DotvvmFeatureFlag other && Equals(other); + public override int GetHashCode() => throw new NotSupportedException("Use ReferenceEqualityComparer"); } public interface IDotvvmFeatureFlagAdditiveConfiguration diff --git a/src/Framework/Framework/Configuration/DotvvmGlobal3StateFeatureFlag.cs b/src/Framework/Framework/Configuration/DotvvmGlobal3StateFeatureFlag.cs index 5237f9deda..e5eed8264b 100644 --- a/src/Framework/Framework/Configuration/DotvvmGlobal3StateFeatureFlag.cs +++ b/src/Framework/Framework/Configuration/DotvvmGlobal3StateFeatureFlag.cs @@ -1,10 +1,10 @@ using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace DotVVM.Framework.Configuration { /// Overrides an automatically enabled feature by always enabling or disabling it for the entire application. - public class DotvvmGlobal3StateFeatureFlag + public class DotvvmGlobal3StateFeatureFlag: IEquatable { [JsonIgnore] public string FlagName { get; } @@ -15,7 +15,8 @@ public DotvvmGlobal3StateFeatureFlag(string flagName) } /// Gets or sets whether the feature is enabled or disabled. - [JsonProperty("enabled")] + [JsonPropertyName("enabled")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] public bool? Enabled { get => _enabled; @@ -60,5 +61,12 @@ public void Freeze() } public override string ToString() => $"Feature flag {FlagName}: {Enabled switch { null => "Default", true => "Enabled", false => "Disabled"}}"; + + public bool Equals(DotvvmGlobal3StateFeatureFlag? other) => + other is not null && this.Enabled == other.Enabled; + + public override bool Equals(object? obj) => obj is DotvvmGlobal3StateFeatureFlag other && Equals(other); + public override int GetHashCode() => throw new NotSupportedException("Use ReferenceEqualityComparer"); + } } diff --git a/src/Framework/Framework/Configuration/DotvvmGlobalFeatureFlag.cs b/src/Framework/Framework/Configuration/DotvvmGlobalFeatureFlag.cs index eb5fff9a71..23b88bb3ec 100644 --- a/src/Framework/Framework/Configuration/DotvvmGlobalFeatureFlag.cs +++ b/src/Framework/Framework/Configuration/DotvvmGlobalFeatureFlag.cs @@ -1,10 +1,10 @@ using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace DotVVM.Framework.Configuration { /// Enables or disables certain DotVVM feature for the entire application. - public class DotvvmGlobalFeatureFlag + public sealed class DotvvmGlobalFeatureFlag: IEquatable { [JsonIgnore] public string FlagName { get; } @@ -18,7 +18,8 @@ public DotvvmGlobalFeatureFlag(string flagName) public DotvvmGlobalFeatureFlag(): this("Unknown") { } /// Gets or sets whether the feature is enabled or disabled. - [JsonProperty("enabled")] + [JsonPropertyName("enabled")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] public bool Enabled { get => _enabled; @@ -56,5 +57,10 @@ public void Freeze() } public override string ToString() => $"Feature flag {FlagName}: {(Enabled ? "Enabled" : "Disabled")}"; + + public bool Equals(DotvvmGlobalFeatureFlag? other) => + other is not null && this.Enabled == other.Enabled; + public override bool Equals(object? obj) => obj is DotvvmGlobalFeatureFlag other && this.Equals(other); + public override int GetHashCode() => throw new NotSupportedException("Use ReferenceEqualityComparer"); } } diff --git a/src/Framework/Framework/Configuration/DotvvmMarkupConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmMarkupConfiguration.cs index d399b2e60e..7154362550 100644 --- a/src/Framework/Framework/Configuration/DotvvmMarkupConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmMarkupConfiguration.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; using DotVVM.Framework.Controls; using DotVVM.Framework.Compilation; using System.Reflection; @@ -9,6 +8,7 @@ using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Compilation.ControlTree.Resolved; using DotVVM.Framework.Compilation.Javascript; +using System.Text.Json.Serialization; namespace DotVVM.Framework.Configuration { @@ -17,21 +17,21 @@ public sealed class DotvvmMarkupConfiguration /// /// Gets the registered control namespaces. /// - [JsonProperty("controls")] + [JsonPropertyName("controls")] public IList Controls => _controls; private readonly FreezableList _controls; /// /// Gets or sets the list of referenced assemblies. /// - [JsonProperty("assemblies")] + [JsonPropertyName("assemblies")] public IList Assemblies => _assemblies; private readonly FreezableList _assemblies; /// /// Gets a list of HTML attribute transforms. /// - //[JsonProperty("htmlAttributeTransforms")] + //[JsonPropertyName("htmlAttributeTransforms")] [JsonIgnore] public IDictionary HtmlAttributeTransforms => _htmlAttributeTransforms; private readonly FreezableDictionary _htmlAttributeTransforms; @@ -39,13 +39,13 @@ public sealed class DotvvmMarkupConfiguration /// /// Gets a list of HTML attribute transforms. /// - [JsonProperty("defaultDirectives")] + [JsonPropertyName("defaultDirectives")] public IDictionary DefaultDirectives => _defaultDirectives; private readonly FreezableDictionary _defaultDirectives; /// /// Gets or sets list of namespaces imported in bindings /// - [JsonProperty("importedNamespaces")] + [JsonPropertyName("importedNamespaces")] public IList ImportedNamespaces { get => _importedNamespaces; @@ -61,7 +61,7 @@ public IList ImportedNamespaces private readonly Lazy _javascriptTranslator; - [JsonProperty("defaultExtensionParameters")] + [JsonPropertyName("defaultExtensionParameters")] public IList DefaultExtensionParameters { get => _defaultExtensionParameters; @@ -78,10 +78,8 @@ public void AddServiceImport(string identifier, Type type) DefaultExtensionParameters.Add(new InjectedServiceExtensionParameter(identifier, new ResolvedTypeDescriptor(type))); } - /// - /// Initializes a new instance of the class. - /// - public DotvvmMarkupConfiguration(Lazy? javascriptConfig = null) + public DotvvmMarkupConfiguration(): this(null) { } + public DotvvmMarkupConfiguration(Lazy? javascriptConfig) { this._javascriptTranslator = javascriptConfig ?? new Lazy(() => new JavascriptTranslatorConfiguration()); this._controls = new FreezableList(); diff --git a/src/Framework/Framework/Configuration/DotvvmPerfWarningsConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmPerfWarningsConfiguration.cs index d9b9dc689b..343bff8f35 100644 --- a/src/Framework/Framework/Configuration/DotvvmPerfWarningsConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmPerfWarningsConfiguration.cs @@ -3,10 +3,10 @@ using System.ComponentModel; using System.Linq; using System.Text; +using System.Text.Json.Serialization; using System.Threading.Tasks; using DotVVM.Framework.Diagnostics; using DotVVM.Framework.Hosting; -using Newtonsoft.Json; namespace DotVVM.Framework.Configuration { @@ -14,7 +14,7 @@ public class DotvvmPerfWarningsConfiguration { /// Gets or sets whether the warnings about potentially bad performance are enabled. By default, it enabled in both Debug and Production environments. /// Before turning it off, we suggest tweaking the warning thresholds if you find the default values to be too noisy. - [JsonProperty("isEnabled", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("isEnabled")] [DefaultValue(true)] public bool IsEnabled { @@ -24,7 +24,7 @@ public bool IsEnabled private bool _isEnabled = true; /// Gets or sets the threshold for the warning about too slow requests. In seconds, by default it's 3 seconds. - [JsonProperty("slowRequestSeconds", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("slowRequestSeconds")] [DefaultValue(3.0)] public double SlowRequestSeconds { @@ -35,7 +35,7 @@ public double SlowRequestSeconds /// Gets or sets the threshold for the warning about too big viewmodels. In bytes, by default it's 5 megabytes. - [JsonProperty("bigViewModelBytes", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("bigViewModelBytes")] [DefaultValue(5 * 1024 * 1024)] public double BigViewModelBytes { diff --git a/src/Framework/Framework/Configuration/DotvvmRuntimeConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmRuntimeConfiguration.cs index 2426fb3de6..a22575cbd3 100644 --- a/src/Framework/Framework/Configuration/DotvvmRuntimeConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmRuntimeConfiguration.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; +using System.Text.Json.Serialization; using DotVVM.Framework.Runtime.Filters; namespace DotVVM.Framework.Configuration @@ -19,7 +19,7 @@ public class DotvvmRuntimeConfiguration /// When enabled, dothtml files are reloaded and recompiled after a change. Note that resources (CSS, JS) are not controlled by this option. /// By default, reloading is only enabled in debug mode. /// - [JsonProperty("reloadMarkupFiles")] + [JsonPropertyName("reloadMarkupFiles")] public DotvvmGlobal3StateFeatureFlag ReloadMarkupFiles { get; } = new("Dotvvm3StateFeatureFlag.ReloadMarkupFiles"); /// @@ -27,11 +27,11 @@ public class DotvvmRuntimeConfiguration /// It is enabled by default in Production mode. /// See to limit the impact of potential decompression bomb. Although compression may be enabled only for specific routes, DotVVM does not check authentication before decompressing the request. /// - [JsonProperty("compressPostbacks")] + [JsonPropertyName("compressPostbacks")] public Dotvvm3StateFeatureFlag CompressPostbacks { get; } = new("DotvvmFeatureFlag.CompressPostbacks"); /// Maximum size of command/staticCommand request body after decompression (does not affect file upload). Default = 128MB, lower limit is a basic protection against decompression bomb attack. Set to -1 to disable the limit. - [JsonProperty("maxPostbackSizeBytes")] + [JsonPropertyName("maxPostbackSizeBytes")] public long MaxPostbackSizeBytes { get; set; } = 1024 * 1024 * 128; // 128 MB /// diff --git a/src/Framework/Framework/Configuration/DotvvmSecurityConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmSecurityConfiguration.cs index 91b10ad723..82e788cb30 100644 --- a/src/Framework/Framework/Configuration/DotvvmSecurityConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmSecurityConfiguration.cs @@ -1,8 +1,8 @@ -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Text.Json.Serialization; namespace DotVVM.Framework.Configuration { @@ -14,7 +14,7 @@ public class DotvvmSecurityConfiguration /// /// Gets or sets name of HTTP cookie used for Session ID /// - [JsonProperty("sessionIdCookieName", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("sessionIdCookieName")] [DefaultValue("dotvvm_sid_{0}")] public string SessionIdCookieName { @@ -26,49 +26,49 @@ public string SessionIdCookieName /// /// When enabled, uses `X-Frame-Options: SAMEORIGIN` instead of DENY /// - [JsonProperty("frameOptionsSameOrigin", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("frameOptionsSameOrigin")] public DotvvmFeatureFlag FrameOptionsSameOrigin { get; } = new("FrameOptionsSameOrigin"); /// /// When enabled, does not add `X-Frame-Options: DENY` header. Enabling will force DotVVM to use SameSite=None on the session cookie /// - [JsonProperty("frameOptionsCrossOrigin", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("frameOptionsCrossOrigin")] public DotvvmFeatureFlag FrameOptionsCrossOrigin { get; } = new("FrameOptionsCrossOrigin"); /// /// When enabled, adds the `X-XSS-Protection: 1; mode=block` header, which enables some basic XSS filtering in browsers. This is enabled by default. /// - [JsonProperty("xssProtectionHeader", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("xssProtectionHeader")] public DotvvmFeatureFlag XssProtectionHeader { get; } = new("XssProtectionHeader", true); /// /// When enabled, adds the `X-Content-Type-Options: nosniff` header, which prevents browsers from incorrectly detecting non-scripts as scripts. This is enabled by default. /// - [JsonProperty("contentTypeOptionsHeader", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("contentTypeOptionsHeader")] public DotvvmFeatureFlag ContentTypeOptionsHeader { get; } = new("ContentTypeOptionsHeader", true); /// /// Verifies Sec-Fetch headers on the GET request coming to dothtml pages. The request must have `Sec-Fetch-Dest: document` or `Sec-Fetch-Site: same-origin` if the request is for SPA. If the FrameOptionsSameOrigin is enabled, DotVVM will also allow `Sec-Fetch-Dest: document` and if FrameOptionsSameOrigin is enabled, DotVVM will also allow iframe from an cross-site request. This protects agains cross-site page scraping. Also prevents potential XSS bug to scrape the non-SPA pages. /// - [JsonProperty("verifySecFetchForPages", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("verifySecFetchForPages")] public DotvvmFeatureFlag VerifySecFetchForPages { get; } = new("VerifySecFetchForPages", true); /// /// Verifies Sec-Fetch headers on the POST request executing staticCommands and commands. The request must have `Sec-Fetch-Site: same-origin`. This protects again cross-site malicious requests even if SameSite cookies and CSRF tokens would fail. It also prevents websites on a subdomain to perform postbacks. /// - [JsonProperty("verifySecFetchForCommands", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("verifySecFetchForCommands")] public DotvvmFeatureFlag VerifySecFetchForCommands { get; } = new("VerifySecFetchForCommands", true); /// /// Requires that requests to dotvvm pages always have the Sec-Fetch-* headers. This may offer a slight protection against server-side request forgery attacks and against attacks exploiting obsolete web browsers (MS IE and Apple IE) /// - [JsonProperty("requireSecFetchHeaders", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("requireSecFetchHeaders")] public DotvvmFeatureFlag RequireSecFetchHeaders { get; } = new("RequireSecFetchHeaders", false); /// /// Include the Referrer-Policy header which disables referrers in the default configuration. Enabled by default. /// - [JsonProperty("referrerPolicy", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("referrerPolicy")] public DotvvmFeatureFlag ReferrerPolicy { get; } = new("ReferrerPolicy", true); /// Value of the referrer-policy header. By default it's no-referrer, if you want referrers on your domain set this to `same-origin`. See for more info: diff --git a/src/Framework/Framework/Configuration/HtmlAttributeTransformConfiguration.cs b/src/Framework/Framework/Configuration/HtmlAttributeTransformConfiguration.cs index 0e9266e055..328758fc7e 100644 --- a/src/Framework/Framework/Configuration/HtmlAttributeTransformConfiguration.cs +++ b/src/Framework/Framework/Configuration/HtmlAttributeTransformConfiguration.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Serialization; using DotVVM.Framework.Controls; using System.Reflection; using DotVVM.Framework.Utils; +using System.Text.Json.Serialization; +using System.Text.Json.Nodes; +using System.Text.Json; namespace DotVVM.Framework.Configuration { @@ -14,7 +14,7 @@ public sealed class HtmlAttributeTransformConfiguration private Lazy instance; - [JsonProperty("type")] + [JsonPropertyName("type")] public Type? Type { get => _type; @@ -23,12 +23,12 @@ public sealed class HtmlAttributeTransformConfiguration private Type? _type; [JsonExtensionData] - public IDictionary? ExtensionData + public IDictionary? ExtensionData { get => _extensionData; set { ThrowIfFrozen(); _extensionData = value; } } - private IDictionary? _extensionData; + private IDictionary? _extensionData; public HtmlAttributeTransformConfiguration() @@ -57,7 +57,7 @@ private IHtmlAttributeTransformer CreateInstance() foreach (var extension in ExtensionData) { var prop = type.GetProperty(extension.Key) ?? throw new Exception($"Property {extension.Key} from ExtensionData was not found."); - prop.SetValue(transformer, extension.Value.ToObject(prop.PropertyType)); + prop.SetValue(transformer, extension.Value.Deserialize(prop.PropertyType)); } } @@ -75,8 +75,6 @@ public void Freeze() { this.isFrozen = true; FreezableDictionary.Freeze(ref this._extensionData); - // unfortunately, the stored JTokens are still mutable :( - // it may get solved at some point, https://github.com/JamesNK/Newtonsoft.Json/issues/468 } } } diff --git a/src/Framework/Framework/Configuration/HtmlTagAttributePair.cs b/src/Framework/Framework/Configuration/HtmlTagAttributePair.cs index b39343e3d3..6bb3c448f0 100644 --- a/src/Framework/Framework/Configuration/HtmlTagAttributePair.cs +++ b/src/Framework/Framework/Configuration/HtmlTagAttributePair.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Text.RegularExpressions; -using Newtonsoft.Json; namespace DotVVM.Framework.Configuration { @@ -39,16 +40,16 @@ public override bool Equals(object? obj) } } - public class HtmlTagAttributePairToStringConverter : JsonConverter + public class HtmlTagAttributePairToStringConverter : JsonConverter { - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, HtmlTagAttributePair value, JsonSerializerOptions options) { - writer.WriteValue(value?.ToString()); + writer.WriteStringValue(value.ToString()); } - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + public override HtmlTagAttributePair Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var value = reader.ReadAsString(); + var value = reader.GetString(); if (value == null) { throw InvalidFormatException(); @@ -59,23 +60,14 @@ public override object ReadJson(JsonReader reader, Type objectType, object? exis { throw InvalidFormatException(); } - - if (existingValue == null) - { - existingValue = new HtmlTagAttributePair(); - } - var pair = (HtmlTagAttributePair) existingValue; - pair.TagName = match.Groups[1].Value; - pair.AttributeName = match.Groups[2].Value; - return pair; + return new() { + TagName = match.Groups[1].Value, + AttributeName = match.Groups[2].Value + }; } private static Exception InvalidFormatException() => - new JsonSerializationException("HTML attribute definition expected! Correct syntax is 'a[href]': { }"); + new Exception("HTML attribute definition expected! Correct syntax is 'a[href]': { }"); - public override bool CanConvert(Type objectType) - { - return objectType == typeof (HtmlTagAttributePair); - } } } diff --git a/src/Framework/Framework/Configuration/RestApiRegistrationHelpers.cs b/src/Framework/Framework/Configuration/RestApiRegistrationHelpers.cs index 0ebb3b9dff..5254122e8d 100644 --- a/src/Framework/Framework/Configuration/RestApiRegistrationHelpers.cs +++ b/src/Framework/Framework/Configuration/RestApiRegistrationHelpers.cs @@ -17,7 +17,7 @@ using DotVVM.Framework.ViewModel.Serialization; using DotVVM.Framework.Utils; using DotVVM.Framework.Binding.Properties; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace DotVVM.Framework.Configuration { diff --git a/src/Framework/Framework/Configuration/ViewCompilationConfiguration.cs b/src/Framework/Framework/Configuration/ViewCompilationConfiguration.cs index 51b2455ded..a32cac7896 100644 --- a/src/Framework/Framework/Configuration/ViewCompilationConfiguration.cs +++ b/src/Framework/Framework/Configuration/ViewCompilationConfiguration.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel; +using System.Text.Json.Serialization; using DotVVM.Framework.Compilation; -using Newtonsoft.Json; namespace DotVVM.Framework.Configuration { @@ -12,7 +12,7 @@ public sealed class ViewCompilationConfiguration /// /// Gets or sets the mode under which the view compilation (pages, controls, ... ) is done. By default, view are precompiled asynchronously after the application starts. /// - [JsonProperty("mode")] + [JsonPropertyName("mode")] [DefaultValue(ViewCompilationMode.AfterApplicationStart)] public ViewCompilationMode Mode { @@ -27,7 +27,7 @@ public ViewCompilationMode Mode /// /// Gets or sets the delay before view compilation will be done. This compilation delay can be set only in precompilation modes. /// - [JsonProperty("backgroundCompilationDelay")] + [JsonPropertyName("backgroundCompilationDelay")] public TimeSpan? BackgroundCompilationDelay { get => backgroundCompilationDelay; @@ -41,7 +41,7 @@ public ViewCompilationMode Mode /// /// Gets or sets whether the view compilation will be performed in parallel or in series. /// - [JsonProperty("compileInParallel")] + [JsonPropertyName("compileInParallel")] public bool CompileInParallel { get => compileInParallel; @@ -56,7 +56,7 @@ public bool CompileInParallel /// /// By default, view precompilation is disabled in Debug mode, to make startup time faster. This options controls this behavior. /// - [JsonProperty("precompileEvenInDebug")] + [JsonPropertyName("precompileEvenInDebug")] public bool PrecompileEvenInDebug { get => precompileEvenInDebug; diff --git a/src/Framework/Framework/Controls/Content.cs b/src/Framework/Framework/Controls/Content.cs index f826f88c26..6d702603a4 100644 --- a/src/Framework/Framework/Controls/Content.cs +++ b/src/Framework/Framework/Controls/Content.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Text.Json; using DotVVM.Framework.Binding; using DotVVM.Framework.Configuration; using DotVVM.Framework.Hosting; -using Newtonsoft.Json; namespace DotVVM.Framework.Controls { @@ -51,14 +51,12 @@ protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext if (viewModule is object && !isInHead) { hasViewModule = true; - var settings = DefaultSerializerSettingsProvider.Instance.GetSettingsCopy(); - settings.StringEscapeHandling = StringEscapeHandling.EscapeHtml; if (viewModule.ViewId == null) throw new ArgumentException($"ViewModule's property {nameof(viewModule.ViewId)} has not been set."); writer.WriteKnockoutDataBindComment("dotvvm-with-view-modules", - $"{{ viewId: {KnockoutHelper.MakeStringLiteral(viewModule.ViewId)}, modules: {JsonConvert.SerializeObject(viewModule.ReferencedModules, settings)} }}" + $"{{ viewId: {KnockoutHelper.MakeStringLiteral(viewModule.ViewId)}, modules: {JsonSerializer.Serialize(viewModule.ReferencedModules, DefaultSerializerSettingsProvider.Instance.Settings)} }}" ); } } diff --git a/src/Framework/Framework/Controls/ContentPlaceHolder.cs b/src/Framework/Framework/Controls/ContentPlaceHolder.cs index 2fde63489a..19d223bcb2 100644 --- a/src/Framework/Framework/Controls/ContentPlaceHolder.cs +++ b/src/Framework/Framework/Controls/ContentPlaceHolder.cs @@ -6,7 +6,6 @@ using DotVVM.Framework.Binding; using DotVVM.Framework.Configuration; using DotVVM.Framework.Hosting; -using Newtonsoft.Json; namespace DotVVM.Framework.Controls { diff --git a/src/Framework/Framework/Controls/DotvvmBindableObject.cs b/src/Framework/Framework/Controls/DotvvmBindableObject.cs index cb45b1d0a6..d85a65a23c 100644 --- a/src/Framework/Framework/Controls/DotvvmBindableObject.cs +++ b/src/Framework/Framework/Controls/DotvvmBindableObject.cs @@ -13,7 +13,7 @@ namespace DotVVM.Framework.Controls [ContainsDotvvmProperties] [ControlMarkupOptions(AllowContent = true)] - [Newtonsoft.Json.JsonConverter(typeof(DotvvmControlDebugJsonConverter))] + [System.Text.Json.Serialization.JsonConverter(typeof(DotvvmControlDebugJsonConverter))] public abstract class DotvvmBindableObject: IDotvvmObjectLike { diff --git a/src/Framework/Framework/Controls/DotvvmControl.cs b/src/Framework/Framework/Controls/DotvvmControl.cs index b03c192a39..a9023913b2 100644 --- a/src/Framework/Framework/Controls/DotvvmControl.cs +++ b/src/Framework/Framework/Controls/DotvvmControl.cs @@ -3,7 +3,6 @@ using DotVVM.Framework.Controls.Infrastructure; using DotVVM.Framework.Hosting; using DotVVM.Framework.Runtime; -using Newtonsoft.Json; using System; using System.Collections; using System.Collections.Generic; diff --git a/src/Framework/Framework/Controls/DotvvmControlDebugJsonConverter.cs b/src/Framework/Framework/Controls/DotvvmControlDebugJsonConverter.cs index e171b3d79a..c386cb053c 100644 --- a/src/Framework/Framework/Controls/DotvvmControlDebugJsonConverter.cs +++ b/src/Framework/Framework/Controls/DotvvmControlDebugJsonConverter.cs @@ -1,53 +1,48 @@ using System; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Configuration; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace DotVVM.Framework.Controls { - internal class DotvvmControlDebugJsonConverter : JsonConverter + internal class DotvvmControlDebugJsonConverter : JsonConverter { // public bool IncludeChildren { get; set; } = false; // public DotvvmConfiguration? Configuration { get; set; } = null; - public override bool CanConvert(Type objectType) => - typeof(DotvvmBindableObject).IsAssignableFrom(objectType); - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => + public override DotvvmBindableObject? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException("Deserializing dotvvm control from JSON is not supported."); - public override void WriteJson(JsonWriter w, object? valueObj, JsonSerializer serializer) + public override void Write(Utf8JsonWriter w, DotvvmBindableObject obj, JsonSerializerOptions options) { - if (valueObj is null) - { - w.WriteNull(); - return; - } - var obj = (DotvvmBindableObject)valueObj; w.WriteStartObject(); w.WritePropertyName("Control"); - w.WriteValue(obj.GetType().Name); + w.WriteStringValue(obj.GetType().Name); - w.WritePropertyName("Properties"); - var properties = new JObject( - from kvp in obj.Properties - let p = kvp.Key - let rawValue = kvp.Value - let isAttached = !p.DeclaringType.IsAssignableFrom(obj.GetType()) - orderby !isAttached, p.Name - let name = isAttached ? p.DeclaringType.Name + "." + p.Name : p.Name - let value = rawValue is null ? JValue.CreateNull() : - rawValue is IBinding ? JValue.CreateString(rawValue.ToString()) : - JToken.FromObject(rawValue, serializer) - select new JProperty(name, value) - ); - properties.WriteTo(w); + w.WriteStartObject("Properties"); + foreach (var kvp in obj.Properties.OrderBy(p => (p.Key.DeclaringType.IsAssignableFrom(obj.GetType()), p.Key.Name))) + { + var (p, rawValue) = kvp; + var isAttached = !p.DeclaringType.IsAssignableFrom(obj.GetType()); + var name = isAttached ? p.DeclaringType.Name + "." + p.Name : p.Name; + if (rawValue is null) + w.WriteNull(name); + else if (rawValue is IBinding) + w.WriteString(name, rawValue.ToString()); + else + { + w.WritePropertyName(name); + JsonSerializer.Serialize(w, rawValue, options); + } + } + w.WriteEndObject(); if (obj is DotvvmControl control) { w.WritePropertyName("LifecycleRequirements"); - w.WriteValue(control.LifecycleRequirements.ToString()); + w.WriteStringValue(control.LifecycleRequirements.ToString()); } diff --git a/src/Framework/Framework/Controls/DotvvmMarkupControl.cs b/src/Framework/Framework/Controls/DotvvmMarkupControl.cs index 61f4ae74ef..87537938db 100644 --- a/src/Framework/Framework/Controls/DotvvmMarkupControl.cs +++ b/src/Framework/Framework/Controls/DotvvmMarkupControl.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Linq; using System.Text; +using System.Text.Json; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Compilation; @@ -13,7 +14,6 @@ using DotVVM.Framework.Configuration; using DotVVM.Framework.Hosting; using DotVVM.Framework.ViewModel.Serialization; -using Newtonsoft.Json; namespace DotVVM.Framework.Controls { @@ -100,7 +100,7 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest if (pinfo.Js is {}) { js.Append("{Key: ") - .Append(JsonConvert.ToString(p.GroupMemberName, '"', StringEscapeHandling.EscapeHtml)) + .Append(KnockoutHelper.MakeStringLiteral(p.GroupMemberName, htmlSafe: true)) .Append(", Value: ") .Append(pinfo.Js) .Append("},"); @@ -125,9 +125,7 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest var viewModule = this.GetValue(Internal.ReferencedViewModuleInfoProperty); if (viewModule is {}) { - var settings = DefaultSerializerSettingsProvider.Instance.GetSettingsCopy(); - settings.StringEscapeHandling = StringEscapeHandling.EscapeHtml; - var binding = $"{{ modules: {JsonConvert.SerializeObject(viewModule.ReferencedModules, settings)} }}"; + var binding = $"{{ modules: {JsonSerializer.Serialize(viewModule.ReferencedModules, DefaultSerializerSettingsProvider.Instance.Settings)} }}"; if (RendersHtmlTag) writer.AddKnockoutDataBind("dotvvm-with-view-modules", binding); else @@ -162,12 +160,9 @@ private PropertySerializeInfo GetPropertySerializationInfo(DotvvmProperty proper { if (ContainsPropertyStaticValue(property)) { - var settings = DefaultSerializerSettingsProvider.Instance.GetSettingsCopy(); - settings.StringEscapeHandling = StringEscapeHandling.EscapeHtml; - return new PropertySerializeInfo( property, - JsonConvert.SerializeObject(GetValue(property), Formatting.None, settings) + JsonSerializer.Serialize(GetValue(property), DefaultSerializerSettingsProvider.Instance.Settings) ); } else if (GetBinding(property) is IValueBinding valueBinding) diff --git a/src/Framework/Framework/Controls/FileUpload.cs b/src/Framework/Framework/Controls/FileUpload.cs index 8d4e634af4..382fbe0338 100644 --- a/src/Framework/Framework/Controls/FileUpload.cs +++ b/src/Framework/Framework/Controls/FileUpload.cs @@ -1,11 +1,12 @@ using System; using System.Net; using System.Text; +using System.Text.Json; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Expressions; +using DotVVM.Framework.Configuration; using DotVVM.Framework.Hosting; using DotVVM.Framework.ResourceManagement; -using Newtonsoft.Json; namespace DotVVM.Framework.Controls { @@ -257,7 +258,7 @@ private void RenderInputControl(IHtmlWriter writer, IDotvvmRequestContext contex writer.AddAttribute("capture", Capture); } - writer.AddKnockoutDataBind("dotvvm-FileUpload", JsonConvert.SerializeObject(new { url = context.TranslateVirtualPath(GetFileUploadHandlerUrl()) })); + writer.AddKnockoutDataBind("dotvvm-FileUpload", JsonSerializer.Serialize(new { url = context.TranslateVirtualPath(GetFileUploadHandlerUrl()) }, DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe)); writer.RenderSelfClosingTag("input"); } diff --git a/src/Framework/Framework/Controls/GridView.cs b/src/Framework/Framework/Controls/GridView.cs index 7579410934..fe197022fe 100644 --- a/src/Framework/Framework/Controls/GridView.cs +++ b/src/Framework/Framework/Controls/GridView.cs @@ -13,8 +13,8 @@ using DotVVM.Framework.Utils; using Microsoft.Extensions.DependencyInjection; using DotVVM.Framework.ViewModel; -using Newtonsoft.Json; using DotVVM.Framework.Configuration; +using System.Text.Json; namespace DotVVM.Framework.Controls { @@ -524,7 +524,7 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest var itemType = ReflectionUtils.GetEnumerableType(GetDataSourceBinding().ResultType); var userColumnMappingService = context.Services.GetRequiredService(); var mapping = userColumnMappingService.GetMapping(itemType!); - var mappingJson = JsonConvert.SerializeObject(mapping); + var mappingJson = JsonSerializer.Serialize(mapping, new JsonSerializerOptions { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); writer.AddKnockoutDataBind("dotvvm-gridviewdataset", $"{{'mapping':{mappingJson},'dataSet':{GetDataSourceBinding().GetKnockoutBindingExpression(this, unwrapped: true)}}}"); base.AddAttributesToRender(writer, context); diff --git a/src/Framework/Framework/Controls/Infrastructure/BodyResourceLinks.cs b/src/Framework/Framework/Controls/Infrastructure/BodyResourceLinks.cs index c5d7dcbdb8..a833b7f652 100644 --- a/src/Framework/Framework/Controls/Infrastructure/BodyResourceLinks.cs +++ b/src/Framework/Framework/Controls/Infrastructure/BodyResourceLinks.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.DependencyInjection; using DotVVM.Framework.Hosting; using DotVVM.Framework.ResourceManagement; -using Newtonsoft.Json; using DotVVM.Framework.ViewModel.Serialization; using DotVVM.Framework.Runtime; @@ -32,7 +31,7 @@ protected override void RenderControl(IHtmlWriter writer, IDotvvmRequestContext writer.RenderSelfClosingTag("input"); // init on load - var initCode = $"window.dotvvm.init({JsonConvert.ToString(CultureInfo.CurrentCulture.Name, '"', StringEscapeHandling.EscapeHtml)});"; + var initCode = $"window.dotvvm.init({KnockoutHelper.MakeStringLiteral(CultureInfo.CurrentCulture.Name)});"; var config = context.Configuration; if (!config.Runtime.CompressPostbacks.IsEnabledForRoute(context.Route?.RouteName, defaultValue: !config.Debug)) { @@ -63,7 +62,7 @@ internal static string RenderWarnings(IDotvvmRequestContext context) foreach (var w in collector.GetWarnings()) { - var msg = JsonConvert.ToString(w.ToString(), '"', StringEscapeHandling.EscapeHtml); + var msg = KnockoutHelper.MakeStringLiteral(w.ToString()); result += $"console.warn({msg});\n"; } return result; diff --git a/src/Framework/Framework/Controls/KnockoutBindingGroup.cs b/src/Framework/Framework/Controls/KnockoutBindingGroup.cs index aadc0a5c07..30dae203bf 100644 --- a/src/Framework/Framework/Controls/KnockoutBindingGroup.cs +++ b/src/Framework/Framework/Controls/KnockoutBindingGroup.cs @@ -1,11 +1,11 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Text.Json; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Configuration; using DotVVM.Framework.ViewModel.Serialization; -using Newtonsoft.Json; namespace DotVVM.Framework.Controls { @@ -64,7 +64,7 @@ public virtual void Add(string name, string expression, bool surroundWithDoubleQ /// Adds the specified value as a JSON serialized constant expression. public virtual void AddValue(string name, object? value) { - var expression = JsonConvert.SerializeObject(value, DefaultSerializerSettingsProvider.Instance.Settings); + var expression = JsonSerializer.Serialize(value, DefaultSerializerSettingsProvider.Instance.Settings); Add(name, expression); } @@ -111,7 +111,7 @@ public override string ToString() if (MayBeUnquoted(Name)) return Name + ": " + Expression; else - return JsonConvert.ToString(Name, '"', StringEscapeHandling.EscapeHtml) + ": " + Expression; + return KnockoutHelper.MakeStringLiteral(Name) + ": " + Expression; } private static bool MayBeUnquoted(string s) diff --git a/src/Framework/Framework/Controls/KnockoutHelper.cs b/src/Framework/Framework/Controls/KnockoutHelper.cs index e0438393ea..bea2069523 100644 --- a/src/Framework/Framework/Controls/KnockoutHelper.cs +++ b/src/Framework/Framework/Controls/KnockoutHelper.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Binding.Properties; @@ -9,7 +12,6 @@ using DotVVM.Framework.Compilation.Javascript.Ast; using DotVVM.Framework.Configuration; using DotVVM.Framework.Utils; -using Newtonsoft.Json; namespace DotVVM.Framework.Controls { @@ -171,7 +173,7 @@ string getContextPath(DotvvmBindableObject? current) var pathFragment = current.GetDataContextPathFragment(); if (pathFragment != null) { - result.Add(JsonConvert.ToString(pathFragment)); + result.Add(MakeStringLiteral(pathFragment)); } current = current.Parent; } @@ -303,14 +305,14 @@ private static string GetPostBackHandlersScript(DotvvmBindableObject control, st if (options.Count == 0) { - sb.Append(JsonConvert.ToString(name)); + sb.Append(MakeStringLiteral(name)); } else { string script = GenerateHandlerOptions(handler, options); sb.Append("["); - sb.Append(JsonConvert.ToString(name)); + sb.Append(MakeStringLiteral(name)); sb.Append(","); sb.Append(script); sb.Append("]"); @@ -392,11 +394,11 @@ private static JsExpression TransformOptionValueToExpression(DotvvmBindableObjec var handlerName = $"concurrency-{mode.ToString().ToLowerInvariant()}"; if ("default".Equals(queueName)) { - return JsonConvert.ToString(handlerName); + return MakeStringLiteral(handlerName); } else { - return $"[{JsonConvert.ToString(handlerName)},{GenerateHandlerOptions(obj, new Dictionary { ["q"] = queueName })}]"; + return $"[{MakeStringLiteral(handlerName)},{GenerateHandlerOptions(obj, new Dictionary { ["q"] = queueName })}]"; } } @@ -475,15 +477,28 @@ public static string GetKnockoutBindingExpression(this DotvvmBindableObject obj, { var binding = obj.GetValueBinding(property); if (binding != null) return binding.GetKnockoutBindingExpression(obj); - return JsonConvert.SerializeObject(obj.GetValue(property), DefaultSerializerSettingsProvider.Instance.Settings); + return JsonSerializer.Serialize(obj.GetValue(property), DefaultSerializerSettingsProvider.Instance.Settings); } /// /// Encodes the string so it can be used in Javascript code. /// - public static string MakeStringLiteral(string value, bool useApos = false) + public static string MakeStringLiteral(string value, bool htmlSafe = true) { - return JsonConvert.ToString(value, useApos ? '\'' : '"', StringEscapeHandling.Default); + var encoder = htmlSafe ? DefaultSerializerSettingsProvider.Instance.HtmlSafeLessParaoidEncoder : JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + if (value.Length < 64) + { + // try to allocate only the result string if it short enough + Span buffer = stackalloc char[128]; + buffer[0] = '"'; + encoder.Encode(source: value, destination: buffer.Slice(1), out var consumed, out var written); + if (consumed == value.Length && written + 2 <= buffer.Length) + { + buffer[written + 1] = '"'; + return buffer.Slice(0, written + 2).ToString(); + } + } + return string.Concat("\"", encoder.Encode(value), "\""); } public static string ConvertToCamelCase(string name) diff --git a/src/Framework/Framework/Controls/Literal.cs b/src/Framework/Framework/Controls/Literal.cs index a8f310e450..813ed32d6a 100644 --- a/src/Framework/Framework/Controls/Literal.cs +++ b/src/Framework/Framework/Controls/Literal.cs @@ -8,7 +8,6 @@ using DotVVM.Framework.Hosting; using DotVVM.Framework.Runtime; using DotVVM.Framework.Utils; -using Newtonsoft.Json; namespace DotVVM.Framework.Controls { @@ -166,7 +165,7 @@ protected override void RenderControl(IHtmlWriter writer, IDotvvmRequestContext valueBindingType = valueBindingType.UnwrapNullableType(); var formattedType = $"\"{valueBindingType?.Name.ToLowerInvariant()}\""; - expression = "dotvvm.globalize.formatString(" + JsonConvert.ToString(FormatString) + ", " + expression + ", " + formattedType + ")"; + expression = "dotvvm.globalize.formatString(" + KnockoutHelper.MakeStringLiteral(FormatString ?? "", htmlSafe: !r.RenderSpanElement) + ", " + expression + ", " + formattedType + ")"; } if (r.RenderSpanElement) diff --git a/src/Framework/Framework/Controls/ModalDialog.cs b/src/Framework/Framework/Controls/ModalDialog.cs index c1bbf9d1c7..4a148c90c0 100644 --- a/src/Framework/Framework/Controls/ModalDialog.cs +++ b/src/Framework/Framework/Controls/ModalDialog.cs @@ -5,7 +5,6 @@ using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Hosting; using DotVVM.Framework.ResourceManagement; -using Newtonsoft.Json; namespace DotVVM.Framework.Controls { diff --git a/src/Framework/Framework/Controls/Repeater.cs b/src/Framework/Framework/Controls/Repeater.cs index c6a3073be1..5b70959243 100644 --- a/src/Framework/Framework/Controls/Repeater.cs +++ b/src/Framework/Framework/Controls/Repeater.cs @@ -184,14 +184,14 @@ protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext { var itemTemplateId = context.ResourceManager.AddTemplateResource(context, clientSideTemplate!); - value.AddValue("name", itemTemplateId); + value.Add("name", KnockoutHelper.MakeStringLiteral(itemTemplateId, htmlSafe: false)); } if (clientSeparator != null) { // separator has to be always rendered as a named template var separatorTemplateId = context.ResourceManager.AddTemplateResource(context, clientSeparator); - value.AddValue("separatorTemplate", separatorTemplateId); + value.Add("separatorTemplate", KnockoutHelper.MakeStringLiteral(separatorTemplateId, htmlSafe: false)); } return ( diff --git a/src/Framework/Framework/Controls/RouteLinkHelpers.cs b/src/Framework/Framework/Controls/RouteLinkHelpers.cs index 4281866015..05c2eb534f 100644 --- a/src/Framework/Framework/Controls/RouteLinkHelpers.cs +++ b/src/Framework/Framework/Controls/RouteLinkHelpers.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using Newtonsoft.Json; using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Routing; @@ -12,6 +11,7 @@ using DotVVM.Framework.Utils; using DotVVM.Framework.Configuration; using System.Collections.Immutable; +using System.Text.Json; namespace DotVVM.Framework.Controls { @@ -154,8 +154,8 @@ private static string GenerateRouteLinkCore(string routeName, RouteLink control, return route.ParameterNames.Any() - ? $"dotvvm.buildRouteUrl({JsonConvert.ToString(route.UrlWithoutTypes)}, {{{parametersExpression}}})" - : JsonConvert.ToString(route.Url); + ? $"dotvvm.buildRouteUrl({KnockoutHelper.MakeStringLiteral(route.UrlWithoutTypes)}, {{{parametersExpression}}})" + : KnockoutHelper.MakeStringLiteral(route.Url); } private static string TranslateRouteParameter(DotvvmBindableObject control, KeyValuePair param, bool caseSensitive = false) @@ -166,11 +166,11 @@ private static string TranslateRouteParameter(DotvvmBindableObject control, K EnsureValidBindingType(binding); expression = (param.Value as IValueBinding)?.GetKnockoutBindingExpression(control) - ?? JsonConvert.SerializeObject((param.Value as IStaticValueBinding)?.Evaluate(control), DefaultSerializerSettingsProvider.Instance.Settings); + ?? JsonSerializer.Serialize((param.Value as IStaticValueBinding)?.Evaluate(control), DefaultSerializerSettingsProvider.Instance.Settings); } else { - expression = JsonConvert.SerializeObject(param.Value, DefaultSerializerSettingsProvider.Instance.Settings); + expression = JsonSerializer.Serialize(param.Value, DefaultSerializerSettingsProvider.Instance.Settings); } return KnockoutHelper.MakeStringLiteral(caseSensitive ? param.Key : param.Key.ToLowerInvariant()) + ": " + expression; } diff --git a/src/Framework/Framework/Controls/TextBox.cs b/src/Framework/Framework/Controls/TextBox.cs index ccc373d38b..4cadb48f1e 100644 --- a/src/Framework/Framework/Controls/TextBox.cs +++ b/src/Framework/Framework/Controls/TextBox.cs @@ -3,7 +3,6 @@ using DotVVM.Framework.Hosting; using DotVVM.Framework.Utils; using System; -using Newtonsoft.Json; using System.Collections.Generic; namespace DotVVM.Framework.Controls diff --git a/src/Framework/Framework/Controls/Validator.cs b/src/Framework/Framework/Controls/Validator.cs index b824115b28..3ef265a065 100644 --- a/src/Framework/Framework/Controls/Validator.cs +++ b/src/Framework/Framework/Controls/Validator.cs @@ -2,16 +2,11 @@ using System.Collections.Generic; using System.Linq; using DotVVM.Framework.Binding; -using DotVVM.Framework.Runtime; -using Newtonsoft.Json; using DotVVM.Framework.Hosting; -using DotVVM.Framework.ViewModel.Serialization; using DotVVM.Framework.Binding.Expressions; -using Microsoft.Extensions.DependencyInjection; using DotVVM.Framework.Configuration; using DotVVM.Framework.Binding.Properties; -using DotVVM.Framework.Compilation.Binding; -using System.Linq.Expressions; +using System.Text.Json; namespace DotVVM.Framework.Controls { @@ -118,8 +113,8 @@ private static void AddValidatedValue(IHtmlWriter writer, IDotvvmRequestContext var optionValue = control.GetValue(property); if (!object.Equals(optionValue, property.DefaultValue)) { - var settings = DefaultSerializerSettingsProvider.Instance.Settings; - bindingGroup.Add(javascriptName, JsonConvert.SerializeObject(optionValue, settings)); + var settings = DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe; + bindingGroup.Add(javascriptName, JsonSerializer.Serialize(optionValue, settings)); } } writer.AddKnockoutDataBind("dotvvm-validationOptions", bindingGroup); diff --git a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs index 9efb2c044c..4f252b365a 100644 --- a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs +++ b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs @@ -59,6 +59,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/Diagnostics/CompilationPageApiPresenter.cs b/src/Framework/Framework/Diagnostics/CompilationPageApiPresenter.cs index f721fc868c..f0ce1f007e 100644 --- a/src/Framework/Framework/Diagnostics/CompilationPageApiPresenter.cs +++ b/src/Framework/Framework/Diagnostics/CompilationPageApiPresenter.cs @@ -1,8 +1,8 @@ -using System.Threading.Tasks; +using System.Text.Json; +using System.Threading.Tasks; using DotVVM.Framework.Compilation; using DotVVM.Framework.Configuration; using DotVVM.Framework.Hosting; -using Newtonsoft.Json; namespace DotVVM.Framework.Diagnostics { @@ -34,7 +34,7 @@ public async Task ProcessRequest(IDotvvmRequestContext context) response.StatusCode = 500; response.ContentType = "application/json"; - await response.WriteAsync(JsonConvert.SerializeObject(compilationService.GetFilesWithFailedCompilation())); + await response.WriteAsync(JsonSerializer.Serialize(compilationService.GetFilesWithFailedCompilation(), DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe)); } } } diff --git a/src/Framework/Framework/Diagnostics/DiagnosticsInformationSender.cs b/src/Framework/Framework/Diagnostics/DiagnosticsInformationSender.cs index 4e446222d0..a30fa787f6 100644 --- a/src/Framework/Framework/Diagnostics/DiagnosticsInformationSender.cs +++ b/src/Framework/Framework/Diagnostics/DiagnosticsInformationSender.cs @@ -3,10 +3,10 @@ using System.IO; using System.Net.Sockets; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using DotVVM.Framework.Configuration; using DotVVM.Framework.Diagnostics.Models; -using Newtonsoft.Json; namespace DotVVM.Framework.Diagnostics { @@ -31,11 +31,10 @@ public async Task SendInformationAsync(DiagnosticsInformation information) try { await client.ConnectAsync(hostname, port.Value); - using (var stream = new StreamWriter(client.GetStream())) + using (var stream = client.GetStream()) { - var settings = DefaultSerializerSettingsProvider.Instance.Settings; - await stream.WriteAsync(JsonConvert.SerializeObject(information, settings)); - await stream.FlushAsync(); + var settings = DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe; + await JsonSerializer.SerializeAsync(stream, information, settings); } } catch (Exception) diff --git a/src/Framework/Framework/Diagnostics/DiagnosticsRenderer.cs b/src/Framework/Framework/Diagnostics/DiagnosticsRenderer.cs index f17bf532e8..ff1d4edb3b 100644 --- a/src/Framework/Framework/Diagnostics/DiagnosticsRenderer.cs +++ b/src/Framework/Framework/Diagnostics/DiagnosticsRenderer.cs @@ -23,15 +23,14 @@ protected override MemoryStream RenderPage(IDotvvmRequestContext context, Dotvvm return html; } - public override Task WriteViewModelResponse(IDotvvmRequestContext context, DotvvmView view) + public override Task WriteViewModelResponse(IDotvvmRequestContext context, DotvvmView view, string viewModel) { if (context.Configuration.Debug && context.Services.GetService() is DiagnosticsRequestTracer tracer) { - var viewModelJson = context.GetSerializedViewModel(); - var vmBytes = Encoding.UTF8.GetBytes(viewModelJson); + var vmBytes = Encoding.UTF8.GetBytes(viewModel); tracer.LogResponseSize(GetCompressedSize(vmBytes), vmBytes.LongLength); } - return base.WriteViewModelResponse(context, view); + return base.WriteViewModelResponse(context, view, viewModel); } private long GetCompressedSize(byte[] bytes) diff --git a/src/Framework/Framework/Diagnostics/DiagnosticsRequestTracer.cs b/src/Framework/Framework/Diagnostics/DiagnosticsRequestTracer.cs index e156e0b028..7de0b8a329 100644 --- a/src/Framework/Framework/Diagnostics/DiagnosticsRequestTracer.cs +++ b/src/Framework/Framework/Diagnostics/DiagnosticsRequestTracer.cs @@ -87,30 +87,14 @@ private DiagnosticsInformation BuildDiagnosticsData(IDotvvmRequestContext reques private RequestDiagnostics BuildRequestDiagnostics(IDotvvmRequestContext request) { return new RequestDiagnostics( - RequestTypeFromContext(request), + request.RequestType, request.HttpContext.Request.Method, request.HttpContext.Request.Url.AbsolutePath, request.HttpContext.Request.Headers.Select(HttpHeaderItem.FromKeyValuePair), - request.ReceivedViewModelJson?.GetValue("viewModel")?.ToString() + request.ReceivedViewModelJson?.RootElement.GetPropertyOrNull("viewModel")?.GetRawText() ); } - private RequestType RequestTypeFromContext(IDotvvmRequestContext context) - { - if (context.ReceivedViewModelJson == null && context.ViewModelJson != null) - { - return RequestType.Get; - } - else if (context.ReceivedViewModelJson != null) - { - return RequestType.Command; - } - else - { - return RequestType.StaticCommand; - } - } - private ResponseDiagnostics BuildResponseDiagnostics(IDotvvmRequestContext request) { return new ResponseDiagnostics @@ -118,8 +102,8 @@ private ResponseDiagnostics BuildResponseDiagnostics(IDotvvmRequestContext reque StatusCode = request.HttpContext.Response.StatusCode, Headers = request.HttpContext.Response.Headers.Select(HttpHeaderItem.FromKeyValuePair) .ToList(), - ViewModelJson = request.ViewModelJson?.GetValue("viewModel")?.ToString(), - ViewModelDiff = request.ViewModelJson?.GetValue("viewModelDiff")?.ToString(), + // ViewModelJson = request.ViewModelJson?.GetValue("viewModel")?.ToString(), // TODO + // ViewModelDiff = request.ViewModelJson?.GetValue("viewModelDiff")?.ToString(), ResponseSize = ResponseSize?.realLength ?? -1, CompressedResponseSize = ResponseSize?.compressedLength ?? -1 }; diff --git a/src/Framework/Framework/Diagnostics/DiagnosticsServerConfiguration.cs b/src/Framework/Framework/Diagnostics/DiagnosticsServerConfiguration.cs index 4061f2e516..e0852393e3 100644 --- a/src/Framework/Framework/Diagnostics/DiagnosticsServerConfiguration.cs +++ b/src/Framework/Framework/Diagnostics/DiagnosticsServerConfiguration.cs @@ -2,8 +2,9 @@ using System.Collections.Generic; using System.IO; using System.Text; +using System.Text.Json; using DotVVM.Framework.Configuration; -using Newtonsoft.Json; +using DotVVM.Framework.Utils; namespace DotVVM.Framework.Diagnostics { @@ -11,13 +12,15 @@ public class DiagnosticsServerConfiguration { private DateTime configurationLastWriteTimeUtc; - [JsonIgnore] public static string? DiagnosticsFilePath => Environment.GetEnvironmentVariable("TEMP") is string tmpPath ? Path.Combine(tmpPath, "DotVVM/diagnosticsConfiguration.json") : null; - public string? HostName { get; set; } - public int Port { get; set; } + + private Configuration config = new(); + + public string? HostName => config.HostName; + public int Port => config.Port; public string? GetFreshHostName() { @@ -44,8 +47,8 @@ private void RefreshConfiguration() configurationLastWriteTimeUtc = info.LastWriteTimeUtc; var diagnosticsJson = File.ReadAllText(path); - var settings = DefaultSerializerSettingsProvider.Instance.Settings; - JsonConvert.PopulateObject(diagnosticsJson, this, settings); + var settings = DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe; + this.config = JsonSerializer.Deserialize(diagnosticsJson, settings).NotNull(); } } catch @@ -53,5 +56,11 @@ private void RefreshConfiguration() // ignored } } + + class Configuration + { + public string? HostName { get; set; } + public int Port { get; set; } + } } } diff --git a/src/Framework/Framework/Diagnostics/DiagnosticsStartupTracer.cs b/src/Framework/Framework/Diagnostics/DiagnosticsStartupTracer.cs index 976cbb0881..dc15bec6eb 100644 --- a/src/Framework/Framework/Diagnostics/DiagnosticsStartupTracer.cs +++ b/src/Framework/Framework/Diagnostics/DiagnosticsStartupTracer.cs @@ -75,7 +75,7 @@ private static DiagnosticsInformation BuildDiagnosticsInformation(ImmutableList< { return new DiagnosticsInformation( new RequestDiagnostics( - RequestType.Init, + DotvvmRequestType.Unknown, "", "{APPLICATION_STARTUP}", Enumerable.Empty(), diff --git a/src/Framework/Framework/Diagnostics/JsonSizeAnalyzer.cs b/src/Framework/Framework/Diagnostics/JsonSizeAnalyzer.cs index 5e176771be..244e9388c5 100644 --- a/src/Framework/Framework/Diagnostics/JsonSizeAnalyzer.cs +++ b/src/Framework/Framework/Diagnostics/JsonSizeAnalyzer.cs @@ -2,10 +2,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Text.Json; using DotVVM.Framework.Utils; using DotVVM.Framework.ViewModel.Serialization; using FastExpressionCompiler; -using Newtonsoft.Json.Linq; +// using Newtonsoft.Json.Linq; namespace DotVVM.Framework.Diagnostics { @@ -19,75 +20,74 @@ public JsonSizeAnalyzer(IViewModelSerializationMapper viewModelMapper) this.viewModelMapper = viewModelMapper; } /// Computes the inclusive and exclusive size of each JSON property. - public JsonSizeProfile Analyze(JObject json) + public JsonSizeProfile Analyze(JsonElement json) { - Dictionary results = new(); - // returns the length of the token. Recursively calls itself for arrays and objects. - AtomicSizeProfile analyzeToken(JToken token) - { - switch (token.Type) - { - case JTokenType.Object: - return new (InclusiveSize: analyzeObject((JObject)token), ExclusiveSize: 2); - case JTokenType.Array: { - var r = new AtomicSizeProfile(0); - foreach (var item in (JArray)token) - { - r += analyzeToken(item); - } - return r; - } - case JTokenType.String: - return new ((((string?)token)?.Length ?? 4) + 2); - case JTokenType.Integer: - // This should be the same as token.ToString().Length, but I didn't want to allocate the string unnecesarily - return new((int)Math.Log10(Math.Abs((long)token) + 1) + 1); - case JTokenType.Float: - return new(((double)token).ToString().Length); - case JTokenType.Boolean: - return new(((bool)token) ? 4 : 5); - case JTokenType.Null: - return new(4); - case JTokenType.Guid: - return new(36 + 2); - case JTokenType.Date: - return new(23 + 2); - default: - Debug.Assert(false, $"Unexpected token type {token.Type}"); - return new(token.ToString().Length); - } - } - int analyzeObject(JObject j) - { - var type = ((string?)j.Property("$type")?.Value)?.Apply(viewModelMapper.GetMapByTypeId); + throw new NotImplementedException(); // TODO + // Dictionary results = new(); + // // returns the length of the token. Recursively calls itself for arrays and objects. + // AtomicSizeProfile analyzeToken(JsonElement token) + // { + // switch (token.ValueKind) + // { + // case JsonValueKind.Object: + // return new (InclusiveSize: analyzeObject(token), ExclusiveSize: 2); + // case JsonValueKind.Array: { + // var r = new AtomicSizeProfile(0); + // foreach (var item in token.EnumerateArray()) + // { + // r += analyzeToken(item); + // } + // return r; + // } + // case JsonValueKind.String: + // return new ((((string?)token)?.Length ?? 4) + 2); + // case JsonValueKind.Integer: + // // This should be the same as token.ToString().Length, but I didn't want to allocate the string unnecesarily + // return new((int)Math.Log10(Math.Abs((long)token) + 1) + 1); + // case JsonValueKind.Number: + // return new(((double)token).ToString().Length); + // case JsonValueKind.True: + // return new(4); + // case JsonValueKind.False: + // return new(5); + // case JsonValueKind.Null: + // return new(4); + // default: + // Debug.Assert(false, $"Unexpected token type {token.ValueKind}"); + // return new(token.ToString().Length); + // } + // } + // int analyzeObject(JsonElement j) + // { + // var type = ((string?)j.GetPropertyOrNull("$type")?.GetString())?.Apply(viewModelMapper.GetMapByTypeId); - var typeName = type?.Type.ToCode(stripNamespace: true) ?? "UnknownType"; - var props = new Dictionary(); + // var typeName = type?.Type.ToCode(stripNamespace: true) ?? "UnknownType"; + // var props = new Dictionary(); - var totalSize = new AtomicSizeProfile(0); - foreach (var prop in j.Properties()) - { - var propSize = analyzeToken(prop.Value); - props[prop.Name] = propSize; + // var totalSize = new AtomicSizeProfile(0); + // foreach (var prop in j.Properties()) + // { + // var propSize = analyzeToken(prop.Value); + // props[prop.Name] = propSize; - totalSize += propSize; - totalSize += 4 + prop.Name.Length; // 2 for the quotes, 1 for :, 1 for , - } + // totalSize += propSize; + // totalSize += 4 + prop.Name.Length; // 2 for the quotes, 1 for :, 1 for , + // } - var classSize = new ClassSizeProfile(totalSize, props); - if (results.TryGetValue(typeName, out var existing)) - { - results[typeName] = existing + classSize; - } - else - { - results[typeName] = classSize; - } - return totalSize.InclusiveSize; - } + // var classSize = new ClassSizeProfile(totalSize, props); + // if (results.TryGetValue(typeName, out var existing)) + // { + // results[typeName] = existing + classSize; + // } + // else + // { + // results[typeName] = classSize; + // } + // return totalSize.InclusiveSize; + // } - var totalSize = analyzeObject(json); - return new JsonSizeProfile(results, totalSize); + // var totalSize = analyzeObject(json); + // return new JsonSizeProfile(results, totalSize); } diff --git a/src/Framework/Framework/Diagnostics/Models/RequestDiagnostics.cs b/src/Framework/Framework/Diagnostics/Models/RequestDiagnostics.cs index e300f6b5d9..10d5be0dd9 100644 --- a/src/Framework/Framework/Diagnostics/Models/RequestDiagnostics.cs +++ b/src/Framework/Framework/Diagnostics/Models/RequestDiagnostics.cs @@ -4,18 +4,9 @@ namespace DotVVM.Framework.Diagnostics.Models { - - public enum RequestType - { - Init, - Get, - Command, - StaticCommand - } - public class RequestDiagnostics { - public RequestDiagnostics(RequestType requestType, string method, string url, IEnumerable headers, string? viewModelJson) + public RequestDiagnostics(DotvvmRequestType requestType, string method, string url, IEnumerable headers, string? viewModelJson) { RequestType = requestType; Method = method; @@ -24,7 +15,7 @@ public RequestDiagnostics(RequestType requestType, string method, string url, IE ViewModelJson = viewModelJson; } - public RequestType RequestType { get; set; } + public DotvvmRequestType RequestType { get; set; } public string Method { get; set; } public string Url { get; set; } public IList Headers { get; set; } diff --git a/src/Framework/Framework/Diagnostics/PerformanceWarningTracer.cs b/src/Framework/Framework/Diagnostics/PerformanceWarningTracer.cs index 640e00c544..fb7a031620 100644 --- a/src/Framework/Framework/Diagnostics/PerformanceWarningTracer.cs +++ b/src/Framework/Framework/Diagnostics/PerformanceWarningTracer.cs @@ -56,44 +56,45 @@ void WarnSlowRequest(TimeSpan totalElapsed) } void WarnLargeViewModel(long viewModelSize, IDotvvmRequestContext context) { - if (context.ViewModelJson is null) - return; + // if (context.ViewModelJson is null) TODO + // return; - try - { - var vmAnalysis = jsonSizeAnalyzer.Analyze(context.ViewModelJson); + // try + // { + // var vmAnalysis = jsonSizeAnalyzer.Analyze(context.ViewModelJson); - var topClasses = - vmAnalysis.Classes - .OrderByDescending(c => c.Value.Size.ExclusiveSize) - .Take(3) - // only classes which have at least 5% impact - .Where(c => c.Value.Size.ExclusiveSize > vmAnalysis.TotalSize / 20) - .ToArray(); - var topProperties = - vmAnalysis.Classes - .SelectMany(c => c.Value.Properties.Select(p => (Key: c.Key + "." + p.Key, p.Value))) - .OrderByDescending(c => c.Value.ExclusiveSize) - .Take(3) - // only properties which have at least 5% impact - .Where(c => c.Value.ExclusiveSize > vmAnalysis.TotalSize / 20) - .ToArray(); + // var topClasses = + // vmAnalysis.Classes + // .OrderByDescending(c => c.Value.Size.ExclusiveSize) + // .Take(3) + // // only classes which have at least 5% impact + // .Where(c => c.Value.Size.ExclusiveSize > vmAnalysis.TotalSize / 20) + // .ToArray(); + // var topProperties = + // vmAnalysis.Classes + // .SelectMany(c => c.Value.Properties.Select(p => (Key: c.Key + "." + p.Key, p.Value))) + // .OrderByDescending(c => c.Value.ExclusiveSize) + // .Take(3) + // // only properties which have at least 5% impact + // .Where(c => c.Value.ExclusiveSize > vmAnalysis.TotalSize / 20) + // .ToArray(); - var byteToPercent = 100.0 / vmAnalysis.TotalSize; + // var byteToPercent = 100.0 / vmAnalysis.TotalSize; - var msg = $"The serialized view model has {viewModelSize / 1024.0 / 1024.0:0.0}MB, which may make your application quite slow. " + - string.Join(", ", - topProperties.Select(c => $"Property {c.Key} takes {c.Value.ExclusiveSize * byteToPercent:0}%").Concat( - topClasses.Select(c => $"Class {c.Key} takes {c.Value.Size.ExclusiveSize * byteToPercent:0}%"))); + // var msg = $"The serialized view model has {viewModelSize / 1024.0 / 1024.0:0.0}MB, which may make your application quite slow. " + + // string.Join(", ", + // topProperties.Select(c => $"Property {c.Key} takes {c.Value.ExclusiveSize * byteToPercent:0}%").Concat( + // topClasses.Select(c => $"Class {c.Key} takes {c.Value.Size.ExclusiveSize * byteToPercent:0}%"))); - logger.Warn(new DotvvmRuntimeWarning( - msg - )); - } - catch (Exception ex) - { - tracerLogger?.LogWarning(ex, $"Failed to analyze view model size. The serialized view model has {viewModelSize / 1024.0 / 1024.0:0.0}MB, which may make your application quite slow"); - } + // logger.Warn(new DotvvmRuntimeWarning( + // msg + // )); + // } + // catch (Exception ex) + // { + // tracerLogger?.LogWarning(ex, $"Failed to analyze view model size. The serialized view model has {viewModelSize / 1024.0 / 1024.0:0.0}MB, which may make your application quite slow"); + // } + tracerLogger?.LogWarning($"Failed to analyze view model size. The serialized view model has {viewModelSize / 1024.0 / 1024.0:0.0}MB, which may make your application quite slow"); } public Task EndRequest(IDotvvmRequestContext context) { diff --git a/src/Framework/Framework/DotVVM.Framework.csproj b/src/Framework/Framework/DotVVM.Framework.csproj index 9bafd8d706..954dae3a9b 100644 --- a/src/Framework/Framework/DotVVM.Framework.csproj +++ b/src/Framework/Framework/DotVVM.Framework.csproj @@ -70,7 +70,7 @@ - + diff --git a/src/Framework/Framework/Hosting/DotvvmPresenter.cs b/src/Framework/Framework/Hosting/DotvvmPresenter.cs index dee997e54a..3512be173e 100644 --- a/src/Framework/Framework/Hosting/DotvvmPresenter.cs +++ b/src/Framework/Framework/Hosting/DotvvmPresenter.cs @@ -21,12 +21,11 @@ using DotVVM.Framework.ViewModel.Serialization; using Microsoft.Extensions.DependencyInjection; using DotVVM.Framework.Runtime.Tracing; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System.Security; using System.Runtime.CompilerServices; using System.Diagnostics; using DotVVM.Framework.ViewModel.Validation; +using System.Text.Json; namespace DotVVM.Framework.Hosting { @@ -92,8 +91,8 @@ public async Task ProcessRequest(IDotvvmRequestContext context) // TODO this should be done by IOutputRender or something like that. IOutputRenderer does not support that, so should we make another IJsonErrorOutputWriter? context.HttpContext.Response.StatusCode = 400; context.HttpContext.Response.ContentType = "application/json; charset=utf-8"; - var settings = DefaultSerializerSettingsProvider.Instance.Settings; - await context.HttpContext.Response.WriteAsync(JsonConvert.SerializeObject(new { action = "invalidCsrfToken", message = ex.Message }, settings)); + var settings = DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe; + await context.HttpContext.Response.WriteAsync(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(new { action = "invalidCsrfToken", message = ex.Message }, settings)); } catch (DotvvmExceptionBase ex) { @@ -199,10 +198,10 @@ public async Task ProcessRequestCore(IDotvvmRequestContext context) else { // perform the postback - string postData; - using (var sr = new StreamReader(ReadRequestBody(context.HttpContext.Request, context.Route?.RouteName))) + ReadOnlyMemory postData; + using (var stream = ReadRequestBody(context.HttpContext.Request, context.Route?.RouteName)) { - postData = await sr.ReadToEndAsync(); + postData = await stream.ReadToMemoryAsnc(); } ViewModelSerializer.PopulateViewModel(context, postData); @@ -233,7 +232,7 @@ public async Task ProcessRequestCore(IDotvvmRequestContext context) await requestTracer.TraceEvent(RequestTracingConstants.LoadCompleted, context); // invoke the postback command - var actionInfo = ViewModelSerializer.ResolveCommand(context, page).NotNull("Command not found?"); + var actionInfo = ViewModelSerializer.ResolveCommand(context, page); // get filters var methodFilters = context.Configuration.Runtime.GlobalFilters.OfType() @@ -283,8 +282,6 @@ public async Task ProcessRequestCore(IDotvvmRequestContext context) } await requestTracer.TraceEvent(RequestTracingConstants.ViewModelSerialized, context); - ViewModelSerializer.BuildViewModel(context, commandResult); - if (context.RequestType == DotvvmRequestType.Navigate) { await OutputRenderer.WriteHtmlResponse(context, page); @@ -294,12 +291,10 @@ public async Task ProcessRequestCore(IDotvvmRequestContext context) Debug.Assert(context.RequestType is DotvvmRequestType.Command or DotvvmRequestType.SpaNavigate); // postback or SPA content var postBackUpdates = OutputRenderer.RenderPostbackUpdatedControls(context, page); - ViewModelSerializer.AddPostBackUpdatedControls(context, postBackUpdates); - // resources must be added after the HTML is rendered - some controls may request resources in the render phase - ViewModelSerializer.AddNewResources(context); + var vmString = ViewModelSerializer.SerializeViewModel(context, commandResult, postBackUpdates, serializeNewResources: true); - await OutputRenderer.WriteViewModelResponse(context, page); + await OutputRenderer.WriteViewModelResponse(context, page, vmString); } await requestTracer.TraceEvent(RequestTracingConstants.OutputRendered, context); @@ -341,22 +336,22 @@ public async Task ProcessRequestCore(IDotvvmRequestContext context) public async Task ProcessStaticCommandRequest(IDotvvmRequestContext context) { + JsonDocument? postData = null; try { - JObject postData; - using (var jsonReader = new JsonTextReader(new StreamReader(ReadRequestBody(context.HttpContext.Request, routeName: null)))) + using (var requestBody = ReadRequestBody(context.HttpContext.Request, routeName: null)) { - postData = await JObject.LoadAsync(jsonReader); + postData = await JsonDocument.ParseAsync(requestBody); } // validate csrf token - context.CsrfToken = (postData["$csrfToken"]?.Value()).NotNull("$csrfToken is required"); + context.CsrfToken = postData.RootElement.GetProperty("$csrfToken"u8).GetString().NotNull("$csrfToken is required"); CsrfProtector.VerifyToken(context, context.CsrfToken); - var knownTypes = postData["knownTypeMetadata"]?.Values().WhereNotNull().ToArray() ?? Array.Empty(); - var argumentPaths = postData["argumentPaths"]?.Values().ToArray(); - var command = (postData["command"]?.Value()).NotNull("command is required"); - var arguments = (postData["args"] as JArray).NotNull("args is required"); + var knownTypes = postData.RootElement.GetPropertyOrNull("knownTypeMetadata"u8)?.EnumerateStringArray().WhereNotNull().ToArray() ?? []; + var argumentPaths = postData.RootElement.GetPropertyOrNull("argumentPaths"u8)?.EnumerateStringArray().ToArray(); + var command = postData.RootElement.GetProperty("command"u8).GetString().NotNull("command is required"); + var arguments = postData.RootElement.GetProperty("args"u8); var executionPlan = StaticCommandExecutor.DecryptPlan(command); var actionInfo = new ActionInfo( @@ -392,6 +387,7 @@ public async Task ProcessStaticCommandRequest(IDotvvmRequestContext context) } finally { + postData?.Dispose(); // returns pooled byte buffers StaticCommandExecutor.DisposeServices(context); } } @@ -463,12 +459,10 @@ async Task RespondWithStaticCommandValidationFailure(ActionInfo action, IDotvvmR context.RequestTypeLabel() ); - var jObject = new JObject - { - [ "modelState" ] = JArray.FromObject(staticCommandModelState.Errors), - [ "action" ] = "validationErrors" - }; - var result = jObject.ToString(); + var result = JsonSerializer.SerializeToUtf8Bytes(new { + action = "validationErrors", + modelState = staticCommandModelState.Errors + }); context.HttpContext.Response.ContentType = "application/json"; await context.HttpContext.Response.WriteAsync(result); diff --git a/src/Framework/Framework/Hosting/DotvvmPropertySerializableList.cs b/src/Framework/Framework/Hosting/DotvvmPropertySerializableList.cs index 45f44103ba..0595ca2603 100644 --- a/src/Framework/Framework/Hosting/DotvvmPropertySerializableList.cs +++ b/src/Framework/Framework/Hosting/DotvvmPropertySerializableList.cs @@ -177,14 +177,14 @@ public record DotvvmPropertyGroupInfo( public record DotvvmControlInfo( string? assembly, Type? baseType, - Type[]? interfaces, - bool isAbstract, - string? defaultContentProperty, - bool withoutContent, - string? markupPrimaryName, - string[]? markupAlternativeNames, - bool isComposite, - ControlPrecompilationMode? precompilationMode + Type[]? interfaces = null, + bool isAbstract = false, + string? defaultContentProperty = null, + bool withoutContent = false, + string? markupPrimaryName = null, + string[]? markupAlternativeNames = null, + bool isComposite = false, + ControlPrecompilationMode? precompilationMode = null ) { } diff --git a/src/Framework/Framework/Hosting/DotvvmRequestContext.cs b/src/Framework/Framework/Hosting/DotvvmRequestContext.cs index a0291b62e8..35f5aa890f 100644 --- a/src/Framework/Framework/Hosting/DotvvmRequestContext.cs +++ b/src/Framework/Framework/Hosting/DotvvmRequestContext.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json.Linq; using DotVVM.Framework.Configuration; using DotVVM.Framework.Routing; using DotVVM.Framework.ResourceManagement; @@ -9,15 +8,15 @@ using DotVVM.Framework.ViewModel.Serialization; using Microsoft.Extensions.DependencyInjection; using DotVVM.Framework.Utils; +using System.Text.Json; namespace DotVVM.Framework.Hosting { public class DotvvmRequestContext : IDotvvmRequestContext { public string? CsrfToken { get; set; } - public JObject? ReceivedViewModelJson { get; set; } + public JsonDocument? ReceivedViewModelJson { get; set; } - public JObject? ViewModelJson { get; set; } /// /// Gets the route that was used for this request. diff --git a/src/Framework/Framework/Hosting/DotvvmRequestContextExtensions.cs b/src/Framework/Framework/Hosting/DotvvmRequestContextExtensions.cs index 913347ffb7..b289cc4ae3 100644 --- a/src/Framework/Framework/Hosting/DotvvmRequestContextExtensions.cs +++ b/src/Framework/Framework/Hosting/DotvvmRequestContextExtensions.cs @@ -167,10 +167,7 @@ public static void FailOnInvalidModelState(this IDotvvmRequestContext context) ); context.HttpContext.Response.ContentType = "application/json"; context.HttpContext.Response - .WriteAsync(context.Services.GetRequiredService().SerializeModelState(context)) - .GetAwaiter().GetResult(); - // ^ we just wait for this Task. This API never was async and the response size is small enough that we can't quite safely wait for the result - // .GetAwaiter().GetResult() preserves stack traces across async calls, thus I like it more than .Wait() + .Write(context.Services.GetRequiredService().SerializeModelState(context)); throw new DotvvmInterruptRequestExecutionException(InterruptReason.ModelValidationFailed, "The ViewModel contains validation errors!"); } } diff --git a/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs b/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs index 6b52195d8e..384fcafa21 100644 --- a/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs +++ b/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs @@ -7,13 +7,16 @@ using System.Reflection; using DotVVM.Framework.Hosting.ErrorPages; using DotVVM.Framework.ResourceManagement; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System.Text; using DotVVM.Framework.Configuration; using Microsoft.Extensions.DependencyInjection; using DotVVM.Framework.Utils; using DotVVM.Framework.Binding.Expressions; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using DotVVM.Framework.Compilation.ControlTree; +using System.Text.Json.Serialization.Metadata; namespace DotVVM.Framework.Hosting.ErrorPages { @@ -160,28 +163,62 @@ public void ObjectBrowser(object? obj) return; } - var settings = new JsonSerializerSettings() { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - DefaultValueHandling = DefaultValueHandling.Ignore, + var settings = new JsonSerializerOptions() { + ReferenceHandler = ReferenceHandler.IgnoreCycles, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, Converters = { new ReflectionTypeJsonConverter(), new ReflectionAssemblyJsonConverter(), - new DotvvmTypeDescriptorJsonConverter(), + new DotvvmTypeDescriptorJsonConverter(), new Controls.DotvvmControlDebugJsonConverter(), - new IgnoreStuffJsonConverter(), + new DelegateJsonConverter(), new BindingDebugJsonConverter(), new DotvvmPropertyJsonConverter() }, + TypeInfoResolver = new IgnoreUnsupportedResolver(), // suppress any errors that occur during serialization (getters may throw exception, ...) - Error = (sender, args) => { - args.ErrorContext.Handled = true; - } + // Error = (sender, args) => { // TODO: how? + // args.ErrorContext.Handled = true; + // } }; - var jobject = JObject.FromObject(obj, JsonSerializer.Create(settings)); - ObjectBrowser(jobject); + var jobject = JsonSerializer.SerializeToElement(obj, settings); + ObjectBrowser(JsonObject.Create(jobject)!); + } + + class IgnoreUnsupportedResolver: DefaultJsonTypeInfoResolver + { + HashSet stack = new HashSet(); + public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) + { + stack.Add(type); + try + { + var info = base.GetTypeInfo(type, options); + foreach (var prop in info!.Properties.ToArray()) + { + if (stack.Contains(prop.PropertyType)) + { + // object currently being resolved -> fine + } + else + { + var converter = prop.CustomConverter ?? options.GetConverter(prop.PropertyType); + if (converter is null) + { + info.Properties.Remove(prop); + } + } + } + return info; + } + finally + { + stack.Remove(type); + } + } } - public void ObjectBrowser(JArray arr) + public void ObjectBrowser(JsonArray arr) { if (arr.Count == 0) { @@ -198,13 +235,17 @@ public void ObjectBrowser(JArray arr)
"); foreach (var p in arr) { - if (p is JObject) + if (p is JsonObject pObj) { - ObjectBrowser((JObject)p); + ObjectBrowser(pObj); } - else if (p is JArray) + else if (p is JsonArray pArr) { - ObjectBrowser((JArray)p); + ObjectBrowser(pArr); + } + else if (p is null) + { + WriteText("null"); } else { @@ -215,7 +256,7 @@ public void ObjectBrowser(JArray arr) } } - public void ObjectBrowser(JObject obj) + public void ObjectBrowser(JsonObject obj) { if (obj.Count == 0) { @@ -238,17 +279,17 @@ public void ObjectBrowser(JObject obj) { WriteText("null"); } - else if (p.Value is JObject) + else if (p.Value is JsonObject pObj) { - ObjectBrowser((JObject)p.Value); + ObjectBrowser(pObj); } - else if (p.Value is JArray) + else if (p.Value is JsonArray pArr) { - ObjectBrowser((JArray)p.Value); + ObjectBrowser(pArr); } else { - WriteText(p.Value.ToString(Formatting.None)); + WriteText(p.Value.ToJsonString()); } Write("
"); } @@ -367,16 +408,15 @@ private void WriteLine(string textToAppend) builder.AppendLine(); } - class IgnoreStuffJsonConverter : JsonConverter + class DelegateJsonConverter : JsonConverter { - public override bool CanConvert(Type objectType) => - objectType.IsDelegate(); - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => + public override Delegate? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, Delegate value, JsonSerializerOptions options) { - writer.WriteValue(""); + writer.WriteStringValue(""); } + } } } diff --git a/src/Framework/Framework/Hosting/HttpRedirectService.cs b/src/Framework/Framework/Hosting/HttpRedirectService.cs index d1aef7f12f..e46440d748 100644 --- a/src/Framework/Framework/Hosting/HttpRedirectService.cs +++ b/src/Framework/Framework/Hosting/HttpRedirectService.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Net; using System.Threading; -using Newtonsoft.Json.Linq; using DotVVM.Framework.Configuration; using DotVVM.Framework.Controls; using DotVVM.Framework.Routing; diff --git a/src/Framework/Framework/Hosting/IDotvvmRequestContext.cs b/src/Framework/Framework/Hosting/IDotvvmRequestContext.cs index 47d066a117..5c68214c5b 100644 --- a/src/Framework/Framework/Hosting/IDotvvmRequestContext.cs +++ b/src/Framework/Framework/Hosting/IDotvvmRequestContext.cs @@ -7,7 +7,7 @@ using DotVVM.Framework.Controls.Infrastructure; using DotVVM.Framework.ResourceManagement; using DotVVM.Framework.Routing; -using Newtonsoft.Json.Linq; +using System.Text.Json; namespace DotVVM.Framework.Hosting { @@ -23,15 +23,13 @@ public interface IDotvvmRequestContext ///
string? CsrfToken { get; set; } - JObject? ReceivedViewModelJson { get; set; } + JsonDocument? ReceivedViewModelJson { get; set; } /// /// Gets the view model for the current request. /// object? ViewModel { get; set; } - JObject? ViewModelJson { get; set; } - /// /// Gets the top-level control representing the whole view for the current request. /// @@ -120,6 +118,11 @@ public interface IDotvvmRequestContext CustomResponsePropertiesManager CustomResponseProperties { get; } } + public class ReceivedViewModelData + { + + } + public enum DotvvmRequestType { Unknown, diff --git a/src/Framework/Framework/Hosting/IHttpResponse.cs b/src/Framework/Framework/Hosting/IHttpResponse.cs index c850698063..f7f7a207d0 100644 --- a/src/Framework/Framework/Hosting/IHttpResponse.cs +++ b/src/Framework/Framework/Hosting/IHttpResponse.cs @@ -1,4 +1,5 @@ -using System.Collections; +using System; +using System.Collections; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -13,9 +14,10 @@ public interface IHttpResponse string? ContentType { get; set; } Stream Body { get; set; } void Write(string text); - void Write(byte[] data); - void Write(byte[] data, int offset, int count); - Task WriteAsync(string text); - Task WriteAsync(string text, CancellationToken token); + void Write(ReadOnlyMemory text); + void Write(ReadOnlyMemory data); + Task WriteAsync(string text, CancellationToken token = default); + Task WriteAsync(ReadOnlyMemory text, CancellationToken token = default); + Task WriteAsync(ReadOnlyMemory data, CancellationToken token = default); } } diff --git a/src/Framework/Framework/Hosting/Middlewares/DotvvmFileUploadMiddleware.cs b/src/Framework/Framework/Hosting/Middlewares/DotvvmFileUploadMiddleware.cs index 1aaef84651..bb76413fd4 100644 --- a/src/Framework/Framework/Hosting/Middlewares/DotvvmFileUploadMiddleware.cs +++ b/src/Framework/Framework/Hosting/Middlewares/DotvvmFileUploadMiddleware.cs @@ -12,7 +12,6 @@ using DotVVM.Framework.ViewModel.Serialization; using Microsoft.AspNet.WebUtilities; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; namespace DotVVM.Framework.Hosting.Middlewares { diff --git a/src/Framework/Framework/Hosting/StaticCommandExecutor.cs b/src/Framework/Framework/Hosting/StaticCommandExecutor.cs index 71c1e2bd6c..a3c38c2152 100644 --- a/src/Framework/Framework/Hosting/StaticCommandExecutor.cs +++ b/src/Framework/Framework/Hosting/StaticCommandExecutor.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text.Json; using System.Threading.Tasks; using DotVVM.Framework.Compilation.Binding; using DotVVM.Framework.Configuration; @@ -9,10 +10,8 @@ using DotVVM.Framework.Security; using DotVVM.Framework.Utils; using DotVVM.Framework.ViewModel; +using DotVVM.Framework.ViewModel.Serialization; using DotVVM.Framework.ViewModel.Validation; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - namespace DotVVM.Framework.Hosting { public class StaticCommandExecutor @@ -22,9 +21,9 @@ public class StaticCommandExecutor private readonly IViewModelProtector viewModelProtector; private readonly IStaticCommandArgumentValidator validator; private readonly DotvvmConfiguration configuration; - private readonly JsonSerializer jsonDeserializer; + private readonly JsonSerializerOptions jsonOptions; - public StaticCommandExecutor(IStaticCommandServiceLoader serviceLoader, IViewModelProtector viewModelProtector, IStaticCommandArgumentValidator validator, DotvvmConfiguration configuration) + public StaticCommandExecutor(IStaticCommandServiceLoader serviceLoader, IViewModelSerializer serializer, IViewModelProtector viewModelProtector, IStaticCommandArgumentValidator validator, DotvvmConfiguration configuration) { this.serviceLoader = serviceLoader; this.viewModelProtector = viewModelProtector; @@ -32,11 +31,13 @@ public StaticCommandExecutor(IStaticCommandServiceLoader serviceLoader, IViewMod this.configuration = configuration; if (configuration.ExperimentalFeatures.UseDotvvmSerializationForStaticCommandArguments.Enabled) { - this.jsonDeserializer = DefaultSerializerSettingsProvider.CreateJsonSerializer(); + this.jsonOptions = serializer.ViewModelJsonOptions; } else { - this.jsonDeserializer = JsonSerializer.Create(); + this.jsonOptions = new JsonSerializerOptions(DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe) { + WriteIndented = configuration.Debug + }; } } #pragma warning restore CS0618 @@ -48,14 +49,14 @@ public StaticCommandInvocationPlan DecryptPlan(string encrypted) } public Task Execute( StaticCommandInvocationPlan plan, - IEnumerable arguments, + JsonElement arguments, IEnumerable? argumentValidationPaths, IDotvvmRequestContext context - ) => Execute(plan, new Queue(arguments), argumentValidationPaths is null ? null : new Queue(argumentValidationPaths), context); + ) => Execute(plan, new Queue(arguments.EnumerateArray()), argumentValidationPaths is null ? null : new Queue(argumentValidationPaths), context); public async Task Execute( StaticCommandInvocationPlan plan, - Queue arguments, + Queue arguments, Queue? argumentValidationPaths, IDotvvmRequestContext context ) @@ -70,7 +71,8 @@ IDotvvmRequestContext context if (!parameterType!.IsAssignableFrom(type)) throw new Exception($"Argument {index} has an invalid type"); var arg = arguments.Dequeue(); - return arg.ToObject(type, this.jsonDeserializer); + using var state = DotvvmSerializationState.Create(true, context.Services, new System.Text.Json.Nodes.JsonObject()); + return JsonSerializer.Deserialize(arg, type, this.jsonOptions); } var methodArgs = new List(); var methodArgsPaths = argumentValidationPaths is null ? null : new List(); diff --git a/src/Framework/Framework/Hosting/VisualStudioHelper.cs b/src/Framework/Framework/Hosting/VisualStudioHelper.cs index 4e0f573450..3c3f26fe64 100644 --- a/src/Framework/Framework/Hosting/VisualStudioHelper.cs +++ b/src/Framework/Framework/Hosting/VisualStudioHelper.cs @@ -1,12 +1,13 @@ using System.Diagnostics; using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; using DotVVM.Framework.Compilation; using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Configuration; using DotVVM.Framework.ResourceManagement; +using DotVVM.Framework.ViewModel.Serialization; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; namespace DotVVM.Framework.Hosting { @@ -30,24 +31,30 @@ internal static string SerializeConfig(DotvvmConfiguration config, bool includeP controls = includeProperties ? DotvvmPropertySerializableList.GetControls(config.ServiceProvider.GetRequiredService()) : null, assemblies = includeProperties ? AssemblySerializableList.CreateFromCache(config.ServiceProvider.GetRequiredService()) : null, }; - return JsonConvert.SerializeObject(obj, Formatting.Indented, new JsonSerializerSettings { - TypeNameHandling = TypeNameHandling.Auto, - TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, - NullValueHandling = NullValueHandling.Ignore, - DefaultValueHandling = DefaultValueHandling.Ignore, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - // suppress any errors that occur during serialization - Error = (sender, args) => { - args.ErrorContext.Handled = true; - }, + return JsonSerializer.Serialize(obj, GetSerializerOptions()); + } + + public static JsonSerializerOptions GetSerializerOptions() + { + return new JsonSerializerOptions { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + // ReferenceHandler = ReferenceHandler.IgnoreCycles, // doesn't work together with JsonObjectCreationHandling.Populate + // Error = (sender, args) => { // TODO: how? https://github.com/dotnet/runtime/issues/38049 + // args.ErrorContext.Handled = true; + // }, + WriteIndented = true, Converters = { - new StringEnumConverter(), new ReflectionTypeJsonConverter(), - new DotvvmTypeDescriptorJsonConverter(), - new ReflectionAssemblyJsonConverter() + new ReflectionAssemblyJsonConverter(), + new DotvvmTypeDescriptorJsonConverter(), + new DotvvmPropertyJsonConverter(), + new DotvvmEnumConverter(), + new DataContextChangeAttributeConverter(), + new DataContextManipulationAttributeConverter() }, - ContractResolver = new DotvvmConfigurationSerializationResolver() - }); + TypeInfoResolver = new DotvvmConfigurationSerializationResolver(), + Encoder = DefaultSerializerSettingsProvider.Instance.HtmlSafeLessParaoidEncoder + }; } public static void DumpConfiguration(DotvvmConfiguration config, string directory) diff --git a/src/Framework/Framework/ResourceManagement/ClientGlobalize/JQueryGlobalizeScriptCreator.cs b/src/Framework/Framework/ResourceManagement/ClientGlobalize/JQueryGlobalizeScriptCreator.cs index 795265e657..2249687264 100644 --- a/src/Framework/Framework/ResourceManagement/ClientGlobalize/JQueryGlobalizeScriptCreator.cs +++ b/src/Framework/Framework/ResourceManagement/ClientGlobalize/JQueryGlobalizeScriptCreator.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json.Linq; using DotVVM.Framework.Utils; using System; using System.Collections.Generic; @@ -7,6 +6,9 @@ using System.Text; using System.Threading.Tasks; using DotVVM.Framework.Compilation.Javascript; +using System.Text.Json.Nodes; +using System.Text.Json; +using DotVVM.Framework.Configuration; namespace DotVVM.Framework.ResourceManagement.ClientGlobalize { @@ -22,83 +24,84 @@ public static class JQueryGlobalizeScriptCreator { "($n)", "-$n", "$-n", "$n-", "(n$)", "-n$", "n-$", "n$-", "-n $", "-$ n", "n $-", "$ n-", "$ -n", "n- $", "($ n)", "(n $)" }; private static readonly string[] currencyPositivePatternStrings = { "$n", "n$", "$ n", "n $" }; - private static readonly JObject defaultJson = JObject.Parse(@"{ - name: 'en', - englishName: 'English', - nativeName: 'English', - isRTL: false, - language: 'en', - numberFormat: { - pattern: ['-n'], - decimals: 2, - ',': ',', - '.': '.', - groupSizes: [3], - '+': '+', - '-': '-', - NaN: 'NaN', - negativeInfinity: '-Infinity', - positiveInfinity: 'Infinity', - percent: { - pattern: ['-n %', 'n %'], - decimals: 2, - groupSizes: [3], - ',': ',', - '.': '.', - symbol: '%' + private static readonly JsonObject defaultJson = JsonNode.Parse(""" +{ + "name": "en", + "englishName": "English", + "nativeName": "English", + "isRTL": false, + "language": "en", + "numberFormat": { + "pattern": ["-n"], + "decimals": 2, + ",": ",", + ".": ".", + "groupSizes": [3], + "+": "+", + "-": "-", + "NaN": "NaN", + "negativeInfinity": "-Infinity", + "positiveInfinity": "Infinity", + "percent": { + "pattern": ["-n %", "n %"], + "decimals": 2, + "groupSizes": [3], + ",": ",", + ".": ".", + "symbol": "%" }, - currency: { - pattern: [ '($n)', '$n' ], - decimals: 2, - groupSizes: [ 3 ], - ',': ',', - '.': '.', - symbol: '$' - } - }, - calendars: { - standard: { - name: 'Gregorian_USEnglish', - '/': '/', - ':': ':', - firstDay: 0, - days: { - names: [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ], - namesAbbr: [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ], - namesShort: [ 'Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa' ] - }, - months: { - names: [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', '' ], - namesAbbr: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', '' ] - }, - AM: [ 'AM', 'am', 'AM' ], - PM: [ 'PM', 'pm', 'PM' ], - eras: [ - { - 'name': 'A.D.', - 'start': null, - 'offset': 0 - } - ], - twoDigitYearMax: 2029, - 'patterns': { - 'd': 'M/d/yyyy', - 'D': 'dddd, MMMM dd, yyyy', - 't': 'h:mm tt', - 'T': 'h:mm:ss tt', - 'f': 'dddd, MMMM dd, yyyy h:mm tt', - 'F': 'dddd, MMMM dd, yyyy h:mm:ss tt', - 'M': 'MMMM dd', - 'Y': 'yyyy MMMM', - 'S': 'yyyy\u0027-\u0027MM\u0027-\u0027dd\u0027T\u0027HH\u0027:\u0027mm\u0027:\u0027ss' - } - } - }, - 'messages': {} + "currency": { + "pattern": [ "($n)", "$n" ], + "decimals": 2, + "groupSizes": [ 3 ], + ",": ",", + ".": ".", + "symbol": "$" + } + }, + "calendars": { + "standard": { + "name": "Gregorian_USEnglish", + "/": "/", + ":": ":", + "firstDay": 0, + "days": { + "names": [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], + "namesAbbr": [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ], + "namesShort": [ "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" ] + }, + "months": { + "names": [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", "" ], + "namesAbbr": [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "" ] + }, + "AM": [ "AM", "am", "AM" ], + "PM": [ "PM", "pm", "PM" ], + "eras": [ + { + "name": "A.D.", + "start": null, + "offset": 0 + } + ], + "twoDigitYearMax": 2029, + "patterns": { + "d": "M/d/yyyy", + "D": "dddd, MMMM dd, yyyy", + "t": "h:mm tt", + "T": "h:mm:ss tt", + "f": "dddd, MMMM dd, yyyy h:mm tt", + "F": "dddd, MMMM dd, yyyy h:mm:ss tt", + "M": "MMMM dd", + "Y": "yyyy MMMM", + "S": "yyyy\u0027-\u0027MM\u0027-\u0027dd\u0027T\u0027HH\u0027:\u0027mm\u0027:\u0027ss" + } + } + }, + "messages": {} } -"); +""")!.AsObject(); - private static JObject CreateNumberInfoJson(NumberFormatInfo ni) + private static JsonObject CreateNumberInfoJson(NumberFormatInfo ni) { var numberFormat = new { @@ -130,7 +133,7 @@ private static JObject CreateNumberInfoJson(NumberFormatInfo ni) symbol = ni.CurrencySymbol } }; - var jobj = JObject.FromObject(numberFormat); + var jobj = JsonSerializer.SerializeToNode(numberFormat)!.AsObject(); jobj[","] = ni.NumberGroupSeparator; jobj["."] = ni.NumberDecimalSeparator; jobj["percent"]![","] = ni.PercentGroupSeparator; @@ -141,7 +144,7 @@ private static JObject CreateNumberInfoJson(NumberFormatInfo ni) return jobj; } - private static JObject CreateDateInfoJson(DateTimeFormatInfo di) + private static JsonObject CreateDateInfoJson(DateTimeFormatInfo di) { var obj = new { @@ -161,33 +164,32 @@ private static JObject CreateDateInfoJson(DateTimeFormatInfo di) PM = new[] { di.PMDesignator, di.PMDesignator.ToLowerInvariant(), di.PMDesignator.ToUpperInvariant() }, eras = di.Calendar.Eras.Select(era => new { offset = 0, start = (string?)null, name = di.GetEraName(era) }).ToArray(), twoDigitYearMax = di.Calendar.TwoDigitYearMax, - patterns = new - { - d = di.ShortDatePattern, - D = di.LongDatePattern, - t = di.ShortTimePattern, - T = di.LongTimePattern, - f = di.LongDatePattern + " " + di.ShortTimePattern, - F = di.LongDatePattern + " " + di.LongTimePattern, - M = di.MonthDayPattern, - Y = di.YearMonthPattern, - g = di.ShortDatePattern + " " + di.ShortTimePattern, - G = di.ShortDatePattern + " " + di.LongTimePattern + patterns = new JsonObject { + ["d"] = di.ShortDatePattern, + ["D"] = di.LongDatePattern, + ["t"] = di.ShortTimePattern, + ["T"] = di.LongTimePattern, + ["f"] = di.LongDatePattern + " " + di.ShortTimePattern, + ["F"] = di.LongDatePattern + " " + di.LongTimePattern, + ["M"] = di.MonthDayPattern, + ["Y"] = di.YearMonthPattern, + ["g"] = di.ShortDatePattern + " " + di.ShortTimePattern, + ["G"] = di.ShortDatePattern + " " + di.LongTimePattern } }; - var jobj = JObject.FromObject(obj); + var jobj = JsonSerializer.SerializeToNode(obj)!.AsObject(); if (!di.MonthNames.SequenceEqual(di.MonthGenitiveNames)) { - var monthsGenitive = jobj["monthsGenitive"] = new JObject(); - monthsGenitive["names"] = JArray.FromObject(di.MonthGenitiveNames); - monthsGenitive["namesAbbr"] = JArray.FromObject(di.AbbreviatedMonthGenitiveNames); + var monthsGenitive = jobj["monthsGenitive"] = new JsonObject(); + monthsGenitive["names"] = JsonSerializer.SerializeToNode(di.MonthGenitiveNames); + monthsGenitive["namesAbbr"] = JsonSerializer.SerializeToNode(di.AbbreviatedMonthGenitiveNames); } - return new JObject() + return new JsonObject() { {"standard", jobj } }; } - public static JObject BuildCultureInfoJson(CultureInfo ci) + public static JsonObject BuildCultureInfoJson(CultureInfo ci) { var cultureInfoClientObj = new { @@ -197,7 +199,7 @@ public static JObject BuildCultureInfoJson(CultureInfo ci) isRTL = ci.TextInfo.IsRightToLeft, language = ci.TwoLetterISOLanguageName }; - var jobj = JObject.FromObject(cultureInfoClientObj); + var jobj = JsonSerializer.SerializeToNode(cultureInfoClientObj)!.AsObject(); jobj["numberFormat"] = CreateNumberInfoJson(ci.NumberFormat); jobj["calendars"] = CreateDateInfoJson(ci.DateTimeFormat); @@ -206,25 +208,24 @@ public static JObject BuildCultureInfoJson(CultureInfo ci) public static string BuildCultureInfoScript(CultureInfo ci) { - var cultureJson = BuildCultureInfoJson(ci).ToString(); - - return $@" -(function(window, undefined) {{ + var cultureJson = BuildCultureInfoJson(ci).ToJsonString(DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe); + return $$""" +(function(window, undefined) { var Globalize; if ( typeof require !== 'undefined' - && typeof exports !== 'undefined' - && typeof module !== 'undefined' ) {{ - // Assume CommonJS - Globalize = require('globalize'); -}} else {{ - // Global variable - Globalize = window.dotvvm_Globalize; -}} -Globalize.addCultureInfo({JavascriptCompilationHelper.CompileConstant(ci.Name)}, 'default', {cultureJson}); -}}(this)); -"; + && typeof exports !== 'undefined' + && typeof module !== 'undefined' ) { + // Assume CommonJS + Globalize = require('globalize'); +} else { + // Global variable + Globalize = window.dotvvm_Globalize; +} +Globalize.addCultureInfo({{JavascriptCompilationHelper.CompileConstant(ci.Name)}}, 'default', {{cultureJson}}); +}(this)); +"""; } } } diff --git a/src/Framework/Framework/ResourceManagement/DotvvmResourceRepository.cs b/src/Framework/Framework/ResourceManagement/DotvvmResourceRepository.cs index 185f23b3f7..eeb337c869 100644 --- a/src/Framework/Framework/ResourceManagement/DotvvmResourceRepository.cs +++ b/src/Framework/Framework/ResourceManagement/DotvvmResourceRepository.cs @@ -1,5 +1,4 @@ -using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -13,6 +12,7 @@ using DotVVM.Framework.Configuration; using System.Collections; using DotVVM.Framework.Runtime; +using System.Text.Json.Serialization; namespace DotVVM.Framework.ResourceManagement { diff --git a/src/Framework/Framework/ResourceManagement/EmbeddedResourceLocation.cs b/src/Framework/Framework/ResourceManagement/EmbeddedResourceLocation.cs index 8e838a261b..1fcdd18308 100644 --- a/src/Framework/Framework/ResourceManagement/EmbeddedResourceLocation.cs +++ b/src/Framework/Framework/ResourceManagement/EmbeddedResourceLocation.cs @@ -1,9 +1,9 @@ using System.IO; using DotVVM.Framework.Hosting; using System.Reflection; -using Newtonsoft.Json; using System; using DotVVM.Framework.Utils; +using System.Text.Json.Serialization; namespace DotVVM.Framework.ResourceManagement { diff --git a/src/Framework/Framework/ResourceManagement/InlineScriptResource.cs b/src/Framework/Framework/ResourceManagement/InlineScriptResource.cs index 252b4eb1ca..b11fac4ef9 100644 --- a/src/Framework/Framework/ResourceManagement/InlineScriptResource.cs +++ b/src/Framework/Framework/ResourceManagement/InlineScriptResource.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.Json.Serialization; using System.Threading; using DotVVM.Framework.Controls; using DotVVM.Framework.Hosting; -using Newtonsoft.Json; namespace DotVVM.Framework.ResourceManagement { @@ -36,6 +36,7 @@ public InlineScriptResource(ILocalResourceLocation resourceLocation, ResourceRen /// /// Gets or sets the javascript code that will be embedded in the page. /// + [JsonIgnore] public string Code { get => code?.Value ?? throw new Exception("`ILocalResourceLocation` cannot be read using property `Code`."); @@ -48,6 +49,12 @@ public string Code } } + [JsonInclude] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName(nameof(Code))] + internal string? CodeJsonHack => code?.Value; // ignore if code is in location + + /// If the script should be executed after the page loads (using the `defer` attribute). public bool Defer { get; } public bool ShouldSerializeCode() => code?.IsValueCreated == true; diff --git a/src/Framework/Framework/ResourceManagement/InlineStylesheetResource.cs b/src/Framework/Framework/ResourceManagement/InlineStylesheetResource.cs index 06fcb49acb..ab3f8d3462 100644 --- a/src/Framework/Framework/ResourceManagement/InlineStylesheetResource.cs +++ b/src/Framework/Framework/ResourceManagement/InlineStylesheetResource.cs @@ -1,9 +1,9 @@ using System; using System.IO; +using System.Text.Json.Serialization; using System.Threading; using DotVVM.Framework.Controls; using DotVVM.Framework.Hosting; -using Newtonsoft.Json; namespace DotVVM.Framework.ResourceManagement { @@ -18,8 +18,16 @@ public class InlineStylesheetResource : ResourceBase /// /// Gets the CSS code that will be embedded in the page. /// + [JsonIgnore] public string Code => code?.Value ?? throw new Exception("`ILocalResourceLocation` cannot be read using property `Code`."); + + [JsonInclude] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName(nameof(Code))] + internal string? CodeJsonHack => code?.Value; // ignore if code is in location + + [JsonConstructor] public InlineStylesheetResource(ILocalResourceLocation resourceLocation) : base(ResourceRenderPosition.Head) { @@ -33,7 +41,6 @@ public InlineStylesheetResource(string code) : this(new InlineResourceLocation(c _ = this.code.Value; } - public bool ShouldSerializeCode() => code?.IsValueCreated == true; internal static void InlineStyleContentGuard(string code) { diff --git a/src/Framework/Framework/ResourceManagement/LinkResourceBase.cs b/src/Framework/Framework/ResourceManagement/LinkResourceBase.cs index 04890b0c23..7ec9391708 100644 --- a/src/Framework/Framework/ResourceManagement/LinkResourceBase.cs +++ b/src/Framework/Framework/ResourceManagement/LinkResourceBase.cs @@ -4,9 +4,9 @@ using DotVVM.Framework.Controls; using DotVVM.Framework.Hosting; using System.IO; -using Newtonsoft.Json; using Microsoft.Extensions.DependencyInjection; using System.ComponentModel; +using System.Text.Json.Serialization; namespace DotVVM.Framework.ResourceManagement { @@ -16,7 +16,17 @@ namespace DotVVM.Framework.ResourceManagement public abstract class LinkResourceBase : ResourceBase, ILinkResource { /// Location property is required! + [JsonIgnore] public IResourceLocation Location { get; set; } + + [JsonPropertyName("LocationType")] + [JsonInclude] + internal Type? LocationTypeJsonHack => Location?.GetType(); + + [JsonPropertyName("Location")] // it will not do converter dispatch on IResourceLocation, but will happily do it on System.Object + [JsonInclude] + internal object? LocationJsonHack => Location; + public ResourceLocationFallback? LocationFallback { get; set; } public string MimeType { get; private set; } [DefaultValue(true)] @@ -74,7 +84,7 @@ protected virtual void RenderFallbackLoadingScript(IHtmlWriter writer, IDotvvmRe { writer.AddAttribute("type", "text/javascript"); writer.RenderBeginTag("script"); - var script = JsonConvert.ToString(link, '\'').Replace("<", "\\u003c"); + var script = KnockoutHelper.MakeStringLiteral(link); writer.WriteUnencodedText(GetLoadingScript(javascriptCondition, script)); writer.RenderEndTag(); } @@ -144,7 +154,11 @@ public class ResourceLocationFallback /// Javascript expression which return true (truthy value) when the script IS NOT correctly loaded ///
public string JavascriptCondition { get; } + [JsonIgnore] public List AlternativeLocations { get; } + + [JsonPropertyName(nameof(AlternativeLocations))] + internal IEnumerable AlternativeLocationsJsonHack => AlternativeLocations; public ResourceLocationFallback(string javascriptCondition, params IResourceLocation[] alternativeLocations) { diff --git a/src/Framework/Framework/ResourceManagement/LocalResourceLocation.cs b/src/Framework/Framework/ResourceManagement/LocalResourceLocation.cs index 57e74c9be6..bfaa9b4d29 100644 --- a/src/Framework/Framework/ResourceManagement/LocalResourceLocation.cs +++ b/src/Framework/Framework/ResourceManagement/LocalResourceLocation.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using DotVVM.Framework.Hosting; using System.Reflection; -using Newtonsoft.Json; using Microsoft.Extensions.DependencyInjection; namespace DotVVM.Framework.ResourceManagement diff --git a/src/Framework/Framework/ResourceManagement/ReflectionAssemblyJsonConverter.cs b/src/Framework/Framework/ResourceManagement/ReflectionAssemblyJsonConverter.cs index 77e8f75862..cb0c48c4b0 100644 --- a/src/Framework/Framework/ResourceManagement/ReflectionAssemblyJsonConverter.cs +++ b/src/Framework/Framework/ResourceManagement/ReflectionAssemblyJsonConverter.cs @@ -1,106 +1,124 @@ -using DotVVM.Framework.Compilation.ControlTree; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Compilation; +using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Compilation.ControlTree.Resolved; -using Newtonsoft.Json; +using DotVVM.Framework.Utils; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; namespace DotVVM.Framework.ResourceManagement { - public class ReflectionAssemblyJsonConverter : JsonConverter + public class ReflectionAssemblyJsonConverter : JsonConverter { - public override bool CanConvert(Type objectType) => typeof(Assembly).IsAssignableFrom(objectType); - - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + public override Assembly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.Value is string name) + if (reader.TokenType == JsonTokenType.String) { - return Assembly.Load(new AssemblyName(name)); + return Assembly.Load(new AssemblyName(reader.GetString()!)); } else throw new NotSupportedException(); } - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, Assembly value, JsonSerializerOptions options) { - writer.WriteValue(((Assembly?)value)?.GetName().ToString()); + writer.WriteStringValue(((Assembly?)value)?.GetName().ToString()); } } - public class ReflectionTypeJsonConverter : JsonConverter + public class ReflectionTypeJsonConverter : JsonConverter { - public override bool CanConvert(Type objectType) => typeof(Type).IsAssignableFrom(objectType); - - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + public override Type Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.Value is string name) + if (reader.TokenType == JsonTokenType.String) { + var name = reader.GetString()!; return Type.GetType(name) ?? throw new Exception($"Cannot find type {name}."); } else throw new NotSupportedException(); } - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, Type t, JsonSerializerOptions options) { - if (value is null) - { - writer.WriteNull(); - return; - } - - var t = ((Type)value); if (t.Assembly == typeof(string).Assembly) - writer.WriteValue(t.FullName); + writer.WriteStringValue(t.FullName); else - writer.WriteValue($"{t.FullName}, {t.Assembly.GetName().Name}"); + writer.WriteStringValue($"{t.FullName}, {t.Assembly.GetName().Name}"); } } - public class DotvvmTypeDescriptorJsonConverter : JsonConverter + public class DotvvmTypeDescriptorJsonConverter : JsonConverter + where T: ITypeDescriptor { - public override bool CanConvert(Type objectType) => typeof(ITypeDescriptor).IsAssignableFrom(objectType); - - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.Value is string name) + if (reader.TokenType == JsonTokenType.String) { - return new ResolvedTypeDescriptor(Type.GetType(name) ?? throw new Exception($"Cannot find type {name}.")); + var name = reader.GetString()!; + ITypeDescriptor result = new ResolvedTypeDescriptor(Type.GetType(name) ?? throw new Exception($"Cannot find type {name}.")); + if (result is T t) + return t; + else throw new NotSupportedException($"Cannot deserialize {typeToConvert}"); } else throw new NotSupportedException(); } - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, T t, JsonSerializerOptions options) { - if (value is null) - { - writer.WriteNull(); - return; - } - var t = ((ITypeDescriptor)value); var coreAssembly = typeof(string).Assembly.GetName().Name; var assembly = t.Assembly?.Split(new char[] { ',' }, 2)[0]; if (assembly is null || assembly == coreAssembly) - writer.WriteValue(t.FullName); + writer.WriteStringValue(t.FullName); else - writer.WriteValue($"{t.FullName}, {assembly}"); + writer.WriteStringValue($"{t.FullName}, {assembly}"); } } - public class DotvvmPropertyJsonConverter : JsonConverter + public class DotvvmPropertyJsonConverter : JsonConverter { - public override bool CanConvert(Type objectType) => - typeof(IPropertyDescriptor).IsAssignableFrom(objectType) || typeof(IPropertyGroupDescriptor).IsAssignableFrom(objectType); - public override bool CanRead => false; - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => + public override IControlAttributeDescriptor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, IControlAttributeDescriptor value, JsonSerializerOptions options) { - if (value is null) + writer.WriteStringValue(value.ToString()); + } + } + + public class DataContextChangeAttributeConverter : JsonConverter + { + public override DataContextChangeAttribute? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); + public override void Write(Utf8JsonWriter writer, DataContextChangeAttribute attribute, JsonSerializerOptions options) + { + WriteObjectReflction(writer, attribute, options); + } + + internal static void WriteObjectReflction(Utf8JsonWriter writer, object attribute, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString("$type", attribute.GetType().ToString()); + var properties = attribute.GetType().GetProperties(); + foreach (var prop in properties) { - writer.WriteNull(); - return; + if (prop.IsDefined(typeof(JsonIgnoreAttribute)) || prop.Name == "TypeId") + continue; + + writer.WritePropertyName(prop.Name); + + JsonSerializer.Serialize(writer, prop.GetValue(attribute), options); } - writer.WriteValue(value.ToString()); + writer.WriteEndObject(); + } + } + + public class DataContextManipulationAttributeConverter : JsonConverter + { + public override DataContextStackManipulationAttribute Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); + public override void Write(Utf8JsonWriter writer, DataContextStackManipulationAttribute value, JsonSerializerOptions options) + { + DataContextChangeAttributeConverter.WriteObjectReflction(writer, value, options); } } } diff --git a/src/Framework/Framework/ResourceManagement/ResourceBase.cs b/src/Framework/Framework/ResourceManagement/ResourceBase.cs index 341cd3623e..acba87839e 100644 --- a/src/Framework/Framework/ResourceManagement/ResourceBase.cs +++ b/src/Framework/Framework/ResourceManagement/ResourceBase.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Net; using DotVVM.Framework.Compilation.Parser; -using Newtonsoft.Json; using DotVVM.Framework.Controls; using DotVVM.Framework.Hosting; diff --git a/src/Framework/Framework/ResourceManagement/ResourceRepositoryJsonConverter.cs b/src/Framework/Framework/ResourceManagement/ResourceRepositoryJsonConverter.cs index 894a5b9d11..ef60e388a9 100644 --- a/src/Framework/Framework/ResourceManagement/ResourceRepositoryJsonConverter.cs +++ b/src/Framework/Framework/ResourceManagement/ResourceRepositoryJsonConverter.cs @@ -1,5 +1,3 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -12,10 +10,12 @@ using DotVVM.Framework.Utils; using DotVVM.Framework.Controls; using DotVVM.Framework.Hosting; +using System.Text.Json.Serialization; +using System.Text.Json; namespace DotVVM.Framework.ResourceManagement { - public class ResourceRepositoryJsonConverter : JsonConverter + public class ResourceRepositoryJsonConverter : JsonConverter { public static Type? UnknownResourceType = null; static (string name, Type type)[] resourceTypeAliases = new [] { @@ -29,66 +29,57 @@ public override bool CanConvert(Type objectType) return objectType == typeof(DotvvmResourceRepository); } - public IResource? TryParseOldResourceFormat(JObject jobj, Type resourceType) + public override DotvvmResourceRepository? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return null; - } - - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - var jobj = JObject.Load(reader); - var repo = existingValue as DotvvmResourceRepository ?? new DotvvmResourceRepository(); - foreach (var prop in jobj) - { - if (resourceTypeAliases.FirstOrDefault(x => x.name == prop.Key) is var r && r.type != null) - { - DeserializeResources((JObject)prop.Value.NotNull(), r.type, serializer, repo); - } - else if (CompiledAssemblyCache.Instance!.FindType(prop.Key) is Type resourceType) - { - DeserializeResources((JObject)prop.Value.NotNull(), resourceType, serializer, repo); - } - else if (UnknownResourceType != null) - { - DeserializeResources((JObject)prop.Value.NotNull(), UnknownResourceType, serializer, repo); - } - else - throw new NotSupportedException(string.Format("resource collection name {0} is not supported", prop.Key)); - } - return repo; + throw new NotImplementedException(); + // var jobj = JObject.Load(reader); + // var repo = existingValue as DotvvmResourceRepository ?? new DotvvmResourceRepository(); + // foreach (var prop in jobj) + // { + // if (resourceTypeAliases.FirstOrDefault(x => x.name == prop.Key) is var r && r.type != null) + // { + // DeserializeResources((JObject)prop.Value.NotNull(), r.type, serializer, repo); + // } + // else if (CompiledAssemblyCache.Instance!.FindType(prop.Key) is Type resourceType) + // { + // DeserializeResources((JObject)prop.Value.NotNull(), resourceType, serializer, repo); + // } + // else if (UnknownResourceType != null) + // { + // DeserializeResources((JObject)prop.Value.NotNull(), UnknownResourceType, serializer, repo); + // } + // else + // throw new NotSupportedException(string.Format("resource collection name {0} is not supported", prop.Key)); + // } + // return repo; } - void DeserializeResources(JObject jobj, Type resourceType, JsonSerializer serializer, DotvvmResourceRepository repo) - { - foreach (var resObj in jobj) - { - try - { - var resource = (IResource)serializer.Deserialize(resObj.Value!.CreateReader(), resourceType).NotNull(); - if (resource is LinkResourceBase linkResource) - { - if (linkResource.Location == null) - { - linkResource.Location = new UnknownResourceLocation(); - } - } + // void DeserializeResources(JObject jobj, Type resourceType, JsonSerializer serializer, DotvvmResourceRepository repo) + // { + // foreach (var resObj in jobj) + // { + // try + // { + // var resource = (IResource)serializer.Deserialize(resObj.Value!.CreateReader(), resourceType).NotNull(); + // if (resource is LinkResourceBase linkResource) + // { + // if (linkResource.Location == null) + // { + // linkResource.Location = new UnknownResourceLocation(); + // } + // } - repo.Register(resObj.Key, resource); - } - catch (Exception ex) - { - repo.Register(resObj.Key, new DeserializationErrorResource(ex, resObj.Value)); - } - } - } + // repo.Register(resObj.Key, resource); + // } + // catch (Exception ex) + // { + // repo.Register(resObj.Key, new DeserializationErrorResource(ex, resObj.Value)); + // } + // } + // } - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, DotvvmResourceRepository value, JsonSerializerOptions options) { - if (value == null) - { - writer.WriteNull(); - return; - } writer.WriteStartObject(); var resources = value as DotvvmResourceRepository ?? throw new NotSupportedException(); foreach (var (name, group) in ( @@ -105,28 +96,29 @@ orderby name foreach (var resource in group) { writer.WritePropertyName(resource.Key); - serializer.Serialize(writer, resource.Value); + JsonSerializer.Serialize(writer, resource.Value, resource.Value.GetType(), options); } writer.WriteEndObject(); } writer.WriteEndObject(); } - public class DeserializationErrorResource : ResourceBase - { - public Exception Error { get; } - public JToken? Json { get; set; } - public DeserializationErrorResource(Exception error, JToken? json) : base(ResourceRenderPosition.Head) - { - this.Error = error; - this.Json = json; - } - public override void Render(IHtmlWriter writer, IDotvvmRequestContext context, string resourceName) - { - throw new NotSupportedException($"Resource could not be deserialized from '{(Json is null ? "null" : Json.ToString())}': \n{Error}"); - } - } + // public class DeserializationErrorResource : ResourceBase + // { + // public Exception Error { get; } + // public JToken? Json { get; set; } + // public DeserializationErrorResource(Exception error, JToken? json) : base(ResourceRenderPosition.Head) + // { + // this.Error = error; + // this.Json = json; + // } + + // public override void Render(IHtmlWriter writer, IDotvvmRequestContext context, string resourceName) + // { + // throw new NotSupportedException($"Resource could not be deserialized from '{(Json is null ? "null" : Json.ToString())}': \n{Error}"); + // } + // } } internal class UnknownResourceLocation : IResourceLocation diff --git a/src/Framework/Framework/ResourceManagement/ScriptResource.cs b/src/Framework/Framework/ResourceManagement/ScriptResource.cs index dec886f26c..cfbb32693e 100644 --- a/src/Framework/Framework/ResourceManagement/ScriptResource.cs +++ b/src/Framework/Framework/ResourceManagement/ScriptResource.cs @@ -4,7 +4,6 @@ using System.Linq; using DotVVM.Framework.Controls; using DotVVM.Framework.Hosting; -using Newtonsoft.Json; namespace DotVVM.Framework.ResourceManagement { @@ -59,7 +58,7 @@ protected override void RenderFallbackLoadingScript(IHtmlWriter writer, IDotvvmR if (!string.IsNullOrEmpty(link)) { - var script = JsonConvert.ToString(link, '\'').Replace("<", "\\u003c"); + var script = KnockoutHelper.MakeStringLiteral(link); var code = GetLoadingScript(javascriptCondition, script); if (Defer) { diff --git a/src/Framework/Framework/ResourceManagement/ViewModuleImportResource.cs b/src/Framework/Framework/ResourceManagement/ViewModuleImportResource.cs index 6e2235e696..11b7668ca8 100644 --- a/src/Framework/Framework/ResourceManagement/ViewModuleImportResource.cs +++ b/src/Framework/Framework/ResourceManagement/ViewModuleImportResource.cs @@ -4,7 +4,6 @@ using System.Text; using DotVVM.Framework.Controls; using DotVVM.Framework.Hosting; -using Newtonsoft.Json; namespace DotVVM.Framework.ResourceManagement { @@ -29,7 +28,7 @@ public ViewModuleImportResource(string[] referencedModules, string name, string[ this.ResourceName = name; this.Dependencies = new string[] { "dotvvm" }.Concat(dependencies).ToArray(); - this.registrationScript = $"dotvvm.viewModules.registerMany({{{string.Join(", ", this.ReferencedModules.Select((m, i) => JsonConvert.ToString(m, '\'', StringEscapeHandling.EscapeHtml) + ": m" + i))}}});"; + this.registrationScript = $"dotvvm.viewModules.registerMany({{{string.Join(", ", this.ReferencedModules.Select((m, i) => KnockoutHelper.MakeStringLiteral(m) + ": m" + i))}}});"; } public void Render(IHtmlWriter writer, IDotvvmRequestContext context, string resourceName) @@ -54,7 +53,7 @@ public void Render(IHtmlWriter writer, IDotvvmRequestContext context, string res writer.WriteUnencodedText("import * as m"); writer.WriteUnencodedText(i.ToString()); writer.WriteUnencodedText(" from "); - writer.WriteUnencodedText(JsonConvert.ToString(location, '\'', StringEscapeHandling.EscapeHtml)); + writer.WriteUnencodedText(KnockoutHelper.MakeStringLiteral(location)); writer.WriteUnencodedText(";"); i += 1; diff --git a/src/Framework/Framework/ResourceManagement/ViewModuleInitResource.cs b/src/Framework/Framework/ResourceManagement/ViewModuleInitResource.cs index 8e97077f08..c70426beb3 100644 --- a/src/Framework/Framework/ResourceManagement/ViewModuleInitResource.cs +++ b/src/Framework/Framework/ResourceManagement/ViewModuleInitResource.cs @@ -3,7 +3,6 @@ using System.Linq; using DotVVM.Framework.Controls; using DotVVM.Framework.Hosting; -using Newtonsoft.Json; namespace DotVVM.Framework.ResourceManagement { diff --git a/src/Framework/Framework/Resources/Scripts/postback/updater.ts b/src/Framework/Framework/Resources/Scripts/postback/updater.ts index 37a65d6e3b..812dae6c31 100644 --- a/src/Framework/Framework/Resources/Scripts/postback/updater.ts +++ b/src/Framework/Framework/Resources/Scripts/postback/updater.ts @@ -7,7 +7,7 @@ import { defer } from '../utils/promise'; const diffEqual = {} export function cleanUpdatedControls(resultObject: any, updatedControls: any = {}) { - for (const id of keys(resultObject.updatedControls)) { + for (const id of keys(resultObject.updatedControls ?? {})) { const control = getElementByDotvvmId(id); if (control) { const dataContext = ko.contextFor(control); @@ -21,7 +21,7 @@ export function cleanUpdatedControls(resultObject: any, updatedControls: any = { } export function restoreUpdatedControls(resultObject: any, updatedControls: any) { - for (const id of keys(resultObject.updatedControls)) { + for (const id of keys(resultObject.updatedControls ?? {})) { const updatedControl = updatedControls[id]; if (updatedControl) { const wrapper = document.createElement(updatedControls[id].parent.tagName || "div"); diff --git a/src/Framework/Framework/Routing/RouteTableJsonConverter.cs b/src/Framework/Framework/Routing/RouteTableJsonConverter.cs index bd3e6154fe..fda515ca12 100644 --- a/src/Framework/Framework/Routing/RouteTableJsonConverter.cs +++ b/src/Framework/Framework/Routing/RouteTableJsonConverter.cs @@ -1,6 +1,4 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -9,50 +7,56 @@ using DotVVM.Framework.Hosting; using System.Diagnostics.CodeAnalysis; using DotVVM.Framework.Utils; +using System.Text.Json.Serialization; +using System.Text.Json; namespace DotVVM.Framework.Routing { - public class RouteTableJsonConverter : JsonConverter + public class RouteTableJsonConverter : JsonConverter { - public override bool CanConvert(Type objectType) => objectType == typeof(DotvvmRouteTable); - - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + public override DotvvmRouteTable Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var rt = existingValue as DotvvmRouteTable; - if (rt == null) return null; - foreach (var prop in (JObject)JObject.ReadFrom(reader)) - { - var route = (JObject)prop.Value.NotNull(); - try - { - rt.Add(prop.Key, route["url"].NotNull("route.url is required").Value(), (route["virtualPath"]?.Value()).NotNull("route.virtualPath is required"), route["defaultValues"]?.ToObject>()); - } - catch (Exception error) - { - rt.Add(prop.Key, new ErrorRoute(route["url"]?.Value(), route["virtualPath"]?.Value(), prop.Key, route["defaultValues"]?.ToObject>(), error)); - } - } - return rt; - } + throw new NotImplementedException(); + // var rt = new DotvvmRouteTable(); + // if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException("Expected StartObject"); + // reader.Read(); - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => WriteJson(writer, (DotvvmRouteTable?)value, serializer); + // while (reader.TokenType == JsonTokenType.PropertyName) + // { + // var routeName = reader.GetString(); + // reader.Read(); + // var route = (JObject)prop.Value.NotNull(); + // try + // { + // rt.Add(prop.Key, route["url"].NotNull("route.url is required").Value(), (route["virtualPath"]?.Value()).NotNull("route.virtualPath is required"), route["defaultValues"]?.ToObject>()); + // } + // catch (Exception error) + // { + // rt.Add(prop.Key, new ErrorRoute(route["url"]?.Value(), route["virtualPath"]?.Value(), prop.Key, route["defaultValues"]?.ToObject>(), error)); + // } + // } + // return rt; + } - public void WriteJson(JsonWriter writer, DotvvmRouteTable? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, DotvvmRouteTable value, JsonSerializerOptions options) { - if (value == null) - { - writer.WriteNull(); - return; - } writer.WriteStartObject(); foreach (var route in value) { - writer.WritePropertyName(route.RouteName); - new JObject() { - ["url"] = route.Url, - ["virtualPath"] = route.VirtualPath, - ["defaultValues"] = JObject.FromObject(route.DefaultValues) - }.WriteTo(writer); + writer.WriteStartObject(route.RouteName); + writer.WriteString("url", route.Url); + writer.WriteString("virtualPath", route.VirtualPath); + if (route.DefaultValues is not null) + { + writer.WriteStartObject("defaultValues"); + foreach (var (paramName, defaultValue) in route.DefaultValues) + { + writer.WritePropertyName(paramName); + JsonSerializer.Serialize(writer, defaultValue, options); + } + writer.WriteEndObject(); + } + writer.WriteEndObject(); } writer.WriteEndObject(); } diff --git a/src/Framework/Framework/Runtime/DefaultOutputRenderer.cs b/src/Framework/Framework/Runtime/DefaultOutputRenderer.cs index a9d1a5faa7..76ae01c8b9 100644 --- a/src/Framework/Framework/Runtime/DefaultOutputRenderer.cs +++ b/src/Framework/Framework/Runtime/DefaultOutputRenderer.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Net; using System.Threading.Tasks; -using Newtonsoft.Json; using DotVVM.Framework.Controls; using DotVVM.Framework.Controls.Infrastructure; using DotVVM.Framework.Hosting; @@ -82,16 +81,15 @@ public virtual IEnumerable<(string name, string html)> RenderPostbackUpdatedCont } - public virtual async Task WriteViewModelResponse(IDotvvmRequestContext context, DotvvmView view) + public virtual async Task WriteViewModelResponse(IDotvvmRequestContext context, DotvvmView view, string serializedViewModel) { // return the response context.HttpContext.Response.ContentType = "application/json; charset=utf-8"; SetCacheHeaders(context.HttpContext); - var serializedViewModel = context.GetSerializedViewModel(); await context.HttpContext.Response.WriteAsync(serializedViewModel); } - public virtual async Task WriteStaticCommandResponse(IDotvvmRequestContext context, string json) + public virtual async Task WriteStaticCommandResponse(IDotvvmRequestContext context, ReadOnlyMemory json) { context.HttpContext.Response.ContentType = "application/json; charset=utf-8"; SetCacheHeaders(context.HttpContext); @@ -105,6 +103,13 @@ public virtual async Task RenderPlainJsonResponse(IHttpContext context, string j SetCacheHeaders(context); await context.Response.WriteAsync(json); } + public virtual async Task RenderPlainJsonResponse(IHttpContext context, ReadOnlyMemory json) + { + context.Response.StatusCode = (int)HttpStatusCode.OK; + context.Response.ContentType = "application/json; charset=utf-8"; + SetCacheHeaders(context); + await context.Response.WriteAsync(json); + } public virtual async Task RenderHtmlResponse(IHttpContext context, string html) { diff --git a/src/Framework/Framework/Runtime/IOutputRenderer.cs b/src/Framework/Framework/Runtime/IOutputRenderer.cs index 619475241c..9b3b9b21d0 100644 --- a/src/Framework/Framework/Runtime/IOutputRenderer.cs +++ b/src/Framework/Framework/Runtime/IOutputRenderer.cs @@ -12,11 +12,12 @@ public interface IOutputRenderer { Task WriteHtmlResponse(IDotvvmRequestContext context, DotvvmView view); - Task WriteViewModelResponse(IDotvvmRequestContext context, DotvvmView view); + Task WriteViewModelResponse(IDotvvmRequestContext context, DotvvmView view, string viewModel); - Task WriteStaticCommandResponse(IDotvvmRequestContext context, string json); + Task WriteStaticCommandResponse(IDotvvmRequestContext context, ReadOnlyMemory json); Task RenderPlainJsonResponse(IHttpContext context, string json); + Task RenderPlainJsonResponse(IHttpContext context, ReadOnlyMemory json); Task RenderHtmlResponse(IHttpContext context, string html); diff --git a/src/Framework/Framework/Security/FakeViewModelProtector.cs b/src/Framework/Framework/Security/FakeViewModelProtector.cs index 2d6611cfcc..80076dc3f7 100644 --- a/src/Framework/Framework/Security/FakeViewModelProtector.cs +++ b/src/Framework/Framework/Security/FakeViewModelProtector.cs @@ -1,28 +1,29 @@  +using System; using DotVVM.Framework.Hosting; namespace DotVVM.Framework.Security { internal class FakeViewModelProtector : IViewModelProtector { - public string Protect(string serializedData, IDotvvmRequestContext context) + public byte[] Protect(byte[] serializedData, IDotvvmRequestContext context) { - return ""; + return []; } public byte[] Protect(byte[] plaintextData, params string[] purposes) { - return new byte[0]; + return []; } - public string Unprotect(string protectedData, IDotvvmRequestContext context) + public byte[] Unprotect(byte[] protectedData, IDotvvmRequestContext context) { - return ""; + return []; } public byte[] Unprotect(byte[] protectedData, params string[] purposes) { - return new byte[0]; + return []; } } } diff --git a/src/Framework/Framework/Security/IViewModelProtector.cs b/src/Framework/Framework/Security/IViewModelProtector.cs index 1f0b87ead6..00e04d33a1 100644 --- a/src/Framework/Framework/Security/IViewModelProtector.cs +++ b/src/Framework/Framework/Security/IViewModelProtector.cs @@ -10,10 +10,10 @@ namespace DotVVM.Framework.Security public interface IViewModelProtector { - string Protect(string serializedData, IDotvvmRequestContext context); + byte[] Protect(byte[] serializedData, IDotvvmRequestContext context); byte[] Protect(byte[] plaintextData, params string[] purposes); - string Unprotect(string protectedData, IDotvvmRequestContext context); + byte[] Unprotect(byte[] protectedData, IDotvvmRequestContext context); byte[] Unprotect(byte[] protectedData, params string[] purposes); } diff --git a/src/Framework/Framework/Storage/FileSystemReturnedFileStorage.cs b/src/Framework/Framework/Storage/FileSystemReturnedFileStorage.cs index 3ab0ab2d93..d482296468 100644 --- a/src/Framework/Framework/Storage/FileSystemReturnedFileStorage.cs +++ b/src/Framework/Framework/Storage/FileSystemReturnedFileStorage.cs @@ -5,13 +5,13 @@ using System.Security; using System.Security.Cryptography; using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using DotVVM.Core.Storage; using DotVVM.Framework.Configuration; using DotVVM.Framework.Hosting; using DotVVM.Framework.Utils; -using Newtonsoft.Json; namespace DotVVM.Framework.Storage { @@ -88,13 +88,11 @@ public async Task StoreFileAsync(Stream stream, ReturnedFileMetadata metad private async Task StoreMetadata(Guid id, ReturnedFileMetadata metadata) { var metadataFilePath = GetMetadataFilePath(id); - var settings = DefaultSerializerSettingsProvider.Instance.Settings; -#if DotNetCore - await File.WriteAllTextAsync( -#else - File.WriteAllText( -#endif - metadataFilePath, JsonConvert.SerializeObject(metadata, settings), Encoding.UTF8); + var settings = DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe; + using (var file = File.Create(metadataFilePath)) + { + await JsonSerializer.SerializeAsync(file, metadata, settings); + } } private string GetDataFilePath(Guid id) @@ -109,9 +107,12 @@ private string GetMetadataFilePath(Guid id) public Task GetFileAsync(Guid id) { - var metadataJson = File.ReadAllText(GetMetadataFilePath(id), Encoding.UTF8); - var settings = DefaultSerializerSettingsProvider.Instance.Settings; - var metadata = JsonConvert.DeserializeObject(metadataJson, settings).NotNull(); + var settings = DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe; + ReturnedFileMetadata metadata; + using (var metadataFile = File.OpenRead(GetMetadataFilePath(id))) + { + metadata = JsonSerializer.Deserialize(metadataFile, settings).NotNull(); + } var stream = new FileStream(GetDataFilePath(id), FileMode.Open, FileAccess.Read); diff --git a/src/Framework/Framework/Testing/TestDotvvmRequestContext.cs b/src/Framework/Framework/Testing/TestDotvvmRequestContext.cs index 1ad27706e5..1809f936a2 100644 --- a/src/Framework/Framework/Testing/TestDotvvmRequestContext.cs +++ b/src/Framework/Framework/Testing/TestDotvvmRequestContext.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Text.Json; using System.Threading; using DotVVM.Framework.Configuration; using DotVVM.Framework.Controls.Infrastructure; @@ -12,7 +13,6 @@ using DotVVM.Framework.Routing; using DotVVM.Framework.Runtime.Tracing; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json.Linq; namespace DotVVM.Framework.Testing { @@ -20,9 +20,8 @@ public class TestDotvvmRequestContext : IDotvvmRequestContext { public IHttpContext HttpContext { get; set; } public string CsrfToken { get; set; } - public JObject ReceivedViewModelJson { get; set; } + public JsonDocument ReceivedViewModelJson { get; set; } public object ViewModel { get; set; } - public JObject ViewModelJson { get; set; } public DotvvmConfiguration Configuration { get; set; } public IDotvvmPresenter Presenter { get; set; } public RouteBase Route { get; set; } @@ -55,7 +54,6 @@ public IServiceProvider Services public CustomResponsePropertiesManager CustomResponseProperties { get; } = new CustomResponsePropertiesManager(); - public TestDotvvmRequestContext() { } public TestDotvvmRequestContext(IServiceProvider services) { diff --git a/src/Framework/Framework/Testing/TestHttpResponse.cs b/src/Framework/Framework/Testing/TestHttpResponse.cs index be5315a07a..cfe0309e54 100644 --- a/src/Framework/Framework/Testing/TestHttpResponse.cs +++ b/src/Framework/Framework/Testing/TestHttpResponse.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using DotVVM.Framework.Hosting; +using DotVVM.Framework.Utils; namespace DotVVM.Framework.Testing { @@ -33,19 +34,28 @@ Stream IHttpResponse.Body set => throw new NotSupportedException(); } - public void Write(string text) => Write(Encoding.UTF8.GetBytes(text)); + public void Write(string text) => Write(StringUtils.Utf8.GetBytes(text)); + public void Write(ReadOnlyMemory text) => Write(StringUtils.Utf8.GetBytes(text.ToArray())); + public void Write(ReadOnlyMemory data) => Body.Write(data.Span); public void Write(byte[] data) => Body.Write(data, 0, data.Length); public void Write(byte[] data, int offset, int count) => Body.Write(data, offset, count); public Task WriteAsync(string text) => WriteAsync(text, default); - public async Task WriteAsync(string text, CancellationToken token) + public Task WriteAsync(ReadOnlyMemory text, CancellationToken token = default) => + WriteAsync(StringUtils.Utf8.GetBytes(text.ToArray()), token); + + public async Task WriteAsync(string text, CancellationToken token) => + await WriteAsync(StringUtils.Utf8.GetBytes(text), token); + + public async Task WriteAsync(ReadOnlyMemory data, CancellationToken token = default) { if (AsyncWriteDelay > TimeSpan.Zero) await Task.Delay(AsyncWriteDelay, token); - Write(text); + Write(data); } + } } diff --git a/src/Framework/Framework/Utils/ExpressionUtils.cs b/src/Framework/Framework/Utils/ExpressionUtils.cs index 47783e3ea1..d2202ac20d 100644 --- a/src/Framework/Framework/Utils/ExpressionUtils.cs +++ b/src/Framework/Framework/Utils/ExpressionUtils.cs @@ -22,6 +22,53 @@ public static Expression While(Expression condition, Expression body) Expression.IfThenElse(condition, body, Expression.Goto(brkLabel)), brkLabel); } + public static Expression Foreach(Expression collection, ParameterExpression item, Expression body) + { + var genericType = collection.Type.IsGenericType ? collection.Type.GetGenericArguments()[0] : typeof(object); + if (collection.Type.IsArray || collection.Type == typeof(string) || genericType == typeof(Span<>) || genericType == typeof(ReadOnlySpan<>)) + return ForeachIndexer(collection, item, body); + + throw new NotImplementedException(); + } + + static Expression ForeachIndexer(Expression collection, ParameterExpression item, Expression body) + { + var block = new List(); + var vars = new List(); + ParameterExpression collectionP; + if (collection is ParameterExpression) + collectionP = (ParameterExpression)collection; + else + { + collectionP = Expression.Parameter(collection.Type); + block.Add(Expression.Assign(collectionP, collection)); + vars.Add(collectionP); + } + var indexP = Expression.Parameter(typeof(int)); + vars.Add(indexP); + block.Add(Expression.Assign(indexP, Expression.Constant(0))); + + var loop = While( + Expression.LessThan(indexP, Expression.Property(collectionP, "Length")), + Expression.Block( + new[] { item }, + Expression.Assign(item, Index(collectionP, indexP)), + body, + Expression.PostIncrementAssign(indexP) + ) + ); + block.Add(loop); + return Expression.Block(vars, block); + } + + static Expression Index(Expression list, Expression index) + { + if (list.Type.IsArray) + return Expression.ArrayIndex(list, index); + else + return Expression.Property(list, list.Type == typeof(string) ? "Chars" : "Item", index); + } + public static Expression ConvertToObject(this Expression expr) { if (expr.Type == typeof(object)) return expr; diff --git a/src/Framework/Framework/Utils/FunctionalExtensions.cs b/src/Framework/Framework/Utils/FunctionalExtensions.cs index 7dd383c860..d8fec648ed 100644 --- a/src/Framework/Framework/Utils/FunctionalExtensions.cs +++ b/src/Framework/Framework/Utils/FunctionalExtensions.cs @@ -104,6 +104,10 @@ public static T NotNull([NotNull] this T? target, string message = "Unexpecte where T : class => target ?? throw new Exception(message); + // public static T NotNull([NotNull] this T? target, string message = "Unexpected null value.") + // where T : struct => + // target ?? throw new Exception(message); + public static SortedDictionary ToSorted(this IDictionary d, IComparer? c = null) where K: notnull => new(d, c ?? Comparer.Default); diff --git a/src/Framework/Framework/Utils/JsonDiffWriter.cs b/src/Framework/Framework/Utils/JsonDiffWriter.cs new file mode 100644 index 0000000000..4578d82a32 --- /dev/null +++ b/src/Framework/Framework/Utils/JsonDiffWriter.cs @@ -0,0 +1,273 @@ +// using System.Linq; +// using System; +// using System.Text.Json; +// using System.Diagnostics; +// using System.Buffers; + +// namespace DotVVM.Framework.Utils +// { +// ref struct JsonDiffWriter +// { +// static void AssertToken(ref Utf8JsonReader reader, JsonTokenType expected) +// { +// if (reader.TokenType != expected) +// throw new JsonException($"Expected {expected} but got {reader.TokenType}."); +// } +// static void CopyValue(ref Utf8JsonReader reader, Utf8JsonWriter writer) +// { +// Debug.Assert(reader.TokenType != JsonTokenType.PropertyName); + +// if (reader.TokenType is not JsonTokenType.StartArray and not JsonTokenType.StartObject) +// { +// if (reader.HasValueSequence) +// writer.WriteRawValue(reader.ValueSequence); +// else +// writer.WriteRawValue(reader.ValueSpan); + +// return; +// } + +// var depth = reader.CurrentDepth; +// while (reader.CurrentDepth >= depth) +// { +// switch (reader.TokenType) +// { +// case JsonTokenType.False: +// case JsonTokenType.True: +// case JsonTokenType.Null: +// case JsonTokenType.String: +// case JsonTokenType.Number: { +// if (reader.HasValueSequence) +// writer.WriteRawValue(reader.ValueSequence); +// else +// writer.WriteRawValue(reader.ValueSpan); +// break; +// } +// case JsonTokenType.PropertyName: { +// var length = reader.HasValueSequence ? reader.ValueSequence.Length : reader.ValueSpan.Length; +// Span buffer = length <= 1024 ? stackalloc byte[(int)length] : new byte[length]; +// var realLength = reader.CopyString(buffer); +// writer.WritePropertyName(buffer.Slice(0, realLength)); +// break; +// } +// case JsonTokenType.StartArray: { +// writer.WriteStartArray(); +// break; +// } +// case JsonTokenType.EndArray: { +// writer.WriteEndArray(); +// break; +// } +// case JsonTokenType.StartObject: { +// writer.WriteStartObject(); +// break; +// } +// case JsonTokenType.EndObject: { +// writer.WriteEndObject(); +// break; +// } +// default: { +// throw new JsonException($"Unexpected token {reader.TokenType}."); +// } +// } +// reader.Read(); +// } +// } + +// public delegate bool? IncludePropertyDelegate(string typeId, ReadOnlySpan propertyName); +// private readonly IncludePropertyDelegate? includePropertyOverride; +// private byte[]? nameBufferRented; +// private Span nameBuffer; +// private int nameBufferPosition; +// private RefList lazyWriteStack; +// private readonly Utf8JsonWriter writer; + +// private JsonDiffWriter( +// IncludePropertyDelegate? includePropertyOverride, +// Span nameBuffer, +// Utf8JsonWriter writer +// ) +// { +// this.includePropertyOverride = includePropertyOverride; +// this.nameBuffer = nameBuffer; +// this.writer = writer; +// } + +// Span ReadName(ref Utf8JsonReader reader) +// { +// var length = reader.CopyString(nameBuffer.Slice(nameBufferPosition)); +// if (length == 0) +// throw new JsonException("Empty property name."); +// if (length < nameBuffer.Length) +// { +// return nameBuffer.Slice(nameBufferPosition, length); +// } +// var newBuffer = ArrayPool.Shared.Rent(length); +// nameBuffer.CopyTo(newBuffer); +// if (nameBufferRented is {}) +// ArrayPool.Shared.Return(nameBufferRented); +// nameBuffer = newBuffer; +// nameBufferRented = newBuffer; +// return ReadName(ref reader); +// } + +// void WriteoutLazyStack() +// { +// // foreach (var x in lazyWriteStack.AsSpan()) +// // { +// // if (x <= 0) +// // { +// // writer.Write TODO +// // } +// // } +// } + +// void AddPropertyToStack(ReadOnlySpan propertyName) +// { +// nameBufferPosition = nameBufferPosition + propertyName.Length; +// lazyWriteStack.Add(nameBufferPosition); +// } + +// void AddArrayToStack() +// { +// lazyWriteStack.Add(0); +// } + +// void DiffObject(in JsonElement source, ref Utf8JsonReader target) +// { +// AssertToken(ref target, JsonTokenType.StartObject); +// if (source.ValueKind != JsonValueKind.Object) +// { +// CopyValue(ref target, writer); +// return; +// } + +// string? typeId = null; +// target.Read(); +// // var typeId = target.TryGetValue("$type", out var t) ? t.Value() : null; + +// while (target.TokenType != JsonTokenType.EndObject) +// { +// AssertToken(ref target, JsonTokenType.PropertyName); +// var propertyName = ReadName(ref target); + +// if (propertyName[0] == '$') +// { +// if (propertyName.SequenceEqual("$type"u8)) +// { +// typeId = target.GetString(); +// } +// } +// else +// { +// if (typeId is {} && includePropertyOverride is {}) +// { +// var include = includePropertyOverride(typeId, propertyName); +// if (include == true) +// { +// writer.WritePropertyName(propertyName); +// CopyValue(ref target, writer); +// continue; +// } +// else if (include == false) +// { +// continue; +// } +// } +// } + +// if (!source.TryGetProperty(propertyName, out var sourceValue)) +// { +// writer.WritePropertyName(propertyName); +// CopyValue(ref target, writer); +// continue; +// } + +// if (sourceValue.ValueKind == JsonValueKind.Object && target.TokenType == JsonTokenType.StartObject) +// { +// writer.WritePropertyName(propertyName); +// DiffObject(sourceValue, ref target); +// } +// else if (sourceValue.ValueKind == JsonValueKind.Array && target.TokenType == JsonTokenType.StartArray) +// { +// writer.WritePropertyName(propertyName); +// DiffArray(sourceValue, ref target); +// } +// else if +// } +// foreach (var item in target) +// { +// if (sourceItem.Type == JTokenType.Array) +// { +// var sourceArr = (JArray)sourceItem; +// var subchanged = false; +// var arrDiff = Diff(sourceArr, (JArray)item.Value, out subchanged, nullOnRemoved); +// if (subchanged) +// { +// diff[item.Key] = arrDiff; +// } +// } +// else if (!JToken.DeepEquals(sourceItem, item.Value)) +// { +// diff[item.Key] = item.Value; +// } +// } + +// if (nullOnRemoved) +// { +// foreach (var item in source) +// { +// if (target[item.Key] == null) diff[item.Key] = JValue.CreateNull(); +// } +// } +// return diff; +// } + + +// ref struct RefList +// { +// Span buffer; +// T[]? rented; +// int count; + +// public RefList(Span initialCapacity) +// { +// this.buffer = initialCapacity; +// this.rented = null; +// this.count = 0; +// } + +// public void Enlarge(int newCapacity) +// { +// if (newCapacity <= buffer.Length) +// return; + +// var newBuffer = ArrayPool.Shared.Rent(Math.Max(newCapacity, buffer.Length * 2)); +// buffer.CopyTo(newBuffer); +// if (rented is {}) +// ArrayPool.Shared.Return(rented); +// rented = newBuffer; +// buffer = newBuffer; +// } + +// public void Add(T item) +// { +// if (count == buffer.Length) +// Enlarge(buffer.Length + 8); +// buffer[count++] = item; +// } + +// public void Clear() +// { +// count = 0; +// } + +// public T? LastOrDefault() => count > 0 ? buffer[count - 1] : default; + +// public ref T this[int index] => ref buffer[index]; +// public ref T Last => ref buffer[count - 1]; + +// public Span AsSpan() => buffer.Slice(0, count); +// } +// } +// } diff --git a/src/Framework/Framework/Utils/JsonUtils.cs b/src/Framework/Framework/Utils/JsonUtils.cs index 09bcbae835..0acfb4d34e 100644 --- a/src/Framework/Framework/Utils/JsonUtils.cs +++ b/src/Framework/Framework/Utils/JsonUtils.cs @@ -1,20 +1,23 @@ -using Newtonsoft.Json.Linq; using System.Linq; using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; -using Newtonsoft.Json; +using System.Text.Json.Nodes; +using System.Buffers; +using Microsoft.Extensions.Options; +using System.Linq.Expressions; +using System.Text.Json; namespace DotVVM.Framework.Utils { public static class JsonUtils { - public static JObject Diff(JObject source, JObject target, bool nullOnRemoved = false, Func<(string TypeId, string Property), bool?>? includePropertyOverride = null) + public static JsonObject Diff(JsonObject source, JsonObject target, bool nullOnRemoved = false, Func<(string TypeId, string Property), bool?>? includePropertyOverride = null) { - var typeId = target.TryGetValue("$type", out var t) ? t.Value() : null; - - var diff = new JObject(); + var typeId = target["$type"]?.GetValue(); + + var diff = new JsonObject(); foreach (var item in target) { if (typeId != null && includePropertyOverride != null && !item.Key.StartsWith("$")) @@ -22,7 +25,7 @@ public static JObject Diff(JObject source, JObject target, bool nullOnRemoved = var include = includePropertyOverride((typeId, item.Key)); if (include == true) { - diff[item.Key] = item.Value; + diff[item.Key] = item.Value?.DeepClone(); continue; } else if (include == false) @@ -31,80 +34,37 @@ public static JObject Diff(JObject source, JObject target, bool nullOnRemoved = } } - var sourceItem = source[item.Key]; - if (sourceItem == null) + if (!source.TryGetPropertyValue(item.Key, out var sourceItem)) { - if (item.Value != null) - { - diff[item.Key] = item.Value; - } + diff[item.Key] = item.Value?.DeepClone(); + continue; } - else if (sourceItem.Type == JTokenType.Date) - { - if (item.Value!.Type != JTokenType.String && item.Value.Type != JTokenType.Date) - diff[item.Key] = item.Value; - else - { - var sourceTime = sourceItem.ToObject(); - try - { - DateTime targetTime; - if (item.Value.Type == JTokenType.Date) - { - targetTime = item.Value.ToObject(); - } - else - { - var targetJson = $@"{{""Time"": ""{item.Value}""}}"; - targetTime = JObject.Parse(targetJson)["Time"]!.ToObject(); - } - if (!sourceTime.Equals(targetTime)) - { - diff[item.Key] = item.Value; - } - } - catch (FormatException) - { - diff[item.Key] = item.Value; - } - catch (JsonReaderException) - { - diff[item.Key] = item.Value; - } - } - } - else if (sourceItem.Type != item.Value!.Type) + var sourceKind = sourceItem?.GetValueKind(); + var targetKind = item.Value?.GetValueKind(); + if (sourceKind != targetKind) { - if (sourceItem.Type == JTokenType.Object || sourceItem.Type == JTokenType.Array - || item.Value.Type == JTokenType.Object || item.Value.Type == JTokenType.Array - || item.Value.ToString() != sourceItem.ToString()) - { - - diff[item.Key] = item.Value; - } + diff[item.Key] = item.Value?.DeepClone(); } - else if (sourceItem.Type == JTokenType.Object) // == item.Value.Type + else if (sourceKind == JsonValueKind.Object) { - var itemDiff = Diff((JObject)sourceItem, (JObject)item.Value, nullOnRemoved); + var itemDiff = Diff((JsonObject)sourceItem!, (JsonObject)item.Value!, nullOnRemoved); if (itemDiff.Count > 0) { diff[item.Key] = itemDiff; } } - else if (sourceItem.Type == JTokenType.Array) + else if (sourceKind == JsonValueKind.Array) { - var sourceArr = (JArray)sourceItem; - var subchanged = false; - var arrDiff = Diff(sourceArr, (JArray)item.Value, out subchanged, nullOnRemoved); + var arrDiff = Diff((JsonArray)sourceItem!, (JsonArray)item.Value!, out var subchanged, nullOnRemoved); if (subchanged) { diff[item.Key] = arrDiff; } } - else if (!JToken.DeepEquals(sourceItem, item.Value)) + else if (!JsonNode.DeepEquals(sourceItem, item.Value)) { - diff[item.Key] = item.Value; + diff[item.Key] = item.Value?.DeepClone(); } } @@ -112,84 +72,86 @@ public static JObject Diff(JObject source, JObject target, bool nullOnRemoved = { foreach (var item in source) { - if (target[item.Key] == null) diff[item.Key] = JValue.CreateNull(); + if (target[item.Key] == null) + diff[item.Key] = null; } } return diff; } - public static JArray Diff(JArray source, JArray target, out bool changed, bool nullOnRemoved = false) + + public static JsonArray Diff(JsonArray source, JsonArray target, out bool changed, bool nullOnRemoved = false) { changed = source.Count != target.Count; - var diffs = new JToken[target.Count]; + var diffs = new JsonNode?[target.Count]; var commonLen = Math.Min(diffs.Length, source.Count); for (int i = 0; i < commonLen; i++) { - if (target[i].Type == JTokenType.Object && source[i].Type == JTokenType.Object) + var targetKind = target[i]?.GetValueKind(); + var sourceKind = source[i]?.GetValueKind(); + if (targetKind == JsonValueKind.Object && sourceKind == JsonValueKind.Object) { - diffs[i] = Diff((JObject)source[i], (JObject)target[i], nullOnRemoved); - if (((JObject)diffs[i]).Count > 0) changed = true; + diffs[i] = Diff(source[i]!.AsObject(), target[i]!.AsObject(), nullOnRemoved); + if (((JsonObject)diffs[i]!).Count > 0) changed = true; } - else if (target[i].Type == JTokenType.Array && source[i].Type == JTokenType.Array) + else if (targetKind == JsonValueKind.Array && sourceKind == JsonValueKind.Array) { - var subchanged = false; - diffs[i] = Diff((JArray)source[i], (JArray)target[i], out subchanged, nullOnRemoved); + diffs[i] = Diff((JsonArray)source[i]!, (JsonArray)target[i]!, out var subchanged, nullOnRemoved); if (subchanged) changed = true; } else { - diffs[i] = target[i]; - if (!JToken.DeepEquals(source[i], target[i])) + diffs[i] = target[i]?.DeepClone(); + if (!JsonNode.DeepEquals(source[i], target[i])) changed = true; } } for (int i = commonLen; i < diffs.Length; i++) { - diffs[i] = target[i]; + diffs[i] = target[i]?.DeepClone(); changed = true; } - return new JArray(diffs); + return new JsonArray(diffs); } - private static JToken PatchItem(JToken target, JToken diff, bool removeOnNull = false) + private static JsonNode? PatchItem(JsonNode? target, JsonNode? diff, bool removeOnNull = false) { - if (target.Type == JTokenType.Object && diff.Type == JTokenType.Object) + if (target is null || diff is null) return diff?.DeepClone(); + + var targetKind = target.GetValueKind(); + var diffKind = diff.GetValueKind(); + if (targetKind == JsonValueKind.Object && diffKind == JsonValueKind.Object) { - Patch((JObject)target, (JObject)diff, removeOnNull); + Patch((JsonObject)target!, (JsonObject)diff!, removeOnNull); return target; } - else if (target.Type == JTokenType.Array && diff.Type == JTokenType.Array) + else if (targetKind == JsonValueKind.Array && diffKind == JsonValueKind.Array) { - return new JArray(((JArray)target).ZipOverhang((JArray)diff, (t, d) => PatchItem(t, d, removeOnNull)).ToArray()); + var targetArray = target!.AsArray(); + var targetItems = targetArray.ToArray(); + targetArray.Clear(); + var diffArray = diff!.AsArray(); + for (int i = 0; i < diffArray.Count; i++) + { + targetArray.Add(i >= targetItems.Length ? diffArray[i]?.DeepClone() : PatchItem(targetItems[i], diffArray[i], removeOnNull)); + } + return targetArray; } else { - return diff; + return diff.DeepClone(); } } - public static void Patch(JObject target, JObject diff, bool removeOnNull = false) + public static void Patch(JsonObject target, JsonObject diff, bool removeOnNull = false) { foreach (var prop in diff) { var val = target[prop.Key]; - if (val == null) target[prop.Key] = prop.Value; - else if (prop.Value!.Type == JTokenType.Null && removeOnNull || (prop.Value as JConstructor)?.Name == "$rm") target.Remove(prop.Key); + if (val == null) target[prop.Key] = prop.Value?.DeepClone(); + else if (prop.Value is null && removeOnNull) target.Remove(prop.Key); else target[prop.Key] = PatchItem(val, prop.Value, removeOnNull); } } - - public static IEnumerable ZipOverhang(this IEnumerable a, IEnumerable b, Func combine) - { - bool am, bm; - using (var ae = a.GetEnumerator()) using (var be = b.GetEnumerator()) - { - while ((am = ae.MoveNext()) & (bm = be.MoveNext())) - { - yield return combine(ae.Current, be.Current); - } - while (bm) { yield return be.Current; bm = be.MoveNext(); } - } - } } } diff --git a/src/Framework/Framework/Utils/MemoryUtils.cs b/src/Framework/Framework/Utils/MemoryUtils.cs new file mode 100644 index 0000000000..c9d5184da9 --- /dev/null +++ b/src/Framework/Framework/Utils/MemoryUtils.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace DotVVM.Framework.Utils +{ + static class MemoryUtils + { + public static Span ToSpan(this MemoryStream stream) => + stream.TryGetBuffer(out var buffer) ? buffer.AsSpan().Slice(0, (int)stream.Length) : stream.ToArray(); + public static Memory ToMemory(this MemoryStream stream) => + stream.TryGetBuffer(out var buffer) ? buffer.AsMemory().Slice(0, (int)stream.Length) : stream.ToArray(); + + public static ReadOnlySpan Readonly(this Span span) => span; + public static ReadOnlyMemory Readonly(this Memory span) => span; + + public static Memory ReadToMemory(this Stream stream) + { + using var buffer = new MemoryStream(); + stream.CopyTo(buffer); + return buffer.ToMemory(); + } + public static async Task> ReadToMemoryAsnc(this Stream stream) + { + using var buffer = new MemoryStream(); + await stream.CopyToAsync(buffer); + return buffer.ToMemory(); + } + } +} diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index 91d6c2aa00..fca087c58c 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -19,8 +19,6 @@ using DotVVM.Framework.Binding; using DotVVM.Framework.Configuration; using FastExpressionCompiler; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; using RecordExceptions; using System.ComponentModel; using DotVVM.Framework.Compilation; @@ -28,6 +26,8 @@ using DotVVM.Framework.ViewModel; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; +using System.Text.Json; namespace DotVVM.Framework.Utils { @@ -569,7 +569,7 @@ public static string GetTypeHash(this Type type) } else if (EnumInfo.IsFlags) { - return JsonConvert.DeserializeObject(JsonConvert.ToString(instance.Value))!; + return JsonSerializer.Deserialize(JsonSerializer.Serialize(instance.Value, DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe))!; } else { diff --git a/src/Framework/Framework/Utils/StringUtils.cs b/src/Framework/Framework/Utils/StringUtils.cs index 9ab1f3a5d7..be0c0a9a21 100644 --- a/src/Framework/Framework/Utils/StringUtils.cs +++ b/src/Framework/Framework/Utils/StringUtils.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using DotVVM.Framework.Binding; using FastExpressionCompiler; @@ -8,6 +9,7 @@ namespace DotVVM.Framework.Utils { public static class StringUtils { + public static readonly UTF8Encoding Utf8 = new UTF8Encoding(false, throwOnInvalidBytes: true); public static string LimitLength(this string source, int length, string ending = "...") { if (length < source.Length) diff --git a/src/Framework/Framework/Utils/SystemTextJsonHacks.cs b/src/Framework/Framework/Utils/SystemTextJsonHacks.cs new file mode 100644 index 0000000000..f65595e5c7 --- /dev/null +++ b/src/Framework/Framework/Utils/SystemTextJsonHacks.cs @@ -0,0 +1,69 @@ +using System; +using System.Buffers; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using DotVVM.Framework.Routing; + +namespace DotVVM.Framework.Utils +{ + static class SystemTextJsonHacks + { + public static void Populate(T obj, string input, JsonSerializerOptions options) + where T: class + { + if (obj == null) throw new ArgumentNullException(nameof(obj)); + + options = new JsonSerializerOptions(options); + options.TypeInfoResolver = new Resolver(options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver(), obj); + + var length = StringUtils.Utf8.GetByteCount(input) + 6; + var bytes = ArrayPool.Shared.Rent(length); + try + { + """{"X":"""u8.CopyTo(bytes.AsSpan().Slice(0, 5)); + StringUtils.Utf8.GetBytes(input, bytes.AsSpan(5)); + bytes[length - 1] = (byte)'}'; + var reader = new Utf8JsonReader(bytes.AsSpan().Slice(0, length)); + var result = JsonSerializer.Deserialize>(ref reader, options)?.X; + if (!object.ReferenceEquals(result, obj)) + { + throw new InvalidOperationException("The object was not populated correctly."); + } + } + finally + { + ArrayPool.Shared.Return(bytes); + } + } + + class PopulateClass + { + [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] + public T X { get; init; } = default!; + } + + class Resolver: IJsonTypeInfoResolver + { + private readonly IJsonTypeInfoResolver inner; + private readonly T populateInstance; + + public Resolver(IJsonTypeInfoResolver inner, T populateInstance) + { + this.inner = inner; + this.populateInstance = populateInstance; + } + public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) + { + var info = inner.GetTypeInfo(type, options); + if (info?.Type == typeof(PopulateClass)) + { + info.CreateObject = () => { + return new PopulateClass { X = populateInstance }; + }; + } + return info; + } + } + } +} diff --git a/src/Framework/Framework/Utils/SystemTextJsonUtils.cs b/src/Framework/Framework/Utils/SystemTextJsonUtils.cs new file mode 100644 index 0000000000..c7532279dc --- /dev/null +++ b/src/Framework/Framework/Utils/SystemTextJsonUtils.cs @@ -0,0 +1,115 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; + +namespace DotVVM.Framework.Utils +{ + static class SystemTextJsonUtils + { + /// Returns the property path to an unfinished JSON value + public static string[] GetFailurePath(ReadOnlySpan data) + { + // TODO: tests + var reader = new Utf8JsonReader(data, false, default); + var path = new Stack<(string? name, int index)>(); + var isArray = false; + int arrayIndex = 0; + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonTokenType.StartObject: + if (isArray) { + isArray = false; + path.Push((null, arrayIndex)); + } + break; + case JsonTokenType.Comment: + break; + case JsonTokenType.StartArray: + isArray = true; + arrayIndex = 0; + break; + case JsonTokenType.EndArray: + isArray = false; + break; + case JsonTokenType.True: + case JsonTokenType.False: + case JsonTokenType.Number: + case JsonTokenType.String: + case JsonTokenType.Null: + case JsonTokenType.EndObject: + if (!isArray) { + var old = path.Pop(); + if (old.name is null) { + isArray = true; + arrayIndex = old.index + 1; + } + } + else { + arrayIndex++; + } + break; + case JsonTokenType.PropertyName: + path.Push((reader.GetString()!, -1)); + break; + case JsonTokenType.None: + goto Done; + } + } + Done: + return path.Reverse().Select(n => n.name ?? n.index.ToString()).ToArray(); + } + + public static JsonElement? GetPropertyOrNull(this in JsonElement jsonObj, ReadOnlySpan name) => + jsonObj.TryGetProperty(name, out var prop) ? prop : null; + public static JsonElement? GetPropertyOrNull(this in JsonElement jsonObj, string name) => + jsonObj.TryGetProperty(name, out var prop) ? prop : null; + + public static IEnumerable EnumerateStringArray(this JsonElement json) + { + foreach (var item in json.EnumerateArray()) { + yield return item.GetString()!; + } + } + + + /// Deserializes JSON primitive values to dotnet primitives, mimicking the Newtonsoft.Json behavior to some degree + public static object DeserializeObject(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + return reader.GetString()!; + } + if (reader.TokenType == JsonTokenType.Number) + { + return reader.GetDouble(); + } + if (reader.TokenType == JsonTokenType.True) + { + return true; + } + if (reader.TokenType == JsonTokenType.False) + { + return false; + } + if (reader.TokenType == JsonTokenType.Null) + { + return null!; + } + return JsonSerializer.Deserialize(ref reader, options)!; + } + + public static T Deserialize(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + if (typeof(T) == typeof(object)) + { + return (T)DeserializeObject(ref reader, options)!; + } + return JsonSerializer.Deserialize(ref reader, options)!; + } + } +} diff --git a/src/Framework/Framework/ViewModel/DotvvmViewModelBase.cs b/src/Framework/Framework/ViewModel/DotvvmViewModelBase.cs index d9e1fe415f..8ef9a15927 100644 --- a/src/Framework/Framework/ViewModel/DotvvmViewModelBase.cs +++ b/src/Framework/Framework/ViewModel/DotvvmViewModelBase.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading.Tasks; using DotVVM.Framework.Compilation.Javascript; using DotVVM.Framework.Hosting; using DotVVM.Framework.ViewModel.Validation; -using Newtonsoft.Json; namespace DotVVM.Framework.ViewModel { diff --git a/src/Framework/Framework/ViewModel/Serialization/ClientExtenderInfo.cs b/src/Framework/Framework/ViewModel/Serialization/ClientExtenderInfo.cs index f9cbc1913d..54932daef0 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ClientExtenderInfo.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ClientExtenderInfo.cs @@ -1,5 +1,5 @@ using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace DotVVM.Framework.ViewModel.Serialization { @@ -11,9 +11,9 @@ public ClientExtenderInfo(string name, object? parameter) Parameter = parameter; } - [JsonProperty("name")] + [JsonPropertyName("name")] public string Name { get; set; } - [JsonProperty("parameter")] + [JsonPropertyName("parameter")] public object? Parameter { get; set; } } } diff --git a/src/Framework/Framework/ViewModel/Serialization/ClientTypeId.cs b/src/Framework/Framework/ViewModel/Serialization/ClientTypeId.cs new file mode 100644 index 0000000000..e08a1ac4c0 --- /dev/null +++ b/src/Framework/Framework/ViewModel/Serialization/ClientTypeId.cs @@ -0,0 +1,94 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using DotVVM.Framework.Utils; + +namespace DotVVM.Framework.ViewModel.Serialization +{ + [StructLayout(LayoutKind.Explicit)] + readonly struct ClientTypeId: IEquatable, IComparable + { + // first byte + [FieldOffset(0)] + readonly ulong a; + [FieldOffset(8)] + readonly ulong b; + + [FieldOffset(0)] + readonly byte controlByte; + [FieldOffset(1)] + readonly byte dataByte1; + private ClientTypeId(ulong a, ulong b) + { + this.a = a; + this.b = b; + } + + private ClientTypeId(bool isHash, ReadOnlySpan data) + { + if (data.Length > 15) throw new ArgumentException("Data too long"); + controlByte = (byte)(data.Length | (isHash ? 0x10 : 0)); + data.CopyTo(MemoryMarshal.CreateSpan(ref dataByte1, data.Length)); + } + + struct Utf8StringCtor {} + private ClientTypeId(ReadOnlySpan utf8Hash, Utf8StringCtor _) + { + if (utf8Hash.Length != 16) throw new ArgumentException("Hash must be 16 bytes long"); + controlByte = (byte)(12 | 0x10); + System.Buffers.Text.Base64.DecodeFromUtf8(utf8Hash, MemoryMarshal.CreateSpan(ref dataByte1, 12), out var _, out var _); + } + + public static ClientTypeId CreateHash(ReadOnlySpan data) => new ClientTypeId(true, data); + public static ClientTypeId CreateString(ReadOnlySpan data) => new ClientTypeId(false, data); + public static ClientTypeId Parse(ReadOnlySpan utf8) => + utf8.Length == 16 ? new ClientTypeId(utf8, default(Utf8StringCtor)) : new ClientTypeId(false, utf8); + + byte Length => (byte)(controlByte & 0xf); + bool IsHash => ((controlByte >> 4) & 1) != 0; + bool IsEmpty => controlByte == 0; + + ReadOnlySpan Data => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(in dataByte1), Length); + + public void WriteJson(System.Text.Json.Utf8JsonWriter writer) + { + if (IsEmpty) writer.WriteNullValue(); + else if (IsHash) + writer.WriteBase64StringValue(Data); + else + writer.WriteStringValue(Data); + } + public void WriteJson(System.Text.Json.Utf8JsonWriter writer, ReadOnlySpan propertyName) + { + if (IsEmpty) writer.WriteNull(propertyName); + else if (IsHash) + writer.WriteBase64String(propertyName, Data); + else + writer.WriteString(propertyName, Data); + } + + public static ClientTypeId ReadJson(ref System.Text.Json.Utf8JsonReader reader) + { + if (reader.TokenType == System.Text.Json.JsonTokenType.Null) return default; + if (reader.TokenType != System.Text.Json.JsonTokenType.String) throw new System.Text.Json.JsonException("Expected string"); + Span buffer = stackalloc byte[16]; + var readBytes = reader.CopyString(buffer); + return readBytes == 16 ? new ClientTypeId(buffer, default(Utf8StringCtor)) : new ClientTypeId(false, buffer.Slice(0, readBytes)); + } + + public override string ToString() + { + if (IsEmpty) return "[Empty]"; + if (IsHash) + return Convert.ToBase64String(Data); + else + return StringUtils.Utf8.GetString(Data); + } + public override int GetHashCode() => HashCode.Combine(a, b); + public override bool Equals(object? obj) => obj is ClientTypeId id && id.a == a && id.b == b; + public bool Equals(ClientTypeId other) => other.a == a && other.b == b; + public int CompareTo(ClientTypeId other) => a == other.a ? b.CompareTo(other.b) : a.CompareTo(other.a); + public static bool operator ==(ClientTypeId left, ClientTypeId right) => left.Equals(right); + public static bool operator !=(ClientTypeId left, ClientTypeId right) => !left.Equals(right); + } +} diff --git a/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs index 7529850f24..f8ddf2aa61 100644 --- a/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs @@ -1,59 +1,60 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Globalization; using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using DotVVM.Framework.Configuration; using DotVVM.Framework.Utils; using DotVVM.Framework.ViewModel; -using Newtonsoft.Json; namespace DotVVM.Framework.ViewModel.Serialization { - public class DotvvmCustomPrimitiveTypeConverter : JsonConverter + public class DotvvmCustomPrimitiveTypeConverter : JsonConverterFactory { - public override bool CanConvert(Type objectType) - { - return ReflectionUtils.IsCustomPrimitiveType(objectType); - } + public override bool CanConvert(Type typeToConvert) => + ReflectionUtils.IsCustomPrimitiveType(typeToConvert); + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => + (JsonConverter)Activator.CreateInstance(typeof(InnerConverter<>).MakeGenericType(typeToConvert))!; - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + class InnerConverter: JsonConverter where T: IDotvvmPrimitiveType { - if (reader.TokenType is JsonToken.String - or JsonToken.Boolean - or JsonToken.Integer - or JsonToken.Float - or JsonToken.Date) + private CustomPrimitiveTypeRegistration registration = ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(typeof(T))!; + // TODO: make this converter factory? + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var registration = ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(objectType)!; - var parseResult = registration.TryParseMethod(Convert.ToString(reader.Value, CultureInfo.InvariantCulture)!); - if (!parseResult.Successful) + if (reader.TokenType is JsonTokenType.String + or JsonTokenType.True + or JsonTokenType.False + or JsonTokenType.Number) { - throw new JsonSerializationException($"The value '{reader.Value}' cannot be deserialized as {objectType} because its TryParse method wasn't able to parse the value!"); + // TODO: utf8 parsing? + var registration = ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(typeToConvert)!; + var str = reader.TokenType is JsonTokenType.String ? reader.GetString() : + reader.HasValueSequence ? StringUtils.Utf8.GetString(reader.ValueSequence.ToArray()) : + StringUtils.Utf8.GetString(reader.ValueSpan); + var parseResult = registration.TryParseMethod(str!); + if (!parseResult.Successful) + { + throw new Exception($"The value '{str}' cannot be deserialized as {typeToConvert} because its TryParse method wasn't able to parse the value!"); + } + return (T)parseResult.Result!; + } + else if (reader.TokenType == JsonTokenType.Null) + { + return default; + } + else + { + throw new Exception($"Token {reader.TokenType} cannot be deserialized as {typeToConvert}! Primitive value in JSON was expected."); } - return parseResult.Result; - } - else if (reader.TokenType == JsonToken.Null) - { - return null; - } - else - { - throw new JsonSerializationException($"Token {reader.TokenType} cannot be deserialized as {objectType}! Primitive value in JSON was expected."); } - } - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - if (value == null) + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { - writer.WriteNull(); - } - else - { - var registration = ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(value.GetType())!; - writer.WriteValue(registration.ToStringMethod(value)); + writer.WriteStringValue(registration.ToStringMethod(value)); } } - - } } diff --git a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs index eb35dc99f4..35138501ae 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs @@ -10,13 +10,16 @@ using DotVVM.Framework.Runtime.Filters; using DotVVM.Framework.Security; using DotVVM.Framework.Utils; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Linq; using DotVVM.Framework.ResourceManagement; using DotVVM.Framework.Binding; using System.Collections.Immutable; using RecordExceptions; +using System.Text.Json; +using System.Buffers; +using System.Text.Json.Nodes; +using System.Net.NetworkInformation; +using System.Diagnostics; +using Microsoft.Extensions.Logging; namespace DotVVM.Framework.ViewModel.Serialization { @@ -35,44 +38,57 @@ 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 ILogger? logger; public bool SendDiff { get; set; } = true; - public Formatting JsonFormatting { get; set; } + 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) + public DefaultViewModelSerializer(DotvvmConfiguration configuration, IViewModelProtector protector, IViewModelSerializationMapper serializationMapper, IViewModelServerCache viewModelServerCache, IViewModelTypeMetadataSerializer viewModelTypeMetadataSerializer, ViewModelJsonConverter viewModelConverter, ILogger? logger) { this.viewModelProtector = protector; - this.JsonFormatting = configuration.Debug ? Formatting.Indented : Formatting.None; this.viewModelMapper = serializationMapper; this.viewModelServerCache = viewModelServerCache; this.viewModelTypeMetadataSerializer = viewModelTypeMetadataSerializer; + this.viewModelConverter = viewModelConverter; + 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, + }; } /// /// Serializes the view model. /// - public string SerializeViewModel(IDotvvmRequestContext context) + public string SerializeViewModel(IDotvvmRequestContext context, object? commandResult = null, IEnumerable<(string name, string html)>? postbackUpdatedControls = null, bool serializeNewResources = false) { var timer = ValueStopwatch.StartNew(); + var utf8json = BuildViewModel(context, commandResult, postbackUpdatedControls, serializeNewResources); - context.ViewModelJson ??= new JObject(); - if (SendDiff && context.ReceivedViewModelJson?["viewModel"] is JObject receivedVM && context.ViewModelJson["viewModel"] is JObject responseVM) - { - context.ViewModelJson["viewModelDiff"] = JsonUtils.Diff(receivedVM, responseVM, false, i => ShouldIncludeProperty(i.TypeId, i.Property)); - context.ViewModelJson.Remove("viewModel"); - } - var result = context.ViewModelJson.ToString(JsonFormatting); + // context.ViewModelJson ??= new JObject(); + // if (SendDiff && context.ReceivedViewModelJson?["viewModel"] is JObject receivedVM && context.ViewModelJson["viewModel"] is JObject responseVM) + // { + // context.ViewModelJson["viewModelDiff"] = JsonUtils.Diff(receivedVM, responseVM, false, i => ShouldIncludeProperty(i.TypeId, i.Property)); + // context.ViewModelJson.Remove("viewModel"); + // } + var result = StringUtils.Utf8.GetString(utf8json.ToSpan()); - context.HttpContext.SetItem("dotvvm-viewmodel-size-bytes", result.Length); // for PerformanceWarningTracer + context.HttpContext.SetItem("dotvvm-viewmodel-size-bytes", utf8json.Length); // for PerformanceWarningTracer var routeLabel = context.RouteLabel(); var requestType = context.RequestTypeLabel(); DotvvmMetrics.ViewModelStringificationTime.Record(timer.ElapsedSeconds, routeLabel, requestType); - DotvvmMetrics.ViewModelSize.Record(result.Length, routeLabel, requestType); + DotvvmMetrics.ViewModelSize.Record(utf8json.Length, routeLabel, requestType); return result; } @@ -101,145 +117,225 @@ public string SerializeViewModel(IDotvvmRequestContext context) /// /// Builds the view model for the client. /// - public void BuildViewModel(IDotvvmRequestContext context, object? commandResult) + public MemoryStream BuildViewModel(IDotvvmRequestContext context, object? commandResult, IEnumerable<(string name, string html)>? postbackUpdatedControls = null, bool serializeNewResources = false) { var timer = ValueStopwatch.StartNew(); // serialize the ViewModel - var serializer = CreateJsonSerializer(); - var viewModelConverter = new ViewModelJsonConverter(IsPostBack(context), viewModelMapper, context.Services); - serializer.Converters.Add(viewModelConverter); - var writer = new JTokenWriter(); + var vmCoreBuffer = new MemoryStream(); + + using var state = DotvvmSerializationState.Create(context.IsPostBack, context.Services); try { - serializer.Serialize(writer, context.ViewModel); + using var jsonWriter = new Utf8JsonWriter(vmCoreBuffer, new JsonWriterOptions { Indented = this.ViewModelJsonOptions.WriteIndented, Encoder = ViewModelJsonOptions.Encoder }); + jsonWriter.WriteStartArray(); // Hack increase indent to align with the rest of the JSON + JsonSerializer.Serialize(jsonWriter, context.ViewModel, context.ViewModel!.GetType(), ViewModelJsonOptions); + jsonWriter.WriteEndArray(); } catch (Exception ex) { - throw new SerializationException(true, context.ViewModel!.GetType(), writer.Path, ex); + var failurePath = SystemTextJsonUtils.GetFailurePath(vmCoreBuffer.ToSpan()); + throw new SerializationException(true, context.ViewModel!.GetType(), string.Join("/", failurePath), ex); } - var viewModelToken = writer.Token.NotNull(); + vmCoreBuffer.Position = 0; string? viewModelCacheId = null; if (context.Configuration.ExperimentalFeatures.ServerSideViewModelCache.IsEnabledForRoute(context.Route?.RouteName)) { - viewModelCacheId = viewModelServerCache.StoreViewModel(context, (JObject)viewModelToken); + viewModelCacheId = viewModelServerCache.StoreViewModel(context, vmCoreBuffer); + vmCoreBuffer.Position = 0; } - // persist CSRF token - if (context.CsrfToken is object) - viewModelToken["$csrfToken"] = context.CsrfToken; + var sentJsonBuffer = new MemoryStream(); + using (var sentJsonWriter = new Utf8JsonWriter(sentJsonBuffer, new JsonWriterOptions { + Indented = this.ViewModelJsonOptions.WriteIndented, + Encoder = ViewModelJsonOptions.Encoder, + SkipValidation = true, // for the hack with WriteRawValue + })) + { + sentJsonWriter.WriteStartObject(); + sentJsonWriter.WritePropertyName("viewModel"u8); + sentJsonWriter.WriteStartObject(); + var coreViewModelHack = TrimJsonObject(vmCoreBuffer.ToSpan()); + if (coreViewModelHack.Length > 0) + sentJsonWriter.WriteRawValue(coreViewModelHack, skipInputValidation: true); // TODO: get rid of this by moving $csrfToken elsewhere? + // persist CSRF token + if (context.CsrfToken is object) + sentJsonWriter.WriteString("$csrfToken"u8, context.CsrfToken); + + // persist encrypted values + if (state.WriteEncryptedValues is not null && + state.WriteEncryptedValues.ToSpan() is not [] and not [(byte)'{', (byte)'}']) + sentJsonWriter.WriteBase64String("$encryptedValues"u8, viewModelProtector.Protect(state.WriteEncryptedValues.ToArray(), context)); + + sentJsonWriter.WriteEndObject(); + + if (viewModelCacheId != null) + { + sentJsonWriter.WriteString("viewModelCacheId"u8, viewModelCacheId); + } + sentJsonWriter.WriteString("url"u8, context.HttpContext?.Request?.Url?.PathAndQuery); + sentJsonWriter.WriteString("virtualDirectory"u8, context.HttpContext?.Request?.PathBase?.Value?.Trim('/') ?? ""); + if (context.ResultIdFragment != null) + { + sentJsonWriter.WriteString("resultIdFragment"u8, context.ResultIdFragment); + } - // persist encrypted values - if (viewModelConverter.EncryptedValues.Count > 0) - viewModelToken["$encryptedValues"] = viewModelProtector.Protect(viewModelConverter.EncryptedValues.ToString(Formatting.None), context); + if (context.RequestType is DotvvmRequestType.Command or DotvvmRequestType.SpaNavigate) + { + sentJsonWriter.WriteString("action"u8, "successfulCommand"u8); + } + else + { + sentJsonWriter.WriteStartArray("renderedResources"u8); + foreach (var resource in context.ResourceManager.GetNamedResourcesInOrder()) + sentJsonWriter.WriteStringValue(resource.Name); + sentJsonWriter.WriteEndArray(); + } - // create result object - var result = new JObject(); - result["viewModel"] = viewModelToken; - if (viewModelCacheId != null) - { - result["viewModelCacheId"] = viewModelCacheId; - } - result["url"] = context.HttpContext?.Request?.Url?.PathAndQuery; - result["virtualDirectory"] = context.HttpContext?.Request?.PathBase?.Value?.Trim('/') ?? ""; - if (context.ResultIdFragment != null) - { - result["resultIdFragment"] = context.ResultIdFragment; - } + if (commandResult != null) + { + sentJsonWriter.WritePropertyName("commandResult"u8); + WriteCommandData(commandResult, sentJsonWriter, sentJsonBuffer); + } + AddCustomPropertiesIfAny(context, sentJsonWriter, sentJsonBuffer); - if (context.RequestType is DotvvmRequestType.Command or DotvvmRequestType.SpaNavigate) - { - result["action"] = "successfulCommand"; - } - else - { - result["renderedResources"] = JArray.FromObject(context.ResourceManager.GetNamedResourcesInOrder().Select(r => r.Name)); + if (serializeNewResources) + { + AddNewResources(context, sentJsonWriter); + } + + if (postbackUpdatedControls is not null) + { + AddPostBackUpdatedControls(context, sentJsonWriter, postbackUpdatedControls); + } + + SerializeTypeMetadata(context, sentJsonWriter, state.UsedSerializationMaps); + sentJsonWriter.WriteEndObject(); } - if (commandResult != null) result["commandResult"] = WriteCommandData(commandResult, serializer, "the command result"); - AddCustomPropertiesIfAny(context, serializer, result); + DotvvmMetrics.ViewModelSerializationTime.Record(timer.ElapsedSeconds, context.RouteLabel(), context.RequestTypeLabel()); + + return sentJsonBuffer; + } - result["typeMetadata"] = SerializeTypeMetadata(context, viewModelConverter); + static ReadOnlySpan TrimJsonObject(ReadOnlySpan json) + { + if (json[0] == '[' && json[json.Length - 1] == ']') + { + json = json.Slice(1, json.Length - 2); + json = TrimStart(TrimEnd(json)); + } + // Trim { and } + if (json.Length < 2 || json[0] != '{' || json[json.Length - 1] != '}') + throw new InvalidOperationException("Internal bug."); + json = json.Slice(1, json.Length - 2); + // trim (ASCII) whitespace from end + return TrimEnd(json); + } - context.ViewModelJson = result; + static ReadOnlySpan TrimStart(ReadOnlySpan json) + { + while (json.Length > 0 && char.IsWhiteSpace((char)json[0])) + json = json.Slice(1); + return json; + } - DotvvmMetrics.ViewModelSerializationTime.Record(timer.ElapsedSeconds, context.RouteLabel(), context.RequestTypeLabel()); + static ReadOnlySpan TrimEnd(ReadOnlySpan json) + { + while (json.Length > 0 && char.IsWhiteSpace((char)json[json.Length - 1])) + json = json.Slice(0, json.Length - 1); + return json; } - private JObject SerializeTypeMetadata(IDotvvmRequestContext context, ViewModelJsonConverter viewModelJsonConverter) + + private void SerializeTypeMetadata(IDotvvmRequestContext context, Utf8JsonWriter writer, IEnumerable usedSerializationMaps) { - var knownTypeIds = context.ReceivedViewModelJson?["knownTypeMetadata"]?.Values().WhereNotNull().ToImmutableHashSet(); - return viewModelTypeMetadataSerializer.SerializeTypeMetadata(viewModelJsonConverter.UsedSerializationMaps, knownTypeIds); + var knownTypeIds = context.ReceivedViewModelJson?.RootElement.GetPropertyOrNull("knownTypeMetadata"u8)?.EnumerateArray().Select(e => e.GetString()).WhereNotNull().ToHashSet(StringComparer.OrdinalIgnoreCase); + viewModelTypeMetadataSerializer.SerializeTypeMetadata(usedSerializationMaps, writer, "typeMetadata"u8, knownTypeIds); } - public void AddNewResources(IDotvvmRequestContext context) + private void AddNewResources(IDotvvmRequestContext context, Utf8JsonWriter writer) { - var renderedResources = new HashSet(context.ReceivedViewModelJson?["renderedResources"]?.Values().WhereNotNull() ?? new string[] { }); - var resourcesObject = BuildResourcesJson(context, rn => !renderedResources.Contains(rn)); - if (resourcesObject.Count > 0) - context.ViewModelJson!["resources"] = resourcesObject; + var renderedResources = + context.ReceivedViewModelJson + ?.RootElement.GetPropertyOrNull("renderedResources"u8) + ?.EnumerateArray().Select(e => e.GetString()) + .WhereNotNull() + .ToHashSet(StringComparer.OrdinalIgnoreCase) ?? new HashSet(); + + var newResources = SerializeResources(context, rn => !renderedResources.Contains(rn)); + if (newResources.Count == 0) + return; + + writer.WriteStartObject("resources"u8); + foreach (var resource in newResources) + { + writer.WriteString(resource.Name, resource.GetRenderedTextCached(context)); + } + writer.WriteEndObject(); } - public string BuildStaticCommandResponse(IDotvvmRequestContext context, object? result, string[]? knownTypeMetadata = null) + public ReadOnlyMemory BuildStaticCommandResponse(IDotvvmRequestContext context, object? result, string[]? knownTypeMetadata = null) { var timer = ValueStopwatch.StartNew(); - var serializer = CreateJsonSerializer(); - var viewModelConverter = new ViewModelJsonConverter(isPostback: true, viewModelMapper, context.Services); - serializer.Converters.Add(viewModelConverter); - var response = new JObject(); - response["result"] = WriteCommandData(result, serializer, "the static command result"); - - var typeMetadata = viewModelTypeMetadataSerializer.SerializeTypeMetadata(viewModelConverter.UsedSerializationMaps, knownTypeMetadata?.ToHashSet()); - if (typeMetadata.Count > 0) + 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 })) { - response["typeMetadata"] = typeMetadata; + writer.WriteStartObject(); + writer.WritePropertyName("result"u8); + WriteCommandData(result, writer, outputBuffer); + + viewModelTypeMetadataSerializer.SerializeTypeMetadata(state.UsedSerializationMaps, writer, "typeMetadata"u8, knownTypeMetadata?.ToHashSet()); + AddCustomPropertiesIfAny(context, writer, outputBuffer); + writer.WriteEndObject(); } - AddCustomPropertiesIfAny(context, serializer, response); - var resultJson = response.ToString(JsonFormatting); - DotvvmMetrics.ViewModelSize.Record(resultJson.Length, context.RouteLabel(), context.RequestTypeLabel()); + DotvvmMetrics.ViewModelSize.Record(outputBuffer.Length, context.RouteLabel(), context.RequestTypeLabel()); DotvvmMetrics.ViewModelSerializationTime.Record(timer.ElapsedSeconds, context.RouteLabel(), context.RequestTypeLabel()); - return resultJson; + return outputBuffer.ToMemory(); } - private static void AddCustomPropertiesIfAny(IDotvvmRequestContext context, JsonSerializer serializer, JObject response) + private void AddCustomPropertiesIfAny(IDotvvmRequestContext context, Utf8JsonWriter writer, MemoryStream outputBuffer) { if (context.CustomResponseProperties.Properties.Count > 0) { - var props = context.CustomResponseProperties.Properties - .Select(s => new JProperty(s.Key, WriteCommandData(s.Value, serializer, $"custom properties['{s.Key}']"))) - .ToArray(); - response["customProperties"] = new JObject(props); + writer.WriteStartObject("customProperties"u8); + foreach (var prop in context.CustomResponseProperties.Properties) + { + writer.WritePropertyName(prop.Key); + WriteCommandData(prop.Value, writer, outputBuffer); + } + writer.WriteEndObject(); } context.CustomResponseProperties.PropertiesSerialized = true; } - private static JToken WriteCommandData(object? data, JsonSerializer serializer, string description) + private void WriteCommandData(object? data, Utf8JsonWriter writer, MemoryStream outputBuffer) { - var writer = new JTokenWriter(); + Debug.Assert(DotvvmSerializationState.Current is {}); try { - serializer.Serialize(writer, data); + JsonSerializer.Serialize(writer, data, this.ViewModelJsonOptions); } catch (Exception ex) { - throw new SerializationException(true, data?.GetType(), writer.Path, ex); + writer.Flush(); + var path = SystemTextJsonUtils.GetFailurePath(outputBuffer.ToSpan()); + throw new SerializationException(true, data?.GetType(), string.Join("/", path), ex); } - return writer.Token.NotNull(); } - protected virtual JsonSerializer CreateJsonSerializer() => DefaultSerializerSettingsProvider.Instance.Settings.Apply(JsonSerializer.Create); - - public JObject BuildResourcesJson(IDotvvmRequestContext context, Func predicate) + private List SerializeResources(IDotvvmRequestContext context, Func predicate) { + var resources = new List(); var manager = context.ResourceManager; - var resourceObj = new JObject(); foreach (var resource in manager.GetNamedResourcesInOrder()) { if (predicate(resource.Name)) { - resourceObj[resource.Name] = JValue.CreateString(resource.GetRenderedTextCached(context)); + resources.Add(resource); } } @@ -248,11 +344,9 @@ public JObject BuildResourcesJson(IDotvvmRequestContext context, Func @@ -261,12 +355,12 @@ public JObject BuildResourcesJson(IDotvvmRequestContext context, Func @@ -275,21 +369,21 @@ public static string GenerateRedirectActionResponse(string url, bool replace, bo internal static string GenerateMissingCachedViewModelResponse() { // create result object - var result = new JObject(); - result["action"] = "viewModelNotCached"; - return result.ToString(Formatting.None); + return JsonSerializer.Serialize(new { action = "viewModelNotCached" }, DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe); } /// /// Serializes the validation errors in case the viewmodel was not valid. /// - public string SerializeModelState(IDotvvmRequestContext context) + public byte[] SerializeModelState(IDotvvmRequestContext context) { // create result object - var result = new JObject(); - result["modelState"] = JArray.FromObject(context.ModelState.Errors); - result["action"] = "validationErrors"; - return result.ToString(JsonFormatting); + using var state = DotvvmSerializationState.Create(isPostback: true, context.Services); + return JsonSerializer.SerializeToUtf8Bytes(new + { + modelState = context.ModelState.Errors, + action = "validationErrors" + }, this.PlainJsonOptions); } @@ -297,49 +391,79 @@ public string SerializeModelState(IDotvvmRequestContext context) /// Populates the view model from the data received from the request. /// /// - public void PopulateViewModel(IDotvvmRequestContext context, string serializedPostData) + public void PopulateViewModel(IDotvvmRequestContext context, ReadOnlyMemory serializedPostData) { // get properties - var data = context.ReceivedViewModelJson = JObject.Parse(serializedPostData); - JObject viewModelToken; - if (data["viewModelCacheId"]?.Value() is string viewModelCacheId) + var vmDocument = context.ReceivedViewModelJson = JsonDocument.Parse(serializedPostData); + var root = vmDocument.RootElement; + JsonElement viewModelElement; + if (root.GetPropertyOrNull("viewModelCacheId"u8)?.GetString() is {} viewModelCacheId) { if (!context.Configuration.ExperimentalFeatures.ServerSideViewModelCache.IsEnabledForRoute(context.Route?.RouteName)) { throw new InvalidOperationException("The server-side viewmodel caching is not enabled for the current route!"); } - viewModelToken = viewModelServerCache.TryRestoreViewModel(context, viewModelCacheId, (JObject)data["viewModelDiff"]!); - data["viewModel"] = viewModelToken; + viewModelElement = viewModelServerCache.TryRestoreViewModel(context, viewModelCacheId, root.GetProperty("viewModelDiff"u8)); } else { - viewModelToken = (JObject)data["viewModel"]!; + viewModelElement = root.GetProperty("viewModel"u8); } // load CSRF token - context.CsrfToken = viewModelToken["$csrfToken"]?.Value(); + context.CsrfToken = viewModelElement.GetPropertyOrNull("$csrfToken"u8)?.GetString(); - ViewModelJsonConverter viewModelConverter; - if (viewModelToken["$encryptedValues"]?.Value() is string encryptedValuesString) + JsonObject readEncryptedValues; + if (viewModelElement.TryGetProperty("$encryptedValues"u8, out var evJson) && evJson.GetBytesFromBase64() is {} encryptedValuesBytes) { // load encrypted values - viewModelConverter = new ViewModelJsonConverter(IsPostBack(context), viewModelMapper, context.Services, JObject.Parse(viewModelProtector.Unprotect(encryptedValuesString, context))); + readEncryptedValues = JsonNode.Parse(viewModelProtector.Unprotect(encryptedValuesBytes, context))!.AsObject(); + readEncryptedValues = new JsonObject([ new("0", readEncryptedValues) ]); // accomodate for the DeserializationHelper hack + // readEncryptedValues = new JsonObject([ new("0", readEncryptedValues) ]); + // readEncryptedValues = new JsonObject([ new("0", readEncryptedValues) ]); + } + else + { + readEncryptedValues = new JsonObject(); } - else viewModelConverter = new ViewModelJsonConverter(IsPostBack(context), viewModelMapper, context.Services); + + using var state = DotvvmSerializationState.Create(isPostback: true, context.Services, readEncryptedValues: readEncryptedValues); // get validation path - context.ModelState.ValidationTargetPath = (string?)data["validationTargetPath"]; + context.ModelState.ValidationTargetPath = root.GetPropertyOrNull("validationTargetPath"u8)?.GetString(); // populate the ViewModel - var serializer = CreateJsonSerializer(); - serializer.Converters.Add(viewModelConverter); - var reader = viewModelToken.CreateReader(); + var reader = new Utf8JsonReader(serializedPostData.Span); + reader.Read(); try { - var newVM = viewModelConverter.Populate(reader, serializer, context.ViewModel!); + if (reader.TokenType != JsonTokenType.StartObject) + throw new Exception("The JSON must start with an object."); + reader.Read(); + while (reader.TokenType == JsonTokenType.PropertyName && !reader.ValueTextEquals("viewModel"u8)) + { + reader.Skip(); + reader.Read(); + } + Debug.Assert(reader.TokenType is JsonTokenType.PropertyName or JsonTokenType.EndObject); + if (reader.TokenType != JsonTokenType.PropertyName) + throw new Exception("The JSON must contain a property \"viewModel\"."); + reader.Read(); + if (reader.TokenType != JsonTokenType.StartObject) + throw new Exception("Property \"viewModel\" must be an object."); + + Debug.Assert(context.ViewModel is not null); + + var converter = this.viewModelConverter.GetConverterCached(context.ViewModel.GetType()); + var newVM = converter.PopulateUntyped(ref reader, context.ViewModel.GetType(), context.ViewModel, ViewModelJsonOptions, state); + // var helperObject = new DeserializationHelper() { ViewModel = context.ViewModel }; + // var newHelper = converter.Populate(ref reader, JsonOptions, helperObject, state); + // Debug.Assert(newHelper == (object)helperObject); + if (newVM != context.ViewModel) { + logger.LogInformation("Instance of root view model {ViewModelType} was replaced during deserialization.", context.ViewModel!.GetType()); context.ViewModel = newVM; if (context.View is not null) context.View.DataContext = newVM; @@ -347,26 +471,32 @@ public void PopulateViewModel(IDotvvmRequestContext context, string serializedPo } catch (Exception ex) { - throw new SerializationException(false, context.ViewModel?.GetType(), reader.Path, ex); + var documentSlice = serializedPostData.Span.Slice(0, (int)reader.BytesConsumed); + var path = SystemTextJsonUtils.GetFailurePath(documentSlice); + throw new SerializationException(false, context.ViewModel?.GetType(), string.Join("/", path), ex); } } /// /// Resolves the command for the specified post data. /// - public ActionInfo? ResolveCommand(IDotvvmRequestContext context, DotvvmView view) + public ActionInfo ResolveCommand(IDotvvmRequestContext context, DotvvmView view) { // get properties - var data = context.ReceivedViewModelJson ?? throw new NotSupportedException("Could not find ReceivedViewModelJson in request context."); - var path = data["currentPath"].NotNull("currentPath is required").Values().ToArray(); - var command = data["command"].NotNull("command is required").Value(); - var controlUniqueId = data["controlUniqueId"]?.Value(); - var args = data["commandArgs"] is JArray argsJson ? - argsJson.Select(a => (Func)(t => a.ToObject(t))).ToArray() : + var data = context.ReceivedViewModelJson?.RootElement ?? throw new NotSupportedException("Could not find ReceivedViewModelJson in request context."); + var path = data.GetProperty("currentPath"u8).EnumerateArray().Select(e => e.GetString().NotNull()).ToArray(); + var command = data.GetProperty("command"u8).GetString(); + var controlUniqueId = data.GetPropertyOrNull("controlUniqueId"u8)?.GetString(); + var args = data.TryGetProperty("commandArgs"u8, out var argsJson) ? + argsJson.EnumerateArray().Select(a => (Func)(t => { + using var state = DotvvmSerializationState.Create(isPostback: true, context.Services); + return JsonSerializer.Deserialize(a, t, ViewModelJsonOptions); + })).ToArray() : new Func[0]; // empty command - if (string.IsNullOrEmpty(command)) return null; + if (string.IsNullOrEmpty(command)) + throw new Exception("Command is not specified!"); // find the command target if (!string.IsNullOrEmpty(controlUniqueId)) @@ -387,20 +517,29 @@ public void PopulateViewModel(IDotvvmRequestContext context, string serializedPo /// /// Adds the post back updated controls. /// - public void AddPostBackUpdatedControls(IDotvvmRequestContext context, IEnumerable<(string name, string html)> postBackUpdatedControls) + private void AddPostBackUpdatedControls(IDotvvmRequestContext context, Utf8JsonWriter writer, IEnumerable<(string name, string html)> postBackUpdatedControls) { - var result = new JObject(); + var first = true; foreach (var (controlId, html) in postBackUpdatedControls) { - result[controlId] = JValue.CreateString(html); + if (first) + { + writer.WriteStartObject("updatedControls"u8); + first = false; + } + writer.WriteString(controlId, html); } - if (context.ViewModelJson == null) + if (!first) { - context.ViewModelJson = new JObject(); + writer.WriteEndObject(); } - - context.ViewModelJson["updatedControls"] = result; } + + // class DeserializationHelper + // { + // [Bind(Name = "viewModel", AllowDynamicDispatch = true)] + // public object? ViewModel { get; set; } = null; + // } } } diff --git a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelServerCache.cs b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelServerCache.cs index 332517940a..8128472cce 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelServerCache.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelServerCache.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Security.Cryptography; using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; using DotVVM.Framework.Hosting; using DotVVM.Framework.Utils; -using Newtonsoft.Json.Bson; -using Newtonsoft.Json.Linq; namespace DotVVM.Framework.ViewModel.Serialization { @@ -20,15 +21,15 @@ public DefaultViewModelServerCache(IViewModelServerStore viewModelStore) this.viewModelStore = viewModelStore; } - public string StoreViewModel(IDotvvmRequestContext context, JObject viewModelToken) + public string StoreViewModel(IDotvvmRequestContext context, Stream data) { - var cacheData = PackViewModel(viewModelToken); + var cacheData = PackViewModel(data); var hash = Convert.ToBase64String(SHA256.Create().ComputeHash(cacheData)); viewModelStore.Store(hash, cacheData); return hash; } - public JObject TryRestoreViewModel(IDotvvmRequestContext context, string viewModelCacheId, JObject viewModelDiffToken) + public JsonElement TryRestoreViewModel(IDotvvmRequestContext context, string viewModelCacheId, JsonElement viewModelDiffToken) { var cachedData = viewModelStore.Retrieve(viewModelCacheId); var routeLabel = new KeyValuePair("route", context.Route!.RouteName); @@ -42,29 +43,30 @@ public JObject TryRestoreViewModel(IDotvvmRequestContext context, string viewMod DotvvmMetrics.ViewModelCacheBytesLoaded.Add(cachedData.Length, routeLabel); var result = UnpackViewModel(cachedData); - JsonUtils.Patch(result, viewModelDiffToken); - return result; + var resultJson = JsonNode.Parse(result)!.AsObject(); + // TODO: this is just bad + JsonUtils.Patch(resultJson, JsonObject.Create(viewModelDiffToken)!); + var jsonData = new MemoryStream(); + using (var writer = new Utf8JsonWriter(jsonData)) + { + resultJson.WriteTo(writer); + } + return JsonDocument.Parse(jsonData.ToMemory()).RootElement; } - protected virtual byte[] PackViewModel(JObject viewModelToken) + protected virtual byte[] PackViewModel(Stream data) { - using (var ms = new MemoryStream()) - using (var bsonWriter = new BsonDataWriter(ms)) + var output = new MemoryStream(); + using (var compressed = new System.IO.Compression.DeflateStream(output, System.IO.Compression.CompressionLevel.Fastest)) { - viewModelToken.WriteTo(bsonWriter); - bsonWriter.Flush(); - - return ms.ToArray(); + data.CopyTo(compressed); } + return output.ToArray(); } - protected virtual JObject UnpackViewModel(byte[] cachedData) + protected virtual Stream UnpackViewModel(byte[] cachedData) { - using (var ms = new MemoryStream(cachedData)) - using (var bsonReader = new BsonDataReader(ms)) - { - return (JObject)JToken.ReadFrom(bsonReader); - } + return new DeflateStream(new MemoryStream(cachedData), CompressionMode.Decompress); } } } diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmByteArrayConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmByteArrayConverter.cs index 4df3b330c0..0a5c66bf31 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmByteArrayConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmByteArrayConverter.cs @@ -2,62 +2,56 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; -using Newtonsoft.Json; namespace DotVVM.Framework.ViewModel.Serialization { - public class DotvvmByteArrayConverter : JsonConverter + public class DotvvmByteArrayConverter : JsonConverter { - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + public override byte[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonToken.Null) + if (reader.TokenType == JsonTokenType.Null) { return null; } - else if (reader.TokenType == JsonToken.StartArray) + else if (reader.TokenType == JsonTokenType.StartArray) { var list = new List(); while (reader.Read()) { switch (reader.TokenType) { - case JsonToken.Integer: - list.Add(Convert.ToByte(reader.Value)); + case JsonTokenType.Number: + list.Add((byte)reader.GetUInt16()); break; - case JsonToken.EndArray: + case JsonTokenType.EndArray: return list.ToArray(); default: - throw new FormatException($"Unexpected token while reading byte array: {reader.TokenType}"); + throw new JsonException($"Unexpected token while reading byte array: {reader.TokenType}"); } } - throw new FormatException($"Unexpected end of array!"); + throw new JsonException($"Unexpected end of array!"); } else { - throw new FormatException($"Expected StartArray token, but instead got {reader.TokenType}!"); + throw new JsonException($"Expected StartArray token, but instead got {reader.TokenType}!"); } } - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, byte[] array, JsonSerializerOptions options) { - if (value == null) + if (array is null) { - writer.WriteNull(); + writer.WriteNullValue(); return; } - - var array = (byte[])value; writer.WriteStartArray(); foreach (var item in array) - writer.WriteValue(item); + writer.WriteNumberValue(item); writer.WriteEndArray(); } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(byte[]); - } } } diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmDateOnlyConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmDateOnlyConverter.cs index f7a138e4da..8e43a9f6dc 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmDateOnlyConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmDateOnlyConverter.cs @@ -1,54 +1,28 @@ using System; using System.Globalization; -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Serialization; namespace DotVVM.Framework.ViewModel.Serialization { - public class DotvvmDateOnlyConverter : JsonConverter + public class DotvvmDateOnlyConverter : JsonConverter { - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, DateOnly date, JsonSerializerOptions options) { - if (value == null) - { - writer.WriteNull(); - } - else - { - var date = (DateOnly)value; - var dateWithoutTimezone = new DateOnly(date.Year, date.Month, date.Day); - writer.WriteValue(dateWithoutTimezone.ToString("O", CultureInfo.InvariantCulture)); - } + var dateWithoutTimezone = new DateOnly(date.Year, date.Month, date.Day); + writer.WriteStringValue(dateWithoutTimezone.ToString("O", CultureInfo.InvariantCulture)); // TODO: utf8 } - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonToken.Null) - { - if (objectType == typeof(DateOnly)) - { - return DateOnly.MinValue; - } - else - { - return null; - } - } - else if (reader.TokenType == JsonToken.Date) - { - return (DateOnly)reader.Value!; - } - else if (reader.TokenType == JsonToken.String - && DateOnly.TryParseExact((string)reader.Value!, "O", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)) + if (reader.TokenType == JsonTokenType.String + && DateOnly.TryParseExact(reader.GetString(), "O", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)) { return date; } - throw new JsonSerializationException("The value specified in the JSON could not be converted to DateTime!"); + throw new Exception("The value specified in the JSON could not be converted to DateTime!"); } - public override bool CanConvert(Type objectType) - { - return objectType == typeof(DateOnly) || objectType == typeof(DateOnly?); - } } } diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmDateTimeConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmDateTimeConverter.cs index daff902202..279b02bdfb 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmDateTimeConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmDateTimeConverter.cs @@ -1,54 +1,26 @@ using System; using System.Globalization; -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Serialization; namespace DotVVM.Framework.ViewModel.Serialization { - public class DotvvmDateTimeConverter : JsonConverter + public class DotvvmDateTimeConverter : JsonConverter { - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, DateTime date, JsonSerializerOptions options) { - if (value == null) - { - writer.WriteNull(); - } - else - { - var date = (DateTime) value; - var dateWithoutTimezone = new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second); - writer.WriteValue(dateWithoutTimezone.ToString("O", CultureInfo.InvariantCulture)); - } + writer.WriteStringValue(new DateTime(date.Ticks, DateTimeKind.Unspecified)); // remove timezone information } - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonToken.Null) - { - if (objectType == typeof(DateTime)) - { - return DateTime.MinValue; - } - else - { - return null; - } - } - else if (reader.TokenType == JsonToken.Date) + if (reader.TokenType == JsonTokenType.String) { - return (DateTime) reader.Value!; - } - else if (reader.TokenType == JsonToken.String - && DateTime.TryParseExact((string)reader.Value!, "O", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)) - { - return date; + var date = reader.GetDateTime(); + return new DateTime(date.Ticks, DateTimeKind.Unspecified); } - throw new JsonSerializationException("The value specified in the JSON could not be converted to DateTime!"); - } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof (DateTime) || objectType == typeof (DateTime?); + throw new JsonException("The value specified in the JSON could not be converted to DateTime!"); } } } diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs index 946d20dca2..381a6fae02 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs @@ -1,86 +1,96 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; using DotVVM.Framework.Utils; -using Newtonsoft.Json; namespace DotVVM.Framework.ViewModel.Serialization { /// /// This converter serializes Dictionary<> as List<KeyValuePair<,>> in order to make dictionaries work with knockout. /// - public class DotvvmDictionaryConverter : JsonConverter + public class DotvvmDictionaryConverter : JsonConverterFactory { - private static Type keyValuePairGenericType = typeof(KeyValuePair<,>); - private static Type listGenericType = typeof(List<>); - private static Type dictionaryEntryType = typeof(DictionaryEntry); - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override bool CanConvert(Type typeToConvert) => + typeToConvert.Implements(typeof(IReadOnlyDictionary<,>)); + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - if (value is not IDictionary dict) + if (typeToConvert.Implements(typeof(IReadOnlyDictionary<,>), out var dictionaryType)) { - writer.WriteNull(); - } - else - { - var attrs = value.GetType().GetGenericArguments(); - var keyValuePair = keyValuePairGenericType.MakeGenericType(attrs); - var listType = listGenericType.MakeGenericType(keyValuePair); - - var itemEnumerator = dict.GetEnumerator(); - - var keyProp = dictionaryEntryType.GetProperty(nameof(DictionaryEntry.Key))!; - var valueProp = dictionaryEntryType.GetProperty(nameof(DictionaryEntry.Value))!; - - var list = Activator.CreateInstance(listType); - var invokeMethod = listType.GetMethod(nameof(List.Add))!; - while (itemEnumerator.MoveNext()) - { - var item = Activator.CreateInstance(keyValuePair, keyProp.GetValue(itemEnumerator.Current), valueProp.GetValue(itemEnumerator.Current)); - invokeMethod.Invoke(list, new[] { item }); - } - - serializer.Serialize(writer, list); + return (JsonConverter?)Activator.CreateInstance(typeof(Converter<,,>).MakeGenericType(dictionaryType.GetGenericArguments().Append(typeToConvert).ToArray())); } + return null; } - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + class Converter : JsonConverter + where TDictionary : IReadOnlyDictionary + where K: notnull { - if (reader.TokenType == JsonToken.Null) + public override void Write(Utf8JsonWriter json, TDictionary value, JsonSerializerOptions options) { - return null; + json.WriteStartArray(); + foreach (var item in value) + { + json.WriteStartObject(); + json.WritePropertyName("Key"u8); + JsonSerializer.Serialize(json, item.Key, options); + json.WritePropertyName("Value"u8); + JsonSerializer.Serialize(json, item.Value, options); + json.WriteEndObject(); + } + json.WriteEndArray(); } - else if (reader.TokenType == JsonToken.StartArray) + public override TDictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - - var attrs = objectType.GetGenericArguments(); - var keyValuePair = keyValuePairGenericType.MakeGenericType(attrs); - var listType = listGenericType.MakeGenericType(keyValuePair); - - var dict = existingValue as IDictionary; - dict ??= (IDictionary)Activator.CreateInstance(objectType)!; - - var keyProp = keyValuePair.GetProperty(nameof(KeyValuePair.Key))!; - var valueProp = keyValuePair.GetProperty(nameof(KeyValuePair.Value))!; - - var value = serializer.Deserialize(reader, listType) as IEnumerable; - if (value is null) throw new Exception($"Could not deserialize object with path '{reader.Path}' as IEnumerable."); - foreach (var item in value) + if (reader.TokenType != JsonTokenType.StartArray) + throw new JsonException($"Expected StartArray, but got {reader.TokenType}."); + reader.Read(); + var dict = new Dictionary(); + while (reader.TokenType != JsonTokenType.EndArray) { - dict[keyProp.GetValue(item)!] = valueProp.GetValue(item); + if (reader.TokenType != JsonTokenType.StartObject) + throw new JsonException($"Expected StartObject, but got {reader.TokenType}."); + reader.Read(); + (K key, V value) item = default; + while (reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType != JsonTokenType.PropertyName) + throw new JsonException($"Expected PropertyName, but got {reader.TokenType}."); + + if (reader.ValueTextEquals("Key"u8)) + { + reader.Read(); + item.key = SystemTextJsonUtils.Deserialize(ref reader, options)!; + reader.Read(); + } + else if (reader.ValueTextEquals("Value"u8)) + { + reader.Read(); + item.value = SystemTextJsonUtils.Deserialize(ref reader, options)!; + reader.Read(); + } + else + { + throw new JsonException($"Unexpected property {reader.GetString()}."); + } + } + dict.Add(item.key!, item.value); + reader.Read(); } - return dict; + reader.Read(); + + if (dict is TDictionary result) + return result; + if (ImmutableDictionary.Empty is TDictionary) + return (TDictionary)(object)dict.ToImmutableDictionary(); + throw new NotSupportedException($"Cannot create instance of {typeToConvert}."); } - return null; } - public override bool CanConvert(Type objectType) - { - return typeof(IDictionary).IsAssignableFrom(objectType) - && ReflectionUtils.ImplementsGenericDefinition(objectType, typeof(IDictionary<,>)); - - } } } diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmEnumConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmEnumConverter.cs new file mode 100644 index 0000000000..749d12d302 --- /dev/null +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmEnumConverter.cs @@ -0,0 +1,381 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using DotVVM.Framework.Utils; + +namespace DotVVM.Framework.ViewModel.Serialization +{ + /// Serializes enums as string, using the , as Newtonsoft.Json did + public class DotvvmEnumConverter : JsonConverterFactory + { + public override bool CanConvert(Type typeToConvert) => + typeToConvert.IsEnum; + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => + (JsonConverter?)CreateConverterGenericMethod.MakeGenericMethod(typeToConvert).Invoke(this, []); + + static MethodInfo CreateConverterGenericMethod = typeof(DotvvmEnumConverter).GetMethod(nameof(CreateConverter), genericParameterCount: 1, []).NotNull(); + public JsonConverter CreateConverter() where TEnum : unmanaged, Enum + { + // if (!ReflectionUtils.EnumInfo.HasEnumMemberField) + // return (JsonConverter)new JsonStringEnumConverter().CreateConverter(typeof(TEnum), options)!; + + var underlyingType = Enum.GetUnderlyingType(typeof(TEnum)); + var isFlags = typeof(TEnum).IsDefined(typeof(FlagsAttribute), false); + var isSigned = underlyingType == typeof(sbyte) || underlyingType == typeof(short) || underlyingType == typeof(int) || underlyingType == typeof(long); + + var fieldList = new List<(TEnum Value, byte[] Name)>(); + var enumToName = new Dictionary(); + foreach (var field in typeof(TEnum).GetFields(BindingFlags.Public | BindingFlags.Static)) + { + if (field.Name == "value__") + continue; + var name = field.GetCustomAttribute()?.Value ?? field.Name; + var nameUtf8 = StringUtils.Utf8.GetBytes(name); + + if (isFlags && name.IndexOfAny([',', ' ']) >= 0) + throw new NotSupportedException("Flags enum cannot have EnumMemberAttribute with comma or a space."); + + var value = (TEnum)field.GetValue(null)!; + fieldList.Add((value, nameUtf8)); + if (!enumToName.ContainsKey(value)) + enumToName.Add(value, nameUtf8); + else + { + if (nameUtf8.Length < enumToName[value].Length || nameUtf8.AsSpan().SequenceCompareTo(enumToName[value]) < 0) + enumToName[value] = nameUtf8; + } + } + var fieldsDedup = enumToName.Select(x => (Value: x.Key, Name: x.Value)) + .OrderByDescending(x => ToBits(x.Value)) + .ToArray(); + + + var maxNameLen = fieldList.Max(x => x.Name.Length); + var nameToEnum = new (TEnum Value, byte[] Name)[maxNameLen + 1][]; + foreach (var field in fieldList.GroupBy(x => x.Name.Length)) // TODO: do we want to allow the client to send the duplicate names? + { + var array = field.ToArray(); + Array.Sort(array, (a, b) => a.Name.AsSpan().SequenceCompareTo(b.Name.AsSpan())); + nameToEnum[field.Key] = array; + } + + ulong allowedBitMap = 0; + if (isFlags) + { + foreach (var field in fieldsDedup) + { + var bits = ToBits(field.Value); + if (bits <= 64) + allowedBitMap |= 1UL << (int)bits; + } + } + else + { + foreach (var field in fieldsDedup) + allowedBitMap |= ToBits(field.Value); + } + + if (isFlags && isSigned) + return new InnerConverter(fieldsDedup, enumToName, nameToEnum, maxNameLen, allowedBitMap); + if (isFlags && !isSigned) + return new InnerConverter(fieldsDedup, enumToName, nameToEnum, maxNameLen, allowedBitMap); + if (!isFlags && isSigned) + return new InnerConverter(fieldsDedup, enumToName, nameToEnum, maxNameLen, allowedBitMap); + if (!isFlags && !isSigned) + return new InnerConverter(fieldsDedup, enumToName, nameToEnum, maxNameLen, allowedBitMap); + throw new NotSupportedException(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static ulong ToBits(TEnum value) where TEnum : unmanaged, Enum + { + if (Unsafe.SizeOf() == 1) + return Unsafe.As(ref value); + if (Unsafe.SizeOf() == 2) + return Unsafe.As(ref value); + if (Unsafe.SizeOf() == 4) + return Unsafe.As(ref value); + if (Unsafe.SizeOf() == 8) + return Unsafe.As(ref value); + throw new NotSupportedException(); + } + + class InnerConverter( + (TEnum Value, byte[] Name)[] fields, // sorted by value (ulong), descending + Dictionary enumToName, + (TEnum Value, byte[] Name)[]?[] nameToEnum, // grouped by length, sorted by name + int maxNameLen, + ulong allowedBitMap // bitmap for first 64 non-flags, or all possible flags combined + ) : JsonConverter + where TEnum : unmanaged, Enum + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static TEnum ReadNumber(ref Utf8JsonReader reader) + { + if (typeof(IsSigned) == typeof(True)) + { + if (Unsafe.SizeOf() == 1) + { + var num = reader.GetSByte(); + return Unsafe.As(ref num); + } + if (Unsafe.SizeOf() == 2) + { + var num = reader.GetInt16(); + return Unsafe.As(ref num); + } + if (Unsafe.SizeOf() == 4) + { + var num = reader.GetInt32(); + return Unsafe.As(ref num); + } + if (Unsafe.SizeOf() == 8) + { + var num = reader.GetInt64(); + return Unsafe.As(ref num); + } + } + else + { + if (Unsafe.SizeOf() == 1) + { + var num = reader.GetByte(); + return Unsafe.As(ref num); + } + if (Unsafe.SizeOf() == 2) + { + var num = reader.GetUInt16(); + return Unsafe.As(ref num); + } + if (Unsafe.SizeOf() == 4) + { + var num = reader.GetUInt32(); + return Unsafe.As(ref num); + } + if (Unsafe.SizeOf() == 8) + { + var num = reader.GetUInt64(); + return Unsafe.As(ref num); + } + } + throw new NotSupportedException(); + } + public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Number) + { + var number = ReadNumber(ref reader); + if (typeof(IsFlags) == typeof(True)) + { + if ((ToBits(number) & ~allowedBitMap) > 0) + ThrowInvalidEnumValue(number); + } + else + { + bool isValid; + if (ToBits(number) <= 64) + isValid = (allowedBitMap & (1UL << (int)ToBits(number))) != 0; + else + isValid = enumToName.ContainsKey(number); + if (!isValid) + ThrowInvalidEnumValue(number); + } + + return number; + } + else if (reader.TokenType == JsonTokenType.String) + { + // TODO: allow numbers in string? + if (typeof(IsFlags) == typeof(False)) + { + Span name = maxNameLen < 512 ? stackalloc byte[maxNameLen + 1] : new byte[maxNameLen + 1]; + var length = reader.CopyString(name); + name = name.Slice(0, length); + return FindEnumName(name); + } + else + { + var valueLength = reader.HasValueSequence ? (int)reader.ValueSequence.Length : reader.ValueSpan.Length; + Span buffer = valueLength < 512 ? stackalloc byte[valueLength] : new byte[valueLength]; + var bufferLength = reader.CopyString(buffer); + buffer = buffer.Slice(0, bufferLength); + + ulong result = 0; + while (true) + { + buffer = buffer.Slice(buffer[0] == ' ' ? 1 : 0); + + var nextIndex = MemoryExtensions.IndexOf(buffer, (byte)','); + if (nextIndex == 0) + return ThrowInvalidEnumName(buffer); + + var token = nextIndex < 0 ? buffer : buffer.Slice(0, nextIndex); + + var value = FindEnumName(token); + result |= ToBits(value); + + if (nextIndex < 0) + break; + buffer = buffer.Slice(nextIndex + 1); + } + + return Unsafe.As(ref result); + } + } + else + return ThrowInvalidToken(reader.TokenType); + } + + TEnum FindEnumName(ReadOnlySpan name) + { + if (name.Length > maxNameLen) + return ThrowInvalidEnumName(name); + var fields = nameToEnum[name.Length]; + if (fields is null) + return ThrowInvalidEnumName(name); + return BinSearch(name, fields, 0, fields.Length); + + static TEnum BinSearch(ReadOnlySpan name, (TEnum Value, byte[] Name)[] fields, int start, int end) + { + TailCall: + if (start == end) + return ThrowInvalidEnumName(name); + + var mid = (start + end) / 2; + var cmp = name.SequenceCompareTo(fields[mid].Name); + if (cmp == 0) + return fields[mid].Value; + if (cmp < 0) + { + // return BinSearch(name, fields, start, mid); + end = mid; + goto TailCall; + } + else + { + // return BinSearch(name, fields, mid + 1, end); + start = mid + 1; + goto TailCall; + } + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowInvalidEnumValue(TEnum value) + { + throw new JsonException($"'{value}' is not valid {typeof(TEnum).Name}."); + } + [MethodImpl(MethodImplOptions.NoInlining)] + static TEnum ThrowInvalidEnumName(ReadOnlySpan token) + { + throw new JsonException($"'{StringUtils.Utf8.GetString(token.ToArray())}' is not a member of {typeof(TEnum).Name}."); + } + [MethodImpl(MethodImplOptions.NoInlining)] + static TEnum ThrowInvalidToken(JsonTokenType token) + { + throw new JsonException($"'{token}' cannot be parsed as enum."); + } + static void WriteAsNumber(Utf8JsonWriter writer, TEnum value) + { + if (typeof(IsSigned) == typeof(True)) + { + if (Unsafe.SizeOf() == 1) + writer.WriteNumberValue(Unsafe.As(ref value)); + else if (Unsafe.SizeOf() == 2) + writer.WriteNumberValue(Unsafe.As(ref value)); + else if (Unsafe.SizeOf() == 4) + writer.WriteNumberValue(Unsafe.As(ref value)); + else if (Unsafe.SizeOf() == 8) + writer.WriteNumberValue(Unsafe.As(ref value)); + else + throw new NotSupportedException(); + } + else + { + if (Unsafe.SizeOf() == 1) + writer.WriteNumberValue(Unsafe.As(ref value)); + else if (Unsafe.SizeOf() == 2) + writer.WriteNumberValue(Unsafe.As(ref value)); + else if (Unsafe.SizeOf() == 4) + writer.WriteNumberValue(Unsafe.As(ref value)); + else if (Unsafe.SizeOf() == 8) + writer.WriteNumberValue(Unsafe.As(ref value)); + else + throw new NotSupportedException(); + } + } + public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) + { + if (enumToName.TryGetValue(value, out var name)) + { + writer.WriteStringValue(name); + return; + } + + if (typeof(IsFlags) == typeof(False)) + { + WriteAsNumber(writer, value); // TODO: do we want to allow the numbers? + } + else + { + var bits = ToBits(value); + if (bits == 0) + { + // if none field existed, we have used it above, see if (enumToName.TryGetValue... + writer.WriteNumberValue(0); + return; + } + + Span buffer = stackalloc byte[512]; + var bufferPosition = 0; + + foreach (var flag in fields) + { + var flagBits = ToBits(flag.Value); + if ((flagBits & ~bits) != 0) + continue; // this flag contains something else, we can't use it + + bits &= ~flagBits; + + if (buffer.Length <= bufferPosition + flag.Name.Length + 2) + { // make sure the output buffer is large enough + var newSize = Math.Max(buffer.Length * 2, bufferPosition + flag.Name.Length + 2); + var newBuffer = new byte[newSize]; + buffer.Slice(0, bufferPosition).CopyTo(newBuffer); + buffer = newBuffer; + } + + if (bufferPosition > 0) + { // insert ', ' + buffer[bufferPosition++] = (byte)','; + if (writer.Options.Indented) + buffer[bufferPosition++] = (byte)' '; + } + + flag.Name.CopyTo(buffer.Slice(bufferPosition)); + bufferPosition += flag.Name.Length; + + if (bits == 0) + break; + } + + if (bits == 0) + writer.WriteStringValue(buffer.Slice(0, bufferPosition)); + else + // it isn't possible to set the required flags using names + WriteAsNumber(writer, value); + } + } + } + + struct True { } + struct False { } + + // struct Comparable + } +} diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmTimeOnlyConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmTimeOnlyConverter.cs index 2a03f596c3..0636c52bc6 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmTimeOnlyConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmTimeOnlyConverter.cs @@ -1,54 +1,28 @@ using System; using System.Globalization; -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Serialization; namespace DotVVM.Framework.ViewModel.Serialization { - public class DotvvmTimeOnlyConverter : JsonConverter + public class DotvvmTimeOnlyConverter : JsonConverter { - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, TimeOnly date, JsonSerializerOptions options) { - if (value == null) - { - writer.WriteNull(); - } - else - { - var date = (TimeOnly)value; - var dateWithoutTimezone = new TimeOnly(date.Hour, date.Minute, date.Second); - writer.WriteValue(dateWithoutTimezone.ToString("O", CultureInfo.InvariantCulture)); - } + var dateWithoutTimezone = new TimeOnly(date.Ticks); + writer.WriteStringValue(dateWithoutTimezone.ToString("O", CultureInfo.InvariantCulture)); // TODO: utf8 } - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonToken.Null) - { - if (objectType == typeof(TimeOnly)) - { - return TimeOnly.MinValue; - } - else - { - return null; - } - } - else if (reader.TokenType == JsonToken.Date) - { - return (TimeOnly)reader.Value!; - } - else if (reader.TokenType == JsonToken.String - && TimeOnly.TryParseExact((string)reader.Value!, "O", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)) + if (reader.TokenType == JsonTokenType.String + && TimeOnly.TryParseExact(reader.GetString(), "O", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)) { return date; } - throw new JsonSerializationException("The value specified in the JSON could not be converted to DateTime!"); + throw new Exception("The value specified in the JSON could not be converted to DateTime!"); } - public override bool CanConvert(Type objectType) - { - return objectType == typeof(TimeOnly) || objectType == typeof(TimeOnly?); - } } } diff --git a/src/Framework/Framework/ViewModel/Serialization/EncryptedValuesReader.cs b/src/Framework/Framework/ViewModel/Serialization/EncryptedValuesReader.cs index 7518df4445..dabd8059b4 100644 --- a/src/Framework/Framework/ViewModel/Serialization/EncryptedValuesReader.cs +++ b/src/Framework/Framework/ViewModel/Serialization/EncryptedValuesReader.cs @@ -3,29 +3,35 @@ using System.Diagnostics; using System.Linq; using System.Security; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; namespace DotVVM.Framework.ViewModel.Serialization { public class EncryptedValuesReader { - Stack<(int prop, JObject? obj)> stack = new(); + Stack<(int prop, JsonObject? obj)> stack = new(); int virtualNests = 0; int lastPropertyIndex = -1; public bool Suppressed { get; private set; } = false; - public EncryptedValuesReader(JObject json) + public EncryptedValuesReader(JsonObject json) { stack.Push((0, json)); } - private JObject? json => stack.Peek().obj; + private JsonObject? json => stack.Peek().obj; - private JProperty? Property(int index) + private bool Property(int index, out JsonNode? node) { var name = index.ToString(); - return virtualNests == 0 ? json?.Property(name) : null; + if (virtualNests > 0 || json is null) + { + node = null; + return false; + } + + return json.TryGetPropertyValue(name, out node); } public void Nest() => Nest(lastPropertyIndex + 1); @@ -35,18 +41,17 @@ public void Nest(int property) if (Suppressed) return; - var prop = Property(property); - if (prop != null) + if (Property(property, out var prop)) { - Debug.Assert(prop.Value.Type == JTokenType.Object); + Debug.Assert(prop is JsonObject, $"Unexpected prop {property}: {prop}"); + json?.Remove(property.ToString()); } else { virtualNests++; } - stack.Push((property, (JObject?)prop?.Value)); // remove read nodes and then make sure that JObject is empty - prop?.Remove(); + stack.Push((property, (JsonObject?)prop)); lastPropertyIndex = -1; } @@ -61,7 +66,7 @@ public void AssertEnd() } else { - if (json!.Properties().Count() > 0) + if (json?.Count > 0) throw SecurityError(); } lastPropertyIndex = stack.Pop().prop; @@ -79,14 +84,13 @@ public void EndSuppress() Suppressed = false; } - public JToken ReadValue(int property) + public JsonNode? ReadValue(int property) { if (Suppressed) throw SecurityError(); - var prop = Property(property); - if (prop == null) throw SecurityError(); - prop.Remove(); - return prop.Value; + if (!Property(property, out var prop)) throw SecurityError(); + json!.Remove(property.ToString()); + return prop; } Exception SecurityError() => new SecurityException("Failed to deserialize viewModel encrypted values"); diff --git a/src/Framework/Framework/ViewModel/Serialization/EncryptedValuesWriter.cs b/src/Framework/Framework/ViewModel/Serialization/EncryptedValuesWriter.cs index c150353b19..ae34597eab 100644 --- a/src/Framework/Framework/ViewModel/Serialization/EncryptedValuesWriter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/EncryptedValuesWriter.cs @@ -1,25 +1,25 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using DotVVM.Framework.Configuration; -using Newtonsoft.Json; namespace DotVVM.Framework.ViewModel.Serialization { public class EncryptedValuesWriter { - JsonWriter writer; - JsonSerializer serializer; + Utf8JsonWriter writer; + JsonSerializerOptions options; Stack propertyIndices = new Stack(); int virtualNests = 0; int lastPropertyIndex = -1; int suppress = 0; public int SuppressedLevel => suppress; - public EncryptedValuesWriter(JsonWriter jsonWriter) + public EncryptedValuesWriter(Utf8JsonWriter jsonWriter) { this.writer = jsonWriter; - serializer = JsonSerializer.Create(DefaultSerializerSettingsProvider.Instance.Settings); + this.options = DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe; } public void Nest() => Nest(lastPropertyIndex + 1); @@ -87,9 +87,19 @@ private void EnsureObjectStarted() { if (virtualNests > 0) { + bool first = true; foreach (var p in propertyIndices.Take(virtualNests).Reverse()) { - WritePropertyName(p); // the property was not written, -1 to write it + if (first && virtualNests == propertyIndices.Count) + { + // no wrapper object + } + else + { + WritePropertyName(p); // the property was not written, -1 to write it + } + first = false; + writer.WriteStartObject(); } virtualNests = 0; @@ -108,7 +118,7 @@ public void WriteValue(int propertyIndex, object value) EnsureObjectStarted(); WritePropertyName(propertyIndex); lastPropertyIndex = propertyIndex; - serializer.Serialize(writer, value); + JsonSerializer.Serialize(writer, value, options); } } } diff --git a/src/Framework/Framework/ViewModel/Serialization/IViewModelSerializationMapper.cs b/src/Framework/Framework/ViewModel/Serialization/IViewModelSerializationMapper.cs index 057c64efd8..0a51c18343 100644 --- a/src/Framework/Framework/ViewModel/Serialization/IViewModelSerializationMapper.cs +++ b/src/Framework/Framework/ViewModel/Serialization/IViewModelSerializationMapper.cs @@ -9,6 +9,7 @@ namespace DotVVM.Framework.ViewModel.Serialization public interface IViewModelSerializationMapper { ViewModelSerializationMap GetMap(Type type); + ViewModelSerializationMap GetMap(); ViewModelSerializationMap GetMapByTypeId(string typeId); } diff --git a/src/Framework/Framework/ViewModel/Serialization/IViewModelSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/IViewModelSerializer.cs index 5f57d4ba56..460e7ca386 100644 --- a/src/Framework/Framework/ViewModel/Serialization/IViewModelSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/IViewModelSerializer.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Text.Json; using DotVVM.Framework.Controls.Infrastructure; using DotVVM.Framework.Hosting; using DotVVM.Framework.Runtime.Filters; @@ -7,19 +9,15 @@ namespace DotVVM.Framework.ViewModel.Serialization { public interface IViewModelSerializer { - void BuildViewModel(IDotvvmRequestContext context, object? commandResult); + JsonSerializerOptions ViewModelJsonOptions { get; } + ReadOnlyMemory BuildStaticCommandResponse(IDotvvmRequestContext context, object? commandResult, string[]? knownTypeMetadata = null); - string 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); - string SerializeViewModel(IDotvvmRequestContext context); + byte[] SerializeModelState(IDotvvmRequestContext context); - string SerializeModelState(IDotvvmRequestContext context); + void PopulateViewModel(IDotvvmRequestContext context, ReadOnlyMemory serializedPostData); - void PopulateViewModel(IDotvvmRequestContext context, string serializedPostData); - - ActionInfo? ResolveCommand(IDotvvmRequestContext context, DotvvmView view); - - void AddPostBackUpdatedControls(IDotvvmRequestContext context, IEnumerable<(string name, string html)> postbackUpdatedControls); - void AddNewResources(IDotvvmRequestContext context); + ActionInfo ResolveCommand(IDotvvmRequestContext context, DotvvmView view); } } diff --git a/src/Framework/Framework/ViewModel/Serialization/IViewModelServerCache.cs b/src/Framework/Framework/ViewModel/Serialization/IViewModelServerCache.cs index bbf544034d..5cd378a9d4 100644 --- a/src/Framework/Framework/ViewModel/Serialization/IViewModelServerCache.cs +++ b/src/Framework/Framework/ViewModel/Serialization/IViewModelServerCache.cs @@ -1,19 +1,20 @@ using System; using System.Collections.Generic; +using System.IO; using System.Security.Cryptography; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using DotVVM.Framework.Hosting; -using Newtonsoft.Json.Linq; namespace DotVVM.Framework.ViewModel.Serialization { public interface IViewModelServerCache { - string StoreViewModel(IDotvvmRequestContext context, JObject viewModelToken); + string StoreViewModel(IDotvvmRequestContext context, Stream data); - JObject TryRestoreViewModel(IDotvvmRequestContext context, string viewModelCacheId, JObject viewModelDiffToken); + JsonElement TryRestoreViewModel(IDotvvmRequestContext context, string viewModelCacheId, JsonElement viewModelDiffToken); } } diff --git a/src/Framework/Framework/ViewModel/Serialization/IViewModelTypeMetadataSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/IViewModelTypeMetadataSerializer.cs index 0cc323c663..086f12be0a 100644 --- a/src/Framework/Framework/ViewModel/Serialization/IViewModelTypeMetadataSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/IViewModelTypeMetadataSerializer.cs @@ -1,11 +1,12 @@ -using System.Collections.Generic; -using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Text.Json; namespace DotVVM.Framework.ViewModel.Serialization { public interface IViewModelTypeMetadataSerializer { - JObject SerializeTypeMetadata(IEnumerable usedSerializationMaps, ISet? knownTypeIds = null); + void SerializeTypeMetadata(IEnumerable usedSerializationMaps, Utf8JsonWriter json, ReadOnlySpan propertyName, ISet? knownTypeIds = null); } } diff --git a/src/Framework/Framework/ViewModel/Serialization/SerialiationMapperAttributeHelper.cs b/src/Framework/Framework/ViewModel/Serialization/SerialiationMapperAttributeHelper.cs new file mode 100644 index 0000000000..2235fbc913 --- /dev/null +++ b/src/Framework/Framework/ViewModel/Serialization/SerialiationMapperAttributeHelper.cs @@ -0,0 +1,31 @@ +using System; +using System.Reflection; +using STJ = System.Text.Json.Serialization; +using NJ = Newtonsoft.Json; + +namespace DotVVM.Framework.ViewModel.Serialization +{ + public static class SerialiationMapperAttributeHelper + { + static readonly Type? JsonConstructorNJ = Type.GetType("Newtonsoft.Json.JsonConstructorAttribute, Newtonsoft.Json"); + static readonly Type? JsonIgnoreNJ = Type.GetType("Newtonsoft.Json.JsonIgnoreAttribute, Newtonsoft.Json"); + static readonly Type? JsonPropertyNJ = Type.GetType("Newtonsoft.Json.JsonPropertyAttribute, Newtonsoft.Json"); + static readonly Type? JsonConverterNJ = Type.GetType("Newtonsoft.Json.JsonConverterAttribute, Newtonsoft.Json"); + + public static bool IsJsonConstructor(MethodBase constructor) => + constructor.IsDefined(typeof(STJ.JsonConstructorAttribute)) || + (JsonConstructorNJ is { } && constructor.IsDefined(JsonConstructorNJ)); + + public static bool IsJsonIgnore(MemberInfo member) + { + if (JsonIgnoreNJ is {} && member.IsDefined(JsonIgnoreNJ)) + return true; + if (member.GetCustomAttribute() is {} ignore) + return ignore.Condition == STJ.JsonIgnoreCondition.Always; + return false; + } + + public static bool HasNewtonsoftJsonConvert(MemberInfo member) => + JsonConverterNJ is { } && member.IsDefined(JsonConverterNJ); + } +} diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs index 65e34b1ceb..65bbbe988c 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs @@ -3,53 +3,32 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using DotVVM.Framework.Configuration; using System.Reflection; using DotVVM.Framework.Utils; using System.Security; using System.Diagnostics; +using System.Text.Json; +using System.IO; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using System.Security.Cryptography.X509Certificates; +using DotVVM.Framework.Controls; namespace DotVVM.Framework.ViewModel.Serialization { /// /// A JSON.NET converter that handles special features of DotVVM ViewModel serialization. /// - public class ViewModelJsonConverter : JsonConverter + public class ViewModelJsonConverter : JsonConverterFactory { private readonly IViewModelSerializationMapper viewModelSerializationMapper; - public ViewModelJsonConverter(bool isPostback, IViewModelSerializationMapper viewModelSerializationMapper, IServiceProvider services, JObject? encryptedValues = null) + public ViewModelJsonConverter(IViewModelSerializationMapper viewModelSerializationMapper) { - Services = services; - IsPostback = isPostback; - EncryptedValues = encryptedValues ?? new JObject(); - evReader = new Lazy(() => { - evWriter = new Lazy(() => { throw new Exception("Can't use EncryptedValuesWriter at the same time as EncryptedValuesReader."); }); - return new EncryptedValuesReader(EncryptedValues); - }); - evWriter = new Lazy(() => { - evReader = new Lazy(() => { throw new Exception("Can't use EncryptedValuesReader at the same time as EncryptedValuesWriter."); }); - return new EncryptedValuesWriter(EncryptedValues.CreateWriter()); - }); this.viewModelSerializationMapper = viewModelSerializationMapper; } - - public JObject EncryptedValues { get; } - private Lazy evReader; - private Lazy evWriter; - - - public HashSet UsedSerializationMaps { get; } = new(); - public IServiceProvider Services { get; } - public bool IsPostback { get; private set; } - - private ViewModelSerializationMap GetSerializationMapForType(Type type) - { - return viewModelSerializationMapper.GetMap(type); - } - public static bool CanConvertType(Type type) => !ReflectionUtils.IsEnumerable(type) && ReflectionUtils.IsComplexType(type) && @@ -64,101 +43,209 @@ public override bool CanConvert(Type objectType) return CanConvertType(objectType); } - /// - /// Reads the JSON representation of the object. - /// - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => CreateConverter(typeToConvert); + public JsonConverter CreateConverter(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)); + + 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); + } + public class VMConverter(ViewModelJsonConverter factory): JsonConverter, IVMConverter { - if (existingValue is {}) + 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) { - Debug.Assert(objectType.IsInstanceOfType(existingValue)); + if (state is null) + throw new ArgumentNullException(nameof(state), "DotvvmSerializationState must be created before calling the ViewModelJsonConverter."); - // the existingValue might be more specific type than objectType. - // this important for deserialization of IPagingOptions which are pre-populated with PagingOptions - // otherwise, we'd not be able to deserialize the interface, because it has no constructor + if (reader.TokenType == JsonTokenType.Null) + { + Debug.Assert(!typeof(T).IsValueType); + return default; + } - objectType = existingValue.GetType(); - } - // handle null keyword - if (reader.TokenType == JsonToken.Null) - { - if (objectType.GetTypeInfo().IsValueType) - throw new InvalidOperationException(string.Format("Received NULL for value type. Path: " + reader.Path)); + ReadObjectStart(ref reader); + var evSuppressed = state.EVReader!.Suppressed; - return null; + try + { + // deserialize + var result = SerializationMap.ReaderFactory.Invoke(ref reader, options, default!, false, state.EVReader, state); + ReadEndObject(ref reader); + return result; + } + finally + { + // safety check: we are not leaking suppressed reader accidentally + if (evSuppressed != state.EVReader.Suppressed) + { + // read everything to prevent any further deserialization + while (reader.Read()) + ; + throw new SecurityException("encrypted values state corrupted."); + } + } } - var evSuppressed = evReader.Value.Suppressed; + static void ReadObjectStart(ref Utf8JsonReader reader) + { + if (reader.TokenType == JsonTokenType.None) reader.Read(); + if (reader.TokenType != JsonTokenType.StartObject) + throw new JsonException($"Expected StartObject token, but reader.TokenType={reader.TokenType}"); + reader.Read(); + } + static void ReadEndObject(ref Utf8JsonReader reader) + { + if (reader.TokenType != JsonTokenType.EndObject) + throw new JsonException($"Expected EndObject token, but reader.TokenType={reader.TokenType}"); + } - try + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => + this.Write(writer, value, options, DotvvmSerializationState.Current!); + public void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options, DotvvmSerializationState state, bool requireTypeField = true) { - // deserialize - var serializationMap = GetSerializationMapForType(objectType); - return serializationMap.ReaderFactory(reader, serializer, existingValue, evReader.Value, Services); + if (state is null) + throw new ArgumentNullException(nameof(state), "DotvvmSerializationState must be created before calling the ViewModelJsonConverter."); + if (value is null) + { + writer.WriteNullValue(); + return; + } + var evSuppressLevel = state.EVWriter!.SuppressedLevel; + try + { + state.UsedSerializationMaps.Add(SerializationMap); + + SerializationMap.WriterFactory.Invoke(writer, value, options, requireTypeField, state.EVWriter, state); + } + finally + { + // safety check: we are not leaking suppressed reader accidentally + if (evSuppressLevel != state.EVWriter.SuppressedLevel) + { + writer.Dispose(); // make sure nothing else is written + throw new SecurityException("encrypted values state corrupted."); + } + } } - finally + + /// + /// 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) { - // safety check: we are not leaking suppressed reader accidentally - if (evSuppressed != evReader.Value.Suppressed) + if (state is null) + throw new ArgumentNullException(nameof(state), "DotvvmSerializationState must be created before calling the ViewModelJsonConverter."); + + if (reader.TokenType == JsonTokenType.Null) + { + Debug.Assert(!typeof(T).IsValueType); + return default; + } + ReadObjectStart(ref reader); + var evSuppressed = state.EVReader!.Suppressed; + try + { + var result = SerializationMap.ReaderFactory.Invoke(ref reader, options, value, value is not null, state.EVReader, state); + ReadEndObject(ref reader); + return result; + } + finally { - // Newtonsoft.Json may catch and consume the exception - kill the reader to be sure that deserialization cannot continue - reader.Close(); - throw new SecurityException("encrypted values state corrupted."); + // safety check: we are not leaking suppressed reader accidentally + if (evSuppressed != state.EVReader.Suppressed) + { + // read everything to prevent any further deserialization + while (reader.Read()) + ; + throw new SecurityException("encrypted values state corrupted."); + } } } + + 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); + public void WriteUntyped(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, DotvvmSerializationState state) => + this.Write(writer, (T)value!, options, state); } + } - /// - /// Writes the JSON representation of the object. - /// - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public class DotvvmSerializationState: IDisposable + { + [ThreadStatic] + private static DotvvmSerializationState? current; + + internal static DotvvmSerializationState? Current => current; + + internal static DotvvmSerializationState Create( + bool isPostback, + IServiceProvider services, + JsonObject? readEncryptedValues = null) { - if (value == null) - { - writer.WriteNull(); - return; - } - var evSuppressLevel = evWriter.Value.SuppressedLevel; - try - { - var serializationMap = GetSerializationMapForType(value.GetType()); - UsedSerializationMaps.Add(serializationMap); - serializationMap.WriterFactory(writer, value, serializer, evWriter.Value, IsPostback); - } - finally + if (current is not null) + throw new InvalidOperationException("ThreadStatic DotvvmSerializationState is already set."); + return current = new DotvvmSerializationState(isPostback, services, readEncryptedValues); + } + + + public IServiceProvider Services { get; } + public bool IsPostback { get; } + public JsonObject? ReadEncryptedValues { get; } + public EncryptedValuesReader? EVReader { get; } + public EncryptedValuesWriter? EVWriter { get; } + private MemoryStream? writeEncryptedValuesData; + private Utf8JsonWriter? writeEncryptedValuesWriter; + public HashSet UsedSerializationMaps { get; } = new(); + + public MemoryStream? WriteEncryptedValues + { + get { - // safety check: we are not leaking suppressed reader accidentally - if (evSuppressLevel != evWriter.Value.SuppressedLevel) - { - // Newtonsoft.Json may catch and consume the exception - kill the writer to be sure that serialization cannot continue - writer.Close(); - throw new SecurityException("encrypted values state corrupted."); - } + this.writeEncryptedValuesWriter?.Flush(); + return writeEncryptedValuesData; } } - /// - /// Populates the specified JObject. - /// - public virtual object Populate(JsonReader reader, JsonSerializer serializer, object value) + + private DotvvmSerializationState(bool isPostback, IServiceProvider services, JsonObject? readEncryptedValues) { - var evSuppressed = evReader.Value.Suppressed; - try + Services = services; + IsPostback = isPostback; + ReadEncryptedValues = readEncryptedValues; + if (readEncryptedValues is not null) { - if (reader.TokenType == JsonToken.None) reader.Read(); - var serializationMap = GetSerializationMapForType(value.GetType()); - return serializationMap.ReaderFactory(reader, serializer, value, evReader.Value, Services); + EVReader = new EncryptedValuesReader(readEncryptedValues); } - finally + else { - // safety check: we are not leaking suppressed reader accidentally - if (evSuppressed != evReader.Value.Suppressed) - { - // Newtonsoft.Json may catch and consume the exception - kill the reader to be sure that deserialization cannot continue - reader.Close(); - throw new SecurityException("encrypted values state corrupted."); - } + writeEncryptedValuesData = new MemoryStream(); + writeEncryptedValuesWriter = new Utf8JsonWriter(writeEncryptedValuesData, new JsonWriterOptions { Indented = false, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + EVWriter = new EncryptedValuesWriter(writeEncryptedValuesWriter); } + + } + + public void Dispose() + { + if (current != this) + throw new InvalidOperationException("ThreadStatic DotvvmSerializationState is different."); + current = null; } } } diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelMapperHelper.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelMapperHelper.cs index fc7cdb2aef..8626e387fd 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelMapperHelper.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelMapperHelper.cs @@ -1,5 +1,4 @@ -using Newtonsoft.Json; -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -22,9 +21,17 @@ public static IViewModelSerializationMapper Map(this IViewModelSerializationMapp return mapper; } + public static IViewModelSerializationMapper Map(this IViewModelSerializationMapper mapper, Action> action) + { + var map = mapper.GetMap(); + action(map); + map.ResetFunctions(); + return mapper; + } + public static void SetConstructor(this ViewModelSerializationMap map, ObjectFactory factory) { - map.SetConstructor(p => factory.Invoke(p, new object[0])); + map.SetConstructorUntyped(p => factory.Invoke(p, [])); } public static void AllowDependencyInjection(this ViewModelSerializationMap map) @@ -69,7 +76,7 @@ public static ViewModelPropertyMap AddClientExtender(this ViewModelPropertyMap p return property; } - public static ViewModelPropertyMap SetJsonConverter(this ViewModelPropertyMap property, JsonConverter converter) + public static ViewModelPropertyMap SetJsonConverter(this ViewModelPropertyMap property, System.Text.Json.Serialization.JsonConverter converter) { property.JsonConverter = converter; return property; diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelPropertyMap.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelPropertyMap.cs index 0cc8682073..3dee1ac2d2 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelPropertyMap.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelPropertyMap.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text.Json.Serialization; using DotVVM.Framework.Compilation; using DotVVM.Framework.ViewModel.Validation; -using Newtonsoft.Json; namespace DotVVM.Framework.ViewModel.Serialization { @@ -25,7 +25,7 @@ public ViewModelPropertyMap(PropertyInfo propertyInfo, string name, ProtectMode public PropertyInfo PropertyInfo { get; set; } - /// Property name, as seen in the serialized JSON and client-side. Note that it will be different than `PropertyInfo.Name`, if `[Bind(Name = X)]` or `[JsonProperty(X)]` is used. + /// Property name, as seen in the serialized JSON and client-side. Note that it will be different than `PropertyInfo.Name`, if `[Bind(Name = X)]` or `[JsonPropertyName(X)]` is used. public string Name { get; set; } /// Client extenders which will be applied to the created knockout observable. @@ -44,6 +44,8 @@ public ViewModelPropertyMap(PropertyInfo propertyInfo, string name, ProtectMode public bool TransferFirstRequest { get; set; } /// When true, an existing object in this property will be preserved during deserialization. A new object will only be created if the property is null, or if we need to call the constructor to set some properties. public bool Populate { get; set; } + /// If true, DotVVM serializer will use JSON converter for the runtime type, instead of resolving one statically + public bool AllowDynamicDispatch { get; set; } /// List of validation rules (~= validation attributes) on this property. Includes rules which can't be run client-side public List ValidationRules { get; } = new(); diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs index ebc8e1ede5..89d5adbbf4 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Linq.Expressions; using DotVVM.Framework.Utils; -using Newtonsoft.Json; using System.Reflection; using DotVVM.Framework.Configuration; using FastExpressionCompiler; @@ -11,18 +10,25 @@ using System.Collections.Immutable; using System.Diagnostics; using Microsoft.Extensions.DependencyInjection; +using System.IO; +using System.Text.Json; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; +using System.Text.Json.Nodes; +using DotVVM.Framework.Compilation.Binding; namespace DotVVM.Framework.ViewModel.Serialization { /// /// Performs the JSON serialization for specified type. /// - public class ViewModelSerializationMap + public abstract class ViewModelSerializationMap { - private readonly DotvvmConfiguration configuration; + protected readonly DotvvmConfiguration configuration; + protected readonly ViewModelJsonConverter viewModelJsonConverter; - public delegate object ReaderDelegate(JsonReader reader, JsonSerializer serializer, object? existingValue, EncryptedValuesReader encryptedValuesReader, IServiceProvider services); - public delegate void WriterDelegate(JsonWriter writer, object obj, JsonSerializer serializer, EncryptedValuesWriter evWriter, bool isPostback); + public delegate T ReaderDelegate(ref Utf8JsonReader reader, JsonSerializerOptions options, T? existingValue, bool populate, EncryptedValuesReader encryptedValuesReader, DotvvmSerializationState state); + public delegate void WriterDelegate(Utf8JsonWriter writer, T obj, JsonSerializerOptions options, bool requireTypeField, EncryptedValuesWriter evWriter, DotvvmSerializationState state); /// /// Gets or sets the object type for this serialization map. @@ -30,6 +36,7 @@ public class ViewModelSerializationMap public Type Type { get; } public MethodBase? Constructor { get; } public ImmutableArray Properties { get; } + public string ClientTypeId { get; } /// Rough structure of Properties when the object was initialized. This is used for hot reload to judge if it can be flushed from the cache. @@ -38,16 +45,21 @@ public class ViewModelSerializationMap /// /// Initializes a new instance of the class. /// - public ViewModelSerializationMap(Type type, IEnumerable properties, MethodBase? constructor, DotvvmConfiguration configuration) + internal ViewModelSerializationMap(Type type, IEnumerable properties, MethodBase? constructor, DotvvmConfiguration configuration) { this.configuration = configuration; + this.viewModelJsonConverter = configuration.ServiceProvider.GetRequiredService(); Type = type; + ClientTypeId = type.GetTypeHash(); Constructor = constructor; Properties = properties.ToImmutableArray(); OriginalProperties = Properties.Select(p => (p.Name, p.Type, p.BindDirection, p.ViewModelProtection)).ToArray(); ValidatePropertyMap(); } + public static ViewModelSerializationMap Create(Type type, IEnumerable properties, MethodBase? constructor, DotvvmConfiguration configuration) => + (ViewModelSerializationMap)Activator.CreateInstance(typeof(ViewModelSerializationMap<>).MakeGenericType(type), properties, constructor, configuration)!; + private void ValidatePropertyMap() { var hashset = new HashSet(); @@ -61,25 +73,36 @@ private void ValidatePropertyMap() } } - public void ResetFunctions() + public abstract void ResetFunctions(); + public abstract void SetConstructorUntyped(Func constructor); + + } + public sealed class ViewModelSerializationMap : ViewModelSerializationMap + { + public ViewModelSerializationMap(IEnumerable properties, MethodBase? constructor, DotvvmConfiguration configuration): + base(typeof(T), properties, constructor, configuration) + { + } + public override void ResetFunctions() { readerFactory = null; writerFactory = null; } - private ReaderDelegate? readerFactory; + private ReaderDelegate? readerFactory; /// /// Gets the JSON reader factory. /// - public ReaderDelegate ReaderFactory => readerFactory ?? (readerFactory = CreateReaderFactory()); - private WriterDelegate? writerFactory; + public ReaderDelegate ReaderFactory => readerFactory ??= CreateReaderFactory(); + private WriterDelegate? writerFactory; /// /// Gets the JSON writer factory. /// - public WriterDelegate WriterFactory => writerFactory ?? (writerFactory = CreateWriterFactory()); - private Func? constructorFactory; + public WriterDelegate WriterFactory => writerFactory ??= CreateWriterFactory(); + private Func? constructorFactory; - public void SetConstructor(Func constructor) => constructorFactory = constructor; + public void SetConstructor(Func constructor) => constructorFactory = constructor; + public override void SetConstructorUntyped(Func constructor) => constructorFactory = s => (T)constructor(s); /// /// Creates the constructor for this object. @@ -87,7 +110,7 @@ public void ResetFunctions() private Expression CallConstructor(Expression services, Dictionary propertyVariables, bool throwImmediately = false) { if (constructorFactory != null) - return Convert(Invoke(Constant(constructorFactory), services), Type); + return Invoke(Constant(constructorFactory), services); if (Constructor is null && Type.IsValueType) { @@ -145,17 +168,17 @@ private Expression CallConstructor(Expression services, Dictionary /// Creates the reader factory. /// - public ReaderDelegate CreateReaderFactory() + public ReaderDelegate CreateReaderFactory() { var block = new List(); - var reader = Expression.Parameter(typeof(JsonReader), "reader"); - var serializer = Expression.Parameter(typeof(JsonSerializer), "serializer"); - var valueParam = Expression.Parameter(typeof(object), "valueParam"); + var reader = Expression.Parameter(typeof(Utf8JsonReader).MakeByRefType(), "reader"); + var jsonOptions = Expression.Parameter(typeof(JsonSerializerOptions), "jsonOptions"); + var value = Expression.Parameter(typeof(T), "value"); + var allowPopulate = Expression.Parameter(typeof(bool), "allowPopulate"); var encryptedValuesReader = Expression.Parameter(typeof(EncryptedValuesReader), "encryptedValuesReader"); - var servicesParameter = Expression.Parameter(typeof(IServiceProvider), "services"); - var value = Expression.Variable(Type, "value"); + var state = Expression.Parameter(typeof(DotvvmSerializationState), "state"); var currentProperty = Expression.Variable(typeof(string), "currentProperty"); - var readerTmp = Expression.Variable(typeof(JsonReader), "readerTmp"); + var readerTmp = Expression.Variable(typeof(Utf8JsonReader), "readerTmp"); // we first read all values into local variables and only then we either call the constructor or set the properties on the object var propertyVars = Properties @@ -171,7 +194,7 @@ public ReaderDelegate CreateReaderFactory() p.PropertyInfo.IsInitOnly())); // We don't want to clone IDotvvmViewModel automatically, because the user is likely to register this specific instance somewhere - if (alwaysCallConstructor && typeof(IDotvvmViewModel).IsAssignableFrom(Type) && Constructor is {} && !Constructor.IsDefined(typeof(JsonConstructorAttribute))) + if (alwaysCallConstructor && typeof(IDotvvmViewModel).IsAssignableFrom(Type) && Constructor is {} && !SerialiationMapperAttributeHelper.IsJsonConstructor(Constructor)) { var cloneReason = Properties.FirstOrDefault(p => p.TransferToServer && p.PropertyInfo.IsInitOnly()) is {} initProperty @@ -180,21 +203,22 @@ public ReaderDelegate CreateReaderFactory() ? $"property {ctorProperty.Name} must be injected into constructor parameter {ctorProperty.ConstructorParameter!.Name}" : "internal bug"; throw new Exception($"Deserialization of {Type.ToCode()} is not allowed, because it implements IDotvvmViewModel and {cloneReason}. To allow cloning the object on deserialization, mark a constructor with [JsonConstructor]."); } - var constructorCall = CallConstructor(servicesParameter, propertyVars, throwImmediately: alwaysCallConstructor); + var constructorCall = CallConstructor(Property(state, "Services"), propertyVars, throwImmediately: alwaysCallConstructor); // curly brackets are used for variables and methods from the context of this factory method - // value = ({Type})valueParam; - block.Add(Assign(value, - Condition(Equal(valueParam, Constant(null)), - alwaysCallConstructor - ? Default(Type) - : constructorCall, - Convert(valueParam, Type) - ))); + // if (!allowPopulate && !alwaysCallConstructor) + // value = new T() + if (!alwaysCallConstructor) + block.Add(IfThen( + Not(allowPopulate), + Assign(value, constructorCall) + )); // get existing values into the local variables if (propertyVars.Count > 0) { + // if (value != null) + // prop_X = value.X; ... block.Add(IfThen( Type.IsValueType ? Constant(true) : NotEqual(value, Constant(null)), Block( @@ -208,11 +232,7 @@ public ReaderDelegate CreateReaderFactory() // add current object to encrypted values, this is needed because one property can potentially contain more objects (is a collection) block.Add(Expression.Call(encryptedValuesReader, nameof(EncryptedValuesReader.Nest), Type.EmptyTypes)); - // if the reader is in an invalid state, throw an exception - // TODO: Change exception type, just Exception is not exactly descriptive - block.Add(ExpressionUtils.Replace((JsonReader rdr) => rdr.TokenType == JsonToken.StartObject ? rdr.Read() : ExpressionUtils.Stub.Throw(new Exception($"TokenType = StartObject was expected.")), reader)); - - var propertiesSwitch = new List(); + var propertiesSwitch = new List<(string fieldName, Expression readExpression)>(); // iterate through all properties even if they're gonna get skipped // it's important for the index to count with all the properties that viewModel contains because the client will send some of them only sometimes @@ -244,19 +264,13 @@ public ReaderDelegate CreateReaderFactory() Expression readEncryptedValue = Expression.Block( Expression.Assign( readerTmp, - ExpressionUtils.Replace( - (EncryptedValuesReader ev) => ev.ReadValue(propertyIndex).CreateReader(), - encryptedValuesReader).OptimizeConstants() + Call(JsonSerializationCodegenFragments.ReadEncryptedValueMethod, Call(encryptedValuesReader, "ReadValue", Type.EmptyTypes, Expression.Constant(propertyIndex))) ), Expression.Call(encryptedValuesReader, "Suppress", Type.EmptyTypes), + Expression.Assign( propertyVar, - Expression.Convert( - ExpressionUtils.Replace( - (JsonSerializer s, JsonReader reader, object existing) => Deserialize(s, reader, property, existing), - serializer, readerTmp, existingValue), - property.Type) - ).OptimizeConstants() + DeserializePropertyValue(property, readerTmp, propertyVar, jsonOptions, state)) ); readEncryptedValue = Expression.TryFinally( @@ -264,12 +278,9 @@ public ReaderDelegate CreateReaderFactory() Expression.Call(encryptedValuesReader, "EndSuppress", Type.EmptyTypes) ); - // if (!encryptedValuesReader.Suppressed) - block.Add(Expression.IfThen( - Expression.Not(isEVSuppressed), - readEncryptedValue - )); + // ...readEncryptedValue + block.Add(IfThen(Not(isEVSuppressed), readEncryptedValue)); } // propertyBlock is the body of this currentProperty's switch case var propertyblock = new List(); @@ -277,87 +288,64 @@ public ReaderDelegate CreateReaderFactory() if (checkEV) { // encryptedValuesReader.Nest({propertyIndex}); - propertyblock.Add(Expression.Call(encryptedValuesReader, nameof(EncryptedValuesReader.Nest), Type.EmptyTypes, Expression.Constant(propertyIndex))); + propertyblock.Add(Call(encryptedValuesReader, nameof(EncryptedValuesReader.Nest), Type.EmptyTypes, Constant(propertyIndex))); } // existing value is either null or the value {property} depending on property.Populate // value.{property} = ({property.Type})Deserialize(serializer, reader, existing value); - propertyblock.Add( - Expression.Assign( + propertyblock.Add(Assign( propertyVar, - Expression.Convert( - ExpressionUtils.Replace((JsonSerializer s, JsonReader j, object existingValue) => - Deserialize(s, j, property, existingValue), - serializer, reader, existingValue), - property.Type) + DeserializePropertyValue(property, reader, propertyVar, jsonOptions, state) )); // reader.Read(); - propertyblock.Add( - Expression.Call(reader, "Read", Type.EmptyTypes)); + propertyblock.Add(Call(reader, "Read", Type.EmptyTypes)); if (checkEV) { // encryptedValuesReader.AssertEnd(); - propertyblock.Add(Expression.Call(encryptedValuesReader, nameof(EncryptedValuesReader.AssertEnd), Type.EmptyTypes)); + propertyblock.Add(Call(encryptedValuesReader, nameof(EncryptedValuesReader.AssertEnd), Type.EmptyTypes)); } - Expression body = Expression.Block(typeof(void), propertyblock); + Expression body = Block(typeof(void), propertyblock); if (readEncrypted) { // only read the property when the reader is suppressed, otherwise do nothing - body = Expression.IfThenElse( + body = IfThenElse( isEVSuppressed, body, - Expression.Block( - Expression.IfThen( - ExpressionUtils.Replace((JsonReader rdr) => rdr.TokenType == JsonToken.StartArray || rdr.TokenType == JsonToken.StartConstructor || rdr.TokenType == JsonToken.StartObject, reader), - Expression.Call(reader, "Skip", Type.EmptyTypes)), - Expression.Call(reader, "Read", Type.EmptyTypes)) + Expression.Call(JsonSerializationCodegenFragments.IgnoreValueMethod, reader) ); } - // create this currentProperty's switch case - // case {property.Name}: - // {propertyBlock} - propertiesSwitch.Add(Expression.SwitchCase( - body, - Expression.Constant(property.Name) - )); + propertiesSwitch.Add((property.Name, body)); } - // WARNING: the following code is not commented out. It's a transcription of the expression below it. Yes, it's long. - // while(reader reads properties and assigns them to currentProperty) - // { + // while(reader.TokenType == JsonToken.PropertyName && (currentProperty = reader.GetString()) != null && reader.Read()) + block.Add(ExpressionUtils.While( + condition: AndAlso( + NotEqual(Property(reader, "TokenType"), Constant(JsonTokenType.EndObject)), + AndAlso( // TODO: get rid of GetString()... + ReferenceNotEqual(Assign(currentProperty, Call(reader, "GetString", Type.EmptyTypes)), Constant(null, typeof(string))), + Call(reader, "Read", Type.EmptyTypes) + ) + ), // switch(currentProperty) - // { - // {propertiesSwitch} + body: ExpressionUtils.Switch( + condition: currentProperty, + // case "{property}": see above + cases: propertiesSwitch.Select(p => SwitchCase(p.readExpression, Constant(p.fieldName))).ToArray(), // default: - // if(reader.TokenType == JsonToken.StartArray || reader.TokenType == JsonToken.Start) - // { - // reader.Skip(); - // } - // reader.Read(); - // } - // } - block.Add(ExpressionUtils.While( - ExpressionUtils.Replace((JsonReader rdr, string val) => rdr.TokenType == JsonToken.PropertyName && - ExpressionUtils.Stub.Assign(val, rdr.Value as string) != null && - rdr.Read(), reader, currentProperty), - ExpressionUtils.Switch(currentProperty, - Expression.Block(typeof(void), - Expression.IfThen( - ExpressionUtils.Replace((JsonReader rdr) => rdr.TokenType == JsonToken.StartArray || rdr.TokenType == JsonToken.StartConstructor || rdr.TokenType == JsonToken.StartObject, reader), - Expression.Call(reader, "Skip", Type.EmptyTypes)), - Expression.Call(reader, "Read", Type.EmptyTypes)), - propertiesSwitch.ToArray()) - )); + // JsonSerializationCodegenFragments.IgnoreValue(ref reader) + defaultCase: Call(JsonSerializationCodegenFragments.IgnoreValueMethod, reader) + ) + )); // close encrypted values // encryptedValuesReader.AssertEnd(); - block.Add(Expression.Call(encryptedValuesReader, nameof(EncryptedValuesReader.AssertEnd), Type.EmptyTypes)); + block.Add(Call(encryptedValuesReader, nameof(EncryptedValuesReader.AssertEnd), Type.EmptyTypes)); // call the constructor if (alwaysCallConstructor) @@ -380,123 +368,42 @@ public ReaderDelegate CreateReaderFactory() } // return value - block.Add(Convert(value, typeof(object))); + block.Add(value); // build the lambda expression - var ex = Expression.Lambda( - Expression.Block(typeof(object), new[] { value, currentProperty, readerTmp }.Concat(propertyVars.Values), block).OptimizeConstants(), - reader, serializer, valueParam, encryptedValuesReader, servicesParameter); - return ex.CompileFast(flags: CompilerFlags.ThrowOnNotSupportedExpression); + var ex = Lambda>( + Block(typeof(T), [ currentProperty, readerTmp, ..propertyVars.Values ], block).OptimizeConstants(), + reader, jsonOptions, value, allowPopulate, encryptedValuesReader, state); + return ex.CompileFast(flags: CompilerFlags.ThrowOnNotSupportedExpression | CompilerFlags.EnableDelegateDebugInfo); } - private static Dictionary writeValueMethods = - (from method in typeof(JsonWriter).GetMethods(BindingFlags.Public | BindingFlags.Instance) - where method.Name == nameof(JsonWriter.WriteValue) - let parameters = method.GetParameters() - where parameters.Length == 1 - let parameterType = parameters[0].ParameterType - where parameterType != typeof(object) && parameterType != typeof(byte[]) - where parameterType != typeof(DateTime) && parameterType != typeof(DateTime?) - where parameterType != typeof(DateTimeOffset) && parameterType != typeof(DateTimeOffset?) - select new { key = parameterType, value = method } - ).ToDictionary(x => x.key, x => x.value); - - private static Expression GetSerializeExpression(ViewModelPropertyMap property, Expression jsonWriter, Expression value, Expression serializer) - { - if (property.JsonConverter?.CanWrite == true) - { - // maybe use the converter. It can't be easily inlined because polymorphism - return ExpressionUtils.Replace((JsonSerializer s, JsonWriter w, object v) => Serialize(s, w, property, v), serializer, jsonWriter, Expression.Convert(value, typeof(object))); - } - else if (writeValueMethods.TryGetValue(value.Type, out var method)) - { - return Expression.Call(jsonWriter, method, new [] { value }); - } - else - { - return Expression.Call(serializer, "Serialize", Type.EmptyTypes, new [] { jsonWriter, Expression.Convert(value, typeof(object)) }); - } - } - - private static void Serialize(JsonSerializer serializer, JsonWriter writer, ViewModelPropertyMap property, object value) - { - if (property.JsonConverter != null && property.JsonConverter.CanWrite && property.JsonConverter.CanConvert(property.Type)) - { - property.JsonConverter.WriteJson(writer, value, serializer); - } - else - { - serializer.Serialize(writer, value); - } - } - - private static object? Deserialize(JsonSerializer serializer, JsonReader reader, ViewModelPropertyMap property, object? existingValue) - { - if (property.JsonConverter != null && property.JsonConverter.CanRead && property.JsonConverter.CanConvert(property.Type)) - { - return property.JsonConverter.ReadJson(reader, property.Type, existingValue, serializer); - } - else if (existingValue != null && property.Populate) - { - if (reader.TokenType == JsonToken.Null) - return null; - else if (reader.TokenType == JsonToken.StartObject) - { - return serializer.Converters.OfType().First().Populate(reader, serializer, existingValue); - } - else - { - serializer.Populate(reader, existingValue); - return existingValue; - } - } - else - { - if (property.Type.IsValueType && reader.TokenType == JsonToken.Null) - { - return Activator.CreateInstance(property.Type); - } - else - { - return serializer.Deserialize(reader, property.Type); - } - } - } - - /// Gets if this object require $type to be serialized. - public bool RequiredTypeField() => true; // possible optimization - types can be inferred from parent metadata in some cases - /// /// Creates the writer factory. /// - public WriterDelegate CreateWriterFactory() + public WriterDelegate CreateWriterFactory() { var block = new List(); - var writer = Expression.Parameter(typeof(JsonWriter), "writer"); - var valueParam = Expression.Parameter(typeof(object), "valueParam"); - var serializer = Expression.Parameter(typeof(JsonSerializer), "serializer"); - var encryptedValuesWriter = Expression.Parameter(typeof(EncryptedValuesWriter), "encryptedValuesWriter"); - var isPostback = Expression.Parameter(typeof(bool), "isPostback"); - var value = Expression.Variable(Type, "value"); + var writer = Parameter(typeof(Utf8JsonWriter), "writer"); + var value = Parameter(Type, "value"); + var jsonOptions = Parameter(typeof(JsonSerializerOptions), "options"); + var requireTypeField = Parameter(typeof(bool), "requireTypeField"); + var encryptedValuesWriter = Parameter(typeof(EncryptedValuesWriter), "encryptedValuesWriter"); + var dotvvmState = Parameter(typeof(DotvvmSerializationState), "dotvvmState"); - // curly brackets are used for variables and methods from the scope of this factory method - // value = ({Type})valueParam; - block.Add(Expression.Assign(value, Expression.Convert(valueParam, Type))); + var isPostback = Property(dotvvmState, "IsPostback"); + // curly brackets are used for variables and methods from the scope of this factory method // writer.WriteStartObject(); - block.Add(Expression.Call(writer, nameof(JsonWriter.WriteStartObject), Type.EmptyTypes)); + block.Add(Call(writer, nameof(Utf8JsonWriter.WriteStartObject), Type.EmptyTypes)); // encryptedValuesWriter.Nest(); - block.Add(Expression.Call(encryptedValuesWriter, nameof(EncryptedValuesWriter.Nest), Type.EmptyTypes)); + block.Add(Call(encryptedValuesWriter, nameof(EncryptedValuesWriter.Nest), Type.EmptyTypes)); - if (this.RequiredTypeField()) - { - // writer.WritePropertyName("$type"); - block.Add(ExpressionUtils.Replace((JsonWriter w) => w.WritePropertyName("$type"), writer)); - - // serializer.Serialize(writer, value.GetType().FullName) - block.Add(ExpressionUtils.Replace((JsonSerializer s, JsonWriter w, string t) => w.WriteValue(t), serializer, writer, Expression.Constant(Type.GetTypeHash()))); - } + // if (requireTypeField) + // writer.WriteString("$type", "{Type}"); + block.Add(IfThen(requireTypeField, + ExpressionUtils.Replace((Utf8JsonWriter w, string t) => w.WriteString("$type", t), writer, Constant(Type.GetTypeHash())) + )); // go through all properties that should be serialized for (int propertyIndex = 0; propertyIndex < Properties.Length; propertyIndex++) @@ -556,11 +463,11 @@ public WriterDelegate CreateWriterFactory() } // writer.WritePropertyName({property.Name}); - propertyBlock.Add(Expression.Call(writer, nameof(JsonWriter.WritePropertyName), Type.EmptyTypes, + propertyBlock.Add(Expression.Call(writer, nameof(Utf8JsonWriter.WritePropertyName), Type.EmptyTypes, Expression.Constant(property.Name))); // serializer.Serialize(serializer, writer, {property}, (object)value.{property.PropertyInfo.Name}); - propertyBlock.Add(GetSerializeExpression(property, writer, prop, serializer)); + propertyBlock.Add(GetSerializeExpression(property, writer, prop, jsonOptions, dotvvmState)); Expression propertyFinally = Expression.Default(typeof(void)); if (checkEV) @@ -597,15 +504,15 @@ public WriterDelegate CreateWriterFactory() } // writer.WriteEndObject(); - block.Add(ExpressionUtils.Replace(w => w.WriteEndObject(), writer)); + block.Add(ExpressionUtils.Replace(w => w.WriteEndObject(), writer)); // encryptedValuesWriter.End(); block.Add(Expression.Call(encryptedValuesWriter, nameof(EncryptedValuesWriter.End), Type.EmptyTypes)); // compile the expression - var ex = Expression.Lambda( - Expression.Block(new[] { value }, block).OptimizeConstants(), writer, valueParam, serializer, encryptedValuesWriter, isPostback); - return ex.CompileFast(flags: CompilerFlags.ThrowOnNotSupportedExpression); + var ex = Lambda>( + Block(new[] { value }, block).OptimizeConstants(), writer, value, jsonOptions, requireTypeField, encryptedValuesWriter, dotvvmState); + return ex.CompileFast(flags: CompilerFlags.ThrowOnNotSupportedExpression | CompilerFlags.EnableDelegateDebugInfo); } /// @@ -621,6 +528,351 @@ private bool CanContainEncryptedValues(Type type) ); } + private JsonConverter? GetPropertyConverter(ViewModelPropertyMap property, Type type) + { + if (property.JsonConverter is null) + return null; + if (property.JsonConverter is JsonConverterFactory factory) + return factory.CreateConverter(type, DefaultSerializerSettingsProvider.Instance.Settings); + return property.JsonConverter; + } + + private Expression CallPropertyConverterRead(JsonConverter converter, 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) + { + // T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, DotvvmSerializationState state) + // T Populate(ref Utf8JsonReader reader, JsonSerializerOptions options, T value, DotvvmSerializationState state) + if (existingValue is null) + return Call(Constant(converter), "Read", Type.EmptyTypes, reader, Expression.Constant(Type), jsonOptions, dotvvmState); + else + return Call(Constant(converter), "Populate", Type.EmptyTypes, reader, jsonOptions, existingValue, dotvvmState); + } + else + { + var read = Call(Constant(converter), "Read", Type.EmptyTypes, reader, Expression.Constant(Type), jsonOptions); + if (read.Type.IsValueType) + return read; + else + return Condition( + test: Equal(Property(reader, "TokenType"), Constant(JsonTokenType.Null)), + ifTrue: Default(read.Type), + ifFalse: read + ); + } + } + + private Expression CallPropertyConverterWrite(JsonConverter converter, Expression writer, Expression value, Expression jsonOptions, Expression dotvvmState) + { + Debug.Assert(writer.Type == typeof(Utf8JsonWriter)); + 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) + { + return Call(Constant(converter), nameof(ViewModelJsonConverter.VMConverter.Write), Type.EmptyTypes, writer, value, jsonOptions, dotvvmState, Expression.Constant(true)); + } + else + { + return Call(Constant(converter), nameof(ViewModelJsonConverter.VMConverter.Write), Type.EmptyTypes, writer, value, jsonOptions); + } + } + + private Expression? TryDeserializePrimitive(Expression reader, Type type) + { + // Utf8JsonReader readerTest = default; + // readerTest.CopyString( + if (type == typeof(bool)) + return Call(reader, "GetBoolean", Type.EmptyTypes); + if (type == typeof(byte)) + return Call(reader, "GetByte", Type.EmptyTypes); + // if (type == typeof(byte[])) + // return Call(reader, "GetBytesFromBase64", Type.EmptyTypes); + // if (type == typeof(DateTime)) + // return Call(reader, "GetDateTime", Type.EmptyTypes); + // if (type == typeof(DateTimeOffset)) + // return Call(reader, "GetDateTimeOffset", Type.EmptyTypes); + if (type == typeof(decimal)) + return Call(reader, "GetDecimal", Type.EmptyTypes); + if (type == typeof(double)) + return Call(reader, "GetDouble", Type.EmptyTypes); + if (type == typeof(Guid)) + return Call(reader, "GetGuid", Type.EmptyTypes); + if (type == typeof(short)) + return Call(reader, "GetInt16", Type.EmptyTypes); + if (type == typeof(int)) + return Call(reader, "GetInt32", Type.EmptyTypes); + if (type == typeof(long)) + return Call(reader, "GetInt64", Type.EmptyTypes); + if (type == typeof(sbyte)) + return Call(reader, "GetSByte", Type.EmptyTypes); + if (type == typeof(float)) + return Call(reader, "GetSingle", Type.EmptyTypes); + if (type == typeof(string)) + return Call(reader, "GetString", Type.EmptyTypes); + if (type == typeof(ushort)) + return Call(reader, "GetUInt16", Type.EmptyTypes); + if (type == typeof(uint)) + return Call(reader, "GetUInt32", Type.EmptyTypes); + if (type == typeof(ulong)) + return Call(reader, "GetUInt64", Type.EmptyTypes); + return null; + } + + private Expression? TrySerializePrimitive(Expression reader, Expression value) + { + var type = value.Type; + Debug.Assert(!ReflectionUtils.IsNullableType(type)); + // Utf8JsonWriter writer = default; + // writer.WriteValue + // Newtonsoft.Json.JsonWriter nj = default; + if (type == typeof(bool)) + return Call(reader, "WriteBooleanValue", Type.EmptyTypes, value); + if (type == typeof(decimal) || type == typeof(double) || type == typeof(float) || + type == typeof(int) || type == typeof(uint) || type == typeof(long) || type == typeof(ulong)) + return Call(reader, "WriteNumberValue", Type.EmptyTypes, value); + if (type == typeof(short) || type == typeof(ushort) || type == typeof(sbyte) || type == typeof(byte)) + return Call(reader, "WriteNumberValue", Type.EmptyTypes, Convert(value, typeof(int))); + if (type == typeof(string) || type == typeof(Guid)) // TODO: datetime too? + return Call(reader, "WriteStringValue", Type.EmptyTypes, value); + + return null; + } + + private Expression DeserializePropertyValue(ViewModelPropertyMap property, Expression reader, Expression existingValue, Expression jsonOptions, Expression dotvvmState) + { + var type = existingValue.Type; + Debug.Assert(type.UnwrapNullableType() == property.Type.UnwrapNullableType(), $"{type} != {property.Type}, property: {property.PropertyInfo.DeclaringType}.{property.Name}"); + + if (existingValue.Type.IsNullable()) + { + return Condition( + test: Equal(Property(reader, "TokenType"), Constant(JsonTokenType.Null)), + ifTrue: Default(type), + ifFalse: Convert(DeserializePropertyValue(property, reader, existingValue.UnwrapNullable(throwOnNull: false), jsonOptions, dotvvmState), type) + ); + } + if (GetPropertyConverter(property, type) is {} customConverter) + { + return CallPropertyConverterRead(customConverter, reader, jsonOptions, dotvvmState, property.Populate ? existingValue : null); + } + + if (TryDeserializePrimitive(reader, type) is {} primitive) + { + return primitive; + } + + if (this.viewModelJsonConverter.CanConvert(type)) + { + var defaultConverter = this.viewModelJsonConverter.CreateConverter(type); + if (property.AllowDynamicDispatch) + { + 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 + dotvvmState); // DotvvmSerializationState state + } + else + { + return CallPropertyConverterRead(defaultConverter, reader, jsonOptions, dotvvmState, property.Populate ? existingValue : null); + } + } + + 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); + } + } + + private Expression GetSerializeExpression(ViewModelPropertyMap property, Expression writer, Expression value, Expression jsonOptions, Expression dotvvmState) + { + Debug.Assert(jsonOptions.Type == typeof(JsonSerializerOptions)); + Debug.Assert(dotvvmState.Type == typeof(DotvvmSerializationState)); + Debug.Assert(value.Type.UnwrapNullableType() == property.Type.UnwrapNullableType(), $"{value.Type} != {property.Type}"); + + if (ReflectionUtils.IsNullableType(value.Type)) + { + return IfThenElse( + Property(value, "HasValue"), + GetSerializeExpression(property, writer, Property(value, "Value"), jsonOptions, dotvvmState), + Call(writer, "WriteNullValue", Type.EmptyTypes) + ); + } + + if (GetPropertyConverter(property, value.Type) is {} converter) + { + return CallPropertyConverterWrite(converter, writer, value, jsonOptions, dotvvmState); + } + if (TrySerializePrimitive(writer, value) is {} primite) + { + return primite; + } + if (this.viewModelJsonConverter.CanConvert(value.Type)) + { + if (property.AllowDynamicDispatch) + { + // TODO: ?? + // return Call( + // JsonSerializationCodegenFragments.DeserializeViewModelDynamicMethod.MakeGenericMethod(value.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 + // dotvvmState); // DotvvmSerializationState state + } + else + { + var defaultConverter = this.viewModelJsonConverter.CreateConverter(value.Type); + return CallPropertyConverterWrite(defaultConverter, writer, value, jsonOptions, dotvvmState); + } + } + + return Call(JsonSerializationCodegenFragments.SerializeValueMethod.MakeGenericMethod(value.Type), writer, jsonOptions, value, Constant(property.AllowDynamicDispatch)); + } } + internal static class JsonSerializationCodegenFragments + { + public static readonly MethodInfo ReadAssertMethod = typeof(JsonSerializationCodegenFragments).GetMethod(nameof(ReadAssert), BindingFlags.NonPublic | BindingFlags.Static).NotNull(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ReadAssert(ref Utf8JsonReader reader, JsonTokenType tokenType) + { + if (reader.TokenType != tokenType) + ThrowToken(tokenType, reader.TokenType); + else + reader.Read(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowToken(JsonTokenType expected, JsonTokenType actual) + { + throw new Exception($"TokenType = {expected} was expected, but {actual} was found."); + } + + public static readonly MethodInfo SerializeValueMethod = typeof(JsonSerializationCodegenFragments).GetMethod(nameof(SerializeValue), BindingFlags.NonPublic | BindingFlags.Static).NotNull(); + private static void SerializeValue(Utf8JsonWriter writer, JsonSerializerOptions options, TValue? value, bool dynamic) + { + if (value is null) + { + writer.WriteNullValue(); + } + else if (dynamic) + { + JsonSerializer.Serialize(writer, (object)value, options); + } + else + { + JsonSerializer.Serialize(writer, value, options); + } + } + + public static readonly MethodInfo DeserializeValueStaticMethod = typeof(JsonSerializationCodegenFragments).GetMethod(nameof(DeserializeValueStatic), BindingFlags.NonPublic | BindingFlags.Static).NotNull(); + private static TValue? DeserializeValueStatic(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + return SystemTextJsonUtils.Deserialize(ref reader, options); + } + + public static readonly MethodInfo DeserializeValueDynamicMethod = typeof(JsonSerializationCodegenFragments).GetMethod(nameof(DeserializeValueDynamic), BindingFlags.NonPublic | BindingFlags.Static).NotNull(); + private static TValue? DeserializeValueDynamic(ref Utf8JsonReader reader, JsonSerializerOptions options, TValue? existingValue, bool populate, ViewModelJsonConverter factory, DotvvmSerializationState state) + where TValue: class + { + Debug.Assert(!typeof(TValue).IsSealed); + var type = existingValue?.GetType(); + if (type is null || type == typeof(TValue)) + return SystemTextJsonUtils.Deserialize(ref reader, options); + + // we actually have to do the dynamic dispatch + // if ViewModelJsonConverter wants to handle the type, we call it directly to support Populate + // otherwise, just JsonSerializer.Deserialize with the specific type + if (factory.CanConvert(type)) + { + var converter = factory.GetConverterCached(type); + return populate ? (TValue?)converter.PopulateUntyped(ref reader, type, existingValue, options, state) + : (TValue?)converter.ReadUntyped(ref reader, type, options, state); + } + + return (TValue?)JsonSerializer.Deserialize(ref reader, type!, options); + } + + 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) + where TVM: class + { + if (reader.TokenType == JsonTokenType.Null) + return default; + + if (existingValue is null && defaultConverter is null) + { + throw new Exception($"Cannot deserialize {typeof(TVM).ToCode()} dynamically, originalValue is null and type is abstract."); + } + + if (defaultConverter is {} && (existingValue is null || existingValue.GetType() == typeof(TVM))) + { + return populate && existingValue is {} + ? defaultConverter.Populate(ref reader, options, existingValue, state) + : defaultConverter.Read(ref reader, typeof(TVM), options, state); + } + + var converter = factory.GetConverterCached(typeof(TVM)); + return populate ? (TVM?)converter.PopulateUntyped(ref reader, typeof(TVM), existingValue, options, state) + : (TVM?)converter.ReadUntyped(ref reader, typeof(TVM), options, state); + } + + public static readonly MethodInfo ReadEncryptedValueMethod = typeof(JsonSerializationCodegenFragments).GetMethod(nameof(ReadEncryptedValue), BindingFlags.NonPublic | BindingFlags.Static).NotNull(); + static Utf8JsonReader ReadEncryptedValue(JsonNode? node) + { + var data = new MemoryStream(); + using (var writer = new Utf8JsonWriter(data, new JsonWriterOptions { Indented = false, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping })) + { + if (node is {}) + node.WriteTo(writer); + else + writer.WriteNullValue(); + } + var reader = new Utf8JsonReader(data.ToSpan()); + reader.Read(); + return reader; + } + + public static readonly MethodInfo IgnoreValueMethod = typeof(JsonSerializationCodegenFragments).GetMethod(nameof(IgnoreValue), BindingFlags.NonPublic | BindingFlags.Static).NotNull(); + static void IgnoreValue(ref Utf8JsonReader reader) + { + if (reader.TokenType is JsonTokenType.StartArray or JsonTokenType.StartObject) + { + reader.Skip(); + } + reader.Read(); + } + + // private static TValue? DeserializeViewModel(ref Utf8JsonReader reader, ViewModelJsonConverter.VMConverter converter, JsonSerializerOptions options, DotvvmSerializationState state, TValue existingValue, bool allowPopulate) + // { + // if (reader.TokenType == JsonTokenType.Null) + // return default; + + // if (allowPopulate) + // { + // return converter.Populate(ref reader, options, existingValue, state); + // } + // else + // { + // return converter.Read(ref reader, typeof(TValue), options, state); + // } + // } + } } diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs index 9e29efd0ad..e9bda6fc57 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs @@ -3,11 +3,13 @@ using System.Linq; using System.Reflection; using DotVVM.Framework.ViewModel.Validation; -using Newtonsoft.Json; using System.Collections.Concurrent; using DotVVM.Framework.Configuration; using DotVVM.Framework.Utils; using DotVVM.Framework.Runtime; +using Microsoft.Extensions.Logging; +using System.Text.Json.Serialization; +using System.Text.Json; namespace DotVVM.Framework.ViewModel.Serialization { @@ -20,31 +22,38 @@ public class ViewModelSerializationMapper : IViewModelSerializationMapper private readonly IViewModelValidationMetadataProvider validationMetadataProvider; private readonly IPropertySerialization propertySerialization; private readonly DotvvmConfiguration configuration; + private readonly ILogger? logger; public ViewModelSerializationMapper(IValidationRuleTranslator validationRuleTranslator, IViewModelValidationMetadataProvider validationMetadataProvider, - IPropertySerialization propertySerialization, DotvvmConfiguration configuration) + IPropertySerialization propertySerialization, DotvvmConfiguration configuration, ILogger? logger) { this.validationRuleTranslator = validationRuleTranslator; this.validationMetadataProvider = validationMetadataProvider; this.propertySerialization = propertySerialization; this.configuration = configuration; + this.logger = logger; HotReloadMetadataUpdateHandler.SerializationMappers.Add(new(this)); } private readonly ConcurrentDictionary serializationMapCache = new(); public ViewModelSerializationMap GetMap(Type type) => serializationMapCache.GetOrAdd(type.GetTypeHash(), t => CreateMap(type)); + public ViewModelSerializationMap GetMap() => (ViewModelSerializationMap)serializationMapCache.GetOrAdd(typeof(T).GetTypeHash(), _ => CreateMap()); public ViewModelSerializationMap GetMapByTypeId(string typeId) => serializationMapCache[typeId]; /// /// Creates the serialization map for specified type. /// - protected virtual ViewModelSerializationMap CreateMap(Type type) + protected virtual ViewModelSerializationMap CreateMap(Type type) => + (ViewModelSerializationMap)CreateMapGenericMethod.MakeGenericMethod(type).Invoke(this, Array.Empty())!; + static MethodInfo CreateMapGenericMethod = typeof(ViewModelSerializationMapper).GetMethod(nameof(CreateMap), 1, BindingFlags.NonPublic | BindingFlags.Instance, null, [], [])!; + protected virtual ViewModelSerializationMap CreateMap() { + var type = typeof(T); // constructor which takes properties as parameters // if it exists, we always need to recreate the viewmodel var valueConstructor = GetConstructor(type); - return new ViewModelSerializationMap(type, GetProperties(type, valueConstructor), valueConstructor, configuration); + return new ViewModelSerializationMap(GetProperties(type, valueConstructor), valueConstructor, configuration); } protected virtual MethodBase? GetConstructor(Type type) @@ -52,13 +61,13 @@ protected virtual ViewModelSerializationMap CreateMap(Type type) if (ReflectionUtils.IsPrimitiveType(type) || ReflectionUtils.IsEnumerable(type)) return null; - if (type.GetMethods(BindingFlags.Static | BindingFlags.Public).FirstOrDefault(c => c.IsDefined(typeof(JsonConstructorAttribute))) is {} factory) + if (type.GetMethods(BindingFlags.Static | BindingFlags.Public).FirstOrDefault(c => SerialiationMapperAttributeHelper.IsJsonConstructor(c)) is {} factory) return factory; if (type.IsAbstract) return null; - if (type.GetConstructors().FirstOrDefault(c => c.IsDefined(typeof(JsonConstructorAttribute))) is {} ctor) + if (type.GetConstructors().FirstOrDefault(c => SerialiationMapperAttributeHelper.IsJsonConstructor(c)) is {} ctor) return ctor; if (type.GetConstructor(Type.EmptyTypes) is {} emptyCtor) @@ -107,7 +116,7 @@ protected virtual IEnumerable GetProperties(Type type, Met Array.Sort(properties, (a, b) => StringComparer.Ordinal.Compare(a.Name, b.Name)); foreach (var property in properties) { - if (property.IsDefined(typeof(JsonIgnoreAttribute))) continue; + if (SerialiationMapperAttributeHelper.IsJsonIgnore(property)) continue; var ctorParam = ctorParams?.GetValueOrDefault(property.Name); @@ -119,10 +128,11 @@ protected virtual IEnumerable GetProperties(Type type, Met transferToServer: ctorParam is {} || IsSetterSupported(property), transferAfterPostback: property.GetMethod != null && property.GetMethod.IsPublic, transferFirstRequest: property.GetMethod != null && property.GetMethod.IsPublic, - populate: ViewModelJsonConverter.CanConvertType(property.PropertyType) && property.GetMethod != null + populate: (ViewModelJsonConverter.CanConvertType(property.PropertyType) || property.PropertyType == typeof(object)) && property.GetMethod != null ); propertyMap.ConstructorParameter = ctorParam; propertyMap.JsonConverter = GetJsonConverter(property); + propertyMap.AllowDynamicDispatch = property.PropertyType.IsAbstract || property.PropertyType == typeof(object); foreach (ISerializationInfoAttribute attr in property.GetCustomAttributes().OfType()) { @@ -133,6 +143,7 @@ protected virtual IEnumerable GetProperties(Type type, Met if (bindAttribute != null) { propertyMap.Bind(bindAttribute.Direction); + propertyMap.AllowDynamicDispatch = bindAttribute.AllowsDynamicDispatch(propertyMap.AllowDynamicDispatch); } var viewModelProtectionAttribute = property.GetCustomAttribute(); @@ -171,6 +182,10 @@ private static bool IsSetterSupported(PropertyInfo property) var converterType = property.GetCustomAttribute()?.ConverterType; if (converterType == null) { + if (SerialiationMapperAttributeHelper.HasNewtonsoftJsonConvert(property)) + { + this.logger?.LogWarning($"Property {property.DeclaringType?.FullName}.{property.Name} has Newtonsoft.Json.JsonConverterAttribute, which is not supported by DotVVM anymore. Use System.Text.Json.Serialization.JsonConverterAttribute instead."); + } return null; } try diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs index 8849d27f08..ca9b6a1aad 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs @@ -3,14 +3,15 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Reflection; +using System.Text.Json; using System.Threading; using DotVVM.Framework.Configuration; using DotVVM.Framework.Runtime; using DotVVM.Framework.Utils; using FastExpressionCompiler; -using Newtonsoft.Json.Linq; namespace DotVVM.Framework.ViewModel.Serialization { @@ -21,7 +22,7 @@ public class ViewModelTypeMetadataSerializer : IViewModelTypeMetadataSerializer private readonly bool debug; private readonly bool serializeValidationRules; private readonly ConcurrentDictionary cachedObjectMetadata = new ConcurrentDictionary(); - private readonly ConcurrentDictionary cachedEnumMetadata = new ConcurrentDictionary(); + private readonly ConcurrentDictionary cachedEnumMetadata = new(); public ViewModelTypeMetadataSerializer(IViewModelSerializationMapper viewModelSerializationMapper, DotvvmConfiguration? config = null) { @@ -32,39 +33,42 @@ public ViewModelTypeMetadataSerializer(IViewModelSerializationMapper viewModelSe HotReloadMetadataUpdateHandler.TypeMetadataSerializer.Add(new(this)); } - public JObject SerializeTypeMetadata(IEnumerable usedSerializationMaps, ISet? ignoredTypes = null) + public void SerializeTypeMetadata(IEnumerable usedSerializationMaps, Utf8JsonWriter json, ReadOnlySpan propertyName, ISet? ignoredTypes = null) { var dependentEnumTypes = new HashSet(); - var resultJson = new JObject(); // serialize object types var queue = new Queue(); var visitedTypes = new HashSet(); foreach (var map in usedSerializationMaps) { - queue.Enqueue(map); visitedTypes.Add(map.Type); + if (ignoredTypes?.Contains(map.ClientTypeId) != true) + queue.Enqueue(map); } + if (queue.Count == 0) + return; + + json.WriteStartObject(propertyName); while (queue.Count > 0) { var map = queue.Dequeue(); - var typeId = GetComplexTypeName(map.Type); + var typeId = map.ClientTypeId; - if (ignoredTypes?.Contains(typeId) != true) - { - var metadata = GetObjectTypeMetadataCopy(map); - resultJson[typeId] = metadata.Metadata; + json.WritePropertyName(typeId); + var metadata = GetObjectTypeMetadataCached(map); + json.WriteRawValue(metadata.MetadataJson, skipInputValidation: true); - dependentEnumTypes.UnionWith(metadata.DependentEnumTypes); + dependentEnumTypes.UnionWith(metadata.DependentEnumTypes); - foreach (var dependentType in metadata.DependentObjectTypes) + foreach (var dependentType in metadata.DependentObjectTypes) + { + if (!visitedTypes.Contains(dependentType)) { - if (!visitedTypes.Contains(dependentType)) - { - visitedTypes.Add(dependentType); + visitedTypes.Add(dependentType); + if (ignoredTypes?.Contains(map.ClientTypeId) != true) queue.Enqueue(viewModelSerializationMapper.GetMap(dependentType)); - } } } } @@ -75,24 +79,22 @@ public JObject SerializeTypeMetadata(IEnumerable used var typeId = GetEnumTypeName(type); if (ignoredTypes?.Contains(typeId) != true) { - resultJson[typeId] = GetEnumTypeMetadataCopy(type); + json.WritePropertyName(typeId); + json.WriteRawValue(GetEnumTypeMetadataCached(type), skipInputValidation: true); } } - - return resultJson; + json.WriteEndObject(); } - private JObject GetEnumTypeMetadataCopy(Type type) + private byte[] GetEnumTypeMetadataCached(Type type) { - var metadata = cachedEnumMetadata.GetOrAdd(type, BuildEnumTypeMetadata); - return (JObject)metadata.DeepClone(); + return cachedEnumMetadata.GetOrAdd(type, BuildEnumTypeMetadata); } - private ObjectMetadataWithDependencies GetObjectTypeMetadataCopy(ViewModelSerializationMap map) + private ObjectMetadataWithDependencies GetObjectTypeMetadataCached(ViewModelSerializationMap map) { var key = new ViewModelSerializationMapWithCulture(map, CultureInfo.CurrentUICulture.Name); - var obj = cachedObjectMetadata.GetOrAdd(key, _ => BuildObjectTypeMetadata(map)); - return new ObjectMetadataWithDependencies((JObject)obj.Metadata.DeepClone(), obj.DependentObjectTypes, obj.DependentEnumTypes); + return cachedObjectMetadata.GetOrAdd(key, _ => BuildObjectTypeMetadata(map)); } private ObjectMetadataWithDependencies BuildObjectTypeMetadata(ViewModelSerializationMap map) @@ -100,133 +102,154 @@ private ObjectMetadataWithDependencies BuildObjectTypeMetadata(ViewModelSerializ var dependentEnumTypes = new HashSet(); var dependentObjectTypes = new HashSet(); - var type = new JObject(); - type["type"] = "object"; + var buffer = new MemoryStream(); + var json = new Utf8JsonWriter(buffer, new JsonWriterOptions { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + json.WriteStartObject(); + json.WriteString("type"u8, "object"u8); if (debug) { - type["debugName"] = map.Type.ToCode(stripNamespace: true); + json.WriteString("debugName"u8, map.Type.ToCode(stripNamespace: true)); } - var properties = new JObject(); + json.WriteStartObject("properties"u8); foreach (var property in map.Properties.Where(p => p.IsAvailableOnClient())) { - var prop = new JObject(); + json.WriteStartObject(property.Name); - prop["type"] = GetTypeIdentifier(property.Type, dependentObjectTypes, dependentEnumTypes); + json.WritePropertyName("type"u8); + WriteTypeIdentifier(json, property.Type, dependentObjectTypes, dependentEnumTypes); if (debug && property.Name != property.PropertyInfo.Name) { - prop["debugName"] = property.PropertyInfo.Name; + json.WriteString("debugName"u8, property.PropertyInfo.Name); } if (property.TransferToServerOnlyInPath) { - prop["post"] = "pathOnly"; + json.WriteString("post"u8, "pathOnly"u8); } - if (!property.TransferToServer) + else if (!property.TransferToServer) { - prop["post"] = "no"; + json.WriteString("post"u8, "no"u8); } if (!property.TransferAfterPostback) { - prop["update"] = "no"; + json.WriteString("update"u8, "no"u8); } if (serializeValidationRules && property.ValidationRules.Any() && property.ClientValidationRules.Any()) { - prop["validationRules"] = JToken.FromObject(property.ClientValidationRules); + json.WritePropertyName("validationRules"u8); + JsonSerializer.Serialize(json, property.ClientValidationRules, DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe); } if (property.ClientExtenders.Any()) { - prop["clientExtenders"] = JToken.FromObject(property.ClientExtenders); + json.WritePropertyName("clientExtenders"u8); + JsonSerializer.Serialize(json, property.ClientExtenders, DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe); } - properties[property.Name] = prop; + json.WriteEndObject(); } - type["properties"] = properties; + json.WriteEndObject(); + json.WriteEndObject(); + json.Dispose(); - return new ObjectMetadataWithDependencies(type, dependentObjectTypes, dependentEnumTypes); + return new ObjectMetadataWithDependencies(buffer.ToArray(), dependentObjectTypes, dependentEnumTypes); } - internal JToken GetTypeIdentifier(Type type, HashSet dependentObjectTypes, HashSet dependentEnumTypes) + internal void WriteTypeIdentifier(Utf8JsonWriter json, Type type, HashSet dependentObjectTypes, HashSet dependentEnumTypes) { if (type.IsEnum) { dependentEnumTypes.Add(type); - return GetEnumTypeName(type); + json.WriteStringValue(GetEnumTypeName(type)); } else if (ReflectionUtils.IsNullable(type)) { - return GetNullableTypeIdentifier(type, dependentObjectTypes, dependentEnumTypes); + json.WriteStartObject(); + json.WriteString("type"u8, "nullable"u8); + json.WritePropertyName("inner"); + WriteTypeIdentifier(json, ReflectionUtils.UnwrapNullableType(type), dependentObjectTypes, dependentEnumTypes); + + json.WriteEndObject(); } else if (ReflectionUtils.IsPrimitiveType(type)) // we intentionally detect this after handling enums and nullable types { if (ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(type) is {}) - { - return GetPrimitiveTypeName(typeof(string)); - } - return GetPrimitiveTypeName(type); + json.WriteStringValue(GetPrimitiveTypeName(typeof(string))); + else + json.WriteStringValue(GetPrimitiveTypeName(type)); } else if (type == typeof(object)) { - return new JObject(new JProperty("type", "dynamic")); + json.WriteStartObject(); + json.WriteString("type"u8, "dynamic"u8); + json.WriteEndObject(); } else if (type.IsGenericType && ReflectionUtils.ImplementsGenericDefinition(type, typeof(IDictionary<,>))) { + json.WriteStartArray(); var attrs = type.GetGenericArguments(); var keyValuePair = typeof(KeyValuePair<,>).MakeGenericType(attrs); - return new JArray(GetTypeIdentifier(keyValuePair, dependentObjectTypes, dependentEnumTypes)); + WriteTypeIdentifier(json, keyValuePair, dependentObjectTypes, dependentEnumTypes); + json.WriteEndArray(); } else if (ReflectionUtils.IsCollection(type)) { - return new JArray(GetTypeIdentifier(ReflectionUtils.GetEnumerableType(type)!, dependentObjectTypes, dependentEnumTypes)); + json.WriteStartArray(); + WriteTypeIdentifier(json, ReflectionUtils.GetEnumerableType(type)!, dependentObjectTypes, dependentEnumTypes); + json.WriteEndArray(); } else { dependentObjectTypes.Add(type); - return GetComplexTypeName(type); + json.WriteStringValue(GetComplexTypeName(type)); } } - private JToken GetNullableTypeIdentifier(Type type, HashSet dependentObjectTypes, HashSet dependentEnumTypes) - { - var n = new JObject(); - n["type"] = "nullable"; - n["inner"] = GetTypeIdentifier(ReflectionUtils.UnwrapNullableType(type), dependentObjectTypes, dependentEnumTypes); - return n; - } - - private JObject BuildEnumTypeMetadata(Type type) + private byte[] BuildEnumTypeMetadata(Type type) { - var e = new JObject(); - e["type"] = "enum"; - e["isFlags"] = ReflectionUtils.GetCustomAttribute(type) != null; + var buffer = new MemoryStream(); + var json = new Utf8JsonWriter(buffer, new JsonWriterOptions { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + json.WriteStartObject(); + json.WriteString("type"u8, "enum"u8); + if (type.IsDefined(typeof(FlagsAttribute))) + { + json.WriteBoolean("isFlags"u8, true); + } if (debug) { - e["debugName"] = type.ToCode(stripNamespace: true); + json.WriteString("debugName"u8, type.ToCode(stripNamespace: true)); } // order of enum values is important on the client (for Flags enum coercion) + // Enum.GetNames and Enum.GetValues return the enums in ascending order (in unsigned value) var underlyingType = Enum.GetUnderlyingType(type); var enumValues = Enum.GetNames(type) - .Select(name => new { + .Zip( + Enum.GetValues(type).Cast().Select(c => Convert.ChangeType(c, underlyingType)), + (name, value) => new { Name = name, - Value = ReflectionUtils.ConvertValue(Enum.Parse(type, name), underlyingType) - }) - .OrderBy(v => v.Value); + Value = unchecked((long)(dynamic)value) + }); - var values = new JObject(); + json.WriteStartObject("values"u8); foreach (var v in enumValues) { - values.Add(ReflectionUtils.ToEnumString(type, v.Name), v.Value is null ? JValue.CreateNull() : JToken.FromObject(v.Value)); + json.WriteNumber( + ReflectionUtils.ToEnumString(type, v.Name), + v.Value + ); } - e["values"] = values; + json.WriteEndObject(); + json.WriteEndObject(); + json.Dispose(); - return e; + return buffer.ToArray(); } private string GetComplexTypeName(Type type) => type.GetTypeHash(); @@ -238,15 +261,15 @@ private JObject BuildEnumTypeMetadata(Type type) readonly struct ObjectMetadataWithDependencies { - public JObject Metadata { get; } + public byte[] MetadataJson { get; } public HashSet DependentObjectTypes { get; } public HashSet DependentEnumTypes { get; } - public ObjectMetadataWithDependencies(JObject metadata, HashSet dependentObjectTypes, HashSet dependentEnumTypes) + public ObjectMetadataWithDependencies(byte[] metadataJson, HashSet dependentObjectTypes, HashSet dependentEnumTypes) { - Metadata = metadata; + MetadataJson = metadataJson; DependentObjectTypes = dependentObjectTypes; DependentEnumTypes = dependentEnumTypes; } diff --git a/src/Framework/Framework/ViewModel/Validation/StaticCommandValidationError.cs b/src/Framework/Framework/ViewModel/Validation/StaticCommandValidationError.cs index 8465014629..d1c2f094c1 100644 --- a/src/Framework/Framework/ViewModel/Validation/StaticCommandValidationError.cs +++ b/src/Framework/Framework/ViewModel/Validation/StaticCommandValidationError.cs @@ -1,6 +1,6 @@ using System; +using System.Text.Json.Serialization; using DotVVM.Framework.Configuration; -using Newtonsoft.Json; namespace DotVVM.Framework.ViewModel.Validation { @@ -9,20 +9,20 @@ public class StaticCommandValidationError /// /// Gets or sets the error message. /// - [JsonProperty("errorMessage")] + [JsonPropertyName("errorMessage")] public string ErrorMessage { get; internal set; } /// /// Gets or sets the argument name /// - [JsonProperty("argumentName")] + [JsonPropertyName("argumentName")] public string? ArgumentName { get; internal set; } /// /// Contains path that can be evaluated on the client side. /// E.g.: /Product/Suppliers/2/Name /// - [JsonProperty("propertyPath")] + [JsonPropertyName("propertyPath")] public string? PropertyPath { get; internal set; } /// diff --git a/src/Framework/Framework/ViewModel/Validation/ValidationErrorFactory.cs b/src/Framework/Framework/ViewModel/Validation/ValidationErrorFactory.cs index 9900ee2f3d..43fed11397 100644 --- a/src/Framework/Framework/ViewModel/Validation/ValidationErrorFactory.cs +++ b/src/Framework/Framework/ViewModel/Validation/ValidationErrorFactory.cs @@ -59,7 +59,7 @@ public static ValidationResult CreateValidationResult(this T vm, string error private static JavascriptTranslator defaultJavaScriptTranslator = new JavascriptTranslator( new JavascriptTranslatorConfiguration(), - new ViewModelSerializationMapper(new ViewModelValidationRuleTranslator(), new AttributeViewModelValidationMetadataProvider(), new DefaultPropertySerialization(), DotvvmConfiguration.CreateDefault())); + new ViewModelSerializationMapper(new ViewModelValidationRuleTranslator(), new AttributeViewModelValidationMetadataProvider(), new DefaultPropertySerialization(), DotvvmConfiguration.CreateDefault(), null)); public static ValidationResult CreateValidationResult(ValidationContext validationContext, string error, params Expression>[] expressions) { diff --git a/src/Framework/Framework/ViewModel/Validation/ViewModelPropertyValidationRule.cs b/src/Framework/Framework/ViewModel/Validation/ViewModelPropertyValidationRule.cs index fff2f8231a..38df238cb0 100644 --- a/src/Framework/Framework/ViewModel/Validation/ViewModelPropertyValidationRule.cs +++ b/src/Framework/Framework/ViewModel/Validation/ViewModelPropertyValidationRule.cs @@ -1,18 +1,18 @@ using System; using System.ComponentModel.DataAnnotations; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace DotVVM.Framework.ViewModel.Validation { public class ViewModelPropertyValidationRule { - [JsonProperty("ruleName")] + [JsonPropertyName("ruleName")] public string? ClientRuleName { get; set; } - [JsonProperty("errorMessage")] + [JsonPropertyName("errorMessage")] public string ErrorMessage => SourceValidationAttribute.FormatErrorMessage(PropertyName); - [JsonProperty("parameters")] + [JsonPropertyName("parameters")] public object?[] Parameters { get; set; } [JsonIgnore] diff --git a/src/Framework/Framework/ViewModel/Validation/ViewModelValidationError.cs b/src/Framework/Framework/ViewModel/Validation/ViewModelValidationError.cs index acc3e0da6f..b26bb923cd 100644 --- a/src/Framework/Framework/ViewModel/Validation/ViewModelValidationError.cs +++ b/src/Framework/Framework/ViewModel/Validation/ViewModelValidationError.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace DotVVM.Framework.ViewModel.Validation { @@ -7,14 +7,14 @@ public class ViewModelValidationError /// /// Gets or sets the error message. /// - [JsonProperty("errorMessage")] + [JsonPropertyName("errorMessage")] public string ErrorMessage { get; internal set; } /// /// Contains path that can be evaluated on the client side. /// E.g.: /Product/Suppliers/2/Name /// - [JsonProperty("propertyPath")] + [JsonPropertyName("propertyPath")] public string? PropertyPath { get; internal set; } /// diff --git a/src/Framework/Hosting.AspNetCore/Hosting/DotvvmHttpResponse.cs b/src/Framework/Hosting.AspNetCore/Hosting/DotvvmHttpResponse.cs index b35ea0eb3f..c8a889659a 100644 --- a/src/Framework/Hosting.AspNetCore/Hosting/DotvvmHttpResponse.cs +++ b/src/Framework/Hosting.AspNetCore/Hosting/DotvvmHttpResponse.cs @@ -1,6 +1,8 @@ -using System.IO; +using System; +using System.IO; using System.Threading; using System.Threading.Tasks; +using DotVVM.Framework.Utils; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -40,28 +42,33 @@ public Stream Body public void Write(string text) { - var writer = new StreamWriter(OriginalResponse.Body) { AutoFlush = true}; - writer.Write(text); + // ASP.NET Core does not support synchronous writes, so we use GetResult() + OriginalResponse.WriteAsync(text).GetAwaiter().GetResult(); } - - public void Write(byte[] data) + public void Write(ReadOnlyMemory text) { - OriginalResponse.Body.Write(data, 0, data.Length); + this.WriteAsync(text).GetAwaiter().GetResult(); } - public void Write(byte[] data, int offset, int count) + public void Write(ReadOnlyMemory data) { - OriginalResponse.Body.Write(data, offset, count); + OriginalResponse.Body.WriteAsync(data).GetAwaiter().GetResult(); } - public Task WriteAsync(string text) + public Task WriteAsync(string text, CancellationToken token = default) { - return OriginalResponse.WriteAsync(text); + return OriginalResponse.WriteAsync(text, token); + } + public Task WriteAsync(ReadOnlyMemory text, CancellationToken token = default) + { + var writer = new StreamWriter(OriginalResponse.Body, StringUtils.Utf8) { AutoFlush = true }; + return writer.WriteAsync(text, token); } - public Task WriteAsync(string text, CancellationToken token) + public Task WriteAsync(ReadOnlyMemory data, CancellationToken token = default) { - return OriginalResponse.WriteAsync(text, token); + var task = OriginalResponse.Body.WriteAsync(data, token); + return task.IsCompletedSuccessfully ? Task.CompletedTask : task.AsTask(); } } } diff --git a/src/Framework/Hosting.AspNetCore/Security/DefaultViewModelProtector.cs b/src/Framework/Hosting.AspNetCore/Security/DefaultViewModelProtector.cs index 7e0514b4ab..1a570f818a 100644 --- a/src/Framework/Hosting.AspNetCore/Security/DefaultViewModelProtector.cs +++ b/src/Framework/Hosting.AspNetCore/Security/DefaultViewModelProtector.cs @@ -21,10 +21,10 @@ public DefaultViewModelProtector(IDataProtectionProvider protectionProvider) this.protectionProvider = protectionProvider; } - public string Protect(string serializedData, IDotvvmRequestContext context) + public byte[] Protect(byte[] serializedData, IDotvvmRequestContext context) { if (serializedData == null) throw new ArgumentNullException(nameof(serializedData)); - if (string.IsNullOrWhiteSpace(serializedData)) throw new ArgumentException("Value cannot be empty or whitespace only string.", nameof(serializedData)); + if (serializedData.Length == 0) throw new ArgumentException("Value cannot be empty string.", nameof(serializedData)); if (context == null) throw new ArgumentNullException(nameof(context)); // Construct protector with purposes @@ -33,9 +33,7 @@ public string Protect(string serializedData, IDotvvmRequestContext context) var protector = this.protectionProvider.CreateProtector(PRIMARY_PURPOSE, userIdentity, requestIdentity); // Return protected view model data - var dataToProtect = Encoding.UTF8.GetBytes(serializedData); - var protectedData = protector.Protect(dataToProtect); - return Convert.ToBase64String(protectedData); + return protector.Protect(serializedData); } public byte[] Protect(byte[] data, params string[] purposes) => @@ -43,10 +41,10 @@ public string Protect(string serializedData, IDotvvmRequestContext context) .CreateProtector(PRIMARY_PURPOSE, purposes) .Protect(data); - public string Unprotect(string protectedData, IDotvvmRequestContext context) + public byte[] Unprotect(byte[] protectedData, IDotvvmRequestContext context) { if (protectedData == null) throw new ArgumentNullException(nameof(protectedData)); - if (string.IsNullOrWhiteSpace(protectedData)) throw new ArgumentException("Value cannot be empty or whitespace only string.", nameof(protectedData)); + if (protectedData.Length == 0) throw new ArgumentException("Value cannot be empty string.", nameof(protectedData)); if (context == null) throw new ArgumentNullException(nameof(context)); // Construct protector with purposes @@ -55,9 +53,7 @@ public string Unprotect(string protectedData, IDotvvmRequestContext context) var protector = this.protectionProvider.CreateProtector(PRIMARY_PURPOSE, userIdentity, requestIdentity); // Return unprotected view model data - var dataToUnprotect = Convert.FromBase64String(protectedData); - var unprotectedData = protector.Unprotect(dataToUnprotect); - return Encoding.UTF8.GetString(unprotectedData); + return protector.Unprotect(protectedData); } public byte[] Unprotect(byte[] protectedData, params string[] purposes) => diff --git a/src/Framework/Testing/ControlTestHelper.cs b/src/Framework/Testing/ControlTestHelper.cs index 13e1fad9b3..4512f582b7 100644 --- a/src/Framework/Testing/ControlTestHelper.cs +++ b/src/Framework/Testing/ControlTestHelper.cs @@ -9,7 +9,6 @@ using DotVVM.Framework.Controls.Infrastructure; using DotVVM.Framework.Hosting; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json.Linq; using DotVVM.Framework.Utils; using System.Text; using System.Linq; @@ -21,9 +20,13 @@ using DotVVM.Framework.Binding; using DotVVM.Framework.Binding.Properties; using DotVVM.Framework.Compilation.ViewCompiler; -using Newtonsoft.Json; using DotVVM.Framework.ResourceManagement; using System.Security.Claims; +using DotVVM.Framework.ViewModel.Serialization; +using System.Runtime.InteropServices; +using System.Text.Json.Nodes; +using System.Text.Json; +using System.Text.Json.Serialization; namespace DotVVM.Framework.Testing { @@ -49,7 +52,7 @@ public ControlTestHelper(bool debug = true, Action? config presenter = (DotvvmPresenter)this.Configuration.ServiceProvider.GetRequiredService(); } - public T GetService() => Configuration.ServiceProvider.GetRequiredService(); + public T GetService() where T: notnull => Configuration.ServiceProvider.GetRequiredService(); public (ControlBuilderDescriptor descriptor, Lazy builder) CompilePage( string markup, @@ -95,9 +98,7 @@ public ControlTestHelper(bool debug = true, Action? config httpContext.Request.Method = "POST"; httpContext.Request.Headers["X-DotVVM-PostBack"] = new[] { "true" }; httpContext.Request.Body = new MemoryStream( - new UTF8Encoding(false).GetBytes( - JsonConvert.SerializeObject(postback) - ) + JsonSerializer.SerializeToUtf8Bytes(postback, DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe) ); } @@ -201,10 +202,12 @@ private PageRunResult CreatePageResult(TestDotvvmRequestContext context) foreach (var attr in attrs) el.RemoveAttribute(attr.NamespaceUri!, attr.LocalName); foreach (var attr in attrs) el.SetAttribute(attr.NamespaceUri!, attr.LocalName, attr.Value); } + var viewModel = context.Services.GetRequiredService().SerializeViewModel(context); + Console.WriteLine(viewModel); return new PageRunResult( this, context.Route.VirtualPath, - context.ViewModelJson, + JsonNode.Parse(viewModel)!.AsObject(), htmlOutput, headResources, bodyResources, @@ -218,28 +221,26 @@ private PageRunResult CreatePageResult(TestDotvvmRequestContext context) public class CommandRunResult { - public CommandRunResult(IDotvvmRequestContext context) + public CommandRunResult(TestDotvvmRequestContext context) { - this.ResultJson = context.ViewModelJson; - context.HttpContext.Response.Body.Position = 0; using var sr = new StreamReader(context.HttpContext.Response.Body); this.ResultText = sr.ReadToEnd(); - if (context.ViewModelJson == null && context.HttpContext.Response.ContentType == "application/json") + if (context.HttpContext.Response.ContentType?.StartsWith("application/json") == true) { - this.ResultJson = JObject.Parse(ResultText); + this.ResultJson = JsonNode.Parse(ResultText)!.AsObject(); } } public string ResultText { get; } - public JObject? ResultJson { get; } - public JObject? ViewModelJson => ResultJson?["viewModel"] as JObject ?? ResultJson?["viewModelDiff"] as JObject; + public JsonObject? ResultJson { get; } + public JsonObject? ViewModelJson => ResultJson?["viewModel"] as JsonObject ?? ResultJson?["viewModelDiff"] as JsonObject; } public class PostbackRequestModel { public PostbackRequestModel( - JObject viewModel, + JsonObject viewModel, string[] currentPath, string command, string? controlUniqueId, @@ -255,17 +256,17 @@ public class PostbackRequestModel ValidationTargetPath = validationTargetPath; } - [JsonProperty("viewModel")] - public JObject ViewModel { get; } - [JsonProperty("currentPath")] + [JsonPropertyName("viewModel")] + public JsonObject ViewModel { get; } + [JsonPropertyName("currentPath")] public string[] CurrentPath { get; } - [JsonProperty("command")] + [JsonPropertyName("command")] public string Command { get; } - [JsonProperty("controlUniqueId")] + [JsonPropertyName("controlUniqueId")] public string? ControlUniqueId { get; } - [JsonProperty("commandArgs")] + [JsonPropertyName("commandArgs")] public object[] CommandArgs { get; } - [JsonProperty("validationTargetPath")] + [JsonPropertyName("validationTargetPath")] public string? ValidationTargetPath { get; } } @@ -274,7 +275,7 @@ public class PageRunResult public PageRunResult( ControlTestHelper testHelper, string filePath, - JObject resultJson, + JsonObject resultJson, string outputString, string? headResources, string? bodyResources, @@ -298,9 +299,8 @@ TestDotvvmRequestContext initialContext public ControlTestHelper TestHelper { get; } public string FilePath { get; } - public JObject ResultJson { get; } - public JObject ViewModelJson => (JObject)ResultJson["viewModel"].NotNull(); - public dynamic ViewModel => ViewModelJson; + public JsonObject ResultJson { get; } + public JsonObject ViewModelJson => (JsonObject)ResultJson["viewModel"].NotNull(); public string OutputString { get; } public string? HeadResources { get; } public string? BodyResources { get; } @@ -359,7 +359,7 @@ public async Task RunCommand(string text, Func? if (applyChanges) { JsonUtils.Patch( - (JObject)this.ResultJson["viewModel"]!, + this.ResultJson["viewModel"]!.AsObject(), r.ViewModelJson! ); } diff --git a/src/Framework/Testing/DotVVM.Framework.Testing.csproj b/src/Framework/Testing/DotVVM.Framework.Testing.csproj index bef84dc8bb..fab9511916 100644 --- a/src/Framework/Testing/DotVVM.Framework.Testing.csproj +++ b/src/Framework/Testing/DotVVM.Framework.Testing.csproj @@ -19,6 +19,8 @@ + + diff --git a/src/Framework/Testing/DotvvmTestHelper.cs b/src/Framework/Testing/DotvvmTestHelper.cs index 6d5999d6fb..f265e21006 100644 --- a/src/Framework/Testing/DotvvmTestHelper.cs +++ b/src/Framework/Testing/DotvvmTestHelper.cs @@ -26,6 +26,7 @@ using DotVVM.Framework.Routing; using DotVVM.Framework.ResourceManagement; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Logging; namespace DotVVM.Framework.Testing { @@ -33,41 +34,37 @@ public static class DotvvmTestHelper { public class FakeProtector : IViewModelProtector { - // I hope I will not see this message anywhere on the web ;) - public const string WarningPrefix = "WARNING - Message not encryped: "; public static readonly byte[] WarningPrefixBytes = Convert.FromBase64String("WARNING/NOT/ENCRYPTED+++"); - public string Protect(string serializedData, IDotvvmRequestContext context) + public byte[] Protect(byte[] serializedData, IDotvvmRequestContext context) { - return WarningPrefix + ": " + serializedData; + // I hope I will not see this message anywhere on the web ;) + return [ ..WarningPrefixBytes, ..serializedData]; } public byte[] Protect(byte[] plaintextData, params string[] purposes) { - var result = new List(); - result.AddRange(WarningPrefixBytes); - result.AddRange(plaintextData); - return result.ToArray(); + return [ ..WarningPrefixBytes, ..plaintextData]; } - public string Unprotect(string protectedData, IDotvvmRequestContext context) + public byte[] Unprotect(byte[] protectedData, IDotvvmRequestContext context) { - if (!protectedData.StartsWith(WarningPrefix + ": ", StringComparison.Ordinal)) throw new SecurityException($""); - return protectedData.Remove(0, WarningPrefix.Length + 2); + if (!protectedData.AsSpan().StartsWith(WarningPrefixBytes)) throw new SecurityException($""); + return protectedData.AsSpan(WarningPrefixBytes.Length).ToArray(); } public byte[] Unprotect(byte[] protectedData, params string[] purposes) { - if (!protectedData.Take(WarningPrefixBytes.Length).SequenceEqual(WarningPrefixBytes)) throw new SecurityException($""); - return protectedData.Skip(WarningPrefixBytes.Length).ToArray(); + if (!protectedData.AsSpan().StartsWith(WarningPrefixBytes)) throw new SecurityException($""); + return protectedData.AsSpan(WarningPrefixBytes.Length).ToArray(); } } public class NopProtector : IViewModelProtector { - public string Protect(string serializedData, IDotvvmRequestContext context) => "XXX"; + public byte[] Protect(byte[] serializedData, IDotvvmRequestContext context) => Convert.FromBase64String("XXXX"); public byte[] Protect(byte[] plaintextData, params string[] purposes) => Convert.FromBase64String("XXXX"); - public string Unprotect(string protectedData, IDotvvmRequestContext context) => throw new NotImplementedException(); + public byte[] Unprotect(byte[] protectedData, IDotvvmRequestContext context) => throw new NotImplementedException(); public byte[] Unprotect(byte[] protectedData, params string[] purposes) => throw new NotImplementedException(); } @@ -111,6 +108,9 @@ public static void RegisterMockServices(IServiceCollection services) public static DotvvmConfiguration CreateConfiguration(Action? customServices = null) => DotvvmConfiguration.CreateDefault(s => { s.AddSingleton(); + LoggingServiceCollectionExtensions.AddLogging(s, log => { + log.AddConsole(); + }); customServices?.Invoke(s); RegisterMockServices(s); }); diff --git a/src/Samples/Api.AspNetCore/Startup.cs b/src/Samples/Api.AspNetCore/Startup.cs index 597f05a9e7..cfa6a5b6f7 100644 --- a/src/Samples/Api.AspNetCore/Startup.cs +++ b/src/Samples/Api.AspNetCore/Startup.cs @@ -15,7 +15,6 @@ using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; -using Newtonsoft.Json; using Swashbuckle.AspNetCore.Swagger; namespace DotVVM.Samples.BasicSamples.Api.AspNetCore diff --git a/src/Samples/Api.AspNetCoreLatest/Startup.cs b/src/Samples/Api.AspNetCoreLatest/Startup.cs index f1ebc5a7ab..147cdc0481 100644 --- a/src/Samples/Api.AspNetCoreLatest/Startup.cs +++ b/src/Samples/Api.AspNetCoreLatest/Startup.cs @@ -15,7 +15,6 @@ using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; -using Newtonsoft.Json; using Swashbuckle.AspNetCore.Swagger; namespace DotVVM.Samples.BasicSamples.Api.AspNetCoreLatest diff --git a/src/Samples/Api.Common/DotVVM.Samples.BasicSamples.Api.Common.csproj b/src/Samples/Api.Common/DotVVM.Samples.BasicSamples.Api.Common.csproj index 81dd209ec7..24f1f271f5 100644 --- a/src/Samples/Api.Common/DotVVM.Samples.BasicSamples.Api.Common.csproj +++ b/src/Samples/Api.Common/DotVVM.Samples.BasicSamples.Api.Common.csproj @@ -11,6 +11,6 @@ all runtime; build; native; contentfiles; analyzers - + diff --git a/src/Samples/AspNetCore/DotVVM.Samples.BasicSamples.AspNetCore.csproj b/src/Samples/AspNetCore/DotVVM.Samples.BasicSamples.AspNetCore.csproj index 3d8e953aba..ccbe3a5562 100644 --- a/src/Samples/AspNetCore/DotVVM.Samples.BasicSamples.AspNetCore.csproj +++ b/src/Samples/AspNetCore/DotVVM.Samples.BasicSamples.AspNetCore.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1 + net6.0 Exe @@ -12,7 +12,7 @@ - + diff --git a/src/Samples/Common/ViewModels/FeatureSamples/DateTimeSerialization/DateTimeSerialization.cs b/src/Samples/Common/ViewModels/FeatureSamples/DateTimeSerialization/DateTimeSerialization.cs index 553d8294ea..c587f2c57f 100644 --- a/src/Samples/Common/ViewModels/FeatureSamples/DateTimeSerialization/DateTimeSerialization.cs +++ b/src/Samples/Common/ViewModels/FeatureSamples/DateTimeSerialization/DateTimeSerialization.cs @@ -5,9 +5,6 @@ using System.Text; using DotVVM.Framework.ViewModel; using DotVVM.Framework.ViewModel.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Linq; namespace DotVVM.Samples.BasicSamples.ViewModels.FeatureSamples.DateTimeSerialization { diff --git a/src/Tests/Binding/JavascriptCompilationTests.cs b/src/Tests/Binding/JavascriptCompilationTests.cs index d387558513..3c1ec25f43 100644 --- a/src/Tests/Binding/JavascriptCompilationTests.cs +++ b/src/Tests/Binding/JavascriptCompilationTests.cs @@ -104,8 +104,8 @@ public void JavascriptCompilation_StringPlus(string expr, string expectedJs) [DataRow("NullableIntProp ^ NullableIntProp", "NullableIntProp()^NullableIntProp()")] [DataRow("NullableIntProp + 10", "NullableIntProp()+10")] [DataRow("NullableIntProp - 10L", "NullableIntProp()-10")] - [DataRow("NullableIntProp / 10.0", "NullableIntProp()/10.0")] - [DataRow("10.0 / NullableIntProp", "10.0/NullableIntProp()")] + [DataRow("NullableIntProp / 10.0", "NullableIntProp()/10")] + [DataRow("10.0 / NullableIntProp", "10/NullableIntProp()")] [DataRow("null / NullableIntProp", "null/NullableIntProp()")] [DataRow("null == NullableIntProp", "null==NullableIntProp()")] [DataRow("10 > NullableIntProp", "10>NullableIntProp()")] diff --git a/src/Tests/Binding/NullPropagationTests.cs b/src/Tests/Binding/NullPropagationTests.cs index 666fd2318d..33aff2bc89 100644 --- a/src/Tests/Binding/NullPropagationTests.cs +++ b/src/Tests/Binding/NullPropagationTests.cs @@ -9,11 +9,11 @@ using DotVVM.Framework.Utils; using DotVVM.Framework.Compilation.Binding; using System.Diagnostics; -using Newtonsoft.Json; using DotVVM.Framework.Configuration; using DotVVM.Framework.Binding.HelperNamespace; using DotVVM.Framework.Testing; using FastExpressionCompiler; +using System.Text.Json; namespace DotVVM.Framework.Tests.Binding { @@ -191,7 +191,7 @@ private void TestExpression(Random rnd, Expression expression, ParameterExpressi Assert.Fail($"Exception {e.Message} while executing null-checked {expr.ToCSharpString()}"); return; } - Assert.AreEqual(JsonConvert.SerializeObject(withNullChecks(args), settings), JsonConvert.SerializeObject(withoutNullChecks(args), settings)); + Assert.AreEqual(JsonSerializer.Serialize(withNullChecks(args), settings), JsonSerializer.Serialize(withoutNullChecks(args), settings)); foreach (var i in Enumerable.Range(0, args.Length).Shuffle(rnd)) { diff --git a/src/Tests/Binding/StaticCommandExecutorTests.cs b/src/Tests/Binding/StaticCommandExecutorTests.cs index 754c02f595..10fe3884a6 100644 --- a/src/Tests/Binding/StaticCommandExecutorTests.cs +++ b/src/Tests/Binding/StaticCommandExecutorTests.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Text.Json; using System.Threading.Tasks; using DotVVM.Framework.Compilation.Binding; using DotVVM.Framework.Compilation.Javascript; @@ -43,7 +44,7 @@ StaticCommandInvocationPlan CreatePlan(Expression methodExpr) async Task Invoke(StaticCommandInvocationPlan plan, params (object value, string path)[] arguments) { var context = DotvvmTestHelper.CreateContext(config, requestType: DotvvmRequestType.StaticCommand); - var a = arguments.Select(t => JToken.FromObject(t.value, DefaultSerializerSettingsProvider.CreateJsonSerializer())); + var a = JsonSerializer.SerializeToElement(arguments.Select(t => t.value).ToArray(), DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe); var p = arguments.Select(t => t.path); return await executor.Execute(plan, a, p, context); } diff --git a/src/Tests/ControlTests/AutoUIResourcesTests.cs b/src/Tests/ControlTests/AutoUIResourcesTests.cs index 34aa13fc60..b319997bb2 100644 --- a/src/Tests/ControlTests/AutoUIResourcesTests.cs +++ b/src/Tests/ControlTests/AutoUIResourcesTests.cs @@ -49,8 +49,8 @@ public async Task ResourceLabelsFormTest(string culture) ); //check.CheckString(r.FormattedHtml, fileExtension: "html", checkName: culture); - r.ViewModel.Entity1.FirstName = "very long value is set in the field"; - r.ViewModel.Entity1.Email = "invalid mail"; + r.ViewModelJson["Entity1"]["FirstName"] = "very long value is set in the field"; + r.ViewModelJson["Entity1"]["Email"] = "invalid mail"; var validationResult = await r.RunCommand("Test()", applyChanges: false, culture: new CultureInfo(culture)); check.CheckString(validationResult.ResultText, fileExtension: "json", checkName: culture); diff --git a/src/Tests/ControlTests/CommandTests.cs b/src/Tests/ControlTests/CommandTests.cs index 52f275c1b9..a130b2049e 100644 --- a/src/Tests/ControlTests/CommandTests.cs +++ b/src/Tests/ControlTests/CommandTests.cs @@ -25,14 +25,14 @@ public async Task RootViewModelIsRecord() { var r = await cth.RunPage(typeof(ViewModel1), " "); - Assert.AreEqual("Text1", (string)r.ViewModel.Text); - Assert.AreEqual("Text2", (string)r.ViewModel.NestedVM.TestProp); - Assert.AreEqual("Text3", (string)r.ViewModel.NestedVM2.TestProp); - Assert.AreEqual(0, (int)r.ViewModel.NestedVM3.A); + Assert.AreEqual("Text1", (string)r.ViewModelJson["Text"]); + Assert.AreEqual("Text2", (string)r.ViewModelJson["NestedVM"]["TestProp"]); + Assert.AreEqual("Text3", (string)r.ViewModelJson["NestedVM2"]["TestProp"]); + Assert.AreEqual(0, (int)r.ViewModelJson["NestedVM3"]["A"]); await r.RunCommand("Text = NestedVM.TestProp + NestedVM2.TestProp + NestedVM3.A"); - Assert.AreEqual("Text2Text30", (string)r.ViewModel.Text); + Assert.AreEqual("Text2Text30", (string)r.ViewModelJson["Text"]); } diff --git a/src/Tests/ControlTests/CompositeControlTests.cs b/src/Tests/ControlTests/CompositeControlTests.cs index d963d7a96b..20173cd5a3 100644 --- a/src/Tests/ControlTests/CompositeControlTests.cs +++ b/src/Tests/ControlTests/CompositeControlTests.cs @@ -149,9 +149,9 @@ public async Task CommandDataContextChange() check.CheckString(r.FormattedHtml, fileExtension: "html"); - Assert.AreEqual(10000000, (int)r.ViewModel.@int); + Assert.AreEqual(10000000, (int)r.ViewModelJson["int"]); await r.RunCommand("Integer = 15", "list-item2".Equals); - Assert.AreEqual(15, (int)r.ViewModel.@int); + Assert.AreEqual(15, (int)r.ViewModelJson["int"]); } [TestMethod] diff --git a/src/Tests/ControlTests/HierarchyRepeaterTests.cs b/src/Tests/ControlTests/HierarchyRepeaterTests.cs index fcd44978ba..7e375edff1 100644 --- a/src/Tests/ControlTests/HierarchyRepeaterTests.cs +++ b/src/Tests/ControlTests/HierarchyRepeaterTests.cs @@ -69,9 +69,9 @@ @viewModel DotVVM.Framework.Tests.ControlTests.HierarchyRepeaterTests.BasicTestV ); await r.RunCommand("_control.Click()", vm => vm is BasicTestViewModel.SaneHierarchicalItem { Label: "A_1_2" }); - Assert.AreEqual("A_1_2", (string)r.ViewModel.SelectedLabel); + Assert.AreEqual("A_1_2", (string)r.ViewModelJson["SelectedLabel"]); await r.RunCommand("_control.Click()", vm => vm is BasicTestViewModel.SaneHierarchicalItem { Label: "A_1" }); - Assert.AreEqual("A_1", (string)r.ViewModel.SelectedLabel); + Assert.AreEqual("A_1", (string)r.ViewModelJson["SelectedLabel"]); check.CheckString( r.OutputString, checkName: clientRendering ? "client" : "server", diff --git a/src/Tests/ControlTests/MarkupControlTests.cs b/src/Tests/ControlTests/MarkupControlTests.cs index 448f717306..ceac1d62e7 100644 --- a/src/Tests/ControlTests/MarkupControlTests.cs +++ b/src/Tests/ControlTests/MarkupControlTests.cs @@ -115,27 +115,27 @@ @baseType DotVVM.Framework.Tests.ControlTests.CustomControlWithProperty } ); - Assert.AreEqual(10000000, (int)r.ViewModel.@int); + Assert.AreEqual(10000000, (int)r.ViewModelJson["int"]); await r.RunCommand("_control.IncrementProperty()", vm => vm is BasicTestViewModel); - Assert.AreEqual(10000001, (int)r.ViewModel.@int); + Assert.AreEqual(10000001, (int)r.ViewModelJson["int"]); await r.RunCommand("P = P - 10", vm => vm is BasicTestViewModel); - Assert.AreEqual(10000000 - 9, (int)r.ViewModel.@int); + Assert.AreEqual(10000000 - 9, (int)r.ViewModelJson["int"]); - Assert.AreEqual(15, (int)r.ViewModel.IntArray[0]); + Assert.AreEqual(15, (int)r.ViewModelJson["IntArray"][0]); await r.RunCommand("_control.IncrementProperty()", 15.Equals); - Assert.AreEqual(16, (int)r.ViewModel.IntArray[0]); + Assert.AreEqual(16, (int)r.ViewModelJson["IntArray"][0]); await r.RunCommand("P = P - 10", 15.Equals); - Assert.AreEqual(6, (int)r.ViewModel.IntArray[0]); + Assert.AreEqual(6, (int)r.ViewModelJson["IntArray"][0]); - Assert.AreEqual(10, (int)r.ViewModel.Collection[0]); + Assert.AreEqual(10, (int)r.ViewModelJson["Collection"][0]); await r.RunCommand("_control.IncrementProperty()", 10.Equals); - Assert.AreEqual(11, (int)r.ViewModel.Collection[0]); + Assert.AreEqual(11, (int)r.ViewModelJson["Collection"][0]); await r.RunCommand("P = P - 10", 10.Equals); - Assert.AreEqual(1, (int)r.ViewModel.Collection[0]); + Assert.AreEqual(1, (int)r.ViewModelJson["Collection"][0]); - Assert.AreEqual(-20, (int)r.ViewModel.Collection[1]); + Assert.AreEqual(-20, (int)r.ViewModelJson["Collection"][1]); await r.RunCommand("_control.IncrementProperty()", (-20).Equals); - Assert.AreEqual(-19, (int)r.ViewModel.Collection[1]); + Assert.AreEqual(-19, (int)r.ViewModelJson["Collection"][1]); // check only the generated static command expressions check.CheckString(r.Html.QuerySelector(".static-command-button").ToHtml(), fileExtension: "html"); diff --git a/src/Tests/ControlTests/SimpleControlTests.cs b/src/Tests/ControlTests/SimpleControlTests.cs index 4d97b6fd7d..b2e335ad39 100644 --- a/src/Tests/ControlTests/SimpleControlTests.cs +++ b/src/Tests/ControlTests/SimpleControlTests.cs @@ -257,11 +257,11 @@ public async Task CommandBinding() 10000000} /> "); - Assert.AreEqual(10000000, (int)r.ViewModel.@int); + Assert.AreEqual(10000000, (int)r.ViewModelJson["int"]); await r.RunCommand("Integer = Integer + 1"); - Assert.AreEqual(10000001, (int)r.ViewModel.@int); + Assert.AreEqual(10000001, (int)r.ViewModelJson["int"]); await r.RunCommand("Integer = Integer - 1"); - Assert.AreEqual(10000000, (int)r.ViewModel.@int); + Assert.AreEqual(10000000, (int)r.ViewModelJson["int"]); // invoking command on disabled button should fail var exception = await Assert.ThrowsExceptionAsync(() => r.RunCommand("Integer = Integer - 1") diff --git a/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html b/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html index 944b1ec558..2ddf27b00e 100644 --- a/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html +++ b/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html @@ -12,7 +12,7 @@ - + diff --git a/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModuleInControl.html b/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModuleInControl.html index 5a85b4e73a..6c9ee52987 100644 --- a/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModuleInControl.html +++ b/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModuleInControl.html @@ -9,13 +9,13 @@ - + - + diff --git a/src/Tests/DotVVM.Framework.Tests.csproj b/src/Tests/DotVVM.Framework.Tests.csproj index b37e5b2397..da7347b78f 100644 --- a/src/Tests/DotVVM.Framework.Tests.csproj +++ b/src/Tests/DotVVM.Framework.Tests.csproj @@ -47,6 +47,8 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Tests/Routing/RouteSerializationTests.cs b/src/Tests/Routing/RouteSerializationTests.cs index 22a5c25891..f8fb80eb88 100644 --- a/src/Tests/Routing/RouteSerializationTests.cs +++ b/src/Tests/Routing/RouteSerializationTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Reflection; using System.Text; +using System.Text.Json; using DotVVM.Framework.Compilation; using DotVVM.Framework.Configuration; using DotVVM.Framework.Hosting; @@ -9,7 +10,6 @@ using DotVVM.Framework.Routing; using DotVVM.Framework.Testing; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; namespace DotVVM.Framework.Tests.Routing { @@ -30,9 +30,8 @@ public void RouteTable_Deserialization() typeof(RouteBase).GetProperty("Url").SetMethod.Invoke(r, new[] { "url3/{a:unsuppotedConstraint}" }); config1.RouteTable.Add("route3", r); - var settings = DefaultSerializerSettingsProvider.Instance.GetSettingsCopy(); - settings.TypeNameHandling = TypeNameHandling.Auto; - var config2 = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(config1, settings), settings); + var settings = VisualStudioHelper.GetSerializerOptions(); + var config2 = JsonSerializer.Deserialize(JsonSerializer.Serialize(config1, settings), settings); Assert.AreEqual(config2.RouteTable["route1"].Url, "url1"); Assert.AreEqual(config2.RouteTable["route2"].Url, "url2/{int:posint}"); diff --git a/src/Tests/Runtime/ConfigurationSerializationTests.cs b/src/Tests/Runtime/ConfigurationSerializationTests.cs index e3cafd825f..f502e6d0fe 100644 --- a/src/Tests/Runtime/ConfigurationSerializationTests.cs +++ b/src/Tests/Runtime/ConfigurationSerializationTests.cs @@ -31,7 +31,7 @@ void checkConfig(DotvvmConfiguration config, bool includeProperties = false, str var serialized = DotVVM.Framework.Hosting.VisualStudioHelper.SerializeConfig(config, includeProperties); // Unify package versions serialized = Regex.Replace(serialized, "Version=[0-9.]+", "Version=***"); - serialized = Regex.Replace(serialized, "\"dotvvmVersion\": \"[0-9]\\.[0-9]\\.[0-9]\\.[0-9]\"", "\"dotvvmVersion\": \"*.*.*.*\""); + serialized = Regex.Replace(serialized, "\"dotvvmVersion\": *\"[0-9]\\.[0-9]\\.[0-9]\\.[0-9]\"", "\"dotvvmVersion\": \"*.*.*.*\""); // Unify all occurrences of mscorlib and system.private.corelib serialized = serialized.Replace("mscorlib, Version=***, Culture=neutral, PublicKeyToken=b77a5c561934e089", "CoreLibrary"); serialized = serialized.Replace("System.Private.CoreLib, Version=***, Culture=neutral, PublicKeyToken=7cec85d7bea7798e", "CoreLibrary"); @@ -41,11 +41,13 @@ void checkConfig(DotvvmConfiguration config, bool includeProperties = false, str serialized = serialized.Replace("System.IServiceProvider, CoreLibrary", "System.IServiceProvider, ComponentLibrary"); serialized = serialized.Replace("System.IServiceProvider, System.ComponentModel, Version=***, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", "System.IServiceProvider, ComponentLibrary"); + Console.WriteLine(serialized); + var jobject = JObject.Parse(serialized); void removeTestStuff(JToken token) { - if (token is object) - foreach (var testControl in ((JObject)token).Properties().Where(p => p.Name.Contains(".Tests.")).ToArray()) + if (token is JObject obj) + foreach (var testControl in obj.Properties().Where(p => p.Name.Contains(".Tests.")).ToArray()) testControl.Remove(); } removeTestStuff(jobject["properties"]); @@ -189,6 +191,11 @@ public void AuxOptions() c.ClientSideValidation = false; c.DefaultCulture = "cs-CZ"; + c.Markup.ViewCompilation.CompileInParallel = false; + c.Markup.ViewCompilation.BackgroundCompilationDelay = TimeSpan.FromSeconds(30); + c.Markup.ViewCompilation.Mode = ViewCompilationMode.Lazy; + c.Runtime.MaxPostbackSizeBytes = 100; + checkConfig(c); } diff --git a/src/Tests/Runtime/DotvvmCompilationExceptionSerializationTests.cs b/src/Tests/Runtime/DotvvmCompilationExceptionSerializationTests.cs index 8ab2d15b1e..118e844480 100644 --- a/src/Tests/Runtime/DotvvmCompilationExceptionSerializationTests.cs +++ b/src/Tests/Runtime/DotvvmCompilationExceptionSerializationTests.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Text; +using System.Text.Json; using DotVVM.Framework.Compilation; using DotVVM.Framework.Configuration; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; namespace DotVVM.Framework.Tests.Runtime { @@ -18,9 +18,9 @@ public void DotvvmCompilationException_SerializationAndDeserialization_WorksCorr new DotvvmCompilationException("Compilation error", new Exception("inner exception")); var settings = DefaultSerializerSettingsProvider.Instance.Settings; - var serializedObject = JsonConvert.SerializeObject(compilationException, settings); + var serializedObject = JsonSerializer.Serialize(compilationException, new JsonSerializerOptions(settings) { WriteIndented = true }); - var deserializedObject = JsonConvert.DeserializeObject(serializedObject, settings); + var deserializedObject = JsonSerializer.Deserialize(serializedObject, settings); } } } diff --git a/src/Tests/Runtime/ResourceManagerTests.cs b/src/Tests/Runtime/ResourceManagerTests.cs index 5f39d14bbb..eb345389da 100644 --- a/src/Tests/Runtime/ResourceManagerTests.cs +++ b/src/Tests/Runtime/ResourceManagerTests.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; using DotVVM.Framework.Configuration; using DotVVM.Framework.Hosting; using DotVVM.Framework.ResourceManagement; @@ -11,6 +10,8 @@ using DotVVM.Framework.Compilation.Parser; using System.Reflection; using DotVVM.Framework.Testing; +using System.Text.Json; +using DotVVM.Framework.Utils; namespace DotVVM.Framework.Tests.Runtime { @@ -71,9 +72,8 @@ public void ResourceManager_ConfigurationDeserialization() }); config1.Resources.RegisterScript("rs8", new JQueryGlobalizeResourceLocation(CultureInfo.GetCultureInfo("en-US"))); - var settings = DefaultSerializerSettingsProvider.Instance.GetSettingsCopy(); - settings.TypeNameHandling = TypeNameHandling.Auto; - var config2 = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(config1, settings), settings); + var settings = VisualStudioHelper.GetSerializerOptions(); + var config2 = JsonSerializer.Deserialize(JsonSerializer.Serialize(config1, settings), settings); //test Assert.IsTrue(config2.Resources.FindResource("rs1") is ScriptResource rs1 && @@ -110,7 +110,7 @@ public void ResourceManager_ConfigurationOldDeserialization() }} }}", ResourceConstants.GlobalizeResourceName); var configuration = DotvvmTestHelper.CreateConfiguration(); - JsonConvert.PopulateObject(json.Replace("'", "\""), configuration); + SystemTextJsonHacks.Populate(configuration, json.Replace("'", "\""), DefaultSerializerSettingsProvider.Instance.Settings); Assert.IsTrue(configuration.Resources.FindResource(ResourceConstants.GlobalizeResourceName) is ScriptResource); Assert.IsTrue(configuration.Resources.FindResource("newResource") is StylesheetResource); @@ -121,7 +121,7 @@ public void JQueryGlobalizeGenerator() { var cultureInfo = new CultureInfo("cs-cz"); var json = JQueryGlobalizeScriptCreator.BuildCultureInfoJson(cultureInfo); - Assert.IsTrue(json.SelectToken("calendars.standard.days.namesAbbr").Values().SequenceEqual(cultureInfo.DateTimeFormat.AbbreviatedDayNames)); + Assert.IsTrue(json["calendars"]["standard"]["days"]["namesAbbr"].AsArray().Select(x => (string)x).SequenceEqual(cultureInfo.DateTimeFormat.AbbreviatedDayNames)); // TODO: add more assertions } } diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.AuxOptions.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.AuxOptions.json index 3b7c192df0..2e1bd62128 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.AuxOptions.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.AuxOptions.json @@ -3,40 +3,16 @@ "config": { "applicationPhysicalPath": "/opt/myApp", "markup": { - "importedNamespaces": [ - { - "namespace": "DotVVM.Framework.Binding.HelperNamespace" - }, - { - "namespace": "System.Linq" - } - ], "ViewCompilation": { - "compileInParallel": true + "mode": "Lazy", + "backgroundCompilationDelay": "00:00:30", + "compileInParallel": false } }, "resources": {}, - "security": { - "xssProtectionHeader": { - "enabled": true - }, - "contentTypeOptionsHeader": { - "enabled": true - }, - "verifySecFetchForPages": { - "enabled": true - }, - "verifySecFetchForCommands": { - "enabled": true - }, - "referrerPolicy": { - "enabled": true - } - }, + "security": {}, "runtime": { - "reloadMarkupFiles": {}, - "compressPostbacks": {}, - "maxPostbackSizeBytes": 134217728 + "maxPostbackSizeBytes": 100 }, "defaultCulture": "cs-CZ", "clientSideValidation": false, @@ -44,9 +20,7 @@ "debug": true, "diagnostics": { "compilationPage": {}, - "perfWarnings": { - "bigViewModelBytes": 5242880.0 - } + "perfWarnings": {} } } } diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.ExperimentalFeatures.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.ExperimentalFeatures.json index 98eedf9e8a..2c9f447ddc 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.ExperimentalFeatures.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.ExperimentalFeatures.json @@ -2,41 +2,11 @@ "dotvvmVersion": "*.*.*.*", "config": { "markup": { - "importedNamespaces": [ - { - "namespace": "DotVVM.Framework.Binding.HelperNamespace" - }, - { - "namespace": "System.Linq" - } - ], - "ViewCompilation": { - "compileInParallel": true - } + "ViewCompilation": {} }, "resources": {}, - "security": { - "xssProtectionHeader": { - "enabled": true - }, - "contentTypeOptionsHeader": { - "enabled": true - }, - "verifySecFetchForPages": { - "enabled": true - }, - "verifySecFetchForCommands": { - "enabled": true - }, - "referrerPolicy": { - "enabled": true - } - }, - "runtime": { - "reloadMarkupFiles": {}, - "compressPostbacks": {}, - "maxPostbackSizeBytes": 134217728 - }, + "security": {}, + "runtime": {}, "defaultCulture": "en-US", "experimentalFeatures": { "lazyCsrfToken": { @@ -47,18 +17,16 @@ ] }, "serverSideViewModelCache": { + "enabled": false, "includedRoutes": [ "r1", "r2" ] } }, - "debug": false, "diagnostics": { "compilationPage": {}, - "perfWarnings": { - "bigViewModelBytes": 5242880.0 - } + "perfWarnings": {} } } } diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.Markup.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.Markup.json index 214eef0115..458e2a1fa6 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.Markup.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.Markup.json @@ -52,41 +52,16 @@ "Inherit": true } ], - "ViewCompilation": { - "compileInParallel": true - } + "ViewCompilation": {} }, "resources": {}, - "security": { - "xssProtectionHeader": { - "enabled": true - }, - "contentTypeOptionsHeader": { - "enabled": true - }, - "verifySecFetchForPages": { - "enabled": true - }, - "verifySecFetchForCommands": { - "enabled": true - }, - "referrerPolicy": { - "enabled": true - } - }, - "runtime": { - "reloadMarkupFiles": {}, - "compressPostbacks": {}, - "maxPostbackSizeBytes": 134217728 - }, + "security": {}, + "runtime": {}, "defaultCulture": "en-US", "experimentalFeatures": {}, - "debug": false, "diagnostics": { "compilationPage": {}, - "perfWarnings": { - "bigViewModelBytes": 5242880.0 - } + "perfWarnings": {} } } } diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.RestAPI.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.RestAPI.json index 5684860fdf..1bb86526e1 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.RestAPI.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.RestAPI.json @@ -2,14 +2,6 @@ "dotvvmVersion": "*.*.*.*", "config": { "markup": { - "importedNamespaces": [ - { - "namespace": "DotVVM.Framework.Binding.HelperNamespace" - }, - { - "namespace": "System.Linq" - } - ], "defaultExtensionParameters": [ { "$type": "DotVVM.Framework.Configuration.RestApiRegistrationHelpers+ApiExtensionParameter, DotVVM.Framework", @@ -18,9 +10,7 @@ "Inherit": true } ], - "ViewCompilation": { - "compileInParallel": true - } + "ViewCompilation": {} }, "resources": { "DotVVM.Framework.ResourceManagement.InlineScriptResource": { @@ -38,45 +28,23 @@ "apiClient_testApi": { "Defer": true, "Location": { - "$type": "DotVVM.Framework.ResourceManagement.FileResourceLocation, DotVVM.Framework", "FilePath": "./apiscript.js", "DebugFilePath": "./apiscript.js" }, + "LocationType": "DotVVM.Framework.ResourceManagement.FileResourceLocation, DotVVM.Framework", "MimeType": "text/javascript", + "VerifyResourceIntegrity": true, "RenderPosition": "Anywhere" } } }, - "security": { - "xssProtectionHeader": { - "enabled": true - }, - "contentTypeOptionsHeader": { - "enabled": true - }, - "verifySecFetchForPages": { - "enabled": true - }, - "verifySecFetchForCommands": { - "enabled": true - }, - "referrerPolicy": { - "enabled": true - } - }, - "runtime": { - "reloadMarkupFiles": {}, - "compressPostbacks": {}, - "maxPostbackSizeBytes": 134217728 - }, + "security": {}, + "runtime": {}, "defaultCulture": "en-US", "experimentalFeatures": {}, - "debug": false, "diagnostics": { "compilationPage": {}, - "perfWarnings": { - "bigViewModelBytes": 5242880.0 - } + "perfWarnings": {} } } } diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index 4f5cde2cf5..7547369705 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -18,17 +18,7 @@ "DotVVM.AutoUI.Annotations, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da", "DotVVM.AutoUI, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da" ], - "importedNamespaces": [ - { - "namespace": "DotVVM.Framework.Binding.HelperNamespace" - }, - { - "namespace": "System.Linq" - } - ], - "ViewCompilation": { - "compileInParallel": true - } + "ViewCompilation": {} }, "resources": { "DotVVM.Framework.ResourceManagement.InlineScriptResource": { @@ -45,12 +35,13 @@ "dotvvm.internal": { "Defer": true, "Location": { - "$type": "DotVVM.Framework.ResourceManagement.EmbeddedResourceLocation, DotVVM.Framework", "Assembly": "DotVVM.Framework, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da", "Name": "DotVVM.Framework.obj.javascript.root_only.dotvvm-root.js", "DebugName": "DotVVM.Framework.obj.javascript.root_only_debug.dotvvm-root.js" }, + "LocationType": "DotVVM.Framework.ResourceManagement.EmbeddedResourceLocation, DotVVM.Framework", "MimeType": "text/javascript", + "VerifyResourceIntegrity": true, "Dependencies": [ "knockout" ], @@ -59,12 +50,13 @@ "dotvvm.internal-spa": { "Defer": true, "Location": { - "$type": "DotVVM.Framework.ResourceManagement.EmbeddedResourceLocation, DotVVM.Framework", "Assembly": "DotVVM.Framework, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da", "Name": "DotVVM.Framework.obj.javascript.root_spa.dotvvm-root.js", "DebugName": "DotVVM.Framework.obj.javascript.root_spa_debug.dotvvm-root.js" }, + "LocationType": "DotVVM.Framework.ResourceManagement.EmbeddedResourceLocation, DotVVM.Framework", "MimeType": "text/javascript", + "VerifyResourceIntegrity": true, "Dependencies": [ "knockout" ], @@ -75,12 +67,13 @@ "dotvvm.debug": { "Defer": true, "Location": { - "$type": "DotVVM.Framework.ResourceManagement.EmbeddedResourceLocation, DotVVM.Framework", "Assembly": "DotVVM.Framework, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da", "Name": "DotVVM.Framework.Resources.Scripts.DotVVM.Debug.js", "DebugName": "DotVVM.Framework.Resources.Scripts.DotVVM.Debug.js" }, + "LocationType": "DotVVM.Framework.ResourceManagement.EmbeddedResourceLocation, DotVVM.Framework", "MimeType": "text/javascript", + "VerifyResourceIntegrity": true, "Dependencies": [ "dotvvm" ], @@ -89,81 +82,64 @@ "globalize": { "Defer": true, "Location": { - "$type": "DotVVM.Framework.ResourceManagement.EmbeddedResourceLocation, DotVVM.Framework", "Assembly": "DotVVM.Framework, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da", "Name": "DotVVM.Framework.Resources.Scripts.Globalize.globalize.min.js", "DebugName": "DotVVM.Framework.Resources.Scripts.Globalize.globalize.min.js" }, + "LocationType": "DotVVM.Framework.ResourceManagement.EmbeddedResourceLocation, DotVVM.Framework", "MimeType": "text/javascript", + "VerifyResourceIntegrity": true, "RenderPosition": "Anywhere" }, "knockout": { "Defer": true, "Location": { - "$type": "DotVVM.Framework.ResourceManagement.EmbeddedResourceLocation, DotVVM.Framework", "Assembly": "DotVVM.Framework, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da", "Name": "DotVVM.Framework.Resources.Scripts.knockout-latest.js", "DebugName": "DotVVM.Framework.Resources.Scripts.knockout-latest.debug.js" }, + "LocationType": "DotVVM.Framework.ResourceManagement.EmbeddedResourceLocation, DotVVM.Framework", "MimeType": "text/javascript", + "VerifyResourceIntegrity": true, "RenderPosition": "Anywhere" } }, "stylesheets": { "dotvvm.fileUpload-css": { "Location": { - "$type": "DotVVM.Framework.ResourceManagement.EmbeddedResourceLocation, DotVVM.Framework", "Assembly": "DotVVM.Framework, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da", "Name": "DotVVM.Framework.Resources.Styles.DotVVM.FileUpload.css", "DebugName": "DotVVM.Framework.Resources.Styles.DotVVM.FileUpload.css" }, - "MimeType": "text/css" + "LocationType": "DotVVM.Framework.ResourceManagement.EmbeddedResourceLocation, DotVVM.Framework", + "MimeType": "text/css", + "VerifyResourceIntegrity": true, + "RenderPosition": "Head" }, "dotvvm.internal-css": { "Location": { - "$type": "DotVVM.Framework.ResourceManagement.EmbeddedResourceLocation, DotVVM.Framework", "Assembly": "DotVVM.Framework, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da", "Name": "DotVVM.Framework.Resources.Styles.DotVVM.Internal.css", "DebugName": "DotVVM.Framework.Resources.Styles.DotVVM.Internal.css" }, - "MimeType": "text/css" + "LocationType": "DotVVM.Framework.ResourceManagement.EmbeddedResourceLocation, DotVVM.Framework", + "MimeType": "text/css", + "VerifyResourceIntegrity": true, + "RenderPosition": "Head" } } }, - "security": { - "xssProtectionHeader": { - "enabled": true - }, - "contentTypeOptionsHeader": { - "enabled": true - }, - "verifySecFetchForPages": { - "enabled": true - }, - "verifySecFetchForCommands": { - "enabled": true - }, - "referrerPolicy": { - "enabled": true - } - }, - "runtime": { - "reloadMarkupFiles": {}, - "compressPostbacks": {}, - "maxPostbackSizeBytes": 134217728 - }, + "security": {}, + "runtime": {}, "defaultCulture": "en-US", "experimentalFeatures": { "explicitAssemblyLoading": { "enabled": true } }, - "debug": false, "diagnostics": { "compilationPage": {}, - "perfWarnings": { - "bigViewModelBytes": 5242880.0 - } + "perfWarnings": {} } }, "properties": { @@ -206,10 +182,7 @@ "DotVVM.AutoUI.Controls.AutoFormBase": { "ExcludeProperties": { "type": "System.String[]", - "defaultValue": { - "$type": "System.String[], CoreLibrary", - "$values": [] - }, + "defaultValue": [], "onlyHardcoded": true, "fromCapability": "FieldSelectorProps" }, @@ -246,10 +219,7 @@ "DotVVM.AutoUI.Controls.AutoGridViewColumns": { "ExcludeProperties": { "type": "System.String[]", - "defaultValue": { - "$type": "System.String[], CoreLibrary", - "$values": [] - }, + "defaultValue": [], "onlyHardcoded": true, "fromCapability": "FieldSelectorProps" }, @@ -418,12 +388,13 @@ "type": "DotVVM.Framework.Binding.Expressions.IValueBinding, DotVVM.Framework", "dataContextChange": [ { - "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute", "PropertyName": "CheckedValue", + "Order": 0, + "AllowMissingProperty": false, "PropertyDependsOn": [ "CheckedValue" - ], - "TypeId": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework" + ] } ], "onlyBindings": true @@ -686,17 +657,18 @@ "type": "System.Collections.Generic.List`1[[DotVVM.Framework.Controls.GridViewColumn, DotVVM.Framework, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da]]", "dataContextChange": [ { - "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute", "PropertyName": "DataSource", + "Order": 0, + "AllowMissingProperty": false, "PropertyDependsOn": [ "DataSource" - ], - "TypeId": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework" + ] }, { - "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute", "Order": 1, - "TypeId": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework" + "PropertyDependsOn": [] } ], "mappingMode": "InnerElement", @@ -706,17 +678,18 @@ "type": "System.Collections.Generic.List`1[[DotVVM.Framework.Controls.Decorator, DotVVM.Framework, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da]]", "dataContextChange": [ { - "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute", "PropertyName": "DataSource", + "Order": 0, + "AllowMissingProperty": false, "PropertyDependsOn": [ "DataSource" - ], - "TypeId": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework" + ] }, { - "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute", "Order": 1, - "TypeId": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework" + "PropertyDependsOn": [] } ], "mappingMode": "InnerElement", @@ -744,17 +717,18 @@ "type": "System.Collections.Generic.List`1[[DotVVM.Framework.Controls.Decorator, DotVVM.Framework, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da]]", "dataContextChange": [ { - "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute", "PropertyName": "DataSource", + "Order": 0, + "AllowMissingProperty": false, "PropertyDependsOn": [ "DataSource" - ], - "TypeId": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework" + ] }, { - "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute", "Order": 1, - "TypeId": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework" + "PropertyDependsOn": [] } ], "mappingMode": "InnerElement", @@ -793,8 +767,7 @@ "AllowSorting": { "type": "System.Boolean", "dataContextManipulation": { - "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework", - "TypeId": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework" + "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute" }, "defaultValue": false, "onlyHardcoded": true @@ -820,16 +793,14 @@ "FilterTemplate": { "type": "DotVVM.Framework.Controls.ITemplate, DotVVM.Framework", "dataContextManipulation": { - "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework", - "TypeId": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework" + "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute" }, "mappingMode": "InnerElement" }, "HeaderCellDecorators": { "type": "System.Collections.Generic.List`1[[DotVVM.Framework.Controls.Decorator, DotVVM.Framework, Version=***, Culture=neutral, PublicKeyToken=23f3607db32275da]]", "dataContextManipulation": { - "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework", - "TypeId": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework" + "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute" }, "mappingMode": "InnerElement", "onlyHardcoded": true @@ -837,30 +808,26 @@ "HeaderCssClass": { "type": "System.String", "dataContextManipulation": { - "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework", - "TypeId": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework" + "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute" } }, "HeaderTemplate": { "type": "DotVVM.Framework.Controls.ITemplate, DotVVM.Framework", "dataContextManipulation": { - "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework", - "TypeId": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework" + "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute" }, "mappingMode": "InnerElement" }, "HeaderText": { "type": "System.String", "dataContextManipulation": { - "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework", - "TypeId": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework" + "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute" } }, "IsEditable": { "type": "System.Boolean", "dataContextManipulation": { - "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework", - "TypeId": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework" + "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute" }, "defaultValue": true, "onlyHardcoded": true @@ -868,8 +835,7 @@ "SortAscendingHeaderCssClass": { "type": "System.String", "dataContextManipulation": { - "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework", - "TypeId": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework" + "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute" }, "defaultValue": "sort-asc", "onlyHardcoded": true @@ -877,8 +843,7 @@ "SortDescendingHeaderCssClass": { "type": "System.String", "dataContextManipulation": { - "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework", - "TypeId": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework" + "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute" }, "defaultValue": "sort-desc", "onlyHardcoded": true @@ -886,16 +851,14 @@ "SortExpression": { "type": "System.String", "dataContextManipulation": { - "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework", - "TypeId": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework" + "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute" }, "onlyHardcoded": true }, "Visible": { "type": "System.Boolean", "dataContextManipulation": { - "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework", - "TypeId": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework" + "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute" }, "defaultValue": true, "onlyBindings": true @@ -903,8 +866,7 @@ "Width": { "type": "System.String", "dataContextManipulation": { - "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework", - "TypeId": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute, DotVVM.Framework" + "$type": "DotVVM.Framework.Compilation.ControlTree.PopDataContextManipulationAttribute" }, "onlyHardcoded": true } @@ -947,17 +909,18 @@ "type": "DotVVM.Framework.Binding.Expressions.IValueBinding`1[[System.Collections.Generic.IEnumerable`1[[System.Object, CoreLibrary]], CoreLibrary]], DotVVM.Framework", "dataContextChange": [ { - "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute", "Order": 1, - "TypeId": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework" + "PropertyDependsOn": [] }, { - "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute", "PropertyName": "DataSource", + "Order": 0, + "AllowMissingProperty": false, "PropertyDependsOn": [ "DataSource" - ], - "TypeId": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework" + ] } ], "required": true, @@ -971,17 +934,18 @@ "type": "DotVVM.Framework.Controls.ITemplate, DotVVM.Framework", "dataContextChange": [ { - "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute", "Order": 1, - "TypeId": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework" + "PropertyDependsOn": [] }, { - "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute", "PropertyName": "DataSource", + "Order": 0, + "AllowMissingProperty": false, "PropertyDependsOn": [ "DataSource" - ], - "TypeId": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework" + ] } ], "mappingMode": "InnerElement", @@ -1047,12 +1011,9 @@ "DotVVM.Framework.Controls.InlineScript": { "Dependencies": { "type": "System.String[]", - "defaultValue": { - "$type": "System.String[], CoreLibrary", - "$values": [ - "dotvvm" - ] - }, + "defaultValue": [ + "dotvvm" + ], "onlyHardcoded": true }, "Script": { @@ -1286,17 +1247,18 @@ "type": "DotVVM.Framework.Controls.ITemplate, DotVVM.Framework", "dataContextChange": [ { - "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute", "PropertyName": "DataSource", + "Order": 0, + "AllowMissingProperty": false, "PropertyDependsOn": [ "DataSource" - ], - "TypeId": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework" + ] }, { - "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute", "Order": 1, - "TypeId": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework" + "PropertyDependsOn": [] } ], "mappingMode": "InnerElement", @@ -1387,17 +1349,18 @@ "type": "DotVVM.Framework.Binding.Expressions.IValueBinding`1[[System.String, CoreLibrary]], DotVVM.Framework", "dataContextChange": [ { - "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute", "PropertyName": "DataSource", + "Order": 0, + "AllowMissingProperty": false, "PropertyDependsOn": [ "DataSource" - ], - "TypeId": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework" + ] }, { - "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute", "Order": 1, - "TypeId": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework" + "PropertyDependsOn": [] } ], "onlyBindings": true @@ -1406,17 +1369,18 @@ "type": "DotVVM.Framework.Binding.Expressions.IValueBinding, DotVVM.Framework", "dataContextChange": [ { - "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute", "PropertyName": "DataSource", + "Order": 0, + "AllowMissingProperty": false, "PropertyDependsOn": [ "DataSource" - ], - "TypeId": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework" + ] }, { - "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute", "Order": 1, - "TypeId": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework" + "PropertyDependsOn": [] } ], "onlyBindings": true @@ -1425,17 +1389,18 @@ "type": "DotVVM.Framework.Binding.Expressions.IValueBinding, DotVVM.Framework", "dataContextChange": [ { - "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute", "PropertyName": "DataSource", + "Order": 0, + "AllowMissingProperty": false, "PropertyDependsOn": [ "DataSource" - ], - "TypeId": "DotVVM.Framework.Binding.ControlPropertyBindingDataContextChangeAttribute, DotVVM.Framework" + ] }, { - "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework", + "$type": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute", "Order": 1, - "TypeId": "DotVVM.Framework.Binding.CollectionElementDataContextChangeAttribute, DotVVM.Framework" + "PropertyDependsOn": [] } ], "onlyBindings": true @@ -1994,29 +1959,23 @@ "precompilationMode": "InServerSideStyles" }, "DotVVM.Framework.Compilation.Styles.ResolvedControlHelper+LazyRuntimeControl": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.AddTemplateDecorator": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.Decorator, DotVVM.Framework", "withoutContent": true }, "DotVVM.Framework.Controls.AuthenticatedView": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.ConfigurableHtmlControl, DotVVM.Framework", "defaultContentProperty": "AuthenticatedTemplate" }, "DotVVM.Framework.Controls.BodyResourceLinks": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.Button": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.ButtonBase, DotVVM.Framework" }, "DotVVM.Framework.Controls.ButtonBase": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework", "interfaces": [ "DotVVM.Framework.Controls.IEventValidationHandler, DotVVM.Framework" @@ -2024,66 +1983,52 @@ "isAbstract": true }, "DotVVM.Framework.Controls.CheckableControlBase": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework", "isAbstract": true }, "DotVVM.Framework.Controls.CheckBox": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.CheckableControlBase, DotVVM.Framework" }, "DotVVM.Framework.Controls.ClaimView": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.ConfigurableHtmlControl, DotVVM.Framework", "defaultContentProperty": "HasClaimTemplate", "withoutContent": true }, "DotVVM.Framework.Controls.ComboBox": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.SelectHtmlControlBase, DotVVM.Framework" }, "DotVVM.Framework.Controls.CompositeControl": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework", "isAbstract": true, "withoutContent": true }, "DotVVM.Framework.Controls.ConcurrencyQueueSetting": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmBindableObject, DotVVM.Framework" }, "DotVVM.Framework.Controls.ConfigurableHtmlControl": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework", "isAbstract": true }, "DotVVM.Framework.Controls.ConfirmPostBackHandler": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.PostBackHandler, DotVVM.Framework" }, "DotVVM.Framework.Controls.Content": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.ContentPlaceHolder": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.ConfigurableHtmlControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.DataItemContainer": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.DataPager": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework", "withoutContent": true }, "DotVVM.Framework.Controls.Decorator": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.DotvvmBindableObject": { - "assembly": "DotVVM.Framework", "baseType": "System.Object", "interfaces": [ "DotVVM.Framework.Controls.IDotvvmObjectLike, DotVVM.Framework" @@ -2091,7 +2036,6 @@ "isAbstract": true }, "DotVVM.Framework.Controls.DotvvmControl": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmBindableObject, DotVVM.Framework", "interfaces": [ "DotVVM.Framework.Controls.IDotvvmControl, DotVVM.Framework", @@ -2100,71 +2044,57 @@ "isAbstract": true }, "DotVVM.Framework.Controls.DotvvmMarkupControl": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.EmptyData": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.ItemsControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.EnvironmentView": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.ConfigurableHtmlControl, DotVVM.Framework", "defaultContentProperty": "IsEnvironmentTemplate", "withoutContent": true }, "DotVVM.Framework.Controls.FileUpload": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework", "withoutContent": true }, "DotVVM.Framework.Controls.GridView": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.ItemsControl, DotVVM.Framework", "defaultContentProperty": "Columns", "withoutContent": true }, "DotVVM.Framework.Controls.GridViewCheckBoxColumn": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.GridViewColumn, DotVVM.Framework", "withoutContent": true }, "DotVVM.Framework.Controls.GridViewColumn": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmBindableObject, DotVVM.Framework", "isAbstract": true }, "DotVVM.Framework.Controls.GridViewTemplateColumn": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.GridViewColumn, DotVVM.Framework", "defaultContentProperty": "ContentTemplate", "withoutContent": true }, "DotVVM.Framework.Controls.GridViewTextColumn": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.GridViewColumn, DotVVM.Framework", "withoutContent": true }, "DotVVM.Framework.Controls.HeadResourceLinks": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.HierarchyRepeater": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.ItemsControl, DotVVM.Framework", "defaultContentProperty": "ItemTemplate", "withoutContent": true }, "DotVVM.Framework.Controls.HierarchyRepeater+HierarchyRepeaterItem": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.HierarchyRepeater+HierarchyRepeaterLevel": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.HtmlGenericControl": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework", "interfaces": [ "DotVVM.Framework.Controls.IControlWithHtmlAttributes, DotVVM.Framework", @@ -2172,178 +2102,140 @@ ] }, "DotVVM.Framework.Controls.HtmlLiteral": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.ConfigurableHtmlControl, DotVVM.Framework", "defaultContentProperty": "Html" }, "DotVVM.Framework.Controls.Infrastructure.DotvvmView": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.Infrastructure.GlobalizeResource": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.Infrastructure.MarkupControlContainer": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework", "withoutContent": true }, "DotVVM.Framework.Controls.Infrastructure.MarkupControlContainer`1": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.Infrastructure.MarkupControlContainer, DotVVM.Framework", "isAbstract": true, "withoutContent": true }, "DotVVM.Framework.Controls.Infrastructure.RawLiteral": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.InlineScript": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework", "defaultContentProperty": "Script" }, "DotVVM.Framework.Controls.ItemsControl": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework", "isAbstract": true }, "DotVVM.Framework.Controls.JsComponent": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.Label": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.LinkButton": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.ButtonBase, DotVVM.Framework" }, "DotVVM.Framework.Controls.ListBox": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.SelectHtmlControlBase, DotVVM.Framework" }, "DotVVM.Framework.Controls.Literal": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework", "withoutContent": true }, "DotVVM.Framework.Controls.ModalDialog": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.MultiSelect": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.MultiSelectHtmlControlBase, DotVVM.Framework" }, "DotVVM.Framework.Controls.MultiSelectHtmlControlBase": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.MultiSelector, DotVVM.Framework", "isAbstract": true }, "DotVVM.Framework.Controls.MultiSelector": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.SelectorBase, DotVVM.Framework", "isAbstract": true }, "DotVVM.Framework.Controls.NamedCommand": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework", "withoutContent": true }, "DotVVM.Framework.Controls.Panel": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.PlaceHolder": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.PostBackHandler": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmBindableObject, DotVVM.Framework", "isAbstract": true }, "DotVVM.Framework.Controls.PrecompiledControlPlaceholder": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.RadioButton": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.CheckableControlBase, DotVVM.Framework" }, "DotVVM.Framework.Controls.Repeater": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.ItemsControl, DotVVM.Framework", "defaultContentProperty": "ItemTemplate", "withoutContent": true }, "DotVVM.Framework.Controls.RequiredResource": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework", "withoutContent": true }, "DotVVM.Framework.Controls.RoleView": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.ConfigurableHtmlControl, DotVVM.Framework", "defaultContentProperty": "IsMemberTemplate", "withoutContent": true }, "DotVVM.Framework.Controls.RouteLink": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.SelectHtmlControlBase": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.Selector, DotVVM.Framework", "isAbstract": true }, "DotVVM.Framework.Controls.Selector": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.SelectorBase, DotVVM.Framework", "isAbstract": true }, "DotVVM.Framework.Controls.SelectorBase": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.ItemsControl, DotVVM.Framework", "isAbstract": true }, "DotVVM.Framework.Controls.SelectorItem": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework", "withoutContent": true }, "DotVVM.Framework.Controls.SpaContentPlaceHolder": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.ContentPlaceHolder, DotVVM.Framework" }, "DotVVM.Framework.Controls.SuppressPostBackHandler": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.PostBackHandler, DotVVM.Framework" }, "DotVVM.Framework.Controls.TemplateHost": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.DotvvmControl, DotVVM.Framework", "withoutContent": true }, "DotVVM.Framework.Controls.TextBox": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework", "withoutContent": true }, "DotVVM.Framework.Controls.UpdateProgress": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework" }, "DotVVM.Framework.Controls.ValidationSummary": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework", "withoutContent": true }, "DotVVM.Framework.Controls.Validator": { - "assembly": "DotVVM.Framework", "baseType": "DotVVM.Framework.Controls.HtmlGenericControl, DotVVM.Framework" } } diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeEmptyConfig.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeEmptyConfig.json index 0d9326ac3f..c8ca65948f 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeEmptyConfig.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeEmptyConfig.json @@ -2,49 +2,16 @@ "dotvvmVersion": "*.*.*.*", "config": { "markup": { - "importedNamespaces": [ - { - "namespace": "DotVVM.Framework.Binding.HelperNamespace" - }, - { - "namespace": "System.Linq" - } - ], - "ViewCompilation": { - "compileInParallel": true - } + "ViewCompilation": {} }, "resources": {}, - "security": { - "xssProtectionHeader": { - "enabled": true - }, - "contentTypeOptionsHeader": { - "enabled": true - }, - "verifySecFetchForPages": { - "enabled": true - }, - "verifySecFetchForCommands": { - "enabled": true - }, - "referrerPolicy": { - "enabled": true - } - }, - "runtime": { - "reloadMarkupFiles": {}, - "compressPostbacks": {}, - "maxPostbackSizeBytes": 134217728 - }, + "security": {}, + "runtime": {}, "defaultCulture": "en-US", "experimentalFeatures": {}, - "debug": false, "diagnostics": { "compilationPage": {}, - "perfWarnings": { - "bigViewModelBytes": 5242880.0 - } + "perfWarnings": {} } } } diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeResources.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeResources.json index 65e5a9ac37..11eac35a4b 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeResources.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeResources.json @@ -2,33 +2,28 @@ "dotvvmVersion": "*.*.*.*", "config": { "markup": { - "importedNamespaces": [ - { - "namespace": "DotVVM.Framework.Binding.HelperNamespace" - }, - { - "namespace": "System.Linq" - } - ], - "ViewCompilation": { - "compileInParallel": true - } + "ViewCompilation": {} }, "resources": { "DotVVM.Framework.ResourceManagement.InlineScriptResource": { "r6": { "Code": "alert(1)", + "Defer": false, "RenderPosition": "Body" }, "r7": { + "Defer": false, "RenderPosition": "Body" } }, "DotVVM.Framework.ResourceManagement.InlineStylesheetResource": { "r4": { - "Code": "body { display: none }" + "Code": "body { display: none }", + "RenderPosition": "Head" }, - "r5": {} + "r5": { + "RenderPosition": "Head" + } }, "DotVVM.Framework.ResourceManagement.TemplateResource": { "r9": { @@ -45,41 +40,27 @@ "r1": { "Defer": true, "Location": { - "$type": "DotVVM.Framework.ResourceManagement.UrlResourceLocation, DotVVM.Framework", "Url": "x", "DebugUrl": "x" }, + "LocationType": "DotVVM.Framework.ResourceManagement.UrlResourceLocation, DotVVM.Framework", "MimeType": "text/javascript", - "IntegrityHash": "hash, maybe" + "VerifyResourceIntegrity": true, + "IntegrityHash": "hash, maybe", + "RenderPosition": "Head" }, "r2": { "Defer": true, "Location": { - "$type": "DotVVM.Framework.ResourceManagement.UrlResourceLocation, DotVVM.Framework", "Url": "x", "DebugUrl": "x" }, + "LocationType": "DotVVM.Framework.ResourceManagement.UrlResourceLocation, DotVVM.Framework", "LocationFallback": { - "JavascriptCondition": "window.x", - "AlternativeLocations": [ - { - "$type": "DotVVM.Framework.ResourceManagement.UrlResourceLocation, DotVVM.Framework", - "Url": "y", - "DebugUrl": "y" - }, - { - "$type": "DotVVM.Framework.ResourceManagement.UrlResourceLocation, DotVVM.Framework", - "Url": "z", - "DebugUrl": "z" - }, - { - "$type": "DotVVM.Framework.ResourceManagement.FileResourceLocation, DotVVM.Framework", - "FilePath": "some-script.js", - "DebugFilePath": "some-script.js" - } - ] + "JavascriptCondition": "window.x" }, "MimeType": "text/javascript", + "VerifyResourceIntegrity": true, "Dependencies": [ "r1" ], @@ -89,45 +70,24 @@ "stylesheets": { "r3": { "Location": { - "$type": "DotVVM.Framework.ResourceManagement.UrlResourceLocation, DotVVM.Framework", "Url": "s", "DebugUrl": "s" }, + "LocationType": "DotVVM.Framework.ResourceManagement.UrlResourceLocation, DotVVM.Framework", "MimeType": "text/css", - "IntegrityHash": "hash, maybe" + "VerifyResourceIntegrity": true, + "IntegrityHash": "hash, maybe", + "RenderPosition": "Head" } } }, - "security": { - "xssProtectionHeader": { - "enabled": true - }, - "contentTypeOptionsHeader": { - "enabled": true - }, - "verifySecFetchForPages": { - "enabled": true - }, - "verifySecFetchForCommands": { - "enabled": true - }, - "referrerPolicy": { - "enabled": true - } - }, - "runtime": { - "reloadMarkupFiles": {}, - "compressPostbacks": {}, - "maxPostbackSizeBytes": 134217728 - }, + "security": {}, + "runtime": {}, "defaultCulture": "en-US", "experimentalFeatures": {}, - "debug": false, "diagnostics": { "compilationPage": {}, - "perfWarnings": { - "bigViewModelBytes": 5242880.0 - } + "perfWarnings": {} } } } diff --git a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeRoutes.json b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeRoutes.json index 3b686a5d55..ed6856553a 100644 --- a/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeRoutes.json +++ b/src/Tests/Runtime/config-tests/ConfigurationSerializationTests.SerializeRoutes.json @@ -2,17 +2,7 @@ "dotvvmVersion": "*.*.*.*", "config": { "markup": { - "importedNamespaces": [ - { - "namespace": "DotVVM.Framework.Binding.HelperNamespace" - }, - { - "namespace": "System.Linq" - } - ], - "ViewCompilation": { - "compileInParallel": true - } + "ViewCompilation": {} }, "routes": { "route1": { @@ -46,36 +36,13 @@ } }, "resources": {}, - "security": { - "xssProtectionHeader": { - "enabled": true - }, - "contentTypeOptionsHeader": { - "enabled": true - }, - "verifySecFetchForPages": { - "enabled": true - }, - "verifySecFetchForCommands": { - "enabled": true - }, - "referrerPolicy": { - "enabled": true - } - }, - "runtime": { - "reloadMarkupFiles": {}, - "compressPostbacks": {}, - "maxPostbackSizeBytes": 134217728 - }, + "security": {}, + "runtime": {}, "defaultCulture": "en-US", "experimentalFeatures": {}, - "debug": false, "diagnostics": { "compilationPage": {}, - "perfWarnings": { - "bigViewModelBytes": 5242880.0 - } + "perfWarnings": {} } } } diff --git a/src/Tests/ViewModel/DefaultViewModelSerializerTests.cs b/src/Tests/ViewModel/DefaultViewModelSerializerTests.cs index e05c1a44de..d5c66b6795 100644 --- a/src/Tests/ViewModel/DefaultViewModelSerializerTests.cs +++ b/src/Tests/ViewModel/DefaultViewModelSerializerTests.cs @@ -16,8 +16,11 @@ using DotVVM.Framework.ViewModel.Serialization; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json.Linq; using DotVVM.Framework.Testing; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; +using DotVVM.Framework.Utils; +using System.Text.Json.Nodes; +using System.Text.Json; namespace DotVVM.Framework.Tests.Runtime { @@ -67,14 +70,13 @@ public void DefaultViewModelSerializer_NoEncryptedValues() Property5 = null }; context.ViewModel = oldViewModel; - serializer.BuildViewModel(context, null); - var result = context.GetSerializedViewModel(); + var result = serializer.SerializeViewModel(context); result = UnwrapSerializedViewModel(result); result = WrapSerializedViewModel(result); var newViewModel = new TestViewModel(); context.ViewModel = newViewModel; - serializer.PopulateViewModel(context, result); + serializer.PopulateViewModel(context, StringUtils.Utf8.GetBytes(result)); Assert.AreEqual(oldViewModel.Property1, newViewModel.Property1); Assert.AreEqual(oldViewModel.Property2, newViewModel.Property2); @@ -145,8 +147,8 @@ public void ViewModelResponse_CommandResult_Serialized() var response = PrepareResponse(viewModel, commandResult); - var responseModel = response["viewModel"].ToObject(); - var responseResult = response["commandResult"].ToObject(); + var responseModel = JsonSerializer.Deserialize(response["viewModel"]); + var responseResult = JsonSerializer.Deserialize(response["commandResult"]); Assert.AreEqual("a", responseModel.PropertyA); Assert.AreEqual(1, responseModel.PropertyB); @@ -163,9 +165,9 @@ public void ViewModelResponse_NoCommandResult_NotPresent() }; var response = PrepareResponse(viewModel, null); - var responseModel = response["viewModel"].ToObject(); + var responseModel = JsonSerializer.Deserialize(response["viewModel"]); - Assert.IsFalse(response.TryGetValue("commandResult", out var _)); + Assert.IsFalse(response.ContainsKey("commandResult")); Assert.AreEqual("a", responseModel.PropertyA); Assert.AreEqual(1, responseModel.PropertyB); } @@ -179,9 +181,9 @@ public void ViewModelResponse_NoCustomProperties_NotPresent() }; var response = PrepareResponse(viewModel, null); - var responseModel = response["viewModel"].ToObject(); + var responseModel = JsonSerializer.Deserialize(response["viewModel"]); - Assert.IsFalse(response.TryGetValue("customProperties", out var _)); + Assert.IsFalse(response.ContainsKey("customProperties")); Assert.AreEqual("a", responseModel.PropertyA); Assert.AreEqual(1, responseModel.PropertyB); } @@ -199,9 +201,9 @@ public void ViewModelResponse_CustomResponseProperties_Serialized() {"prop2", "Hello"} }); - var responseModel = response["viewModel"].ToObject(); - var prop1 = response["customProperties"]["prop1"].ToObject(); - var prop2 = response["customProperties"]["prop2"].ToObject(); + var responseModel = JsonSerializer.Deserialize(response["viewModel"]); + var prop1 = JsonSerializer.Deserialize(response["customProperties"]["prop1"]); + var prop2 = response["customProperties"]["prop2"].GetValue(); Assert.AreEqual("a", responseModel.PropertyA); Assert.AreEqual(1, responseModel.PropertyB); @@ -218,9 +220,9 @@ public void StaticCommandResponse_CustomResponseProperties_Serialized() {"prop2", "Hello"} }); - var commandResult = response["result"].ToObject(); - var prop1 = response["customProperties"]["prop1"].ToObject(); - var prop2 = response["customProperties"]["prop2"].ToObject(); + var commandResult = response["result"].GetValue(); + var prop1 = JsonSerializer.Deserialize(response["customProperties"]["prop1"]); + var prop2 = response["customProperties"]["prop2"].GetValue(); Assert.AreEqual("Test", commandResult); @@ -233,11 +235,11 @@ public void StaticCommandResponse_NoCustomResponseProperties_NotPresent() { var response = PrepareStaticCommandResponse("Test"); - var commandResult = response["result"].ToObject(); + var commandResult = response["result"].GetValue(); Assert.AreEqual("Test", commandResult); - Assert.IsFalse(response.TryGetValue("customProperties", out var _)); + Assert.IsFalse(response.ContainsKey("customProperties")); } [TestMethod] @@ -255,7 +257,7 @@ public void StaticCommandResponse_AlreadySerializedProperties_Throws() public void ViewModelResponse_AlreadySerializedProperties_Throws() { context.ViewModel = new TestViewModel { }; - serializer.BuildViewModel(context, null); + serializer.SerializeViewModel(context, null); Assert.ThrowsException(() => { @@ -263,7 +265,7 @@ public void ViewModelResponse_AlreadySerializedProperties_Throws() }); } - private JObject PrepareResponse(object viewModel, object commandResult, Dictionary customProperties = null) + private JsonObject PrepareResponse(object viewModel, object commandResult, Dictionary customProperties = null) { context.ViewModel = viewModel; @@ -272,13 +274,12 @@ private JObject PrepareResponse(object viewModel, object commandResult, Dictiona context.CustomResponseProperties.Add(prop.Key, prop.Value); } - serializer.BuildViewModel(context, commandResult); - var result = context.GetSerializedViewModel(); + var result = serializer.SerializeViewModel(context, commandResult); - return JObject.Parse(result); + return JsonNode.Parse(result).AsObject(); } - private JObject PrepareStaticCommandResponse(object commandResult, Dictionary customProperties = null) + private JsonObject PrepareStaticCommandResponse(object commandResult, Dictionary customProperties = null) { foreach (var prop in customProperties ?? new Dictionary()) { @@ -287,7 +288,7 @@ private JObject PrepareStaticCommandResponse(object commandResult, Dictionary(() => { - try - { - serializer.BuildViewModel(context, null); - context.GetSerializedViewModel(); - } - catch (Exception ex) - { - throw ex.InnerException; - } + var e = XAssert.ThrowsAny(() => { + serializer.SerializeViewModel(context); }); + var eBase = e.GetBaseException(); + Assert.AreEqual("The property ReadOnlyProperty of type System.Int32 uses the Protect attribute, therefore its Bind Direction must be set to Both.", eBase.Message); } public class ViewModelWithInvalidProtectionSettings @@ -440,14 +435,14 @@ public void DefaultViewModelSerializer_SignedAndEncryptedValue_NullableInt_NullV }; context.ViewModel = oldViewModel; - serializer.BuildViewModel(context, null); - var result = context.GetSerializedViewModel(); + + var result = serializer.SerializeViewModel(context); result = UnwrapSerializedViewModel(result); result = WrapSerializedViewModel(result); var newViewModel = new TestViewModel5(); context.ViewModel = newViewModel; - serializer.PopulateViewModel(context, result); + serializer.PopulateViewModel(context, StringUtils.Utf8.GetBytes(result)); Assert.AreEqual(oldViewModel.ProtectedNullable, newViewModel.ProtectedNullable); } @@ -467,14 +462,13 @@ public void DefaultViewModelSerializer_Enum() Property1 = TestEnum.Second }; context.ViewModel = oldViewModel; - serializer.BuildViewModel(context, null); - var result = context.GetSerializedViewModel(); + var result = serializer.SerializeViewModel(context); result = UnwrapSerializedViewModel(result); result = WrapSerializedViewModel(result); var newViewModel = new EnumTestViewModel(); context.ViewModel = newViewModel; - serializer.PopulateViewModel(context, result); + serializer.PopulateViewModel(context, StringUtils.Utf8.GetBytes(result)); Assert.IsFalse(result.Contains(typeof(TestEnum).FullName)); Assert.AreEqual(oldViewModel.Property1, newViewModel.Property1); @@ -517,14 +511,13 @@ public void DefaultViewModelSerializer_EnumInCollection() }; context.ViewModel = oldViewModel; - serializer.BuildViewModel(context, null); - var result = context.GetSerializedViewModel(); + var result = serializer.SerializeViewModel(context); result = UnwrapSerializedViewModel(result); result = WrapSerializedViewModel(result); var newViewModel = new EnumCollectionTestViewModel() { Children = new List() }; context.ViewModel = newViewModel; - serializer.PopulateViewModel(context, result); + serializer.PopulateViewModel(context, StringUtils.Utf8.GetBytes(result)); Assert.IsFalse(result.Contains(typeof(TestEnum).FullName)); Assert.AreEqual(oldViewModel.Property1, newViewModel.Property1); @@ -564,7 +557,7 @@ private static string WrapSerializedViewModel(string result) /// private static string UnwrapSerializedViewModel(string result) { - return JObject.Parse(result)["viewModel"].ToString(); + return JsonNode.Parse(result)["viewModel"].ToString(); } } diff --git a/src/Tests/ViewModel/JsonDiffTests.cs b/src/Tests/ViewModel/JsonDiffTests.cs index bcbee27dec..977da704bb 100644 --- a/src/Tests/ViewModel/JsonDiffTests.cs +++ b/src/Tests/ViewModel/JsonDiffTests.cs @@ -3,21 +3,21 @@ using DotVVM.Framework.Utils; using DotVVM.Framework.Testing; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Text.Json.Nodes; +using System.Text.Json; +using DotVVM.Framework.Hosting; namespace DotVVM.Framework.Tests.ViewModel { [TestClass] public class JsonDiffTests { - - private JsonSerializer serializer = new JsonSerializer { TypeNameHandling = TypeNameHandling.Auto }; + private JsonSerializerOptions serializerOptions = VisualStudioHelper.GetSerializerOptions(); public JsonDiffTests() { @@ -27,11 +27,12 @@ public JsonDiffTests() [TestMethod] public void JsonDiff_SimpleTest() { - var a = JObject.Parse("{name:'djsfsh',ahoj:45}"); - var b = JObject.Parse("{name:'djsfsh',ahoj:42}"); + var a = JsonNode.Parse("""{"name":"djsfsh","ahoj":45}""")!.AsObject(); + var b = JsonNode.Parse("""{"name":"djsfsh","ahoj":42}""")!.AsObject(); var diff = JsonUtils.Diff(a, b); JsonUtils.Patch(a, diff); - Assert.IsTrue(JToken.DeepEquals(a, b)); + Assert.IsTrue(JsonNode.DeepEquals(a, b)); + Assert.AreEqual("""{"ahoj":42}""", diff.ToJsonString()); } [TestMethod] @@ -56,9 +57,9 @@ public void JsonDiff_Configuration_AddingRoute() CreateDiff(c => c.RouteTable.Add("Route2", "Path2", "View2.dothtml")), CreateDiff(c => c.RouteTable.Add("Route3", "Path3/{Name}", "View3.dothtml", new { Name = "defaultname" })) ); - Assert.IsTrue(config.RouteTable.Any(r => r.RouteName == "Route1")); - Assert.IsTrue(config.RouteTable.Any(r => r.RouteName == "Route2")); - Assert.IsTrue(config.RouteTable.Any(r => r.RouteName == "Route3")); + XAssert.Contains("Route1", config.RouteTable.Select(r => r.RouteName)); + XAssert.Contains("Route2", config.RouteTable.Select(r => r.RouteName)); + XAssert.Contains("Route3", config.RouteTable.Select(r => r.RouteName)); Assert.AreEqual("View1.dothtml", config.RouteTable.Single(r => r.RouteName == "Route1").VirtualPath); Assert.AreEqual("defaultname", config.RouteTable.Single(r => r.RouteName == "Route3").DefaultValues["Name"]); } @@ -66,46 +67,48 @@ public void JsonDiff_Configuration_AddingRoute() [TestMethod] public void JsonDiff_BusinessPackFilter_NoThrow() { - var source = JObject.Parse( + var source = JsonNode.Parse( @"{ - ""FieldName"": ""DateRequiredBy"", - ""FieldDisplayName"": null, - ""Operator"": ""LessThan"", - ""FormatString"": null, - ""Value"": ""2019-08-07T00:00:00"", - ""Type"": ""FilterCondition"" -}"); - var target = JObject.Parse( + ""FieldName"": ""DateRequiredBy"", + ""FieldDisplayName"": null, + ""Operator"": ""LessThan"", + ""FormatString"": null, + ""Value"": ""2019-08-07T00:00:00"", + ""Type"": ""FilterCondition"" +}")!.AsObject(); + var target = JsonNode.Parse( @"{ - ""FieldName"": ""Code"", - ""FieldDisplayName"": null, - ""Operator"": ""Equal"", - ""FormatString"": null, - ""Value"": ""HHK"", - ""Type"": ""FilterCondition"" -}"); + ""FieldName"": ""Code"", + ""FieldDisplayName"": null, + ""Operator"": ""Equal"", + ""FormatString"": null, + ""Value"": ""HHK"", + ""Type"": ""FilterCondition"" +}")!.AsObject(); var diff = JsonUtils.Diff(source, target); + Assert.AreEqual("""{"FieldName":"Code","Operator":"Equal","Value":"HHK"}""", diff.ToJsonString()); } - private JObject CreateDiff(Action fn) + private JsonObject CreateDiff(Action fn) { var config = DotvvmTestHelper.CreateConfiguration(); - var json0 = JObject.FromObject(config, serializer); + var json0 = JsonSerializer.SerializeToNode(config, serializerOptions)!.AsObject(); fn(config); - var json1 = JObject.FromObject(config, serializer); + var json1 = JsonSerializer.SerializeToNode(config, serializerOptions)!.AsObject(); return JsonUtils.Diff(json0, json1); } - private DotvvmConfiguration ApplyPatches(DotvvmConfiguration init, params JObject[] patches) + private DotvvmConfiguration ApplyPatches(DotvvmConfiguration init, params JsonObject[] patches) { - var json = JObject.FromObject(init, serializer); + var json = JsonSerializer.SerializeToNode(init, serializerOptions)!.AsObject(); foreach (var p in patches) { + Console.WriteLine("Applying patch: " + p.ToJsonString()); JsonUtils.Patch(json, p); } - return json.ToObject(serializer); + return JsonSerializer.Deserialize(json, serializerOptions); } - private DotvvmConfiguration ApplyPatches(params JObject[] patches) => ApplyPatches(DotvvmTestHelper.CreateConfiguration(), patches); + private DotvvmConfiguration ApplyPatches(params JsonObject[] patches) => ApplyPatches(DotvvmTestHelper.CreateConfiguration(), patches); } } diff --git a/src/Tests/ViewModel/JsonPatchTests.cs b/src/Tests/ViewModel/JsonPatchTests.cs index c6b883daf0..c39d5f72e4 100644 --- a/src/Tests/ViewModel/JsonPatchTests.cs +++ b/src/Tests/ViewModel/JsonPatchTests.cs @@ -1,8 +1,8 @@ using System; using System.Linq; +using System.Text.Json.Nodes; using DotVVM.Framework.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json.Linq; namespace DotVVM.Framework.Tests.ViewModel { @@ -11,61 +11,57 @@ public class JsonPatchTests { [DataTestMethod] - [DataRow(null, "['a', 'b']")] - [DataRow("['a', 'd']", "['a', 'd']")] - [DataRow("['a', 'b', 'c']", "['a', 'b', 'c']")] - [DataRow("['a']", "['a']")] - public void JsonPatch_Arrays_Primitive(string modifiedJson, string resultJson) + [DataRow(null, """["a", "b"]""")] + [DataRow("""["a", "d"]""", """["a", "d"]""")] + [DataRow("""["a", "b", "c"]""", """["a", "b", "c"]""")] + [DataRow("""["a"]""", """["a"]""")] + public void JsonPatch_Arrays_Primitive(string diffJson, string resultJson) { - var original = JObject.Parse(WrapArray("['a', 'b']")); - var modified = JObject.Parse(WrapArray(modifiedJson)); - var result = JObject.Parse(WrapArray(resultJson)); - JsonUtils.Patch(original, modified); - Assert.IsTrue(JToken.DeepEquals(original, result)); + CheckCore(WrapArray("""["a", "b"]"""), WrapArray(diffJson), WrapArray(resultJson)); } [DataTestMethod] - [DataRow("{ }", "{ 'a': 1, 'b': 'a' }")] - [DataRow("{ 'a': 2 }", "{ 'a': 2, 'b': 'a' }")] - [DataRow("{ 'a': 2, c: null }", "{ 'a': 2, 'b': 'a', 'c': null }")] - [DataRow("{ 'a': 2 }", "{ 'a': 2, 'b': 'a' }")] - public void JsonPatch_Objects(string modifiedJson, string resultJson) + [DataRow("""{ }""", """{ "a": 1, "b": "a" }""")] + [DataRow("""{ "a": 2 }""", """{ "a": 2, "b": "a" }""")] + [DataRow("""{ "a": 2, "c": null }""", """{ "a": 2, "b": "a", "c": null }""")] + [DataRow("""{ "a": 2 }""", """{ "a": 2, "b": "a" }""")] + public void JsonPatch_Objects(string diffJson, string resultJson) { - var original = JObject.Parse("{ 'a': 1, 'b': 'a' }"); - var modified = JObject.Parse(modifiedJson); - var result = JObject.Parse(resultJson); - JsonUtils.Patch(original, modified); - Assert.IsTrue(JToken.DeepEquals(original, result)); + CheckCore("""{ "a": 1, "b": "a" }""", diffJson, resultJson); } [DataTestMethod] - [DataRow("{ }", "{ 'a': 1, 'bparent': { 'b': 'a', 'c': 1 } }")] - [DataRow("{ 'a': 2, 'b': 'a' }", "{ 'a': 2, 'b': 'a', 'bparent': { 'b': 'a', 'c': 1 } }")] - [DataRow("{ 'bparent': { 'c': 2 } }", "{ 'a': 1, 'bparent': { 'b': 'a', 'c': 2 } }")] - [DataRow("{ 'bparent': { 'c': 2, 'd': 3 } }", "{ 'a': 1, 'bparent': { 'b': 'a', 'c': 2, 'd': 3 } }")] - [DataRow("{ 'bparent': { 'b': 'b' } }", "{ 'a': 1, 'bparent': { 'b': 'b', 'c': 1 } }")] - public void JsonPatch_Objects_Nested(string modifiedJson, string resultJson) + [DataRow("""{ }""", """{ "a": 1, "bparent": { "b": "a", "c": 1 } }""")] + [DataRow("""{ "a": 2, "b": "a" }""", """{ "a": 2, "b": "a", "bparent": { "b": "a", "c": 1 } }""")] + [DataRow("""{ "bparent": { "c": 2 } }""", """{ "a": 1, "bparent": { "b": "a", "c": 2 } }""")] + [DataRow("""{ "bparent": { "c": 2, "d": 3 } }""", """{ "a": 1, "bparent": { "b": "a", "c": 2, "d": 3 } }""")] + [DataRow("""{ "bparent": { "b": "b" } }""", """{ "a": 1, "bparent": { "b": "b", "c": 1 } }""")] + public void JsonPatch_Objects_Nested(string diffJson, string resultJson) { - var original = JObject.Parse("{ 'a': 1, 'bparent': { 'b': 'a', 'c': 1 } }"); - var modified = JObject.Parse(modifiedJson); - var result = JObject.Parse(resultJson); - JsonUtils.Patch(original, modified); - Assert.IsTrue(JToken.DeepEquals(original, result)); + CheckCore("""{ "a": 1, "bparent": { "b": "a", "c": 1 } }""", diffJson, resultJson); } [DataTestMethod] - [DataRow(null, "[{ 'a': 1 }, { 'a': 2 }]")] - [DataRow("[{ 'a': 3 }, {}]", "[{ 'a': 3 }, { 'a': 2 }]")] - [DataRow("[{}, {}, { 'a': 3 }]", "[{ 'a': 1 }, { 'a': 2 }, { 'a': 3 }]")] - [DataRow("[{ 'a': 2 }]", "[{ 'a': 2 }]")] - [DataRow("[{}]", "[{ 'a': 1 }]")] - public void JsonPatch_ObjectArray(string modifiedJson, string resultJson) + [DataRow(null, """[{ "a": 1 }, { "a": 2 }]""")] + [DataRow("""[{ "a": 3 }, {}]""", """[{ "a": 3 }, { "a": 2 }]""")] + [DataRow("""[{}, {}, { "a": 3 }]""", """[{ "a": 1 }, { "a": 2 }, { "a": 3 }]""")] + [DataRow("""[{ "a": 2 }]""", """[{ "a": 2 }]""")] + [DataRow("""[{}]""", """[{ "a": 1 }]""")] + public void JsonPatch_ObjectArray(string diffJson, string resultJson) { - var original = JObject.Parse(WrapArray("[{ 'a': 1 }, { 'a': 2 }]")); - var modified = JObject.Parse(WrapArray(modifiedJson)); - var result = JObject.Parse(WrapArray(resultJson)); - JsonUtils.Patch(original, modified); - Assert.IsTrue(JToken.DeepEquals(original, result)); + CheckCore(WrapArray("""[{ "a": 1 }, { "a": 2 }]"""), WrapArray(diffJson), WrapArray(resultJson)); + } + + private void CheckCore(string originalJson, string diffJson, string resultJson) + { + var original = JsonNode.Parse(originalJson)!.AsObject(); + var modified = original.DeepClone().AsObject(); + var diff = JsonNode.Parse(diffJson)!.AsObject(); + var result = JsonNode.Parse(resultJson)!.AsObject(); + JsonUtils.Patch(modified, diff); + Assert.IsTrue(JsonNode.DeepEquals(modified, result), $"Expected: {result}, modified: {modified}"); + var newDiff = JsonUtils.Diff(original, modified); + Assert.IsTrue(JsonNode.DeepEquals(diff, newDiff), $"Original diff: {diff}, computed diff: {newDiff}"); } private string WrapArray(string a) @@ -75,7 +71,7 @@ private string WrapArray(string a) return "{}"; } - return "{'array': " + a + "}"; + return "{\"array\": " + a + "}"; } } } diff --git a/src/Tests/ViewModel/SerializerErrorTests.cs b/src/Tests/ViewModel/SerializerErrorTests.cs index 3f7afa4429..793aef2a61 100644 --- a/src/Tests/ViewModel/SerializerErrorTests.cs +++ b/src/Tests/ViewModel/SerializerErrorTests.cs @@ -1,8 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using DotVVM.Framework.ViewModel.Serialization; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System.IO; using System; using System.Collections.Generic; @@ -13,6 +11,7 @@ using DotVVM.Framework.Testing; using System.Text; using DotVVM.Framework.Controls; +using System.Text.Json.Serialization; namespace DotVVM.Framework.Tests.ViewModel { diff --git a/src/Tests/ViewModel/SerializerTests.cs b/src/Tests/ViewModel/SerializerTests.cs index 57289c54bd..6b2e4d8f06 100644 --- a/src/Tests/ViewModel/SerializerTests.cs +++ b/src/Tests/ViewModel/SerializerTests.cs @@ -1,8 +1,8 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using DotVVM.Framework.ViewModel.Serialization; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using STJ = System.Text.Json; +using STJS = System.Text.Json.Serialization; using System.IO; using System; using System.Collections.Generic; @@ -11,60 +11,64 @@ using DotVVM.Framework.Compilation.Parser; using DotVVM.Framework.Configuration; using DotVVM.Framework.Testing; +using DotVVM.Framework.Utils; using System.Text; using DotVVM.Framework.Controls; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Runtime.Serialization; namespace DotVVM.Framework.Tests.ViewModel { [TestClass] public class SerializerTests { - static ViewModelJsonConverter CreateConverter(bool isPostback, JObject encryptedValues = null) + static ViewModelJsonConverter jsonConverter = DotvvmTestHelper.DefaultConfig.ServiceProvider.GetRequiredService(); + static STJ.JsonSerializerOptions jsonOptions = new STJ.JsonSerializerOptions(DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe) { + Converters = { jsonConverter }, + WriteIndented = true + }; + static DotvvmSerializationState CreateState(bool isPostback, JsonObject? readEncryptedValues = null) { var config = DotvvmTestHelper.DefaultConfig; - return new ViewModelJsonConverter( + return DotvvmSerializationState.Create( isPostback, - config.ServiceProvider.GetRequiredService(), config.ServiceProvider, - encryptedValues + readEncryptedValues is null ? null : new JsonObject([ new("0", readEncryptedValues) ]) ); } - static string Serialize(T viewModel, out JObject encryptedValues, bool isPostback = false) + static string Serialize(T viewModel, out JsonObject encryptedValues, bool isPostback = false) { - encryptedValues = new JObject(); - var settings = DefaultSerializerSettingsProvider.Instance.Settings; - var serializer = JsonSerializer.Create(settings); - serializer.Converters.Add(CreateConverter(isPostback, encryptedValues)); + using var state = CreateState(isPostback, null); - var output = new StringWriter(); - serializer.Serialize(output, viewModel); - return output.ToString(); + var json = STJ.JsonSerializer.Serialize(viewModel, jsonOptions); + var ev = state.WriteEncryptedValues.ToSpan(); + encryptedValues = ev.Length > 0 ? JsonNode.Parse(ev).AsObject() : new JsonObject(); + return json; } - static T Deserialize(string json, JObject encryptedValues = null) + static T Deserialize(string json, JsonObject encryptedValues = null) { - var settings = DefaultSerializerSettingsProvider.Instance.Settings; - var serializer = JsonSerializer.Create(settings); - serializer.Converters.Add(CreateConverter(true, encryptedValues)); + using var state = CreateState(false, encryptedValues ?? new JsonObject()); - return serializer.Deserialize(new JsonTextReader(new StringReader(json))); + return STJ.JsonSerializer.Deserialize(json, jsonOptions); } - static T PopulateViewModel(string json, T existingValue, JObject encryptedValues = null) + static T PopulateViewModel(string json, T existingValue, JsonObject encryptedValues = null) { - var settings = DefaultSerializerSettingsProvider.Instance.Settings; - var serializer = JsonSerializer.Create(settings); - var dotvvmConverter = CreateConverter(true, encryptedValues); - serializer.Converters.Add(dotvvmConverter); - return (T)dotvvmConverter.Populate(new JsonTextReader(new StringReader(json)), serializer, existingValue); + 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); } - internal static (T vm, JObject json) SerializeAndDeserialize(T viewModel, bool isPostback = false) + internal static (T vm, JsonObject json) SerializeAndDeserialize(T viewModel, bool isPostback = false) { var json = Serialize(viewModel, out var encryptedValues, isPostback); var viewModel2 = Deserialize(json, encryptedValues); - return (viewModel2, JObject.Parse(json)); + return (viewModel2, JsonNode.Parse(json).AsObject()); } [TestMethod] @@ -171,24 +175,19 @@ public void Support_NestedMixedProtectedData() [DataRow(new byte[] { 1, 2, 3 })] public void CustomJsonConverters_ByteArray(byte[] array) { + var converter = new DotvvmByteArrayConverter(); using var stream = new MemoryStream(); // Serialize array - using (var writer = new JsonTextWriter(new StreamWriter(stream, Encoding.UTF8, 4096, leaveOpen: true))) + using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping })) { - new DotvvmByteArrayConverter().WriteJson(writer, array, new JsonSerializer()); - writer.Flush(); + converter.Write(writer, array, DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe); } // Deserialize array - stream.Position = 0; byte[] deserialized; - using (var reader = new JsonTextReader(new StreamReader(stream, Encoding.UTF8))) - { - while (reader.TokenType == JsonToken.None) - reader.Read(); - - deserialized = (byte[])new DotvvmByteArrayConverter().ReadJson(reader, typeof(byte[]), null, new JsonSerializer()); - } + var reader = new Utf8JsonReader(stream.ToSpan()); + reader.Read(); + deserialized = (byte[])converter.Read(ref reader, typeof(byte[]), DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe); CollectionAssert.AreEqual(array, deserialized); } @@ -228,7 +227,10 @@ public void SupportTuples() } ) }; - var obj2 = SerializeAndDeserialize(obj, isPostback: true).vm; + var (obj2, json) = SerializeAndDeserialize(obj, isPostback: true); + + Assert.AreEqual("""{"Item1":9,"Item2":8,"Item3":7,"Item4":6}""", json["P1"].ToJsonString(new JsonSerializerOptions { WriteIndented = false })); + Assert.AreEqual("", json["P2"].ToString()); Assert.AreEqual(obj.P1, obj2.P1); Assert.AreEqual(obj.P2, obj2.P2); @@ -389,7 +391,7 @@ public void SupportConstructorInjection() Assert.AreEqual(obj.Property1, obj2.Property1); Assert.AreEqual(obj.GetService(), obj2.GetService()); Assert.AreEqual(obj.Property1, (string)json["Property1"]); - Assert.IsNull(json.Property("Service")); + Assert.IsNull(json["Service"]); } [TestMethod] @@ -406,7 +408,116 @@ public void SupportsSignedDictionary() CollectionAssert.Contains(obj2.SignedDictionary, new KeyValuePair("a", "x")); CollectionAssert.Contains(obj2.SignedDictionary, new KeyValuePair("b", "y")); Assert.AreEqual(obj.SignedDictionary.Count, obj2.SignedDictionary.Count); - Assert.IsNotNull(json.Property("SignedDictionary")); + Assert.IsNotNull(json["SignedDictionary"]); + } + + [TestMethod] + public void SupportsDateTime() + { + var obj = new TestViewModelWithDateTimes() { + DateTime1 = new DateTime(2000, 1, 1, 15, 0, 0, DateTimeKind.Utc), + DateTime2 = new DateTime(2000, 1, 1, 15, 0, 0, DateTimeKind.Local), + DateTime3 = new DateTime(2000, 1, 1, 15, 0, 0, DateTimeKind.Unspecified), + DateOnly = new DateOnly(2000, 1, 1), + TimeOnly = new TimeOnly(15, 0, 0) + }; + var (obj2, json) = SerializeAndDeserialize(obj); + Console.WriteLine(json); + Assert.AreEqual(obj.DateTime1, obj2.DateTime1); + Assert.AreEqual(DateTimeKind.Unspecified, obj2.DateTime1.Kind); + Assert.AreEqual(obj.DateTime2, obj2.DateTime2); + Assert.AreEqual(DateTimeKind.Unspecified, obj2.DateTime2.Kind); + Assert.AreEqual(obj.DateTime3, obj2.DateTime3); + Assert.AreEqual(DateTimeKind.Unspecified, obj2.DateTime3.Kind); + Assert.AreEqual(obj.DateOnly, obj2.DateOnly); + Assert.AreEqual(obj.TimeOnly, obj2.TimeOnly); + + Assert.AreEqual("2000-01-01", json["DateOnly"].GetValue()); + Assert.AreEqual("15:00:00.0000000", json["TimeOnly"].GetValue()); + Assert.AreEqual("2000-01-01T15:00:00", json["DateTime1"].GetValue()); + Assert.AreEqual("2000-01-01T15:00:00", json["DateTime2"].GetValue()); + Assert.AreEqual("2000-01-01T15:00:00", json["DateTime3"].GetValue()); + } + + [TestMethod] + public void SupportsEnums() + { + var obj = new TestViewModelWithEnums() { + Byte = TestViewModelWithEnums.ByteEnum.C, + SByte = TestViewModelWithEnums.SByteEnum.B, + UInt16 = TestViewModelWithEnums.UInt16Enum.B, + Int16 = TestViewModelWithEnums.Int16Enum.D, + UInt32 = TestViewModelWithEnums.UInt32Enum.B, + Int32 = TestViewModelWithEnums.Int32Enum.D, + UInt64 = TestViewModelWithEnums.UInt64Enum.B, + Int64 = TestViewModelWithEnums.Int64Enum.B, + DateTimeKind = DateTimeKind.Utc, + DuplicateName = TestViewModelWithEnums.DuplicateNameEnum.DAndAlsoLonger, + EnumMember = TestViewModelWithEnums.EnumMemberEnum.B, + Int32Flags = TestViewModelWithEnums.Int32FlagsEnum.ABC, + UInt64Flags = TestViewModelWithEnums.UInt64FlagsEnum.F1 | TestViewModelWithEnums.UInt64FlagsEnum.F2 | TestViewModelWithEnums.UInt64FlagsEnum.F64 + }; + var (obj2, json) = SerializeAndDeserialize(obj); + + Assert.AreEqual("member-b", json["EnumMember"].GetValue()); + Assert.AreEqual(obj.Byte, obj2.Byte); + Assert.AreEqual(obj.SByte, obj2.SByte); + Assert.AreEqual(obj.UInt16, obj2.UInt16); + Assert.AreEqual(obj.Int16, obj2.Int16); + Assert.AreEqual(obj.UInt32, obj2.UInt32); + Assert.AreEqual(obj.Int32, obj2.Int32); + Assert.AreEqual(obj.UInt64, obj2.UInt64); + Assert.AreEqual(obj.Int64, obj2.Int64); + Assert.AreEqual(obj.DateTimeKind, obj2.DateTimeKind); + Assert.AreEqual(obj.DuplicateName, obj2.DuplicateName); + Assert.AreEqual(obj.EnumMember, obj2.EnumMember); + Assert.AreEqual(obj.Int32Flags, obj2.Int32Flags); + Assert.AreEqual(obj.UInt64Flags, obj2.UInt64Flags); + } + + [DataTestMethod] + [DataRow(DateTimeKind.Local, "'Local'", true)] + [DataRow(TestViewModelWithEnums.ByteEnum.A, "'A'", true)] + [DataRow(TestViewModelWithEnums.ByteEnum.B, "'B'", true)] + [DataRow(TestViewModelWithEnums.ByteEnum.C, "'C'", true)] + [DataRow(TestViewModelWithEnums.EnumMemberEnum.A, "'member-a'", true)] + [DataRow(TestViewModelWithEnums.DuplicateNameEnum.A, "'A'", true)] + [DataRow(TestViewModelWithEnums.DuplicateNameEnum.B, "'A'", true)] + [DataRow(TestViewModelWithEnums.DuplicateNameEnum.C, "'C'", true)] + [DataRow(TestViewModelWithEnums.DuplicateNameEnum.DAndAlsoLonger, "'D'", true)] + [DataRow(TestViewModelWithEnums.Int32FlagsEnum.ABC, "'a+b+c'", true)] + [DataRow(TestViewModelWithEnums.Int32FlagsEnum.A | TestViewModelWithEnums.Int32FlagsEnum.BCD, "'b+c+d,a'", true)] + [DataRow((TestViewModelWithEnums.Int32FlagsEnum)2356543, "2356543", false)] + [DataRow((TestViewModelWithEnums.Int32FlagsEnum)0, "0", true)] + [DataRow((TestViewModelWithEnums.UInt64FlagsEnum)0, "0", true)] + [DataRow(TestViewModelWithEnums.UInt64FlagsEnum.F1 | TestViewModelWithEnums.UInt64FlagsEnum.F2 | TestViewModelWithEnums.UInt64FlagsEnum.F64, "'F64,F2,F1'", true)] + public void TestEnumSerialization(object enumValue, string serializedValue, bool canDeserialize) + { + var json = JsonSerializer.Serialize(enumValue, DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe); + Assert.AreEqual(serializedValue.Replace("'", "\""), json); + if (canDeserialize) + { + var deserialized = JsonSerializer.Deserialize(json, enumValue.GetType(), DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe); + Assert.AreEqual(enumValue, deserialized); + } + else + { + XAssert.ThrowsAny(() => JsonSerializer.Deserialize(json, enumValue.GetType(), DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe)); + } + } + + [TestMethod] + public void TestEnumSerializationRoundtrip() + { + foreach (var type in typeof(TestViewModelWithEnums).GetNestedTypes()) + { + foreach (var value in Enum.GetValues(type)) + { + var json = JsonSerializer.Serialize(value, DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe); + var deserialized = JsonSerializer.Deserialize(json, value.GetType(), DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe); + Assert.AreEqual(value, deserialized, message: $"{value} != {deserialized} for enum value {type.Name}.{value}"); + } + } } [TestMethod] @@ -517,7 +628,7 @@ public class TestViewModelWithBind { [Bind(Name = "property ONE")] public string P1 { get; set; } = "value 1"; - [JsonProperty("property TWO")] + [JsonPropertyName("property TWO")] public string P2 { get; set; } = "value 2"; [Bind(Direction.ClientToServer)] public string ClientToServer { get; set; } = "default"; @@ -583,4 +694,76 @@ public class ParentClassWithBrokenGetters { public ClassWithBrokenGetters NestedVM { get; set; } = new ClassWithBrokenGetters(); } + + public class TestViewModelWithEnums + { + public DateTimeKind DateTimeKind { get; set; } + public DuplicateNameEnum DuplicateName { get; set; } + public Int32Enum Int32 { get; set; } + public SByteEnum SByte { get; set; } + public Int16Enum Int16 { get; set; } + public Int64Enum Int64 { get; set; } + public ByteEnum Byte { get; set; } + public UInt16Enum UInt16 { get; set; } + public UInt32Enum UInt32 { get; set; } + public UInt64Enum UInt64 { get; set; } + public EnumMemberEnum EnumMember { get; set; } + public Int32FlagsEnum Int32Flags { get; set; } + public UInt64FlagsEnum UInt64Flags { get; set; } + + public enum DuplicateNameEnum { A = 0, B = 0, C = 1, DButLonger = 2, D = 2, DAndAlsoLonger = 2 } + + public enum Int32Enum : int { A, B = int.MinValue, C = -1, D = int.MaxValue } + public enum SByteEnum : sbyte { A, B } + public enum Int16Enum : short { A, B = short.MinValue, C = -1, D = short.MaxValue } + public enum Int64Enum : long { A, B = long.MinValue, C = -1, D = long.MaxValue } + public enum ByteEnum : byte { A, B, C = 255 } + public enum UInt16Enum : ushort { A, B = ushort.MaxValue } + public enum UInt32Enum : uint { A, B = uint.MaxValue } + public enum UInt64Enum : ulong { A, B = ulong.MaxValue } + public enum EnumMemberEnum + { + [EnumMember(Value = "member-a")] + A, + [EnumMember(Value = "member-b")] + B + } + + [Flags] + public enum Int32FlagsEnum : int + { + [EnumMember(Value = "a")] + A = 1, + [EnumMember(Value = "b")] + B = 2, + [EnumMember(Value = "c")] + C = 4, + [EnumMember(Value = "a+b+c")] + ABC = 7, + [EnumMember(Value = "a+b+c+d+e")] + ABCDE = 31, + [EnumMember(Value = "b+c+d")] + BCD = 14, + [EnumMember(Value = "everything")] + Everything = -1 + } + + [Flags] + public enum UInt64FlagsEnum: long + { + F1 = 1, + F2 = 2, + F64 = 1L << 63, + } + } + + public class TestViewModelWithDateTimes + { + public DateTime DateTime1 { get; set; } + public DateTime DateTime2 { get; set; } + public DateTime DateTime3 { get; set; } + + public DateOnly DateOnly { get; set; } + public TimeOnly TimeOnly { get; set; } + } } diff --git a/src/Tests/ViewModel/ViewModelSerializationMapperTests.cs b/src/Tests/ViewModel/ViewModelSerializationMapperTests.cs index 419181cddb..b477ef73db 100644 --- a/src/Tests/ViewModel/ViewModelSerializationMapperTests.cs +++ b/src/Tests/ViewModel/ViewModelSerializationMapperTests.cs @@ -1,13 +1,15 @@ using System; using System.Collections.Generic; using System.Text; +using System.Text.Json.Serialization; using DotVVM.Framework.Configuration; +using DotVVM.Framework.Testing; using DotVVM.Framework.ViewModel; using DotVVM.Framework.ViewModel.Serialization; using DotVVM.Framework.ViewModel.Validation; using FastExpressionCompiler; +using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; namespace DotVVM.Framework.Tests.ViewModel { @@ -18,10 +20,7 @@ public class ViewModelSerializationMapperTests [TestMethod] public void ViewModelSerializationMapper_Name_JsonPropertyVsBindAttribute() { - var mapper = new ViewModelSerializationMapper(new ViewModelValidationRuleTranslator(), - new AttributeViewModelValidationMetadataProvider(), - new DefaultPropertySerialization(), - DotvvmConfiguration.CreateDefault()); + var mapper = DotvvmTestHelper.DefaultConfig.ServiceProvider.GetRequiredService(); var map = mapper.GetMap(typeof(JsonPropertyVsBindAttribute)); Assert.AreEqual("NoAttribute", map.Property("NoAttribute").Name); @@ -36,14 +35,11 @@ public void ViewModelSerializationMapper_Name_JsonPropertyVsBindAttribute() [TestMethod] public void ViewModelSerializationMapper_Name_MemberShadowing() { - var mapper = new ViewModelSerializationMapper(new ViewModelValidationRuleTranslator(), - new AttributeViewModelValidationMetadataProvider(), - new DefaultPropertySerialization(), - DotvvmConfiguration.CreateDefault()); - - Assert.ThrowsException(() => mapper.GetMap(typeof(MemberShadowingViewModelB)), - $"Detected member shadowing on property \"{nameof(MemberShadowingViewModelB.Property)}\" " + - $"while building serialization map for \"{typeof(MemberShadowingViewModelB).ToCode()}\""); + var mapper = DotvvmTestHelper.DefaultConfig.ServiceProvider.GetRequiredService(); + + var exception = XAssert.ThrowsAny(() => mapper.GetMap(typeof(MemberShadowingViewModelB))); + XAssert.IsType(exception.GetBaseException()); + XAssert.Equal($"Detected member shadowing on property \"{nameof(MemberShadowingViewModelB.Property)}\" while building serialization map for \"{typeof(MemberShadowingViewModelB).ToCode()}\"", exception.GetBaseException().Message); } public class MemberShadowingViewModelA @@ -69,18 +65,17 @@ public class JsonPropertyVsBindAttribute [Bind] public string BindWithoutName { get; set; } - [JsonProperty("jsonProperty1")] + [JsonPropertyName("jsonProperty1")] public string JsonPropertyWithName { get; set; } - [JsonProperty] public string JsonPropertyWithoutName { get; set; } [Bind(Name = "bind2")] - [JsonProperty("jsonProperty2")] + [JsonPropertyName("jsonProperty2")] public string BothWithName { get; set; } [Bind()] - [JsonProperty("jsonProperty3")] + [JsonPropertyName("jsonProperty3")] public string BindWithoutNameJsonPropertyWithName { get; set; } } diff --git a/src/Tests/ViewModel/ViewModelTypeMetadataSerializerTests.cs b/src/Tests/ViewModel/ViewModelTypeMetadataSerializerTests.cs index 0edb24a9e4..f89a4542ce 100644 --- a/src/Tests/ViewModel/ViewModelTypeMetadataSerializerTests.cs +++ b/src/Tests/ViewModel/ViewModelTypeMetadataSerializerTests.cs @@ -10,24 +10,27 @@ using DotVVM.Framework.ViewModel.Serialization; using DotVVM.Framework.ViewModel.Validation; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using DotVVM.Framework.Testing; +using Microsoft.Extensions.DependencyInjection; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace DotVVM.Framework.Tests.ViewModel { [TestClass] public class ViewModelTypeMetadataSerializerTests { - private static ViewModelSerializationMapper mapper; + private static IViewModelSerializationMapper mapper = DotvvmTestHelper.DefaultConfig.ServiceProvider.GetRequiredService(); - [ClassInitialize] - public static void ClassInit(TestContext context) + string GetSerializedString(Action action) { - mapper = new ViewModelSerializationMapper(new ViewModelValidationRuleTranslator(), - new AttributeViewModelValidationMetadataProvider(), - new DefaultPropertySerialization(), - DotvvmConfiguration.CreateDefault()); + var buffer = new System.IO.MemoryStream(); + using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping })) + { + action(writer); + } + return Encoding.UTF8.GetString(buffer.ToArray()); } #if DotNetCore @@ -56,21 +59,29 @@ public void ViewModelTypeMetadata_TypeName(Type type, string expected) var typeMetadataSerializer = new ViewModelTypeMetadataSerializer(mapper); var dependentObjectTypes = new HashSet(); var dependentEnumTypes = new HashSet(); - var result = typeMetadataSerializer.GetTypeIdentifier(type, dependentObjectTypes, dependentEnumTypes); - Assert.AreEqual(expected.Replace("'", "\""), result.ToString(Formatting.None)); + var typeName = GetSerializedString(json => typeMetadataSerializer.WriteTypeIdentifier(json, type, dependentObjectTypes, dependentEnumTypes)); + Assert.AreEqual(expected.Replace("'", "\""), typeName); } #endif + JsonObject SerializeMetadata(ViewModelTypeMetadataSerializer serializer, params Type[] types) + { + var json = GetSerializedString(writer => { + writer.WriteStartObject(); + serializer.SerializeTypeMetadata(types.Select(mapper.GetMap), writer, "typeMetadata"u8); + writer.WriteEndObject(); + }); + return JsonNode.Parse(json).AsObject()["typeMetadata"].AsObject(); + } + [TestMethod] public void ViewModelTypeMetadata_TypeMetadata() { CultureUtils.RunWithCulture("en-US", () => { var typeMetadataSerializer = new ViewModelTypeMetadataSerializer(mapper); - var result = typeMetadataSerializer.SerializeTypeMetadata(new[] - { - mapper.GetMap(typeof(TestViewModel)) - }); + + var result = SerializeMetadata(typeMetadataSerializer, typeof(TestViewModel)); var checker = new OutputChecker("testoutputs"); checker.CheckJsonObject(result); @@ -82,12 +93,12 @@ public void ViewModelTypeMetadata_ValidationRules() { CultureUtils.RunWithCulture("en-US", () => { var typeMetadataSerializer = new ViewModelTypeMetadataSerializer(mapper); - var result = typeMetadataSerializer.SerializeTypeMetadata(new[] { mapper.GetMap(typeof(TestViewModel)) }); + var result = SerializeMetadata(typeMetadataSerializer, typeof(TestViewModel)); - var rules = XAssert.IsType(result[typeof(TestViewModel).GetTypeHash()]["properties"]["ServerToClient"]["validationRules"]); + var rules = XAssert.IsType(result[typeof(TestViewModel).GetTypeHash()]["properties"]["ServerToClient"]["validationRules"]); XAssert.Single(rules); - Assert.AreEqual("required", rules[0]["ruleName"].Value()); - Assert.AreEqual("ServerToClient is required!", rules[0]["errorMessage"].Value()); + Assert.AreEqual("required", rules[0]["ruleName"].GetValue()); + Assert.AreEqual("ServerToClient is required!", rules[0]["errorMessage"].GetValue()); }); } @@ -99,7 +110,7 @@ public void ViewModelTypeMetadata_ValidationDisabled() config.ClientSideValidation = false; var typeMetadataSerializer = new ViewModelTypeMetadataSerializer(mapper, config); - var result = typeMetadataSerializer.SerializeTypeMetadata(new[] { mapper.GetMap(typeof(TestViewModel)) }); + var result = SerializeMetadata(typeMetadataSerializer, typeof(TestViewModel)); XAssert.Null(result[typeof(TestViewModel).GetTypeHash()]["properties"]["ServerToClient"]["validationRules"]); }); @@ -118,7 +129,7 @@ class TestViewModel [Bind(Name = "property ONE")] public Guid P1 { get; set; } - [JsonProperty("property TWO")] + [JsonPropertyName("property TWO")] public SampleEnum?[] P2 { get; set; } [Bind(Direction.ClientToServer)] diff --git a/src/Tools/HotReload/AspNetCore/DotVVM.HotReload.AspNetCore.csproj b/src/Tools/HotReload/AspNetCore/DotVVM.HotReload.AspNetCore.csproj index 0a6ced6b27..66edfe7232 100644 --- a/src/Tools/HotReload/AspNetCore/DotVVM.HotReload.AspNetCore.csproj +++ b/src/Tools/HotReload/AspNetCore/DotVVM.HotReload.AspNetCore.csproj @@ -2,7 +2,7 @@ DotVVM.HotReload.AspNetCore - netcoreapp3.1;net6.0 + net6.0 true dotvvmwizard.snk true From 9da17ea9b0a86b1173d7780bbc2329b6e9449ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Thu, 14 Mar 2024 22:47:34 +0100 Subject: [PATCH 02/20] Fix microsoft NuGet packages versions to 6.0.0, unless newer is needed --- .../Framework/Compilation/CompiledAssemblyCache.cs | 2 +- .../Compilation/DefaultControlBuilderFactory.cs | 10 +++++----- .../DotvvmViewCompilationServiceExtensions.cs | 3 +++ .../Framework/Configuration/DotvvmConfiguration.cs | 2 +- .../Framework/Configuration/ServiceLocator.cs | 2 +- .../Controls/Infrastructure/BodyResourceLinks.cs | 2 +- .../DotVVMServiceCollectionExtensions.cs | 5 ++++- .../DependencyInjection/DotvvmBuilderExtensions.cs | 2 +- src/Framework/Framework/DotVVM.Framework.csproj | 11 +++++------ .../Serialization/DefaultViewModelSerializer.cs | 2 +- .../Hosting.Owin/DotVVM.Framework.Hosting.Owin.csproj | 4 ++-- .../DotVVM.Samples.ApplicationInsights.Owin.csproj | 4 ++-- ...otVVM.Samples.BasicSamples.AspNetCoreLatest.csproj | 2 +- .../DotVVM.Samples.MiniProfiler.Owin.csproj | 2 +- .../Owin/DotVVM.Samples.BasicSamples.Owin.csproj | 6 +++--- src/Tools/CommandLine/DotVVM.CommandLine.csproj | 2 +- .../DotVVM.Framework.Tools.SeleniumGenerator.csproj | 2 +- .../DotVVM.Tools.StartupPerfTester.csproj | 2 +- .../DotVVM.Tracing.ApplicationInsights.Owin.csproj | 4 ++-- .../DotVVM.Tracing.ApplicationInsights.csproj | 2 +- 20 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/Framework/Framework/Compilation/CompiledAssemblyCache.cs b/src/Framework/Framework/Compilation/CompiledAssemblyCache.cs index 8aea8818ed..433913c3d0 100644 --- a/src/Framework/Framework/Compilation/CompiledAssemblyCache.cs +++ b/src/Framework/Framework/Compilation/CompiledAssemblyCache.cs @@ -150,7 +150,7 @@ public Assembly[] GetAllAssemblies() #if DotNetCore // auto-loads all referenced assemblies recursively - var newA = DependencyContext.Default.GetDefaultAssemblyNames().Select(Assembly.Load).Distinct().ToArray(); + var newA = DependencyContext.Default!.GetDefaultAssemblyNames().Select(Assembly.Load).Distinct().ToArray(); #else // this doesn't load new assemblies, but it is done in InvokeStaticConstructorsOnAllControls var newA = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).ToArray(); diff --git a/src/Framework/Framework/Compilation/DefaultControlBuilderFactory.cs b/src/Framework/Framework/Compilation/DefaultControlBuilderFactory.cs index 2621b3695f..34a39cb9c2 100644 --- a/src/Framework/Framework/Compilation/DefaultControlBuilderFactory.cs +++ b/src/Framework/Framework/Compilation/DefaultControlBuilderFactory.cs @@ -143,19 +143,19 @@ void editCompilationException(DotvvmCompilationException ex) } Interlocked.Increment(ref DotvvmMetrics.BareCounters.ViewsCompiledOk); - compilationService.RegisterCompiledView(file.FileName, descriptor, null); + compilationService?.RegisterCompiledView(file.FileName, descriptor, null); return result; } catch (DotvvmCompilationException ex) { Interlocked.Increment(ref DotvvmMetrics.BareCounters.ViewsCompiledFailed); editCompilationException(ex); - compilationService.RegisterCompiledView(file.FileName, descriptor, ex); + compilationService?.RegisterCompiledView(file.FileName, descriptor, ex); throw; } catch (Exception ex) { - compilationService.RegisterCompiledView(file.FileName, descriptor, ex); + compilationService?.RegisterCompiledView(file.FileName, descriptor, ex); throw; } finally @@ -176,12 +176,12 @@ void editCompilationException(DotvvmCompilationException ex) catch (DotvvmCompilationException ex) { editCompilationException(ex); - compilationService.RegisterCompiledView(file.FileName, null, ex); + compilationService?.RegisterCompiledView(file.FileName, null, ex); throw; } catch (Exception ex) { - compilationService.RegisterCompiledView(file.FileName, null, ex); + compilationService?.RegisterCompiledView(file.FileName, null, ex); throw; } } diff --git a/src/Framework/Framework/Compilation/DotvvmViewCompilationServiceExtensions.cs b/src/Framework/Framework/Compilation/DotvvmViewCompilationServiceExtensions.cs index 3d5a95919c..a3cbab3643 100644 --- a/src/Framework/Framework/Compilation/DotvvmViewCompilationServiceExtensions.cs +++ b/src/Framework/Framework/Compilation/DotvvmViewCompilationServiceExtensions.cs @@ -12,6 +12,9 @@ public static Task Precompile(this ViewCompilationConfiguration compilationConfi { return Task.Run(async () => { var compilationService = config.ServiceProvider.GetService(); + if (compilationService is null) + return; + if (compilationConfiguration.BackgroundCompilationDelay != null) { await Task.Delay(compilationConfiguration.BackgroundCompilationDelay.Value); diff --git a/src/Framework/Framework/Configuration/DotvvmConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmConfiguration.cs index 021c05b125..6f664f4347 100644 --- a/src/Framework/Framework/Configuration/DotvvmConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmConfiguration.cs @@ -250,7 +250,7 @@ public static DotvvmConfiguration CreateDefault(Action? regi { var services = CreateDefaultServiceCollection(); registerServices?.Invoke(services); - return new ServiceLocator(services, serviceProviderFactoryMethod).GetService(); + return new ServiceLocator(services, serviceProviderFactoryMethod).GetService().NotNull(); } /// diff --git a/src/Framework/Framework/Configuration/ServiceLocator.cs b/src/Framework/Framework/Configuration/ServiceLocator.cs index 8b5126d0ba..ada9c3c70e 100644 --- a/src/Framework/Framework/Configuration/ServiceLocator.cs +++ b/src/Framework/Framework/Configuration/ServiceLocator.cs @@ -39,7 +39,7 @@ private IServiceProvider BuildServiceProvider() return BuildServiceProvider(serviceCollection.NotNull()); } - public T GetService() + public T? GetService() => GetServiceProvider().GetService(); /// diff --git a/src/Framework/Framework/Controls/Infrastructure/BodyResourceLinks.cs b/src/Framework/Framework/Controls/Infrastructure/BodyResourceLinks.cs index a833b7f652..877a117065 100644 --- a/src/Framework/Framework/Controls/Infrastructure/BodyResourceLinks.cs +++ b/src/Framework/Framework/Controls/Infrastructure/BodyResourceLinks.cs @@ -58,7 +58,7 @@ internal static string RenderWarnings(IDotvvmRequestContext context) var result = ""; // propagate warnings to JS console var collector = context.Services.GetService(); - if (!collector.Enabled) return result; + if (collector is null || !collector.Enabled) return result; foreach (var w in collector.GetWarnings()) { diff --git a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs index 4f252b365a..f560451e36 100644 --- a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs +++ b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs @@ -92,7 +92,7 @@ public static IServiceCollection RegisterDotVVMServices(IServiceCollection servi services.AddScoped(s => { var config = s.GetRequiredService(); - return (config.Diagnostics.PerfWarnings.IsEnabled ? (IRequestTracer)s.GetService() : null) ?? NullRequestTracer.Instance; + return (config.Diagnostics.PerfWarnings.IsEnabled ? (IRequestTracer?)s.GetService() : null) ?? NullRequestTracer.Instance; }); services.TryAddSingleton(); services.TryAddScoped(); @@ -143,12 +143,15 @@ public static IServiceCollection RegisterDotVVMServices(IServiceCollection servi public static void ConfigureWithServices(this IServiceCollection services, Action configure) where TObject: class + where TService: notnull { services.AddSingleton>(s => new ConfigureOptions(o => configure(o, s.GetRequiredService()))); } public static void ConfigureWithServices(this IServiceCollection services, Action configure) where TObject: class + where TService1: notnull + where TService2: notnull { services.AddSingleton>(s => new ConfigureOptions(o => configure(o, s.GetRequiredService(), s.GetRequiredService()))); } diff --git a/src/Framework/Framework/DependencyInjection/DotvvmBuilderExtensions.cs b/src/Framework/Framework/DependencyInjection/DotvvmBuilderExtensions.cs index a057af1936..c5bf0d77d4 100644 --- a/src/Framework/Framework/DependencyInjection/DotvvmBuilderExtensions.cs +++ b/src/Framework/Framework/DependencyInjection/DotvvmBuilderExtensions.cs @@ -98,7 +98,7 @@ public static IDotvvmServiceCollection AddDiagnosticServices(this IDotvvmService services.Services.AddScoped(); services.Services.AddScoped(s => { var config = s.GetRequiredService(); - return (config.Debug ? (IRequestTracer)s.GetService() : null) ?? NullRequestTracer.Instance; + return (config.Debug ? (IRequestTracer?)s.GetService() : null) ?? NullRequestTracer.Instance; }); return services; diff --git a/src/Framework/Framework/DotVVM.Framework.csproj b/src/Framework/Framework/DotVVM.Framework.csproj index 954dae3a9b..03c2996c2b 100644 --- a/src/Framework/Framework/DotVVM.Framework.csproj +++ b/src/Framework/Framework/DotVVM.Framework.csproj @@ -46,17 +46,16 @@ - - - - - + + + - + + diff --git a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs index 35138501ae..9fb65b0f54 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs @@ -463,7 +463,7 @@ public void PopulateViewModel(IDotvvmRequestContext context, ReadOnlyMemory - - + + diff --git a/src/Samples/ApplicationInsights.Owin/DotVVM.Samples.ApplicationInsights.Owin.csproj b/src/Samples/ApplicationInsights.Owin/DotVVM.Samples.ApplicationInsights.Owin.csproj index 85d3e57047..7babe354c1 100644 --- a/src/Samples/ApplicationInsights.Owin/DotVVM.Samples.ApplicationInsights.Owin.csproj +++ b/src/Samples/ApplicationInsights.Owin/DotVVM.Samples.ApplicationInsights.Owin.csproj @@ -37,7 +37,7 @@ - + @@ -53,6 +53,6 @@ - + diff --git a/src/Samples/AspNetCoreLatest/DotVVM.Samples.BasicSamples.AspNetCoreLatest.csproj b/src/Samples/AspNetCoreLatest/DotVVM.Samples.BasicSamples.AspNetCoreLatest.csproj index 20546e72c1..e35a379212 100644 --- a/src/Samples/AspNetCoreLatest/DotVVM.Samples.BasicSamples.AspNetCoreLatest.csproj +++ b/src/Samples/AspNetCoreLatest/DotVVM.Samples.BasicSamples.AspNetCoreLatest.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Samples/MiniProfiler.Owin/DotVVM.Samples.MiniProfiler.Owin.csproj b/src/Samples/MiniProfiler.Owin/DotVVM.Samples.MiniProfiler.Owin.csproj index e56bedeaa5..1b1dad4def 100644 --- a/src/Samples/MiniProfiler.Owin/DotVVM.Samples.MiniProfiler.Owin.csproj +++ b/src/Samples/MiniProfiler.Owin/DotVVM.Samples.MiniProfiler.Owin.csproj @@ -23,7 +23,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj b/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj index 1b85ac7325..6bdf08e033 100644 --- a/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj +++ b/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj @@ -28,9 +28,9 @@ - - - + + + diff --git a/src/Tools/CommandLine/DotVVM.CommandLine.csproj b/src/Tools/CommandLine/DotVVM.CommandLine.csproj index b071d635c0..99c963d69b 100644 --- a/src/Tools/CommandLine/DotVVM.CommandLine.csproj +++ b/src/Tools/CommandLine/DotVVM.CommandLine.csproj @@ -33,7 +33,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Tools/SeleniumGenerator/DotVVM.Framework.Tools.SeleniumGenerator.csproj b/src/Tools/SeleniumGenerator/DotVVM.Framework.Tools.SeleniumGenerator.csproj index 24feb61bf3..ef72bcb600 100644 --- a/src/Tools/SeleniumGenerator/DotVVM.Framework.Tools.SeleniumGenerator.csproj +++ b/src/Tools/SeleniumGenerator/DotVVM.Framework.Tools.SeleniumGenerator.csproj @@ -18,7 +18,7 @@ - + $(DefineConstants);RELEASE diff --git a/src/Tools/StartupPerfTester/DotVVM.Tools.StartupPerfTester.csproj b/src/Tools/StartupPerfTester/DotVVM.Tools.StartupPerfTester.csproj index 4f67baefca..8d72c78677 100644 --- a/src/Tools/StartupPerfTester/DotVVM.Tools.StartupPerfTester.csproj +++ b/src/Tools/StartupPerfTester/DotVVM.Tools.StartupPerfTester.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Tracing/ApplicationInsights.Owin/DotVVM.Tracing.ApplicationInsights.Owin.csproj b/src/Tracing/ApplicationInsights.Owin/DotVVM.Tracing.ApplicationInsights.Owin.csproj index 80f82fc59a..1769b858ca 100644 --- a/src/Tracing/ApplicationInsights.Owin/DotVVM.Tracing.ApplicationInsights.Owin.csproj +++ b/src/Tracing/ApplicationInsights.Owin/DotVVM.Tracing.ApplicationInsights.Owin.csproj @@ -10,8 +10,8 @@ Application Insights Tracing module for OWIN and DotVVM - - + + diff --git a/src/Tracing/ApplicationInsights/DotVVM.Tracing.ApplicationInsights.csproj b/src/Tracing/ApplicationInsights/DotVVM.Tracing.ApplicationInsights.csproj index b6391ebce5..ea10f6f8a4 100644 --- a/src/Tracing/ApplicationInsights/DotVVM.Tracing.ApplicationInsights.csproj +++ b/src/Tracing/ApplicationInsights/DotVVM.Tracing.ApplicationInsights.csproj @@ -12,7 +12,7 @@ - + From 87381520e49561c55d1991efc1ae20d5fbd2df06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Thu, 14 Mar 2024 23:38:33 +0100 Subject: [PATCH 03/20] STJ: fix some UI tests --- .../DefaultSerializerSettingsProvider.cs | 2 + .../DotvvmControlDebugJsonConverter.cs | 3 +- .../Framework/Controls/KnockoutHelper.cs | 2 +- .../Framework/DotVVM.Framework.csproj | 2 + .../Hosting/ErrorPages/ErrorPageTemplate.cs | 30 ++- src/Framework/Framework/System.Index.cs | 171 ++++++++++++++++++ .../Framework/Testing/TestHttpResponse.cs | 7 +- .../Framework/Utils/ReflectionUtils.cs | 7 + src/Framework/Framework/Utils/StringUtils.cs | 34 ++++ .../Framework/Utils/SystemTextJsonHacks.cs | 2 +- .../Framework/Utils/SystemTextJsonUtils.cs | 45 ++++- .../ViewModel/Serialization/ClientTypeId.cs | 39 +++- .../CustomPrimitiveTypeJsonConverter.cs | 4 +- .../DefaultViewModelSerializer.cs | 16 +- .../Serialization/DotvvmDateOnlyConverter.cs | 1 - .../DotvvmDictionaryConverter.cs | 4 +- .../Serialization/DotvvmEnumConverter.cs | 3 +- .../Serialization/DotvvmObjectConverter.cs | 33 ++++ .../Serialization/ViewModelJsonConverter.cs | 1 + .../ViewModelSerializationMap.cs | 17 +- .../ViewModelSerializationMapper.cs | 5 +- .../ViewModelTypeMetadataSerializer.cs | 2 +- ...Samples.BasicSamples.Api.AspNetCore.csproj | 12 +- .../Tests/Feature/StringInterpolationTests.cs | 2 +- 24 files changed, 392 insertions(+), 52 deletions(-) create mode 100644 src/Framework/Framework/System.Index.cs create mode 100644 src/Framework/Framework/ViewModel/Serialization/DotvvmObjectConverter.cs diff --git a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs index e99321c68a..cc9c526c87 100644 --- a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs +++ b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs @@ -35,11 +35,13 @@ private JsonSerializerOptions CreateSettings() new DotvvmDateTimeConverter(), new DotvvmDateOnlyConverter(), new DotvvmTimeOnlyConverter(), + new DotvvmObjectConverter(), new DotvvmEnumConverter(), new DotvvmDictionaryConverter(), new DotvvmByteArrayConverter(), new DotvvmCustomPrimitiveTypeConverter() }, + NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, Encoder = HtmlSafeLessParaoidEncoder, MaxDepth = defaultMaxSerializationDepth }; diff --git a/src/Framework/Framework/Controls/DotvvmControlDebugJsonConverter.cs b/src/Framework/Framework/Controls/DotvvmControlDebugJsonConverter.cs index c386cb053c..175a026cdd 100644 --- a/src/Framework/Framework/Controls/DotvvmControlDebugJsonConverter.cs +++ b/src/Framework/Framework/Controls/DotvvmControlDebugJsonConverter.cs @@ -24,7 +24,8 @@ public override void Write(Utf8JsonWriter w, DotvvmBindableObject obj, JsonSeria w.WriteStartObject("Properties"); foreach (var kvp in obj.Properties.OrderBy(p => (p.Key.DeclaringType.IsAssignableFrom(obj.GetType()), p.Key.Name))) { - var (p, rawValue) = kvp; + var p = kvp.Key; + var rawValue = kvp.Value; var isAttached = !p.DeclaringType.IsAssignableFrom(obj.GetType()); var name = isAttached ? p.DeclaringType.Name + "." + p.Name : p.Name; if (rawValue is null) diff --git a/src/Framework/Framework/Controls/KnockoutHelper.cs b/src/Framework/Framework/Controls/KnockoutHelper.cs index bea2069523..c52a9bbf32 100644 --- a/src/Framework/Framework/Controls/KnockoutHelper.cs +++ b/src/Framework/Framework/Controls/KnockoutHelper.cs @@ -491,7 +491,7 @@ public static string MakeStringLiteral(string value, bool htmlSafe = true) // try to allocate only the result string if it short enough Span buffer = stackalloc char[128]; buffer[0] = '"'; - encoder.Encode(source: value, destination: buffer.Slice(1), out var consumed, out var written); + encoder.Encode(source: value.AsSpan(), destination: buffer.Slice(1), out var consumed, out var written); if (consumed == value.Length && written + 2 <= buffer.Length) { buffer[written + 1] = '"'; diff --git a/src/Framework/Framework/DotVVM.Framework.csproj b/src/Framework/Framework/DotVVM.Framework.csproj index 03c2996c2b..f7e45e989d 100644 --- a/src/Framework/Framework/DotVVM.Framework.csproj +++ b/src/Framework/Framework/DotVVM.Framework.csproj @@ -9,6 +9,8 @@ DotVVM true enable + + true diff --git a/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs b/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs index 384fcafa21..af26de7e0a 100644 --- a/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs +++ b/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs @@ -17,6 +17,7 @@ using System.Text.Json.Serialization; using DotVVM.Framework.Compilation.ControlTree; using System.Text.Json.Serialization.Metadata; +using System.Text.Encodings.Web; namespace DotVVM.Framework.Hosting.ErrorPages { @@ -171,9 +172,9 @@ public void ObjectBrowser(object? obj) new ReflectionAssemblyJsonConverter(), new DotvvmTypeDescriptorJsonConverter(), new Controls.DotvvmControlDebugJsonConverter(), - new DelegateJsonConverter(), new BindingDebugJsonConverter(), - new DotvvmPropertyJsonConverter() + new DotvvmPropertyJsonConverter(), + new UnsupportedTypeJsonConverterFactory(), }, TypeInfoResolver = new IgnoreUnsupportedResolver(), // suppress any errors that occur during serialization (getters may throw exception, ...) @@ -289,7 +290,7 @@ public void ObjectBrowser(JsonObject obj) } else { - WriteText(p.Value.ToJsonString()); + WriteText(p.Value.ToJsonString(new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping })); } Write(""); } @@ -408,15 +409,26 @@ private void WriteLine(string textToAppend) builder.AppendLine(); } - class DelegateJsonConverter : JsonConverter + class UnsupportedTypeJsonConverterFactory : JsonConverterFactory { - public override Delegate? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => - throw new NotImplementedException(); - public override void Write(Utf8JsonWriter writer, Delegate value, JsonSerializerOptions options) + public override bool CanConvert(Type typeToConvert) => + typeToConvert.IsDelegate() || typeof(MemberInfo).IsAssignableFrom(typeToConvert); + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => + (JsonConverter?)Activator.CreateInstance(typeof(Inner<>).MakeGenericType([ typeToConvert ])); + + class Inner : JsonConverter { - writer.WriteStringValue(""); + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + if (value is null) + writer.WriteNullValue(); + else if (value is Delegate) + writer.WriteStringValue(""); + else + writer.WriteStringValue(value.ToString()); + } } - } } } diff --git a/src/Framework/Framework/System.Index.cs b/src/Framework/Framework/System.Index.cs new file mode 100644 index 0000000000..19be24888b --- /dev/null +++ b/src/Framework/Framework/System.Index.cs @@ -0,0 +1,171 @@ +// From https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Index.cs +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if CSharp8Polyfill +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System +{ + /// Represent a type can be used to index a collection either from the start or the end. + /// + /// Index is used by the C# compiler to support the new index syntax + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; + /// int lastElement = someArray[^1]; // lastElement = 5 + /// + /// +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + readonly struct Index : IEquatable + { + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new Index(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new Index(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + return new Index(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + return ~_value; + else + return _value; + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + int offset = _value; + if (IsFromEnd) + { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals([NotNullWhen(true)] object? value) => value is Index && _value == ((Index)value)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + return ToStringFromEnd(); + + return ((uint)Value).ToString(); + } + + private static void ThrowValueArgumentOutOfRange_NeedNonNegNumException() + { +#if SYSTEM_PRIVATE_CORELIB + throw new ArgumentOutOfRangeException("value", SR.ArgumentOutOfRange_NeedNonNegNum); +#else + throw new ArgumentOutOfRangeException("value", "value must be non-negative"); +#endif + } + + private string ToStringFromEnd() + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + Span span = stackalloc char[11]; // 1 for ^ and 10 for longest possible uint value + bool formatted = ((uint)Value).TryFormat(span.Slice(1), out int charsWritten); + Debug.Assert(formatted); + span[0] = '^'; + return new string(span.Slice(0, charsWritten + 1)); +#else + return '^' + Value.ToString(); +#endif + } + } +} +#endif diff --git a/src/Framework/Framework/Testing/TestHttpResponse.cs b/src/Framework/Framework/Testing/TestHttpResponse.cs index cfe0309e54..c6912aaf32 100644 --- a/src/Framework/Framework/Testing/TestHttpResponse.cs +++ b/src/Framework/Framework/Testing/TestHttpResponse.cs @@ -37,7 +37,12 @@ Stream IHttpResponse.Body public void Write(string text) => Write(StringUtils.Utf8.GetBytes(text)); public void Write(ReadOnlyMemory text) => Write(StringUtils.Utf8.GetBytes(text.ToArray())); - public void Write(ReadOnlyMemory data) => Body.Write(data.Span); + public void Write(ReadOnlyMemory data) => +#if DotNetCore + Body.Write(data.Span); +#else + Body.Write(data.Span.ToArray(), 0, data.Span.Length); +#endif public void Write(byte[] data) => Body.Write(data, 0, data.Length); public void Write(byte[] data, int offset, int count) => Body.Write(data, offset, count); diff --git a/src/Framework/Framework/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index fca087c58c..bc6aa0870e 100644 --- a/src/Framework/Framework/Utils/ReflectionUtils.cs +++ b/src/Framework/Framework/Utils/ReflectionUtils.cs @@ -334,6 +334,13 @@ public static IEnumerable GetNumericTypes() type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>) ); + public static bool IsJsonDom(Type type) => + type == typeof(JsonElement) || + type == typeof(JsonDocument) || + typeof(System.Text.Json.Nodes.JsonNode).IsAssignableFrom(type) || + type.Namespace == "Newtonsoft.Json.Linq"; + + public static bool IsEnumerable(Type type) { return typeof(IEnumerable).IsAssignableFrom(type); diff --git a/src/Framework/Framework/Utils/StringUtils.cs b/src/Framework/Framework/Utils/StringUtils.cs index be0c0a9a21..bbc7a46dea 100644 --- a/src/Framework/Framework/Utils/StringUtils.cs +++ b/src/Framework/Framework/Utils/StringUtils.cs @@ -10,6 +10,40 @@ namespace DotVVM.Framework.Utils public static class StringUtils { public static readonly UTF8Encoding Utf8 = new UTF8Encoding(false, throwOnInvalidBytes: true); + + public static string Utf8Decode(byte[] bytes) => + Utf8.GetString(bytes); + public static string Utf8Decode(ReadOnlySpan bytes) + { +#if DotNetCore + return Utf8.GetString(bytes); +#else + unsafe + { + fixed (byte* pBytes = bytes) + { + return Utf8.GetString(pBytes, bytes.Length); + } + } +#endif + } + public static int Utf8Encode(ReadOnlySpan str, Span bytes) + { +#if DotNetCore + return Utf8.GetBytes(str, bytes); +#else + unsafe + { + fixed (byte* pBytes = bytes) + { + fixed (char* pStr = str) + { + return Utf8.GetBytes(pStr, str.Length, pBytes, bytes.Length); + } + } + } +#endif + } public static string LimitLength(this string source, int length, string ending = "...") { if (length < source.Length) diff --git a/src/Framework/Framework/Utils/SystemTextJsonHacks.cs b/src/Framework/Framework/Utils/SystemTextJsonHacks.cs index f65595e5c7..130cf968da 100644 --- a/src/Framework/Framework/Utils/SystemTextJsonHacks.cs +++ b/src/Framework/Framework/Utils/SystemTextJsonHacks.cs @@ -22,7 +22,7 @@ public static void Populate(T obj, string input, JsonSerializerOptions option try { """{"X":"""u8.CopyTo(bytes.AsSpan().Slice(0, 5)); - StringUtils.Utf8.GetBytes(input, bytes.AsSpan(5)); + StringUtils.Utf8Encode(input, bytes.AsSpan(5)); bytes[length - 1] = (byte)'}'; var reader = new Utf8JsonReader(bytes.AsSpan().Slice(0, length)); var result = JsonSerializer.Deserialize>(ref reader, options)?.X; diff --git a/src/Framework/Framework/Utils/SystemTextJsonUtils.cs b/src/Framework/Framework/Utils/SystemTextJsonUtils.cs index c7532279dc..ab935c2150 100644 --- a/src/Framework/Framework/Utils/SystemTextJsonUtils.cs +++ b/src/Framework/Framework/Utils/SystemTextJsonUtils.cs @@ -76,9 +76,44 @@ public static IEnumerable EnumerateStringArray(this JsonElement json) } } + public static void WriteFloatValue(Utf8JsonWriter writer, double number) + { +#if DotNetCore + if (double.IsFinite(number)) +#else + if (!double.IsInfinity(number) && double.IsNaN(number)) +#endif + writer.WriteNumberValue(number); + else + WriteNonFiniteFloatValue(writer, (float)number); + } + public static void WriteFloatValue(Utf8JsonWriter writer, float number) + { +#if DotNetCore + if (float.IsFinite(number)) +#else + if (!float.IsInfinity(number) && float.IsNaN(number)) +#endif + writer.WriteNumberValue(number); + else + WriteNonFiniteFloatValue(writer, number); + } + + static void WriteNonFiniteFloatValue(Utf8JsonWriter writer, float number) + { + if (double.IsNaN(number)) + writer.WriteStringValue("NaN"u8); + else if (double.IsPositiveInfinity(number)) + writer.WriteStringValue("+Infinity"u8); + else if (double.IsNegativeInfinity(number)) + writer.WriteStringValue("-Infinity"u8); + else + throw new NotSupportedException(); + } + /// Deserializes JSON primitive values to dotnet primitives, mimicking the Newtonsoft.Json behavior to some degree - public static object DeserializeObject(ref Utf8JsonReader reader, JsonSerializerOptions options) + public static object? DeserializeObject(ref Utf8JsonReader reader, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.String) { @@ -90,17 +125,17 @@ public static object DeserializeObject(ref Utf8JsonReader reader, JsonSerializer } if (reader.TokenType == JsonTokenType.True) { - return true; + return BoxingUtils.True; } if (reader.TokenType == JsonTokenType.False) { - return false; + return BoxingUtils.False; } if (reader.TokenType == JsonTokenType.Null) { - return null!; + return null; } - return JsonSerializer.Deserialize(ref reader, options)!; + return JsonElement.ParseValue(ref reader); } public static T Deserialize(ref Utf8JsonReader reader, JsonSerializerOptions options) diff --git a/src/Framework/Framework/ViewModel/Serialization/ClientTypeId.cs b/src/Framework/Framework/ViewModel/Serialization/ClientTypeId.cs index e08a1ac4c0..230a4b96cc 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ClientTypeId.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ClientTypeId.cs @@ -28,7 +28,17 @@ private ClientTypeId(bool isHash, ReadOnlySpan data) { if (data.Length > 15) throw new ArgumentException("Data too long"); controlByte = (byte)(data.Length | (isHash ? 0x10 : 0)); - data.CopyTo(MemoryMarshal.CreateSpan(ref dataByte1, data.Length)); +#if DotNetCore + data.CopyTo(MemoryMarshal.CreateSpan(ref dataByte1, 15)); +#else + unsafe + { + fixed (byte* ptr = &dataByte1) + { + data.CopyTo(new Span(ptr, 15)); + } + } +#endif } struct Utf8StringCtor {} @@ -36,7 +46,17 @@ private ClientTypeId(ReadOnlySpan utf8Hash, Utf8StringCtor _) { if (utf8Hash.Length != 16) throw new ArgumentException("Hash must be 16 bytes long"); controlByte = (byte)(12 | 0x10); +#if DotNetCore System.Buffers.Text.Base64.DecodeFromUtf8(utf8Hash, MemoryMarshal.CreateSpan(ref dataByte1, 12), out var _, out var _); +#else + unsafe + { + fixed (byte* ptr = &dataByte1) + { + System.Buffers.Text.Base64.DecodeFromUtf8(utf8Hash, new Span(ptr, 12), out var _, out var _); + } + } +#endif } public static ClientTypeId CreateHash(ReadOnlySpan data) => new ClientTypeId(true, data); @@ -48,7 +68,12 @@ private ClientTypeId(ReadOnlySpan utf8Hash, Utf8StringCtor _) bool IsHash => ((controlByte >> 4) & 1) != 0; bool IsEmpty => controlByte == 0; - ReadOnlySpan Data => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(in dataByte1), Length); + ReadOnlySpan Data => +#if DotNetCore + MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(in dataByte1), Length); +#else + throw new NotImplementedException(); +#endif public void WriteJson(System.Text.Json.Utf8JsonWriter writer) { @@ -80,11 +105,15 @@ public override string ToString() { if (IsEmpty) return "[Empty]"; if (IsHash) - return Convert.ToBase64String(Data); + return Convert.ToBase64String(Data +#if !DotNetCore + .ToArray() +#endif + ); else - return StringUtils.Utf8.GetString(Data); + return StringUtils.Utf8Decode(Data); } - public override int GetHashCode() => HashCode.Combine(a, b); + public override int GetHashCode() => (a, b).GetHashCode(); public override bool Equals(object? obj) => obj is ClientTypeId id && id.a == a && id.b == b; public bool Equals(ClientTypeId other) => other.a == a && other.b == b; public int CompareTo(ClientTypeId other) => a == other.a ? b.CompareTo(other.b) : a.CompareTo(other.a); diff --git a/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs index f8ddf2aa61..cdd87f3239 100644 --- a/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs @@ -32,8 +32,8 @@ or JsonTokenType.False // TODO: utf8 parsing? var registration = ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(typeToConvert)!; var str = reader.TokenType is JsonTokenType.String ? reader.GetString() : - reader.HasValueSequence ? StringUtils.Utf8.GetString(reader.ValueSequence.ToArray()) : - StringUtils.Utf8.GetString(reader.ValueSpan); + reader.HasValueSequence ? StringUtils.Utf8Decode(reader.ValueSequence.ToArray()) : + StringUtils.Utf8Decode(reader.ValueSpan); var parseResult = registration.TryParseMethod(str!); if (!parseResult.Successful) { diff --git a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs index 9fb65b0f54..e9c4ca9070 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs @@ -82,7 +82,7 @@ public string SerializeViewModel(IDotvvmRequestContext context, object? commandR // context.ViewModelJson["viewModelDiff"] = JsonUtils.Diff(receivedVM, responseVM, false, i => ShouldIncludeProperty(i.TypeId, i.Property)); // context.ViewModelJson.Remove("viewModel"); // } - var result = StringUtils.Utf8.GetString(utf8json.ToSpan()); + var result = StringUtils.Utf8Decode(utf8json.ToSpan()); context.HttpContext.SetItem("dotvvm-viewmodel-size-bytes", utf8json.Length); // for PerformanceWarningTracer var routeLabel = context.RouteLabel(); @@ -199,14 +199,14 @@ public MemoryStream BuildViewModel(IDotvvmRequestContext context, object? comman } AddCustomPropertiesIfAny(context, sentJsonWriter, sentJsonBuffer); - if (serializeNewResources) + if (postbackUpdatedControls is not null) { - AddNewResources(context, sentJsonWriter); + AddPostBackUpdatedControls(context, sentJsonWriter, postbackUpdatedControls); } - if (postbackUpdatedControls is not null) + if (serializeNewResources) { - AddPostBackUpdatedControls(context, sentJsonWriter, postbackUpdatedControls); + AddNewResources(context, sentJsonWriter); } SerializeTypeMetadata(context, sentJsonWriter, state.UsedSerializationMaps); @@ -419,9 +419,7 @@ public void PopulateViewModel(IDotvvmRequestContext context, ReadOnlyMemory (Func)(t => { - using var state = DotvvmSerializationState.Create(isPostback: true, context.Services); + using var state = DotvvmSerializationState.Create(isPostback: true, context.Services, readEncryptedValues: new JsonObject()); return JsonSerializer.Deserialize(a, t, ViewModelJsonOptions); })).ToArray() : new Func[0]; diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmDateOnlyConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmDateOnlyConverter.cs index 8e43a9f6dc..d6b07b7c9d 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmDateOnlyConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmDateOnlyConverter.cs @@ -23,6 +23,5 @@ public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, Jso throw new Exception("The value specified in the JSON could not be converted to DateTime!"); } - } } diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs index 381a6fae02..b55e7ba2aa 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs @@ -75,7 +75,9 @@ public override void Write(Utf8JsonWriter json, TDictionary value, JsonSerialize } else { - throw new JsonException($"Unexpected property {reader.GetString()}."); + reader.Read(); + reader.Skip(); + reader.Read(); } } dict.Add(item.key!, item.value); diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmEnumConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmEnumConverter.cs index 749d12d302..93ebb62fcb 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmEnumConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmEnumConverter.cs @@ -6,6 +6,7 @@ using System.Runtime.Serialization; using System.Text.Json; using System.Text.Json.Serialization; +using DotVVM.Framework.Compilation.Javascript; using DotVVM.Framework.Utils; namespace DotVVM.Framework.ViewModel.Serialization @@ -18,7 +19,7 @@ public class DotvvmEnumConverter : JsonConverterFactory public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => (JsonConverter?)CreateConverterGenericMethod.MakeGenericMethod(typeToConvert).Invoke(this, []); - static MethodInfo CreateConverterGenericMethod = typeof(DotvvmEnumConverter).GetMethod(nameof(CreateConverter), genericParameterCount: 1, []).NotNull(); + static MethodInfo CreateConverterGenericMethod = (MethodInfo)MethodFindingHelper.GetMethodFromExpression(() => default(DotvvmEnumConverter)!.CreateConverter()); public JsonConverter CreateConverter() where TEnum : unmanaged, Enum { // if (!ReflectionUtils.EnumInfo.HasEnumMemberField) diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmObjectConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmObjectConverter.cs new file mode 100644 index 0000000000..3275b1f800 --- /dev/null +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmObjectConverter.cs @@ -0,0 +1,33 @@ +using System; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using DotVVM.Framework.Utils; + +namespace DotVVM.Framework.ViewModel.Serialization +{ + public class DotvvmObjectConverter : JsonConverter + { + public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + } + else if (typeof(object) == value.GetType()) + { + writer.WriteStartObject(); + writer.WriteEndObject(); + } + else + { + JsonSerializer.Serialize(writer, value, value.GetType(), options); + } + } + + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return SystemTextJsonUtils.DeserializeObject(ref reader, options); + } + } +} diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs index 65bbbe988c..e97e995d21 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs @@ -33,6 +33,7 @@ public ViewModelJsonConverter(IViewModelSerializationMapper viewModelSerializati !ReflectionUtils.IsEnumerable(type) && ReflectionUtils.IsComplexType(type) && !ReflectionUtils.IsTupleLike(type) && + !ReflectionUtils.IsJsonDom(type) && type != typeof(object); /// diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs index 89d5adbbf4..e0ef658f97 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs @@ -625,7 +625,7 @@ private Expression CallPropertyConverterWrite(JsonConverter converter, Expressio return null; } - private Expression? TrySerializePrimitive(Expression reader, Expression value) + private Expression? TrySerializePrimitive(Expression writer, Expression value) { var type = value.Type; Debug.Assert(!ReflectionUtils.IsNullableType(type)); @@ -633,14 +633,19 @@ private Expression CallPropertyConverterWrite(JsonConverter converter, Expressio // writer.WriteValue // Newtonsoft.Json.JsonWriter nj = default; if (type == typeof(bool)) - return Call(reader, "WriteBooleanValue", Type.EmptyTypes, value); - if (type == typeof(decimal) || type == typeof(double) || type == typeof(float) || + return Call(writer, "WriteBooleanValue", Type.EmptyTypes, value); + if (type == typeof(float) || type == typeof(double)) + return Call( + typeof(SystemTextJsonUtils).GetMethod(nameof(SystemTextJsonUtils.WriteFloatValue), [ typeof(Utf8JsonWriter), type ])!, + writer, value + ); + if (type == typeof(decimal) || type == typeof(int) || type == typeof(uint) || type == typeof(long) || type == typeof(ulong)) - return Call(reader, "WriteNumberValue", Type.EmptyTypes, value); + return Call(writer, "WriteNumberValue", Type.EmptyTypes, value); if (type == typeof(short) || type == typeof(ushort) || type == typeof(sbyte) || type == typeof(byte)) - return Call(reader, "WriteNumberValue", Type.EmptyTypes, Convert(value, typeof(int))); + return Call(writer, "WriteNumberValue", Type.EmptyTypes, Convert(value, typeof(int))); if (type == typeof(string) || type == typeof(Guid)) // TODO: datetime too? - return Call(reader, "WriteStringValue", Type.EmptyTypes, value); + return Call(writer, "WriteStringValue", Type.EmptyTypes, value); return null; } diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs index e9bda6fc57..571a5bb686 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging; using System.Text.Json.Serialization; using System.Text.Json; +using DotVVM.Framework.Compilation.Javascript; namespace DotVVM.Framework.ViewModel.Serialization { @@ -46,7 +47,9 @@ public class ViewModelSerializationMapper : IViewModelSerializationMapper /// protected virtual ViewModelSerializationMap CreateMap(Type type) => (ViewModelSerializationMap)CreateMapGenericMethod.MakeGenericMethod(type).Invoke(this, Array.Empty())!; - static MethodInfo CreateMapGenericMethod = typeof(ViewModelSerializationMapper).GetMethod(nameof(CreateMap), 1, BindingFlags.NonPublic | BindingFlags.Instance, null, [], [])!; + static MethodInfo CreateMapGenericMethod = + // typeof(ViewModelSerializationMapper).GetMethod(nameof(CreateMap), 1, BindingFlags.NonPublic | BindingFlags.Instance, null, [], [])!; + (MethodInfo)MethodFindingHelper.GetMethodFromExpression(() => default(ViewModelSerializationMapper)!.CreateMap()); protected virtual ViewModelSerializationMap CreateMap() { var type = typeof(T); diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs index ca9b6a1aad..a29931a037 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelTypeMetadataSerializer.cs @@ -183,7 +183,7 @@ internal void WriteTypeIdentifier(Utf8JsonWriter json, Type type, HashSet else json.WriteStringValue(GetPrimitiveTypeName(type)); } - else if (type == typeof(object)) + else if (type == typeof(object) || ReflectionUtils.IsJsonDom(type)) { json.WriteStartObject(); json.WriteString("type"u8, "dynamic"u8); diff --git a/src/Samples/Api.AspNetCore/DotVVM.Samples.BasicSamples.Api.AspNetCore.csproj b/src/Samples/Api.AspNetCore/DotVVM.Samples.BasicSamples.Api.AspNetCore.csproj index 1e2f0ab8dc..65891aed81 100644 --- a/src/Samples/Api.AspNetCore/DotVVM.Samples.BasicSamples.Api.AspNetCore.csproj +++ b/src/Samples/Api.AspNetCore/DotVVM.Samples.BasicSamples.Api.AspNetCore.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 @@ -19,11 +19,11 @@ - - - - - + + + + + diff --git a/src/Samples/Tests/Tests/Feature/StringInterpolationTests.cs b/src/Samples/Tests/Tests/Feature/StringInterpolationTests.cs index 649967eb15..d4e8d5bb2a 100644 --- a/src/Samples/Tests/Tests/Feature/StringInterpolationTests.cs +++ b/src/Samples/Tests/Tests/Feature/StringInterpolationTests.cs @@ -73,7 +73,7 @@ public void Feature_StringInterpolation_StandardDateFormatTest() var text6 = browser.Single("date-format6", SelectByDataUi); var text7 = browser.Single("date-format7", SelectByDataUi); - AssertUI.TextEquals(text1, "No format: 2016-07-15T03:15:00.0000000"); + AssertUI.TextEquals(text1, "No format: 2016-07-15T03:15:00"); AssertUI.TextEquals(text2, "D format: Friday, July 15, 2016 |X| d format: 7/15/2016"); AssertUI.TextEquals(text3, "F format: Friday, July 15, 2016 3:15:00 AM |X| f format: Friday, July 15, 2016 3:15 AM"); AssertUI.TextEquals(text4, "G format: 7/15/2016 3:15:00 AM |X| g format: 7/15/2016 3:15 AM"); From 2262709ab0c57d4d14792a6ee0067bedab59dedf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 15 Mar 2024 15:24:50 +0100 Subject: [PATCH 04/20] STJ migration: fix view model size warning --- .../ViewModel/DefaultPropertySerialization.cs | 25 ++- .../Javascript/JsViewModelPropertyAdjuster.cs | 2 +- .../Diagnostics/DiagnosticsRequestTracer.cs | 7 + .../Framework/Diagnostics/JsonSizeAnalyzer.cs | 157 +++++++++++------- .../Diagnostics/PerformanceWarningTracer.cs | 86 +++++----- .../Framework/Hosting/DotvvmPresenter.cs | 2 +- .../Runtime/Tracing/IRequestTracer.cs | 3 + .../Runtime/Tracing/NullRequestTracer.cs | 5 + .../Tracing/RequestTracingExtensions.cs | 9 + src/Framework/Framework/Utils/MemoryUtils.cs | 7 +- .../Framework/Utils/SystemTextJsonHacks.cs | 2 +- .../Framework/Utils/SystemTextJsonUtils.cs | 24 ++- .../DefaultViewModelSerializer.cs | 6 +- .../Serialization/DotvvmEnumConverter.cs | 46 +++-- .../SerialiationMapperAttributeHelper.cs | 1 - .../Hosting/DotvvmHttpResponse.cs | 31 +++- .../Security/DefaultViewModelProtector.cs | 16 +- .../Binding/JavascriptCompilationTests.cs | 22 +++ .../ApplicationInsightsTracer.cs | 5 + .../MiniProfiler.Shared/MiniProfilerTracer.cs | 57 ++++--- 20 files changed, 334 insertions(+), 179 deletions(-) diff --git a/src/Framework/Core/ViewModel/DefaultPropertySerialization.cs b/src/Framework/Core/ViewModel/DefaultPropertySerialization.cs index ef97c13eae..ae6d73a24a 100644 --- a/src/Framework/Core/ViewModel/DefaultPropertySerialization.cs +++ b/src/Framework/Core/ViewModel/DefaultPropertySerialization.cs @@ -1,10 +1,13 @@ -using System.Reflection; +using System; +using System.Reflection; using System.Text.Json.Serialization; namespace DotVVM.Framework.ViewModel { public class DefaultPropertySerialization : IPropertySerialization { + static readonly Type? JsonPropertyNJ = Type.GetType("Newtonsoft.Json.JsonPropertyAttribute, Newtonsoft.Json"); + static readonly PropertyInfo? JsonPropertyNJPropertyName = JsonPropertyNJ?.GetProperty("PropertyName"); public string ResolveName(PropertyInfo propertyInfo) { var bindAttribute = propertyInfo.GetCustomAttribute(); @@ -16,13 +19,23 @@ public string ResolveName(PropertyInfo propertyInfo) } } - if (string.IsNullOrEmpty(bindAttribute?.Name)) + // use JsonPropertyName name if Bind attribute is not present or doesn't specify it + var jsonPropertyAttribute = propertyInfo.GetCustomAttribute(); + if (!string.IsNullOrEmpty(jsonPropertyAttribute?.Name)) { - // use JsonProperty name if Bind attribute is not present or doesn't specify it - var jsonPropertyAttribute = propertyInfo.GetCustomAttribute(); - if (!string.IsNullOrEmpty(jsonPropertyAttribute?.Name)) + return jsonPropertyAttribute!.Name!; + } + + if (JsonPropertyNJ is not null) + { + var jsonPropertyNJAttribute = propertyInfo.GetCustomAttribute(JsonPropertyNJ); + if (jsonPropertyNJAttribute is not null) { - return jsonPropertyAttribute!.Name!; + var name = (string?)JsonPropertyNJPropertyName!.GetValue(jsonPropertyNJAttribute); + if (!string.IsNullOrEmpty(name)) + { + return name; + } } } diff --git a/src/Framework/Framework/Compilation/Javascript/JsViewModelPropertyAdjuster.cs b/src/Framework/Framework/Compilation/Javascript/JsViewModelPropertyAdjuster.cs index 08ebf725c2..1b60fd096f 100644 --- a/src/Framework/Framework/Compilation/Javascript/JsViewModelPropertyAdjuster.cs +++ b/src/Framework/Framework/Compilation/Javascript/JsViewModelPropertyAdjuster.cs @@ -117,7 +117,7 @@ protected override void DefaultVisit(JsNode node) if (node.Annotation() is { Type: {}, SerializationMap: null } vmAnnotation) { - if (!vmAnnotation.Type.IsPrimitive && vmAnnotation.Type != typeof(void)) + if (!ReflectionUtils.IsPrimitiveType(vmAnnotation.Type) && vmAnnotation.Type != typeof(void) && !ReflectionUtils.IsCollection(vmAnnotation.Type)) vmAnnotation.SerializationMap = mapper.GetMap(vmAnnotation.Type); } } diff --git a/src/Framework/Framework/Diagnostics/DiagnosticsRequestTracer.cs b/src/Framework/Framework/Diagnostics/DiagnosticsRequestTracer.cs index 7de0b8a329..7e22cdca6e 100644 --- a/src/Framework/Framework/Diagnostics/DiagnosticsRequestTracer.cs +++ b/src/Framework/Framework/Diagnostics/DiagnosticsRequestTracer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading.Tasks; using DotVVM.Framework.Diagnostics.Models; @@ -36,6 +37,12 @@ public Task TraceEvent(string eventName, IDotvvmRequestContext context) return TaskUtils.GetCompletedTask(); } + + public void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Lazy viewModelBuffer) + { + // TODO + } + private EventTiming CreateEventTiming(string eventName) { return new EventTiming( diff --git a/src/Framework/Framework/Diagnostics/JsonSizeAnalyzer.cs b/src/Framework/Framework/Diagnostics/JsonSizeAnalyzer.cs index 244e9388c5..43042706f5 100644 --- a/src/Framework/Framework/Diagnostics/JsonSizeAnalyzer.cs +++ b/src/Framework/Framework/Diagnostics/JsonSizeAnalyzer.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Linq; using System.Text.Json; +using DotVVM.Framework.Compilation.Binding; using DotVVM.Framework.Utils; using DotVVM.Framework.ViewModel.Serialization; using FastExpressionCompiler; @@ -20,74 +21,105 @@ public JsonSizeAnalyzer(IViewModelSerializationMapper viewModelMapper) this.viewModelMapper = viewModelMapper; } /// Computes the inclusive and exclusive size of each JSON property. - public JsonSizeProfile Analyze(JsonElement json) + public JsonSizeProfile Analyze(ReadOnlySpan json, Type? rootViewModelType) { - throw new NotImplementedException(); // TODO - // Dictionary results = new(); - // // returns the length of the token. Recursively calls itself for arrays and objects. - // AtomicSizeProfile analyzeToken(JsonElement token) - // { - // switch (token.ValueKind) - // { - // case JsonValueKind.Object: - // return new (InclusiveSize: analyzeObject(token), ExclusiveSize: 2); - // case JsonValueKind.Array: { - // var r = new AtomicSizeProfile(0); - // foreach (var item in token.EnumerateArray()) - // { - // r += analyzeToken(item); - // } - // return r; - // } - // case JsonValueKind.String: - // return new ((((string?)token)?.Length ?? 4) + 2); - // case JsonValueKind.Integer: - // // This should be the same as token.ToString().Length, but I didn't want to allocate the string unnecesarily - // return new((int)Math.Log10(Math.Abs((long)token) + 1) + 1); - // case JsonValueKind.Number: - // return new(((double)token).ToString().Length); - // case JsonValueKind.True: - // return new(4); - // case JsonValueKind.False: - // return new(5); - // case JsonValueKind.Null: - // return new(4); - // default: - // Debug.Assert(false, $"Unexpected token type {token.ValueKind}"); - // return new(token.ToString().Length); - // } - // } - // int analyzeObject(JsonElement j) - // { - // var type = ((string?)j.GetPropertyOrNull("$type")?.GetString())?.Apply(viewModelMapper.GetMapByTypeId); + var reader = new Utf8JsonReader(json); + return Analyze(ref reader, rootViewModelType); + } + + /// Computes the inclusive and exclusive size of each JSON property. + public JsonSizeProfile Analyze(ref Utf8JsonReader json, Type? rootViewModelType) + { + Dictionary results = new(); + // returns the length of the token. Recursively calls itself for arrays and objects. + AtomicSizeProfile analyzeNode(ref Utf8JsonReader json, Type? type) + { + switch (json.TokenType) + { + case JsonTokenType.StartObject: + return new (InclusiveSize: analyzeObject(ref json, type), ExclusiveSize: 2); + case JsonTokenType.StartArray: { + json.Read(); + var elementType = type?.GetEnumerableType(); + var r = new AtomicSizeProfile(0); + while (json.TokenType != JsonTokenType.EndArray) + { + r = new AtomicSizeProfile(r.InclusiveSize + analyzeNode(ref json, elementType).InclusiveSize); + json.Read(); + } + if (json.TokenType != JsonTokenType.EndArray) + throw new JsonException($"Expected EndArray, found {json.TokenType}."); + return r; + } + case JsonTokenType.String: + return new (json.GetValueLength() + 2); + case JsonTokenType.Number: + return new (json.GetValueLength()); + case JsonTokenType.True: + return new(4); + case JsonTokenType.False: + return new(5); + case JsonTokenType.Null: + return new(4); + default: { + Debug.Assert(false, $"Unexpected token type {json.TokenType}"); + var start = json.TokenStartIndex; + json.Skip(); + return new((int)(json.BytesConsumed - start)); + } + } + } + int analyzeObject(ref Utf8JsonReader json, Type? type) + { + var typeMap = type is null ? null : viewModelMapper.GetMap(type); + var typeName = type?.ToCode(stripNamespace: true) ?? "UnknownType"; - // var typeName = type?.Type.ToCode(stripNamespace: true) ?? "UnknownType"; - // var props = new Dictionary(); + var props = new Dictionary(); - // var totalSize = new AtomicSizeProfile(0); - // foreach (var prop in j.Properties()) - // { - // var propSize = analyzeToken(prop.Value); - // props[prop.Name] = propSize; + var startIndex = json.TokenStartIndex; + var exclusiveSize = 2; - // totalSize += propSize; - // totalSize += 4 + prop.Name.Length; // 2 for the quotes, 1 for :, 1 for , - // } + json.AssertRead(JsonTokenType.StartObject); + while (json.TokenType == JsonTokenType.PropertyName) + { + var propName = json.GetString().NotNull(); + var propNameLength = json.GetValueLength(); + json.Read(); + if (propName == "$type") + { + typeMap = viewModelMapper.GetMapByTypeId(json.GetString().NotNull("$type")); + type = typeMap.Type; + typeName = typeMap.Type.ToCode(stripNamespace: true); + } - // var classSize = new ClassSizeProfile(totalSize, props); - // if (results.TryGetValue(typeName, out var existing)) - // { - // results[typeName] = existing + classSize; - // } - // else - // { - // results[typeName] = classSize; - // } - // return totalSize.InclusiveSize; - // } + var propertyMap = typeMap?.Properties.FirstOrDefault(p => p.Name == propName); - // var totalSize = analyzeObject(json); - // return new JsonSizeProfile(results, totalSize); + var propSize = analyzeNode(ref json, propertyMap?.Type); + props[propertyMap?.PropertyInfo?.Name ?? propName] = propSize + new AtomicSizeProfile(propNameLength + 4); // 2 for the quotes, 1 for :, 1 for , + exclusiveSize += propNameLength + 4; + json.Read(); + } + if (json.TokenType != JsonTokenType.EndObject) + throw new JsonException($"Expected EndObject but found {json.TokenType}"); + + var inclusiveSize = (int)(json.BytesConsumed - startIndex); + var classSize = new ClassSizeProfile(new AtomicSizeProfile(inclusiveSize, exclusiveSize), props); + if (results.TryGetValue(typeName, out var existing)) + { + results[typeName] = existing + classSize; + } + else + { + results[typeName] = classSize; + } + return inclusiveSize; + } + + if (json.TokenType == JsonTokenType.None) + json.AssertRead(JsonTokenType.None); + + var totalSize = analyzeObject(ref json, rootViewModelType); + return new JsonSizeProfile(results, totalSize); } @@ -122,7 +154,6 @@ int ExclusiveSize public static AtomicSizeProfile operator +(AtomicSizeProfile a, AtomicSizeProfile b) => new AtomicSizeProfile(a.InclusiveSize + b.InclusiveSize, a.ExclusiveSize + b.ExclusiveSize); public static AtomicSizeProfile operator +(AtomicSizeProfile a, int c) => new AtomicSizeProfile(a.InclusiveSize + c, a.ExclusiveSize + c); - } } } diff --git a/src/Framework/Framework/Diagnostics/PerformanceWarningTracer.cs b/src/Framework/Framework/Diagnostics/PerformanceWarningTracer.cs index fb7a031620..02d831ea20 100644 --- a/src/Framework/Framework/Diagnostics/PerformanceWarningTracer.cs +++ b/src/Framework/Framework/Diagnostics/PerformanceWarningTracer.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading.Tasks; using DotVVM.Framework.Configuration; using DotVVM.Framework.Hosting; using DotVVM.Framework.Runtime; using DotVVM.Framework.Runtime.Tracing; +using DotVVM.Framework.Utils; using Microsoft.Extensions.Logging; namespace DotVVM.Framework.Diagnostics @@ -54,47 +56,54 @@ void WarnSlowRequest(TimeSpan totalElapsed) "We recommend using MiniProfiler when to keep an eye on runtime performance: https://www.dotvvm.com/docs/latest/pages/concepts/diagnostics-and-profiling/miniprofiler" )); } - void WarnLargeViewModel(long viewModelSize, IDotvvmRequestContext context) + + + + public void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Lazy viewModelBuffer) { - // if (context.ViewModelJson is null) TODO - // return; + if (viewModelSize >= config.BigViewModelBytes) + { + WarnLargeViewModel(context, viewModelSize, viewModelBuffer.Value); + } + } - // try - // { - // var vmAnalysis = jsonSizeAnalyzer.Analyze(context.ViewModelJson); + void WarnLargeViewModel(IDotvvmRequestContext context, int viewModelSize, Stream viewModelBuffer) + { + try + { + var vmAnalysis = jsonSizeAnalyzer.Analyze(viewModelBuffer.ReadToMemory().Span, context.ViewModel?.GetType()); - // var topClasses = - // vmAnalysis.Classes - // .OrderByDescending(c => c.Value.Size.ExclusiveSize) - // .Take(3) - // // only classes which have at least 5% impact - // .Where(c => c.Value.Size.ExclusiveSize > vmAnalysis.TotalSize / 20) - // .ToArray(); - // var topProperties = - // vmAnalysis.Classes - // .SelectMany(c => c.Value.Properties.Select(p => (Key: c.Key + "." + p.Key, p.Value))) - // .OrderByDescending(c => c.Value.ExclusiveSize) - // .Take(3) - // // only properties which have at least 5% impact - // .Where(c => c.Value.ExclusiveSize > vmAnalysis.TotalSize / 20) - // .ToArray(); + var topClasses = + vmAnalysis.Classes + .OrderByDescending(c => c.Value.Size.ExclusiveSize) + .Take(3) + // only classes which have at least 5% impact + .Where(c => c.Value.Size.ExclusiveSize > vmAnalysis.TotalSize / 20) + .ToArray(); + var topProperties = + vmAnalysis.Classes + .SelectMany(c => c.Value.Properties.Select(p => (Key: c.Key + "." + p.Key, p.Value))) + .OrderByDescending(c => c.Value.ExclusiveSize) + .Take(3) + // only properties which have at least 5% impact + .Where(c => c.Value.ExclusiveSize > vmAnalysis.TotalSize / 20) + .ToArray(); - // var byteToPercent = 100.0 / vmAnalysis.TotalSize; + var byteToPercent = 100.0 / vmAnalysis.TotalSize; - // var msg = $"The serialized view model has {viewModelSize / 1024.0 / 1024.0:0.0}MB, which may make your application quite slow. " + - // string.Join(", ", - // topProperties.Select(c => $"Property {c.Key} takes {c.Value.ExclusiveSize * byteToPercent:0}%").Concat( - // topClasses.Select(c => $"Class {c.Key} takes {c.Value.Size.ExclusiveSize * byteToPercent:0}%"))); + var msg = $"The serialized view model has {viewModelSize / 1024.0 / 1024.0:0.0}MB, which may make your application quite slow. " + + string.Join(", ", + topProperties.Select(c => $"Property {c.Key} takes {c.Value.ExclusiveSize * byteToPercent:0}%").Concat( + topClasses.Select(c => $"Class {c.Key} takes {c.Value.Size.ExclusiveSize * byteToPercent:0}%"))); - // logger.Warn(new DotvvmRuntimeWarning( - // msg - // )); - // } - // catch (Exception ex) - // { - // tracerLogger?.LogWarning(ex, $"Failed to analyze view model size. The serialized view model has {viewModelSize / 1024.0 / 1024.0:0.0}MB, which may make your application quite slow"); - // } - tracerLogger?.LogWarning($"Failed to analyze view model size. The serialized view model has {viewModelSize / 1024.0 / 1024.0:0.0}MB, which may make your application quite slow"); + logger.Warn(new DotvvmRuntimeWarning( + msg + )); + } + catch (Exception ex) + { + tracerLogger?.LogWarning(ex, $"Failed to analyze view model size. The serialized view model has {viewModelSize / 1024.0 / 1024.0:0.0}MB, which may make your application quite slow"); + } } public Task EndRequest(IDotvvmRequestContext context) { @@ -102,13 +111,6 @@ public Task EndRequest(IDotvvmRequestContext context) if (elapsed.TotalSeconds > config.SlowRequestSeconds) WarnSlowRequest(elapsed); - - var viewModelSize = context.HttpContext.GetItem("dotvvm-viewmodel-size-bytes"); - if (viewModelSize > config.BigViewModelBytes) - { - WarnLargeViewModel(viewModelSize, context); - } - return Task.CompletedTask; } public Task EndRequest(IDotvvmRequestContext context, Exception exception) diff --git a/src/Framework/Framework/Hosting/DotvvmPresenter.cs b/src/Framework/Framework/Hosting/DotvvmPresenter.cs index 3512be173e..3039de8d07 100644 --- a/src/Framework/Framework/Hosting/DotvvmPresenter.cs +++ b/src/Framework/Framework/Hosting/DotvvmPresenter.cs @@ -201,7 +201,7 @@ public async Task ProcessRequestCore(IDotvvmRequestContext context) ReadOnlyMemory postData; using (var stream = ReadRequestBody(context.HttpContext.Request, context.Route?.RouteName)) { - postData = await stream.ReadToMemoryAsnc(); + postData = await stream.ReadToMemoryAsync(); } ViewModelSerializer.PopulateViewModel(context, postData); diff --git a/src/Framework/Framework/Runtime/Tracing/IRequestTracer.cs b/src/Framework/Framework/Runtime/Tracing/IRequestTracer.cs index 1f28c9bb62..652f4510d6 100644 --- a/src/Framework/Framework/Runtime/Tracing/IRequestTracer.cs +++ b/src/Framework/Framework/Runtime/Tracing/IRequestTracer.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Threading.Tasks; using DotVVM.Framework.Hosting; @@ -8,6 +9,8 @@ public interface IRequestTracer { Task TraceEvent(string eventName, IDotvvmRequestContext context); + void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Lazy viewModelBuffer); + Task EndRequest(IDotvvmRequestContext context); Task EndRequest(IDotvvmRequestContext context, Exception exception); diff --git a/src/Framework/Framework/Runtime/Tracing/NullRequestTracer.cs b/src/Framework/Framework/Runtime/Tracing/NullRequestTracer.cs index e46ad0de92..7097863b2a 100644 --- a/src/Framework/Framework/Runtime/Tracing/NullRequestTracer.cs +++ b/src/Framework/Framework/Runtime/Tracing/NullRequestTracer.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Threading.Tasks; using DotVVM.Framework.Hosting; using DotVVM.Framework.Utils; @@ -24,6 +25,10 @@ public Task EndRequest(IDotvvmRequestContext context, Exception exception) return TaskUtils.GetCompletedTask(); } + public void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Lazy viewModelBuffer) + { + } + public readonly static NullRequestTracer Instance = new NullRequestTracer(); } diff --git a/src/Framework/Framework/Runtime/Tracing/RequestTracingExtensions.cs b/src/Framework/Framework/Runtime/Tracing/RequestTracingExtensions.cs index daec9e97d7..82bc879270 100644 --- a/src/Framework/Framework/Runtime/Tracing/RequestTracingExtensions.cs +++ b/src/Framework/Framework/Runtime/Tracing/RequestTracingExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Runtime.CompilerServices; using System.Threading.Tasks; using DotVVM.Framework.Hosting; @@ -16,6 +17,14 @@ public static async Task TracingEvent(this IEnumerable requestTr } } + public static void TracingSerialized(this IEnumerable requestTracers, IDotvvmRequestContext context, int viewModelSize, Func stream) + { + foreach (var tracer in requestTracers) + { + tracer.ViewModelSerialized(context, viewModelSize, new Lazy(stream)); + } + } + public static async Task TracingEndRequest(this IEnumerable requestTracers, IDotvvmRequestContext context) { foreach (var tracer in requestTracers) diff --git a/src/Framework/Framework/Utils/MemoryUtils.cs b/src/Framework/Framework/Utils/MemoryUtils.cs index c9d5184da9..07bc0cd3b0 100644 --- a/src/Framework/Framework/Utils/MemoryUtils.cs +++ b/src/Framework/Framework/Utils/MemoryUtils.cs @@ -11,6 +11,11 @@ static class MemoryUtils public static Memory ToMemory(this MemoryStream stream) => stream.TryGetBuffer(out var buffer) ? buffer.AsMemory().Slice(0, (int)stream.Length) : stream.ToArray(); + public static MemoryStream CloneReadOnly(this MemoryStream stream) + { + return new MemoryStream(stream.GetBuffer(), 0, (int)stream.Length, false); + } + public static ReadOnlySpan Readonly(this Span span) => span; public static ReadOnlyMemory Readonly(this Memory span) => span; @@ -20,7 +25,7 @@ public static Memory ReadToMemory(this Stream stream) stream.CopyTo(buffer); return buffer.ToMemory(); } - public static async Task> ReadToMemoryAsnc(this Stream stream) + public static async Task> ReadToMemoryAsync(this Stream stream) { using var buffer = new MemoryStream(); await stream.CopyToAsync(buffer); diff --git a/src/Framework/Framework/Utils/SystemTextJsonHacks.cs b/src/Framework/Framework/Utils/SystemTextJsonHacks.cs index 130cf968da..64dd476e8a 100644 --- a/src/Framework/Framework/Utils/SystemTextJsonHacks.cs +++ b/src/Framework/Framework/Utils/SystemTextJsonHacks.cs @@ -22,7 +22,7 @@ public static void Populate(T obj, string input, JsonSerializerOptions option try { """{"X":"""u8.CopyTo(bytes.AsSpan().Slice(0, 5)); - StringUtils.Utf8Encode(input, bytes.AsSpan(5)); + StringUtils.Utf8Encode(input.AsSpan(), bytes.AsSpan(5)); bytes[length - 1] = (byte)'}'; var reader = new Utf8JsonReader(bytes.AsSpan().Slice(0, length)); var result = JsonSerializer.Deserialize>(ref reader, options)?.X; diff --git a/src/Framework/Framework/Utils/SystemTextJsonUtils.cs b/src/Framework/Framework/Utils/SystemTextJsonUtils.cs index ab935c2150..dfaa4cf76c 100644 --- a/src/Framework/Framework/Utils/SystemTextJsonUtils.cs +++ b/src/Framework/Framework/Utils/SystemTextJsonUtils.cs @@ -76,12 +76,32 @@ public static IEnumerable EnumerateStringArray(this JsonElement json) } } + public static void AssertToken(this in Utf8JsonReader reader, JsonTokenType type) + { + if (reader.TokenType != type) + { + throw new JsonException($"Expected token of type {type}, but got {reader.TokenType}."); + } + } + + public static void AssertRead(this ref Utf8JsonReader reader, JsonTokenType type) + { + AssertToken(in reader, type); + if (!reader.Read()) + throw new JsonException($"Expected token of type {type}, but got {reader.TokenType}."); + } + + public static int GetValueLength(this in Utf8JsonReader reader) + { + return reader.HasValueSequence ? checked((int)reader.ValueSequence.Length) : reader.ValueSpan.Length; + } + public static void WriteFloatValue(Utf8JsonWriter writer, double number) { #if DotNetCore if (double.IsFinite(number)) #else - if (!double.IsInfinity(number) && double.IsNaN(number)) + if (!double.IsInfinity(number) && !double.IsNaN(number)) #endif writer.WriteNumberValue(number); else @@ -92,7 +112,7 @@ public static void WriteFloatValue(Utf8JsonWriter writer, float number) #if DotNetCore if (float.IsFinite(number)) #else - if (!float.IsInfinity(number) && float.IsNaN(number)) + if (!float.IsInfinity(number) && !float.IsNaN(number)) #endif writer.WriteNumberValue(number); else diff --git a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs index e9c4ca9070..1474120baa 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs @@ -20,6 +20,8 @@ using System.Net.NetworkInformation; using System.Diagnostics; using Microsoft.Extensions.Logging; +using DotVVM.Framework.Runtime.Tracing; +using Microsoft.Extensions.DependencyInjection; namespace DotVVM.Framework.ViewModel.Serialization { @@ -40,7 +42,6 @@ public record SerializationException(bool Serialize, Type? ViewModelType, string private readonly IViewModelTypeMetadataSerializer viewModelTypeMetadataSerializer; private readonly ViewModelJsonConverter viewModelConverter; private readonly ILogger? logger; - public bool SendDiff { get; set; } = true; public JsonSerializerOptions ViewModelJsonOptions { get; } @@ -82,9 +83,10 @@ public string SerializeViewModel(IDotvvmRequestContext context, object? commandR // context.ViewModelJson["viewModelDiff"] = JsonUtils.Diff(receivedVM, responseVM, false, i => ShouldIncludeProperty(i.TypeId, i.Property)); // context.ViewModelJson.Remove("viewModel"); // } + var requestTracers = context.Services.GetService>(); + requestTracers?.TracingSerialized(context, (int)utf8json.Length, () => utf8json.CloneReadOnly()); var result = StringUtils.Utf8Decode(utf8json.ToSpan()); - context.HttpContext.SetItem("dotvvm-viewmodel-size-bytes", utf8json.Length); // for PerformanceWarningTracer var routeLabel = context.RouteLabel(); var requestType = context.RequestTypeLabel(); DotvvmMetrics.ViewModelStringificationTime.Record(timer.ElapsedSeconds, routeLabel, requestType); diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmEnumConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmEnumConverter.cs index 93ebb62fcb..201f1d1f38 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmEnumConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmEnumConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -202,30 +203,39 @@ public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe else { var valueLength = reader.HasValueSequence ? (int)reader.ValueSequence.Length : reader.ValueSpan.Length; - Span buffer = valueLength < 512 ? stackalloc byte[valueLength] : new byte[valueLength]; - var bufferLength = reader.CopyString(buffer); - buffer = buffer.Slice(0, bufferLength); - - ulong result = 0; - while (true) + byte[]? rentedBuffer = null; + try { - buffer = buffer.Slice(buffer[0] == ' ' ? 1 : 0); + Span buffer = valueLength < 512 ? stackalloc byte[valueLength] : (rentedBuffer = ArrayPool.Shared.Rent(valueLength)); + var bufferLength = reader.CopyString(buffer); + buffer = buffer.Slice(0, bufferLength); - var nextIndex = MemoryExtensions.IndexOf(buffer, (byte)','); - if (nextIndex == 0) - return ThrowInvalidEnumName(buffer); + ulong result = 0; + while (true) + { + buffer = buffer.Slice(buffer[0] == ' ' ? 1 : 0); - var token = nextIndex < 0 ? buffer : buffer.Slice(0, nextIndex); + var nextIndex = MemoryExtensions.IndexOf(buffer, (byte)','); + if (nextIndex == 0) + return ThrowInvalidEnumName(buffer); - var value = FindEnumName(token); - result |= ToBits(value); + var token = nextIndex < 0 ? buffer : buffer.Slice(0, nextIndex); - if (nextIndex < 0) - break; - buffer = buffer.Slice(nextIndex + 1); - } + var value = FindEnumName(token); + result |= ToBits(value); + + if (nextIndex < 0) + break; + buffer = buffer.Slice(nextIndex + 1); + } - return Unsafe.As(ref result); + return Unsafe.As(ref result); + } + finally + { + if (rentedBuffer is {}) + ArrayPool.Shared.Return(rentedBuffer); + } } } else diff --git a/src/Framework/Framework/ViewModel/Serialization/SerialiationMapperAttributeHelper.cs b/src/Framework/Framework/ViewModel/Serialization/SerialiationMapperAttributeHelper.cs index 2235fbc913..98a160e59d 100644 --- a/src/Framework/Framework/ViewModel/Serialization/SerialiationMapperAttributeHelper.cs +++ b/src/Framework/Framework/ViewModel/Serialization/SerialiationMapperAttributeHelper.cs @@ -9,7 +9,6 @@ public static class SerialiationMapperAttributeHelper { static readonly Type? JsonConstructorNJ = Type.GetType("Newtonsoft.Json.JsonConstructorAttribute, Newtonsoft.Json"); static readonly Type? JsonIgnoreNJ = Type.GetType("Newtonsoft.Json.JsonIgnoreAttribute, Newtonsoft.Json"); - static readonly Type? JsonPropertyNJ = Type.GetType("Newtonsoft.Json.JsonPropertyAttribute, Newtonsoft.Json"); static readonly Type? JsonConverterNJ = Type.GetType("Newtonsoft.Json.JsonConverterAttribute, Newtonsoft.Json"); public static bool IsJsonConstructor(MethodBase constructor) => diff --git a/src/Framework/Hosting.Owin/Hosting/DotvvmHttpResponse.cs b/src/Framework/Hosting.Owin/Hosting/DotvvmHttpResponse.cs index 61db22e6c0..f1f1be0f4a 100644 --- a/src/Framework/Hosting.Owin/Hosting/DotvvmHttpResponse.cs +++ b/src/Framework/Hosting.Owin/Hosting/DotvvmHttpResponse.cs @@ -1,4 +1,7 @@ -using System.IO; +using System; +using System.Buffers; +using System.IO; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Owin; @@ -51,15 +54,33 @@ public void Write(byte[] data, int offset, int count) { OriginalResponse.Body.Write(data, offset, count); } + public void Write(ReadOnlyMemory text) + { + OriginalResponse.Write(text.ToString()); + } + public void Write(ReadOnlyMemory data) + { + if (MemoryMarshal.TryGetArray(data, out var array)) + OriginalResponse.Write(array.Array, array.Offset, array.Count); + else + OriginalResponse.Write(data.ToArray()); + } + public Task WriteAsync(ReadOnlyMemory data, CancellationToken token = default) + { + if (MemoryMarshal.TryGetArray(data, out var array)) + return OriginalResponse.WriteAsync(array.Array, array.Offset, array.Count, token); + else + return OriginalResponse.WriteAsync(data.ToArray(), token); + } - public Task WriteAsync(string text) + public Task WriteAsync(ReadOnlyMemory text, CancellationToken token = default) { - return OriginalResponse.WriteAsync(text); + return OriginalResponse.WriteAsync(text.ToString(), token); } - public Task WriteAsync(string text, CancellationToken token) + public Task WriteAsync(string text, CancellationToken token = default) { return OriginalResponse.WriteAsync(text, token); } } -} \ No newline at end of file +} diff --git a/src/Framework/Hosting.Owin/Security/DefaultViewModelProtector.cs b/src/Framework/Hosting.Owin/Security/DefaultViewModelProtector.cs index a079c943cd..8378d72d19 100644 --- a/src/Framework/Hosting.Owin/Security/DefaultViewModelProtector.cs +++ b/src/Framework/Hosting.Owin/Security/DefaultViewModelProtector.cs @@ -22,10 +22,10 @@ public DefaultViewModelProtector(IDataProtectionProvider protectionProvider) this.protectionProvider = protectionProvider; } - public string Protect(string serializedData, IDotvvmRequestContext context) + public byte[] Protect(byte[] serializedData, IDotvvmRequestContext context) { if (serializedData == null) throw new ArgumentNullException(nameof(serializedData)); - if (string.IsNullOrWhiteSpace(serializedData)) throw new ArgumentException("Value cannot be empty or whitespace only string.", nameof(serializedData)); + if (serializedData.Length == 0) throw new ArgumentException("Value cannot be empty or whitespace only string.", nameof(serializedData)); if (context == null) throw new ArgumentNullException(nameof(context)); // Construct protector with purposes @@ -34,9 +34,7 @@ public string Protect(string serializedData, IDotvvmRequestContext context) var protector = this.protectionProvider.Create(PRIMARY_PURPOSE, userIdentity, requestIdentity); // Return protected view model data - var dataToProtect = Encoding.UTF8.GetBytes(serializedData); - var protectedData = protector.Protect(dataToProtect); - return Convert.ToBase64String(protectedData); + return protector.Protect(serializedData); } public byte[] Protect(byte[] data, params string[] purposes) => @@ -44,10 +42,10 @@ public string Protect(string serializedData, IDotvvmRequestContext context) .Create(ConcatPurposes(PRIMARY_PURPOSE, purposes)) .Protect(data); - public string Unprotect(string protectedData, IDotvvmRequestContext context) + public byte[] Unprotect(byte[] protectedData, IDotvvmRequestContext context) { if (protectedData == null) throw new ArgumentNullException(nameof(protectedData)); - if (string.IsNullOrWhiteSpace(protectedData)) throw new ArgumentException("Value cannot be empty or whitespace only string.", nameof(protectedData)); + if (protectedData.Length == 0) throw new ArgumentException("Value cannot be empty or whitespace only string.", nameof(protectedData)); if (context == null) throw new ArgumentNullException(nameof(context)); // Construct protector with purposes @@ -56,9 +54,7 @@ public string Unprotect(string protectedData, IDotvvmRequestContext context) var protector = this.protectionProvider.Create(PRIMARY_PURPOSE, userIdentity, requestIdentity); // Return unprotected view model data - var dataToUnprotect = Convert.FromBase64String(protectedData); - var unprotectedData = protector.Unprotect(dataToUnprotect); - return Encoding.UTF8.GetString(unprotectedData); + return protector.Unprotect(protectedData); } public byte[] Unprotect(byte[] protectedData, params string[] purposes) => diff --git a/src/Tests/Binding/JavascriptCompilationTests.cs b/src/Tests/Binding/JavascriptCompilationTests.cs index 3c1ec25f43..3f178cb3c1 100644 --- a/src/Tests/Binding/JavascriptCompilationTests.cs +++ b/src/Tests/Binding/JavascriptCompilationTests.cs @@ -17,6 +17,7 @@ using Microsoft.Extensions.DependencyInjection; using DotVVM.Framework.Utils; using DotVVM.Framework.ViewModel; +using System.Text.Json.Serialization; namespace DotVVM.Framework.Tests.Binding { @@ -1395,6 +1396,16 @@ public void JavascriptCompilation_CustomPrimitiveParse() Assert.AreEqual("VehicleNumber()==\"123\"", result); } + [DataTestMethod] + // [DataRow("BindAttribute", "bind_attribute")] + // [DataRow("SystemTextJson", "system_text_json")] + [DataRow("NewtonsoftJson", "newtonsoft_json")] + public void JavascriptCompilation_PropertyRenaming(string csharpName, string jsName) + { + var result = CompileBinding($"{csharpName} == 0", typeof(TestPropertyRenamingViewModel)); + Assert.AreEqual($"{jsName}()==0", result); + } + public class TestMarkupControl: DotvvmMarkupControl { public string SomeProperty @@ -1407,6 +1418,17 @@ public string SomeProperty public string NonUsableProperty { get; set; } } + + + public class TestPropertyRenamingViewModel + { + [Bind(Name = "bind_attribute")] + public int BindAttribute { get; } + [JsonPropertyName("system_text_json")] + public int SystemTextJson { get; } + [Newtonsoft.Json.JsonProperty("newtonsoft_json")] + public int NewtonsoftJson { get; } + } } public class TestExtensionParameterConflictViewModel diff --git a/src/Tracing/ApplicationInsights/ApplicationInsightsTracer.cs b/src/Tracing/ApplicationInsights/ApplicationInsightsTracer.cs index 094bd39c3a..13c9d7115c 100644 --- a/src/Tracing/ApplicationInsights/ApplicationInsightsTracer.cs +++ b/src/Tracing/ApplicationInsights/ApplicationInsightsTracer.cs @@ -5,6 +5,7 @@ using DotVVM.Framework.Utils; using Microsoft.ApplicationInsights; using System.Diagnostics; +using System.IO; namespace DotVVM.Tracing.ApplicationInsights { @@ -38,5 +39,9 @@ public Task EndRequest(IDotvvmRequestContext context, Exception exception) return TaskUtils.GetCompletedTask(); } + + public void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Lazy viewModelBuffer) + { + } } } diff --git a/src/Tracing/MiniProfiler.Shared/MiniProfilerTracer.cs b/src/Tracing/MiniProfiler.Shared/MiniProfilerTracer.cs index 196889ae62..ab2f4bb798 100644 --- a/src/Tracing/MiniProfiler.Shared/MiniProfilerTracer.cs +++ b/src/Tracing/MiniProfiler.Shared/MiniProfilerTracer.cs @@ -5,16 +5,17 @@ using System; using Microsoft.Extensions.DependencyInjection; using StackExchange.Profiling; +using System.IO; namespace DotVVM.Tracing.MiniProfiler -{ +{ using MiniProfiler = StackExchange.Profiling.MiniProfiler; public class MiniProfilerTracer : IRequestTracer, IMiniProfilerRequestTracer - { - public MiniProfilerTracer(IRequestTimingStorage storage) - { - this.storage = storage; - } + { + public MiniProfilerTracer(IRequestTimingStorage storage) + { + this.storage = storage; + } private IRequestTimingStorage storage; public Task EndRequest(IDotvvmRequestContext context) @@ -40,23 +41,23 @@ private MiniProfiler GetProfilerCurrent() } public Task TraceEvent(string eventName, IDotvvmRequestContext context) - { + { EnsureProfilerStarted(); SetEventNameStopCurrentTiming(eventName); storage.Current = (Timing)GetProfilerCurrent().Step(string.Empty); return TaskUtils.GetCompletedTask(); - } - - private void EnsureProfilerStarted() - { + } + + private void EnsureProfilerStarted() + { if (GetProfilerCurrent() == null) { MiniProfiler.StartNew(); - } - } - + } + } + private void SetEventNameStopCurrentTiming(string eventName) { if (storage.Current != null) @@ -64,18 +65,22 @@ private void SetEventNameStopCurrentTiming(string eventName) storage.Current.Name = eventName; storage.Current.Stop(); } - } - - public Timing Step(string name) - { + } + + public Timing Step(string name) + { EnsureProfilerStarted(); - return GetProfilerCurrent().Step(name); - } - - public Timing StepIf(string name, long minDuration, bool includeChildren = false) - { + return GetProfilerCurrent().Step(name); + } + + public Timing StepIf(string name, long minDuration, bool includeChildren = false) + { EnsureProfilerStarted(); - return GetProfilerCurrent().StepIf(name, minDuration, includeChildren); - } + return GetProfilerCurrent().StepIf(name, minDuration, includeChildren); + } + + public void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Lazy viewModelBuffer) + { + } } -} \ No newline at end of file +} From e8e5cd1c0189232aae6775f408d20994fb4722a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sun, 17 Mar 2024 11:32:17 +0100 Subject: [PATCH 05/20] STJ migration: fix server-side view model cache --- src/Framework/Framework/System.Index.cs | 7 +- src/Framework/Framework/System.Range.cs | 132 +++++++++++ .../Framework/Utils/JsonPatchWriter.cs | 205 ++++++++++++++++++ src/Framework/Framework/Utils/MemoryUtils.cs | 17 ++ .../Framework/Utils/ReadOnlyMemoryStream.cs | 204 +++++++++++++++++ .../DefaultViewModelSerializer.cs | 164 ++++++++------ .../DefaultViewModelServerCache.cs | 47 +++- .../Serialization/IViewModelServerCache.cs | 2 +- .../Serialization/ViewModelJsonConverter.cs | 25 ++- .../ViewModelSerializationMap.cs | 14 +- ...ulesServerSideTests.IncludeViewModule.html | 6 +- 11 files changed, 713 insertions(+), 110 deletions(-) create mode 100644 src/Framework/Framework/System.Range.cs create mode 100644 src/Framework/Framework/Utils/JsonPatchWriter.cs create mode 100644 src/Framework/Framework/Utils/ReadOnlyMemoryStream.cs diff --git a/src/Framework/Framework/System.Index.cs b/src/Framework/Framework/System.Index.cs index 19be24888b..1985b076b2 100644 --- a/src/Framework/Framework/System.Index.cs +++ b/src/Framework/Framework/System.Index.cs @@ -17,12 +17,7 @@ namespace System /// int lastElement = someArray[^1]; // lastElement = 5 /// /// -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - readonly struct Index : IEquatable + internal readonly struct Index : IEquatable { private readonly int _value; diff --git a/src/Framework/Framework/System.Range.cs b/src/Framework/Framework/System.Range.cs new file mode 100644 index 0000000000..24dde7f50b --- /dev/null +++ b/src/Framework/Framework/System.Range.cs @@ -0,0 +1,132 @@ +// From https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Range.cs +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if CSharp8Polyfill +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +#if !DotNetCore +using System.Numerics.Hashing; +#endif + +namespace System +{ + /// Represent a range has start and end indexes. + /// + /// Range is used by the C# compiler to support the range syntax. + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; + /// int[] subArray1 = someArray[0..2]; // { 1, 2 } + /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } + /// + /// + internal readonly struct Range : IEquatable + { + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + /// Construct a Range object using the start and end indexes. + /// Represent the inclusive start index of the range. + /// Represent the exclusive end index of the range. + public Range(Index start, Index end) + { + Start = start; + End = end; + } + + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals([NotNullWhen(true)] object? value) => + value is Range r && + r.Start.Equals(Start) && + r.End.Equals(End); + + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object + public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); + + /// Returns the hash code for this instance. + public override int GetHashCode() + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + return HashCode.Combine(Start.GetHashCode(), End.GetHashCode()); +#else + return (Start.GetHashCode(), End.GetHashCode()).GetHashCode(); +#endif + } + + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString() + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + Span span = stackalloc char[2 + (2 * 11)]; // 2 for "..", then for each index 1 for '^' and 10 for longest possible uint + int pos = 0; + + if (Start.IsFromEnd) + { + span[0] = '^'; + pos = 1; + } + bool formatted = ((uint)Start.Value).TryFormat(span.Slice(pos), out int charsWritten); + Debug.Assert(formatted); + pos += charsWritten; + + span[pos++] = '.'; + span[pos++] = '.'; + + if (End.IsFromEnd) + { + span[pos++] = '^'; + } + formatted = ((uint)End.Value).TryFormat(span.Slice(pos), out charsWritten); + Debug.Assert(formatted); + pos += charsWritten; + + return new string(span.Slice(0, pos)); +#else + return Start.ToString() + ".." + End.ToString(); +#endif + } + + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt(Index start) => new Range(start, Index.End); + + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt(Index end) => new Range(Index.Start, end); + + /// Create a Range object starting from first element to the end. + public static Range All => new Range(Index.Start, Index.End); + + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (int Offset, int Length) GetOffsetAndLength(int length) + { + int start = Start.GetOffset(length); + int end = End.GetOffset(length); + + if ((uint)end > (uint)length || (uint)start > (uint)end) + { + ThrowArgumentOutOfRangeException(); + } + + return (start, end - start); + } + + private static void ThrowArgumentOutOfRangeException() + { + throw new ArgumentOutOfRangeException("length"); + } + } +} +#endif diff --git a/src/Framework/Framework/Utils/JsonPatchWriter.cs b/src/Framework/Framework/Utils/JsonPatchWriter.cs new file mode 100644 index 0000000000..9ef23da61e --- /dev/null +++ b/src/Framework/Framework/Utils/JsonPatchWriter.cs @@ -0,0 +1,205 @@ +// using System.Linq; +// using System; +// using System.Text.Json; +// using System.Diagnostics; +// using System.Buffers; +// using System.Collections.Generic; + +// namespace DotVVM.Framework.Utils +// { +// ref struct JsonPatchWriter +// { +// static void CopyValue(ref Utf8JsonReader reader, Utf8JsonWriter writer) +// { +// Debug.Assert(reader.TokenType != JsonTokenType.PropertyName); + +// if (reader.TokenType is not JsonTokenType.StartArray and not JsonTokenType.StartObject) +// { +// if (reader.HasValueSequence) +// writer.WriteRawValue(reader.ValueSequence); +// else +// writer.WriteRawValue(reader.ValueSpan); + +// return; +// } + +// var depth = reader.CurrentDepth; +// while (reader.CurrentDepth >= depth) +// { +// switch (reader.TokenType) +// { +// case JsonTokenType.False: +// case JsonTokenType.True: +// case JsonTokenType.Null: +// case JsonTokenType.String: +// case JsonTokenType.Number: { +// if (reader.HasValueSequence) +// writer.WriteRawValue(reader.ValueSequence); +// else +// writer.WriteRawValue(reader.ValueSpan); +// break; +// } +// case JsonTokenType.PropertyName: { +// var length = reader.HasValueSequence ? reader.ValueSequence.Length : reader.ValueSpan.Length; +// Span buffer = length <= 1024 ? stackalloc byte[(int)length] : new byte[length]; +// var realLength = reader.CopyString(buffer); +// writer.WritePropertyName(buffer.Slice(0, realLength)); +// break; +// } +// case JsonTokenType.StartArray: { +// writer.WriteStartArray(); +// break; +// } +// case JsonTokenType.EndArray: { +// writer.WriteEndArray(); +// break; +// } +// case JsonTokenType.StartObject: { +// writer.WriteStartObject(); +// break; +// } +// case JsonTokenType.EndObject: { +// writer.WriteEndObject(); +// break; +// } +// default: { +// throw new JsonException($"Unexpected token {reader.TokenType}."); +// } +// } +// reader.Read(); +// } +// } + +// private readonly Utf8JsonWriter writer; +// private List patchStack; +// private Span nameBuffer; +// private byte[] nameBufferRented; + +// private JsonPatchWriter( +// Utf8JsonWriter writer, +// JsonElement patch, +// Span nameBuffer +// ) +// { +// this.writer = writer; +// } + +// Span ReadName(ref Utf8JsonReader reader) +// { +// var length = reader.CopyString(nameBuffer); +// if (length < nameBuffer.Length) +// { +// return nameBuffer.Slice(length); +// } +// var newBuffer = ArrayPool.Shared.Rent(length); +// nameBuffer.CopyTo(newBuffer); +// if (nameBufferRented is {}) +// ArrayPool.Shared.Return(nameBufferRented); +// nameBuffer = newBuffer; +// nameBufferRented = newBuffer; +// return ReadName(ref reader); +// } + +// private void Patch(ref Utf8JsonReader original, JsonElement patchValue) +// { +// var patchKind = patchValue.ValueKind; +// if (patchKind == JsonValueKind.Object && original.TokenType == JsonTokenType.StartObject) +// { +// PatchObject(ref original, patchValue); +// } +// else if (patchKind == JsonValueKind.Array && original.TokenType == JsonTokenType.StartArray) +// { +// PatchArray(ref original, patchValue); +// } +// else +// { +// patchValue.WriteTo(writer); +// } +// } + +// void PatchObject(ref Utf8JsonReader original, JsonElement patch) +// { +// original.AssertToken(JsonTokenType.StartObject); +// if (patch.ValueKind != JsonValueKind.Object) +// { +// patch.WriteTo(writer); +// return; +// } +// writer.WriteStartObject(); +// original.Read(); + +// var patchedProperties = 0; +// while (original.TokenType == JsonTokenType.PropertyName) +// { +// var propertyName = ReadName(ref original); +// original.Read(); +// writer.WritePropertyName(propertyName); + +// if (!patch.TryGetProperty(propertyName, out var patchValue)) +// { +// CopyValue(ref original, writer); +// continue; +// } + +// patchedProperties += 1; + +// Patch(ref original, patchValue); +// } +// original.AssertToken(JsonTokenType.EndObject); + +// var remainingProperties = -patchedProperties; +// foreach (var p in patch.EnumerateObject()) +// { +// remainingProperties += 1; +// } +// if (remainingProperties > 0) +// { +// throw new JsonException("Patching failed"); +// } + +// writer.WriteEndObject(); +// } + +// void PatchArray(ref Utf8JsonReader original, JsonElement patch) +// { +// using var patchEnumerator = patch.EnumerateArray(); +// original.AssertRead(JsonTokenType.StartArray); +// writer.WriteStartArray(); + +// while (original.TokenType != JsonTokenType.EndArray) +// { +// if (!patchEnumerator.MoveNext()) +// { +// while (original.TokenType != JsonTokenType.EndArray) +// { +// original.Skip(); +// original.Read(); +// } +// } + +// var patchKind = patchEnumerator.Current.ValueKind; +// var tokenType = original.TokenType; +// if (patchKind == JsonValueKind.Object && tokenType == JsonTokenType.StartObject) +// { +// PatchObject(ref original, patchEnumerator.Current); +// } +// else if (patchKind == JsonValueKind.Array && tokenType == JsonTokenType.StartArray) +// { +// PatchArray(ref original, patchEnumerator.Current); +// } +// else +// { +// patchEnumerator.Current.WriteTo(writer); +// } +// original.Read(); +// } + +// while (patchEnumerator.MoveNext()) +// { +// patchEnumerator.Current.WriteTo(writer); +// } + +// writer.WriteEndArray(); +// } +// } +// } diff --git a/src/Framework/Framework/Utils/MemoryUtils.cs b/src/Framework/Framework/Utils/MemoryUtils.cs index 07bc0cd3b0..0802829c7d 100644 --- a/src/Framework/Framework/Utils/MemoryUtils.cs +++ b/src/Framework/Framework/Utils/MemoryUtils.cs @@ -31,5 +31,22 @@ public static async Task> ReadToMemoryAsync(this Stream stream) await stream.CopyToAsync(buffer); return buffer.ToMemory(); } + + public static int CopyTo(this Stream stream, byte[] buffer, int offset) + { + var readBytesTotal = 0; + + while (true) + { + var maxLength = buffer.Length - readBytesTotal - offset; + if (maxLength == 0) + return readBytesTotal; + var count = stream.Read(buffer, readBytesTotal + offset, maxLength); + if (count == 0) + return readBytesTotal; + + readBytesTotal += count; + } + } } } diff --git a/src/Framework/Framework/Utils/ReadOnlyMemoryStream.cs b/src/Framework/Framework/Utils/ReadOnlyMemoryStream.cs new file mode 100644 index 0000000000..5f3ca10b04 --- /dev/null +++ b/src/Framework/Framework/Utils/ReadOnlyMemoryStream.cs @@ -0,0 +1,204 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// stolen from https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/IO/ReadOnlyMemoryStream.cs + +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO +{ + /// Provides a for the contents of a . + internal sealed class ReadOnlyMemoryStream : Stream + { + private ReadOnlyMemory _content; + private int _position; + private bool _isOpen; + + public ReadOnlyMemoryStream(ReadOnlyMemory content) + { + _content = content; + _isOpen = true; + } + + public override bool CanRead => _isOpen; + public override bool CanSeek => _isOpen; + public override bool CanWrite => false; + + private void EnsureNotClosed() + { + if (!_isOpen) + { + throw new ObjectDisposedException(null, "ReadOnlyMemoryStream is closed"); + } + } + + public override long Length + { + get + { + EnsureNotClosed(); + return _content.Length; + } + } + + public override long Position + { + get + { + EnsureNotClosed(); + return _position; + } + set + { + EnsureNotClosed(); + if (value < 0 || value > int.MaxValue) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _position = (int)value; + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + EnsureNotClosed(); + + long pos = + origin == SeekOrigin.Begin ? offset : + origin == SeekOrigin.Current ? _position + offset : + origin == SeekOrigin.End ? _content.Length + offset : + throw new ArgumentOutOfRangeException(nameof(origin)); + + if (pos > int.MaxValue) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + else if (pos < 0) + { + throw new IOException("Seek out of range."); + } + + _position = (int)pos; + return _position; + } + + public override int ReadByte() + { + EnsureNotClosed(); + + ReadOnlySpan s = _content.Span; + return _position < s.Length ? s[_position++] : -1; + } + + public override int Read(byte[] buffer, int offset, int count) + { + ValidateBufferArguments(buffer, offset, count); + return ReadBuffer(new Span(buffer, offset, count)); + } + +#if DotNetCore + public override int Read(Span buffer) => ReadBuffer(buffer); +#endif + + private int ReadBuffer(Span buffer) + { + EnsureNotClosed(); + + int remaining = _content.Length - _position; + + if (remaining <= 0 || buffer.Length == 0) + { + return 0; + } + else if (remaining <= buffer.Length) + { + _content.Span.Slice(_position).CopyTo(buffer); + _position = _content.Length; + return remaining; + } + else + { + _content.Span.Slice(_position, buffer.Length).CopyTo(buffer); + _position += buffer.Length; + return buffer.Length; + } + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateBufferArguments(buffer, offset, count); + EnsureNotClosed(); + return cancellationToken.IsCancellationRequested ? + Task.FromCanceled(cancellationToken) : + Task.FromResult(ReadBuffer(new Span(buffer, offset, count))); + } + +#if DotNetCore + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default(CancellationToken)) + { + EnsureNotClosed(); + return new ValueTask(ReadBuffer(buffer.Span)); + } +#endif + +#if DotNetCore + public override void CopyTo(Stream destination, int bufferSize) + { + EnsureNotClosed(); + if (_content.Length > _position) + { + destination.Write(_content.Span.Slice(_position)); + _position = _content.Length; + } + } + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + EnsureNotClosed(); + if (_content.Length > _position) + { + ReadOnlyMemory content = _content.Slice(_position); + _position = _content.Length; + return destination.WriteAsync(content, cancellationToken).AsTask(); + } + else + { + return Task.CompletedTask; + } + } +#endif + + public override void Flush() { } + + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + protected override void Dispose(bool disposing) + { + _isOpen = false; + _content = default; + base.Dispose(disposing); + } + + private static new void ValidateBufferArguments(byte[] buffer, int offset, int count) + { + if (buffer is null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), "offset < 0"); + } + + if ((uint)count > buffer.Length - offset) + { + throw new ArgumentOutOfRangeException(nameof(count), "count + offset > buffer.Length"); + } + } + } +} diff --git a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs index 1474120baa..59fa0ce5b7 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs @@ -116,108 +116,121 @@ public string SerializeViewModel(IDotvvmRequestContext context, object? commandR bool IsPostBack(IDotvvmRequestContext c) => c.RequestType is DotvvmRequestType.Command or DotvvmRequestType.StaticCommand; + (int vmStart, int vmEnd) WriteViewModelJson(Utf8JsonWriter writer, IDotvvmRequestContext context, DotvvmSerializationState state) + { + var converter = this.viewModelConverter.GetConverterCached(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); + + writer.Flush(); + var vmEnd = (int)writer.BytesCommitted; + + // persist CSRF token + if (context.CsrfToken is object) + writer.WriteString("$csrfToken"u8, context.CsrfToken); + + // persist encrypted values + if (state.WriteEncryptedValues is not null && + state.WriteEncryptedValues.ToSpan() is not [] and not [(byte)'{', (byte)'}']) + writer.WriteBase64String("$encryptedValues"u8, viewModelProtector.Protect(state.WriteEncryptedValues.ToArray(), context)); + + writer.WriteEndObject(); + + return (vmStart, vmEnd); + } + + private string? StoreViewModelCache(IDotvvmRequestContext context, MemoryStream buffer, (int, int) viewModelBodyPosition) + { + if (!context.Configuration.ExperimentalFeatures.ServerSideViewModelCache.IsEnabledForRoute(context.Route?.RouteName)) + return null; + + var vmBody = buffer.ToMemory()[viewModelBodyPosition.Item1..viewModelBodyPosition.Item2]; + + return viewModelServerCache.StoreViewModel(context, new ReadOnlyMemoryStream(vmBody)); + } + /// /// Builds the view model for the client. /// public MemoryStream BuildViewModel(IDotvvmRequestContext context, object? commandResult, IEnumerable<(string name, string html)>? postbackUpdatedControls = null, bool serializeNewResources = false) { var timer = ValueStopwatch.StartNew(); - // serialize the ViewModel - var vmCoreBuffer = new MemoryStream(); - - using var state = DotvvmSerializationState.Create(context.IsPostBack, context.Services); - try - { - using var jsonWriter = new Utf8JsonWriter(vmCoreBuffer, new JsonWriterOptions { Indented = this.ViewModelJsonOptions.WriteIndented, Encoder = ViewModelJsonOptions.Encoder }); - jsonWriter.WriteStartArray(); // Hack increase indent to align with the rest of the JSON - JsonSerializer.Serialize(jsonWriter, context.ViewModel, context.ViewModel!.GetType(), ViewModelJsonOptions); - jsonWriter.WriteEndArray(); - } - catch (Exception ex) - { - var failurePath = SystemTextJsonUtils.GetFailurePath(vmCoreBuffer.ToSpan()); - throw new SerializationException(true, context.ViewModel!.GetType(), string.Join("/", failurePath), ex); - } - vmCoreBuffer.Position = 0; - string? viewModelCacheId = null; - if (context.Configuration.ExperimentalFeatures.ServerSideViewModelCache.IsEnabledForRoute(context.Route?.RouteName)) - { - viewModelCacheId = viewModelServerCache.StoreViewModel(context, vmCoreBuffer); - vmCoreBuffer.Position = 0; - } + (int, int) viewModelBodyPosition; - var sentJsonBuffer = new MemoryStream(); - using (var sentJsonWriter = new Utf8JsonWriter(sentJsonBuffer, new JsonWriterOptions { + var buffer = new MemoryStream(); + using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions { Indented = this.ViewModelJsonOptions.WriteIndented, Encoder = ViewModelJsonOptions.Encoder, - SkipValidation = true, // for the hack with WriteRawValue + //SkipValidation = true, // for the hack with WriteRawValue })) { - sentJsonWriter.WriteStartObject(); - sentJsonWriter.WritePropertyName("viewModel"u8); - sentJsonWriter.WriteStartObject(); - var coreViewModelHack = TrimJsonObject(vmCoreBuffer.ToSpan()); - if (coreViewModelHack.Length > 0) - sentJsonWriter.WriteRawValue(coreViewModelHack, skipInputValidation: true); // TODO: get rid of this by moving $csrfToken elsewhere? - // persist CSRF token - if (context.CsrfToken is object) - sentJsonWriter.WriteString("$csrfToken"u8, context.CsrfToken); - - // persist encrypted values - if (state.WriteEncryptedValues is not null && - state.WriteEncryptedValues.ToSpan() is not [] and not [(byte)'{', (byte)'}']) - sentJsonWriter.WriteBase64String("$encryptedValues"u8, viewModelProtector.Protect(state.WriteEncryptedValues.ToArray(), context)); - - sentJsonWriter.WriteEndObject(); - - if (viewModelCacheId != null) + using var state = DotvvmSerializationState.Create(context.IsPostBack, context.Services); + writer.WriteStartObject(); + + writer.WritePropertyName("viewModel"u8); + try { - sentJsonWriter.WriteString("viewModelCacheId"u8, viewModelCacheId); + viewModelBodyPosition = WriteViewModelJson(writer, context, state); } - sentJsonWriter.WriteString("url"u8, context.HttpContext?.Request?.Url?.PathAndQuery); - sentJsonWriter.WriteString("virtualDirectory"u8, context.HttpContext?.Request?.PathBase?.Value?.Trim('/') ?? ""); + catch (Exception ex) + { + writer.Flush(); + var failurePath = SystemTextJsonUtils.GetFailurePath(buffer.ToSpan()); + throw new SerializationException(true, context.ViewModel!.GetType(), string.Join("/", failurePath), ex); + } + + if (StoreViewModelCache(context, buffer, viewModelBodyPosition) is {} viewModelCacheId) + { + writer.WriteString("viewModelCacheId"u8, viewModelCacheId); + } + writer.WriteString("url"u8, context.HttpContext?.Request?.Url?.PathAndQuery); + writer.WriteString("virtualDirectory"u8, context.HttpContext?.Request?.PathBase?.Value?.Trim('/') ?? ""); if (context.ResultIdFragment != null) { - sentJsonWriter.WriteString("resultIdFragment"u8, context.ResultIdFragment); + writer.WriteString("resultIdFragment"u8, context.ResultIdFragment); } if (context.RequestType is DotvvmRequestType.Command or DotvvmRequestType.SpaNavigate) { - sentJsonWriter.WriteString("action"u8, "successfulCommand"u8); + writer.WriteString("action"u8, "successfulCommand"u8); } else { - sentJsonWriter.WriteStartArray("renderedResources"u8); + writer.WriteStartArray("renderedResources"u8); foreach (var resource in context.ResourceManager.GetNamedResourcesInOrder()) - sentJsonWriter.WriteStringValue(resource.Name); - sentJsonWriter.WriteEndArray(); + writer.WriteStringValue(resource.Name); + writer.WriteEndArray(); } if (commandResult != null) { - sentJsonWriter.WritePropertyName("commandResult"u8); - WriteCommandData(commandResult, sentJsonWriter, sentJsonBuffer); + writer.WritePropertyName("commandResult"u8); + WriteCommandData(commandResult, writer, buffer); } - AddCustomPropertiesIfAny(context, sentJsonWriter, sentJsonBuffer); + AddCustomPropertiesIfAny(context, writer, buffer); if (postbackUpdatedControls is not null) { - AddPostBackUpdatedControls(context, sentJsonWriter, postbackUpdatedControls); + AddPostBackUpdatedControls(context, writer, postbackUpdatedControls); } if (serializeNewResources) { - AddNewResources(context, sentJsonWriter); + AddNewResources(context, writer); } - SerializeTypeMetadata(context, sentJsonWriter, state.UsedSerializationMaps); - sentJsonWriter.WriteEndObject(); + SerializeTypeMetadata(context, writer, state.UsedSerializationMaps); + writer.WriteEndObject(); } DotvvmMetrics.ViewModelSerializationTime.Record(timer.ElapsedSeconds, context.RouteLabel(), context.RequestTypeLabel()); - return sentJsonBuffer; + return buffer; } static ReadOnlySpan TrimJsonObject(ReadOnlySpan json) @@ -399,6 +412,7 @@ public void PopulateViewModel(IDotvvmRequestContext context, ReadOnlyMemory? cachedViewModel = null; if (root.GetPropertyOrNull("viewModelCacheId"u8)?.GetString() is {} viewModelCacheId) { if (!context.Configuration.ExperimentalFeatures.ServerSideViewModelCache.IsEnabledForRoute(context.Route?.RouteName)) @@ -406,7 +420,8 @@ public void PopulateViewModel(IDotvvmRequestContext context, ReadOnlyMemory TryRestoreViewModel(IDotvvmRequestContext context, string viewModelCacheId, JsonElement viewModelDiffToken) { var cachedData = viewModelStore.Retrieve(viewModelCacheId); var routeLabel = new KeyValuePair("route", context.Route!.RouteName); @@ -42,31 +43,53 @@ public JsonElement TryRestoreViewModel(IDotvvmRequestContext context, string vie DotvvmMetrics.ViewModelCacheHit.Add(1, routeLabel); DotvvmMetrics.ViewModelCacheBytesLoaded.Add(cachedData.Length, routeLabel); - var result = UnpackViewModel(cachedData); - var resultJson = JsonNode.Parse(result)!.AsObject(); - // TODO: this is just bad - JsonUtils.Patch(resultJson, JsonObject.Create(viewModelDiffToken)!); - var jsonData = new MemoryStream(); - using (var writer = new Utf8JsonWriter(jsonData)) + var unpacked = UnpackViewModel(cachedData); + var unpackedBuffer = ArrayPool.Shared.Rent(unpacked.length + 2); + try { - resultJson.WriteTo(writer); + var copiedLength = unpacked.data.CopyTo(unpackedBuffer, 1); + if (copiedLength != unpacked.length) + throw new Exception($"DefaultViewModelServerCache.PackViewModel returned incorrect length"); + unpackedBuffer[0] = (byte)'{'; + unpackedBuffer[unpacked.length + 1] = (byte)'}'; + + var resultJson = JsonNode.Parse(unpackedBuffer.AsSpan()[..(unpacked.length + 2)])!.AsObject(); + // TODO: this is just bad + JsonUtils.Patch(resultJson, JsonObject.Create(viewModelDiffToken)!); + var jsonData = new MemoryStream(); + using (var writer = new Utf8JsonWriter(jsonData)) + { + resultJson.WriteTo(writer); + } + return jsonData.ToMemory(); + } + finally + { + ArrayPool.Shared.Return(unpackedBuffer); } - return JsonDocument.Parse(jsonData.ToMemory()).RootElement; } protected virtual byte[] PackViewModel(Stream data) { var output = new MemoryStream(); - using (var compressed = new System.IO.Compression.DeflateStream(output, System.IO.Compression.CompressionLevel.Fastest)) + using (var compressed = new System.IO.Compression.DeflateStream(output, System.IO.Compression.CompressionLevel.Fastest, leaveOpen: true)) { data.CopyTo(compressed); } + output.Write(BitConverter.GetBytes((int)data.Position), 0, 4); // 4 bytes uncompressed length at the end + return output.ToArray(); } - protected virtual Stream UnpackViewModel(byte[] cachedData) + protected virtual (Stream data, int length) UnpackViewModel(byte[] cachedData) { - return new DeflateStream(new MemoryStream(cachedData), CompressionMode.Decompress); + var inflate = new DeflateStream(new ReadOnlyMemoryStream(cachedData.AsMemory()[..^4]), CompressionMode.Decompress); + var length = BitConverter.ToInt32(cachedData.AsSpan()[^4..] +#if !DotNetCore + .ToArray(), 0 +#endif + ); + return (inflate, length); } } } diff --git a/src/Framework/Framework/ViewModel/Serialization/IViewModelServerCache.cs b/src/Framework/Framework/ViewModel/Serialization/IViewModelServerCache.cs index 5cd378a9d4..3b2423de40 100644 --- a/src/Framework/Framework/ViewModel/Serialization/IViewModelServerCache.cs +++ b/src/Framework/Framework/ViewModel/Serialization/IViewModelServerCache.cs @@ -14,7 +14,7 @@ public interface IViewModelServerCache string StoreViewModel(IDotvvmRequestContext context, Stream data); - JsonElement TryRestoreViewModel(IDotvvmRequestContext context, string viewModelCacheId, JsonElement viewModelDiffToken); + ReadOnlyMemory TryRestoreViewModel(IDotvvmRequestContext context, string viewModelCacheId, JsonElement viewModelDiffToken); } } diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs index e97e995d21..e03d35b8a0 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs @@ -58,7 +58,7 @@ 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); + public void WriteUntyped(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, DotvvmSerializationState state, bool requireTypeField = true, bool wrapObject = true); } public class VMConverter(ViewModelJsonConverter factory): JsonConverter, IVMConverter { @@ -115,7 +115,7 @@ static void ReadEndObject(ref Utf8JsonReader reader) public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => this.Write(writer, value, options, DotvvmSerializationState.Current!); - public void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options, DotvvmSerializationState state, bool requireTypeField = true) + public void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options, DotvvmSerializationState state, bool requireTypeField = true, bool wrapObject = true) { if (state is null) throw new ArgumentNullException(nameof(state), "DotvvmSerializationState must be created before calling the ViewModelJsonConverter."); @@ -127,9 +127,24 @@ public void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options, var evSuppressLevel = state.EVWriter!.SuppressedLevel; try { - state.UsedSerializationMaps.Add(SerializationMap); + if (requireTypeField) + { + // $type not required -> serialization map is already known from the parent + state.UsedSerializationMaps.Add(SerializationMap); + } + if (wrapObject) + { + writer.WriteStartObject(); + } + state.EVWriter.Nest(); SerializationMap.WriterFactory.Invoke(writer, value, options, requireTypeField, state.EVWriter, state); + + if (wrapObject) + { + writer.WriteEndObject(); + } + state.EVWriter.End(); } finally { @@ -182,8 +197,8 @@ public void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options, 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); - public void WriteUntyped(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, DotvvmSerializationState state) => - this.Write(writer, (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 e0ef658f97..57f0948ada 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs @@ -393,12 +393,6 @@ public WriterDelegate CreateWriterFactory() var isPostback = Property(dotvvmState, "IsPostback"); // curly brackets are used for variables and methods from the scope of this factory method - // writer.WriteStartObject(); - block.Add(Call(writer, nameof(Utf8JsonWriter.WriteStartObject), Type.EmptyTypes)); - - // encryptedValuesWriter.Nest(); - block.Add(Call(encryptedValuesWriter, nameof(EncryptedValuesWriter.Nest), Type.EmptyTypes)); - // if (requireTypeField) // writer.WriteString("$type", "{Type}"); block.Add(IfThen(requireTypeField, @@ -503,12 +497,6 @@ public WriterDelegate CreateWriterFactory() block.Add(Expression.Label(endPropertyLabel)); } - // writer.WriteEndObject(); - block.Add(ExpressionUtils.Replace(w => w.WriteEndObject(), writer)); - - // encryptedValuesWriter.End(); - block.Add(Expression.Call(encryptedValuesWriter, nameof(EncryptedValuesWriter.End), Type.EmptyTypes)); - // compile the expression var ex = Lambda>( Block(new[] { value }, block).OptimizeConstants(), writer, value, jsonOptions, requireTypeField, encryptedValuesWriter, dotvvmState); @@ -576,7 +564,7 @@ private Expression CallPropertyConverterWrite(JsonConverter converter, Expressio // void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options, DotvvmSerializationState state) if (converter is ViewModelJsonConverter.IVMConverter) { - return Call(Constant(converter), nameof(ViewModelJsonConverter.VMConverter.Write), Type.EmptyTypes, writer, value, jsonOptions, dotvvmState, Expression.Constant(true)); + return Call(Constant(converter), nameof(ViewModelJsonConverter.VMConverter.Write), Type.EmptyTypes, writer, value, jsonOptions, dotvvmState, Constant(true), Constant(true)); } else { diff --git a/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html b/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html index 2ddf27b00e..032e690caf 100644 --- a/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html +++ b/src/Tests/ControlTests/testoutputs/ViewModulesServerSideTests.IncludeViewModule.html @@ -22,6 +22,7 @@ From ef5a75904d50de0934555e304bae55d4a980f03c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Herceg?= Date: Fri, 22 Mar 2024 14:34:23 +0100 Subject: [PATCH 06/20] Fixed OWIN sample dependencies and binding redirects --- src/Samples/Api.Owin/Web.config | 14 ++++++++++++- .../ApplicationInsights.Owin/Web.config | 16 +++++++++++++-- src/Samples/MiniProfiler.Owin/Web.config | 12 +++++++++++ .../DotVVM.Samples.BasicSamples.Owin.csproj | 2 ++ src/Samples/Owin/DotvvmServiceConfigurator.cs | 5 +++++ src/Samples/Owin/Web.config | 20 ++++++++++++++++++- 6 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/Samples/Api.Owin/Web.config b/src/Samples/Api.Owin/Web.config index f00ebb64ea..ee6826f27e 100644 --- a/src/Samples/Api.Owin/Web.config +++ b/src/Samples/Api.Owin/Web.config @@ -66,6 +66,18 @@ + + + + + + + + + + + + @@ -73,4 +85,4 @@ - + \ No newline at end of file diff --git a/src/Samples/ApplicationInsights.Owin/Web.config b/src/Samples/ApplicationInsights.Owin/Web.config index 6063647d33..13319db1d1 100644 --- a/src/Samples/ApplicationInsights.Owin/Web.config +++ b/src/Samples/ApplicationInsights.Owin/Web.config @@ -48,7 +48,7 @@ - + @@ -105,5 +105,17 @@ + + + + + + + + + + + + - + \ No newline at end of file diff --git a/src/Samples/MiniProfiler.Owin/Web.config b/src/Samples/MiniProfiler.Owin/Web.config index 40cc6cbbf4..7ecb3c4b1e 100644 --- a/src/Samples/MiniProfiler.Owin/Web.config +++ b/src/Samples/MiniProfiler.Owin/Web.config @@ -77,5 +77,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj b/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj index 6bdf08e033..6fe4fce94e 100644 --- a/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj +++ b/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj @@ -28,6 +28,8 @@ + + diff --git a/src/Samples/Owin/DotvvmServiceConfigurator.cs b/src/Samples/Owin/DotvvmServiceConfigurator.cs index eef93199fe..e99cb7c040 100644 --- a/src/Samples/Owin/DotvvmServiceConfigurator.cs +++ b/src/Samples/Owin/DotvvmServiceConfigurator.cs @@ -1,6 +1,8 @@ using DotVVM.Framework.Configuration; using DotVVM.Samples.Common; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using ILoggerFactory = Microsoft.Extensions.Logging.ILoggerFactory; namespace DotVVM.Samples.BasicSamples { @@ -11,6 +13,9 @@ public void ConfigureServices(IDotvvmServiceCollection services) CommonConfiguration.ConfigureServices(services); services.AddDefaultTempStorages("Temp"); services.AddHotReload(); + + services.Services.AddSingleton(_ => LoggerFactory.Create(c => c.AddConsole())); + services.Services.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); } } } diff --git a/src/Samples/Owin/Web.config b/src/Samples/Owin/Web.config index b5f4f6e328..d6f8287db8 100644 --- a/src/Samples/Owin/Web.config +++ b/src/Samples/Owin/Web.config @@ -53,5 +53,23 @@ + + + + + + + + + + + + + + + + + + - + \ No newline at end of file From 168af467469ffb8eace222be5c6dc403890f15dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Mon, 15 Apr 2024 20:12:38 +0200 Subject: [PATCH 07/20] STJ migration: remove Newtonsoft, rewrite static command plans --- src/Framework/Core/DotVVM.Core.csproj | 1 - .../StaticCommandExecutionPlanSerializer.cs | 145 ++++++++++-------- .../Framework/DotVVM.Framework.csproj | 1 - .../Framework/Hosting/DotvvmPresenter.cs | 2 +- .../Hosting/StaticCommandExecutor.cs | 7 +- .../JQueryGlobalizeScriptCreator.cs | 1 + .../Framework/Utils/SystemTextJsonUtils.cs | 27 +++- .../SerialiationMapperAttributeHelper.cs | 1 - ...VM.Samples.ApplicationInsights.Owin.csproj | 2 - .../DotVVM.Samples.MiniProfiler.Owin.csproj | 2 - .../StaticCommandPlanSerializationTests.cs | 31 ++-- ...ts.MarkupControl_PassingStaticCommand.html | 4 +- 12 files changed, 137 insertions(+), 87 deletions(-) diff --git a/src/Framework/Core/DotVVM.Core.csproj b/src/Framework/Core/DotVVM.Core.csproj index 738f8d37ed..9d1f019448 100644 --- a/src/Framework/Core/DotVVM.Core.csproj +++ b/src/Framework/Core/DotVVM.Core.csproj @@ -16,7 +16,6 @@ - diff --git a/src/Framework/Framework/Compilation/Binding/StaticCommandExecutionPlanSerializer.cs b/src/Framework/Framework/Compilation/Binding/StaticCommandExecutionPlanSerializer.cs index 1ad70fa819..3e7e09b440 100644 --- a/src/Framework/Framework/Compilation/Binding/StaticCommandExecutionPlanSerializer.cs +++ b/src/Framework/Framework/Compilation/Binding/StaticCommandExecutionPlanSerializer.cs @@ -1,27 +1,30 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; using DotVVM.Framework.Security; using DotVVM.Framework.Utils; using DotVVM.Framework.ViewModel; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace DotVVM.Framework.Compilation.Binding { public static class StaticCommandExecutionPlanSerializer { - public static JToken SerializePlan(StaticCommandInvocationPlan plan) + public static JsonNode SerializePlan(StaticCommandInvocationPlan plan) { var hasOverloads = HasOverloads(plan.Method); - var array = new JArray( - new JValue(GetTypeFullName(plan.Method.DeclaringType!)), - new JValue(plan.Method.Name), - new JArray(plan.Method.GetGenericArguments().Select(GetTypeFullName)), - new JValue(plan.Method.GetParameters().Length), - hasOverloads ? new JArray(plan.Method.GetParameters().Select(p => GetTypeFullName(p.ParameterType))) : JValue.CreateNull(), - JToken.FromObject(plan.Arguments.Select(a => (byte)a.Type).ToArray()) + var array = new JsonArray( + JsonValue.Create(GetTypeFullName(plan.Method.DeclaringType!)), + JsonValue.Create(plan.Method.Name), + new JsonArray(plan.Method.GetGenericArguments().Select(t => JsonValue.Create(GetTypeFullName(t))).ToArray()), + JsonValue.Create(plan.Method.GetParameters().Length), + hasOverloads + ? new JsonArray(plan.Method.GetParameters().Select(p => JsonValue.Create(GetTypeFullName(p.ParameterType))).ToArray()) + : null, + JsonValue.Create(Convert.ToBase64String(plan.Arguments.Select(a => (byte)a.Type).ToArray())) ); var parameters = (new ParameterInfo[plan.Method.IsStatic ? 0 : 1]).Concat(plan.Method.GetParameters()).ToArray(); foreach (var (arg, parameter) in plan.Arguments.Zip(parameters, (a, b) => (a, b))) @@ -29,24 +32,24 @@ public static JToken SerializePlan(StaticCommandInvocationPlan plan) if (arg.Type == StaticCommandParameterType.Argument) { if ((parameter?.ParameterType ?? plan.Method.DeclaringType!).Equals(arg.Arg)) - array.Add(JValue.CreateNull()); + array.Add(null); else - array.Add(new JValue(arg.Arg!.CastTo().Apply(GetTypeFullName))); + array.Add(JsonValue.Create(GetTypeFullName((Type)arg.Arg!))); } else if (arg.Type == StaticCommandParameterType.Constant) { - array.Add(arg.Arg is null ? JValue.CreateNull() : JToken.FromObject(arg.Arg)); + array.Add(arg.Arg is null ? null : JsonSerializer.SerializeToNode(arg.Arg, parameter.ParameterType)); } else if (arg.Type == StaticCommandParameterType.DefaultValue) { - array.Add(JValue.CreateNull()); + array.Add(null); } else if (arg.Type == StaticCommandParameterType.Inject) { if ((parameter?.ParameterType ?? plan.Method.DeclaringType!).Equals(arg.Arg)) - array.Add(JValue.CreateNull()); + array.Add(null); else - array.Add(new JValue(arg.Arg!.CastTo().Apply(GetTypeFullName))); + array.Add(JsonValue.Create(GetTypeFullName((Type)arg.Arg!))); } else if (arg.Type == StaticCommandParameterType.Invocation) { @@ -54,8 +57,6 @@ public static JToken SerializePlan(StaticCommandInvocationPlan plan) } else throw new NotSupportedException(arg.Type.ToString()); } - while (array.Last!.Type == JTokenType.Null) - array.Last.Remove(); return array; } @@ -66,10 +67,10 @@ private static bool HasOverloads(MethodInfo method) private static string GetTypeFullName(Type type) => $"{type.FullName}, {type.Assembly.GetName().Name}"; - public static byte[] EncryptJson(JToken json, IViewModelProtector protector) + public static byte[] EncryptJson(JsonNode json, IViewModelProtector protector) { var stream = new MemoryStream(); - using (var writer = new JsonTextWriter(new StreamWriter(stream))) + using (var writer = new Utf8JsonWriter(stream)) { json.WriteTo(writer); } @@ -79,30 +80,55 @@ public static string[] GetEncryptionPurposes() { return [ "StaticCommand" ]; } - public static StaticCommandInvocationPlan DeserializePlan(JToken planInJson) + + static string?[]? ReadStringArray(ref Utf8JsonReader json) + { + if (json.TokenType == JsonTokenType.Null) + { + json.AssertRead(); + return null; + } + + json.AssertRead(JsonTokenType.StartArray); + var result = new List(); + while (true) + { + if (json.TokenType == JsonTokenType.EndArray) + { + json.AssertRead(); + return result.ToArray(); + } + result.Add(json.ReadString()); + } + } + public static StaticCommandInvocationPlan DeserializePlan(ref Utf8JsonReader json) { - var jarray = (JArray)planInJson; - if (jarray.Count < 6) - throw new NotSupportedException("Invalid static command plan."); - var typeName = jarray[0]!.Value()!; - var methodName = jarray[1]!.Value()!; - var genericTypeNames = jarray[2]!.Value(); - var parametersCount = jarray[3]!.Value(); - var parameterTypeNames = jarray[4]!.Value(); + if (json.TokenType == JsonTokenType.None) + { + json.AssertRead(); + } + json.AssertRead(JsonTokenType.StartArray); + var declaringType = Type.GetType(json.ReadString().NotNull()); + var methodName = json.ReadString().NotNull(); + var genericTypeNames = ReadStringArray(ref json); + var parametersCount = json.GetInt32(); + json.AssertRead(); + var parameterTypeNames = ReadStringArray(ref json); var hasOtherOverloads = parameterTypeNames != null; - var argTypes = jarray[5]!.ToObject()!.Select(a => (StaticCommandParameterType)a).ToArray(); + var argTypes = json.GetBytesFromBase64(); + json.AssertRead(); MethodInfo? method; if (hasOtherOverloads) { // There are multiple overloads available, therefore exact parameters need to be resolved first - var parameters = parameterTypeNames!.Select(n => Type.GetType(n.Value()!).NotNull()).ToArray(); - method = Type.GetType(typeName)?.GetMethod(methodName, parameters); + var parameters = parameterTypeNames!.Select(n => Type.GetType(n.NotNull()).NotNull()).ToArray(); + method = declaringType?.GetMethod(methodName, parameters); } else { // There are no overloads - method = Type.GetType(typeName)?.GetMethods().SingleOrDefault(m => m.Name == methodName && m.GetParameters().Length == parametersCount); + method = declaringType?.GetMethods().SingleOrDefault(m => m.Name == methodName && m.GetParameters().Length == parametersCount); } if (method == null || !method.IsDefined(typeof(AllowStaticCommandAttribute))) @@ -110,42 +136,41 @@ public static StaticCommandInvocationPlan DeserializePlan(JToken planInJson) if (method.IsGenericMethod) { - var generics = genericTypeNames.NotNull().Select(n => Type.GetType(n.Value()!).NotNull()).ToArray(); + var generics = genericTypeNames.NotNull().Select(n => Type.GetType(n.NotNull()).NotNull()).ToArray(); method = method.MakeGenericMethod(generics); } var methodParameters = method.GetParameters(); - var args = argTypes - .Select((a, i) => (type: a, arg: jarray.Count <= i + 6 ? JValue.CreateNull() : jarray[i + 6], parameter: (method.IsStatic ? methodParameters[i] : (i == 0 ? null : methodParameters[i - 1])))) - .Select((a) => { - switch (a.type) - { - case StaticCommandParameterType.Argument: - case StaticCommandParameterType.Inject: - if (a.arg.Type == JTokenType.Null) - return new StaticCommandParameterPlan(a.type, a.parameter?.ParameterType ?? method.DeclaringType.NotNull()); - else - return new StaticCommandParameterPlan(a.type, a.arg.Value()!.Apply(Type.GetType)); - case StaticCommandParameterType.Constant: - return new StaticCommandParameterPlan(a.type, a.arg.ToObject(a.parameter?.ParameterType ?? method.DeclaringType.NotNull())); - case StaticCommandParameterType.DefaultValue: - return new StaticCommandParameterPlan(a.type, a.parameter?.DefaultValue); - case StaticCommandParameterType.Invocation: - return new StaticCommandParameterPlan(a.type, DeserializePlan(a.arg)); - default: - throw new NotSupportedException($"{a.type}"); - } - }).ToArray(); + if (!method.IsStatic) + { + methodParameters = [ null, ..methodParameters ]; + } + var args = new StaticCommandParameterPlan[methodParameters.Length]; + for (var i = 0; i < args.Length; i++) + { + var type = (StaticCommandParameterType)argTypes[i]; + var paramType = methodParameters[i]?.ParameterType ?? method.DeclaringType.NotNull(); + args[i] = type switch + { + StaticCommandParameterType.Argument or StaticCommandParameterType.Inject => + new StaticCommandParameterPlan(type, json.TokenType == JsonTokenType.Null ? paramType : Type.GetType(json.GetString())), + StaticCommandParameterType.Constant => + new StaticCommandParameterPlan(type, JsonSerializer.Deserialize(ref json, paramType)), + StaticCommandParameterType.DefaultValue => + new StaticCommandParameterPlan(type, methodParameters[i].DefaultValue), + StaticCommandParameterType.Invocation => + new StaticCommandParameterPlan(type, DeserializePlan(ref json)), + _ => throw new NotSupportedException(type.ToString()) + }; + json.AssertRead(); + } return new StaticCommandInvocationPlan(method, args); } - public static JToken DecryptJson(byte[] data, IViewModelProtector protector) + public static byte[] DecryptJson(byte[] data, IViewModelProtector protector) { - using (var reader = new JsonTextReader(new StreamReader(new MemoryStream(protector.Unprotect(data, GetEncryptionPurposes()))))) - { - return JToken.ReadFrom(reader); - } + return protector.Unprotect(data, GetEncryptionPurposes()); } } } diff --git a/src/Framework/Framework/DotVVM.Framework.csproj b/src/Framework/Framework/DotVVM.Framework.csproj index f7e45e989d..1388c874dd 100644 --- a/src/Framework/Framework/DotVVM.Framework.csproj +++ b/src/Framework/Framework/DotVVM.Framework.csproj @@ -47,7 +47,6 @@ - diff --git a/src/Framework/Framework/Hosting/DotvvmPresenter.cs b/src/Framework/Framework/Hosting/DotvvmPresenter.cs index 3039de8d07..9199baf18f 100644 --- a/src/Framework/Framework/Hosting/DotvvmPresenter.cs +++ b/src/Framework/Framework/Hosting/DotvvmPresenter.cs @@ -350,7 +350,7 @@ public async Task ProcessStaticCommandRequest(IDotvvmRequestContext context) var knownTypes = postData.RootElement.GetPropertyOrNull("knownTypeMetadata"u8)?.EnumerateStringArray().WhereNotNull().ToArray() ?? []; var argumentPaths = postData.RootElement.GetPropertyOrNull("argumentPaths"u8)?.EnumerateStringArray().ToArray(); - var command = postData.RootElement.GetProperty("command"u8).GetString().NotNull("command is required"); + var command = postData.RootElement.GetProperty("command"u8).GetBytesFromBase64(); var arguments = postData.RootElement.GetProperty("args"u8); var executionPlan = StaticCommandExecutor.DecryptPlan(command); diff --git a/src/Framework/Framework/Hosting/StaticCommandExecutor.cs b/src/Framework/Framework/Hosting/StaticCommandExecutor.cs index a3c38c2152..8ae0d4d90b 100644 --- a/src/Framework/Framework/Hosting/StaticCommandExecutor.cs +++ b/src/Framework/Framework/Hosting/StaticCommandExecutor.cs @@ -42,10 +42,11 @@ public StaticCommandExecutor(IStaticCommandServiceLoader serviceLoader, IViewMod } #pragma warning restore CS0618 - public StaticCommandInvocationPlan DecryptPlan(string encrypted) + public StaticCommandInvocationPlan DecryptPlan(byte[] encrypted) { - var decrypted = StaticCommandExecutionPlanSerializer.DecryptJson(Convert.FromBase64String(encrypted), viewModelProtector); - return StaticCommandExecutionPlanSerializer.DeserializePlan(decrypted); + var decrypted = StaticCommandExecutionPlanSerializer.DecryptJson(encrypted, viewModelProtector); + var reader = new Utf8JsonReader(decrypted.AsSpan()); + return StaticCommandExecutionPlanSerializer.DeserializePlan(ref reader); } public Task Execute( StaticCommandInvocationPlan plan, diff --git a/src/Framework/Framework/ResourceManagement/ClientGlobalize/JQueryGlobalizeScriptCreator.cs b/src/Framework/Framework/ResourceManagement/ClientGlobalize/JQueryGlobalizeScriptCreator.cs index 2249687264..18dfe5d5ed 100644 --- a/src/Framework/Framework/ResourceManagement/ClientGlobalize/JQueryGlobalizeScriptCreator.cs +++ b/src/Framework/Framework/ResourceManagement/ClientGlobalize/JQueryGlobalizeScriptCreator.cs @@ -165,6 +165,7 @@ private static JsonObject CreateDateInfoJson(DateTimeFormatInfo di) eras = di.Calendar.Eras.Select(era => new { offset = 0, start = (string?)null, name = di.GetEraName(era) }).ToArray(), twoDigitYearMax = di.Calendar.TwoDigitYearMax, patterns = new JsonObject { + // must be JsonObject, otherwise we get "Members 'd' and 'D' on type '<>f__AnonymousType...' cannot both bind with parameter 'd' in the deserialization constructor." ["d"] = di.ShortDatePattern, ["D"] = di.LongDatePattern, ["t"] = di.ShortTimePattern, diff --git a/src/Framework/Framework/Utils/SystemTextJsonUtils.cs b/src/Framework/Framework/Utils/SystemTextJsonUtils.cs index dfaa4cf76c..378147080b 100644 --- a/src/Framework/Framework/Utils/SystemTextJsonUtils.cs +++ b/src/Framework/Framework/Utils/SystemTextJsonUtils.cs @@ -87,10 +87,33 @@ public static void AssertToken(this in Utf8JsonReader reader, JsonTokenType type public static void AssertRead(this ref Utf8JsonReader reader, JsonTokenType type) { AssertToken(in reader, type); + AssertRead(ref reader); + } + + public static void AssertRead(this ref Utf8JsonReader reader) + { if (!reader.Read()) - throw new JsonException($"Expected token of type {type}, but got {reader.TokenType}."); + throw new JsonException($"Expected end of stream."); } - + + public static string? ReadString(this ref Utf8JsonReader reader) + { + if (reader.TokenType is JsonTokenType.Null) + { + return null!; + } + else if (reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName) + { + var value = reader.GetString()!; + reader.AssertRead(); + return value; + } + else + { + throw new JsonException($"Expected string, but got {reader.TokenType}."); + } + } + public static int GetValueLength(this in Utf8JsonReader reader) { return reader.HasValueSequence ? checked((int)reader.ValueSequence.Length) : reader.ValueSpan.Length; diff --git a/src/Framework/Framework/ViewModel/Serialization/SerialiationMapperAttributeHelper.cs b/src/Framework/Framework/ViewModel/Serialization/SerialiationMapperAttributeHelper.cs index 98a160e59d..c6fb2c3324 100644 --- a/src/Framework/Framework/ViewModel/Serialization/SerialiationMapperAttributeHelper.cs +++ b/src/Framework/Framework/ViewModel/Serialization/SerialiationMapperAttributeHelper.cs @@ -1,7 +1,6 @@ using System; using System.Reflection; using STJ = System.Text.Json.Serialization; -using NJ = Newtonsoft.Json; namespace DotVVM.Framework.ViewModel.Serialization { diff --git a/src/Samples/ApplicationInsights.Owin/DotVVM.Samples.ApplicationInsights.Owin.csproj b/src/Samples/ApplicationInsights.Owin/DotVVM.Samples.ApplicationInsights.Owin.csproj index 7babe354c1..ade018c851 100644 --- a/src/Samples/ApplicationInsights.Owin/DotVVM.Samples.ApplicationInsights.Owin.csproj +++ b/src/Samples/ApplicationInsights.Owin/DotVVM.Samples.ApplicationInsights.Owin.csproj @@ -50,8 +50,6 @@ - - diff --git a/src/Samples/MiniProfiler.Owin/DotVVM.Samples.MiniProfiler.Owin.csproj b/src/Samples/MiniProfiler.Owin/DotVVM.Samples.MiniProfiler.Owin.csproj index 1b1dad4def..eb0454fe2c 100644 --- a/src/Samples/MiniProfiler.Owin/DotVVM.Samples.MiniProfiler.Owin.csproj +++ b/src/Samples/MiniProfiler.Owin/DotVVM.Samples.MiniProfiler.Owin.csproj @@ -41,8 +41,6 @@ - - diff --git a/src/Tests/Binding/StaticCommandPlanSerializationTests.cs b/src/Tests/Binding/StaticCommandPlanSerializationTests.cs index 3cc33bf0e6..9719e3cbb0 100644 --- a/src/Tests/Binding/StaticCommandPlanSerializationTests.cs +++ b/src/Tests/Binding/StaticCommandPlanSerializationTests.cs @@ -3,11 +3,12 @@ using System.Linq; using System.Linq.Expressions; using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; using DotVVM.Framework.Compilation.Binding; using DotVVM.Framework.ViewModel; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json.Linq; namespace DotVVM.Framework.Tests.Binding { @@ -34,13 +35,19 @@ private void AssertPlansAreIdentical(StaticCommandInvocationPlan original, Stati } } + private StaticCommandInvocationPlan Deserialize(JsonNode json) + { + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json.ToJsonString())); + return StaticCommandExecutionPlanSerializer.DeserializePlan(ref reader); + } + [TestMethod] public void StaticCommandPlanSerialization_MethodOverloads1_DeserializedPlanIsIdentical() { var plan = MakeInvocationPlan(() => StaticCommandMethodCollection.Method(123), new StaticCommandParameterPlan(StaticCommandParameterType.Constant, 123)); var json = StaticCommandExecutionPlanSerializer.SerializePlan(plan); - var deserializedPlan = StaticCommandExecutionPlanSerializer.DeserializePlan(json); + var deserializedPlan = Deserialize(json); AssertPlansAreIdentical(plan, deserializedPlan); } @@ -51,7 +58,7 @@ public void StaticCommandPlanSerialization_MethodOverloads2_DeserializedPlanIsId var plan = MakeInvocationPlan(() => StaticCommandMethodCollection.Method(123f), new StaticCommandParameterPlan(StaticCommandParameterType.Constant, 123f)); var json = StaticCommandExecutionPlanSerializer.SerializePlan(plan); - var deserializedPlan = StaticCommandExecutionPlanSerializer.DeserializePlan(json); + var deserializedPlan = Deserialize(json); AssertPlansAreIdentical(plan, deserializedPlan); } @@ -63,11 +70,11 @@ public void StaticCommandPlanSerialization_NotOverloadedMethod_DoNotTransferPara new StaticCommandParameterPlan(StaticCommandParameterType.Constant, 123)); var json = StaticCommandExecutionPlanSerializer.SerializePlan(plan); - var jarray = (JArray)json; + var jarray = (JsonArray)json; // Parameters count - Assert.AreEqual(1, jarray[3].Value()); + Assert.AreEqual(1, (int)jarray[3]); // No parameters info is sent because method name and arguments are enough to match correct method - Assert.AreEqual(JValue.CreateNull(), jarray[4]); + Assert.IsNull(jarray[4]); } [TestMethod] @@ -77,13 +84,13 @@ public void StaticCommandPlanSerialization_OverloadedMethod_TransferParameterTyp new StaticCommandParameterPlan(StaticCommandParameterType.Constant, 123)); var json = StaticCommandExecutionPlanSerializer.SerializePlan(plan); - var jarray = (JArray)json; + var jarray = (JsonArray)json; // Parameters count - Assert.AreEqual(1, jarray[3].Value()); + Assert.AreEqual(1, (int)jarray[3]); // Parameters info is sent because method has multiple overloads - Assert.AreNotEqual(JValue.CreateNull(), jarray[4]); - Assert.IsInstanceOfType(jarray[4], typeof(JArray)); - var parameterTypeName = jarray[4][0].Value(); + Assert.IsNotNull(jarray[4]); + Assert.IsInstanceOfType(jarray[4], typeof(JsonArray)); + var parameterTypeName = (string)jarray[4][0]; Assert.AreEqual(typeof(int), Type.GetType(parameterTypeName)); } @@ -93,7 +100,7 @@ public void StaticCommandPlanSerialization_NotUsableInStaticCommand_Throws() { var plan = MakeInvocationPlan(() => StaticCommandMethodCollection.MethodNotUsableInStaticCommand()); var json = StaticCommandExecutionPlanSerializer.SerializePlan(plan); - var deserializedPlan = StaticCommandExecutionPlanSerializer.DeserializePlan(json); + var deserializedPlan = Deserialize(json); } static class StaticCommandMethodCollection diff --git a/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_PassingStaticCommand.html b/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_PassingStaticCommand.html index 1f82d436d3..ffa39139b1 100644 --- a/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_PassingStaticCommand.html +++ b/src/Tests/ControlTests/testoutputs/MarkupControlTests.MarkupControl_PassingStaticCommand.html @@ -4,7 +4,7 @@
public bool AllowsDynamicDispatch(bool defaultValue) => _allowDynamicDispatch ?? defaultValue; diff --git a/src/Framework/Framework/Binding/Expressions/CommandBindingExpression.cs b/src/Framework/Framework/Binding/Expressions/CommandBindingExpression.cs index 6e5f4cc477..8d68009242 100644 --- a/src/Framework/Framework/Binding/Expressions/CommandBindingExpression.cs +++ b/src/Framework/Framework/Binding/Expressions/CommandBindingExpression.cs @@ -2,13 +2,8 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Linq.Expressions; -using System.Net; -using System.Reflection; using System.Threading.Tasks; using DotVVM.Framework.Binding.Properties; -using DotVVM.Framework.Compilation; -using DotVVM.Framework.Compilation.Binding; using DotVVM.Framework.Compilation.Javascript; using DotVVM.Framework.Compilation.Javascript.Ast; using DotVVM.Framework.Controls; diff --git a/src/Framework/Framework/Compilation/DotvvmViewCompilationServiceExtensions.cs b/src/Framework/Framework/Compilation/DotvvmViewCompilationServiceExtensions.cs index a3cbab3643..32147a7b11 100644 --- a/src/Framework/Framework/Compilation/DotvvmViewCompilationServiceExtensions.cs +++ b/src/Framework/Framework/Compilation/DotvvmViewCompilationServiceExtensions.cs @@ -11,9 +11,7 @@ public static class DotvvmViewCompilationServiceExtensions public static Task Precompile(this ViewCompilationConfiguration compilationConfiguration, DotvvmConfiguration config, IStartupTracer startupTracer) { return Task.Run(async () => { - var compilationService = config.ServiceProvider.GetService(); - if (compilationService is null) - return; + var compilationService = config.ServiceProvider.GetRequiredService(); if (compilationConfiguration.BackgroundCompilationDelay != null) { diff --git a/src/Framework/Framework/Compilation/Javascript/Ast/JsLiteral.cs b/src/Framework/Framework/Compilation/Javascript/Ast/JsLiteral.cs index d58495de28..18ac10a5a8 100644 --- a/src/Framework/Framework/Compilation/Javascript/Ast/JsLiteral.cs +++ b/src/Framework/Framework/Compilation/Javascript/Ast/JsLiteral.cs @@ -22,7 +22,14 @@ public sealed class JsLiteral : JsExpression /// public string LiteralValue { - get => JavascriptCompilationHelper.CompileConstant(Value, htmlSafe: false).Replace("<", "\\u003C"); + // this is a compile-time AST node, so the values should not be controlled by an adversary + // plus, the value often contains base-64 encoded data (command IDs) which is affected by + // System.Text.Json the HTML-safe encoder (they encode + sign) + // However, since the value may end up in a HTML comment, we manually escape < and > to be on the safe side + // (> is necessary to escape the comment, > just to be sure) + get => JavascriptCompilationHelper.CompileConstant(Value, htmlSafe: false) + .Replace("<", "\\u003C") + .Replace(">", "\\u003E"); set => Value = JsonDocument.Parse(value).RootElement; } diff --git a/src/Framework/Framework/Compilation/Javascript/JavascriptCompilationHelper.cs b/src/Framework/Framework/Compilation/Javascript/JavascriptCompilationHelper.cs index 70e23552e9..c514cb4646 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptCompilationHelper.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptCompilationHelper.cs @@ -1,12 +1,11 @@ using System; +using System.Globalization; using System.Linq; using System.Text.Json; -using System.Text.Json.Serialization; using DotVVM.Framework.Compilation.Javascript.Ast; using DotVVM.Framework.Configuration; using DotVVM.Framework.Controls; using DotVVM.Framework.Utils; -using DotVVM.Framework.ViewModel.Serialization; namespace DotVVM.Framework.Compilation.Javascript { @@ -17,8 +16,8 @@ public static class JavascriptCompilationHelper null => "null", true => "true", false => "false", + int i => i.ToString(CultureInfo.InvariantCulture).DotvvmInternString(trySystemIntern: false), string s => KnockoutHelper.MakeStringLiteral(s, htmlSafe), - int i => i.ToString(), _ => JsonSerializer.Serialize(obj, htmlSafe ? DefaultSerializerSettingsProvider.Instance.Settings : DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe) }; diff --git a/src/Framework/Framework/Compilation/NamespaceImport.cs b/src/Framework/Framework/Compilation/NamespaceImport.cs index 3cae311696..17a01c5e3b 100644 --- a/src/Framework/Framework/Compilation/NamespaceImport.cs +++ b/src/Framework/Framework/Compilation/NamespaceImport.cs @@ -8,12 +8,12 @@ namespace DotVVM.Framework.Compilation { - public struct NamespaceImport: IEquatable + public readonly struct NamespaceImport: IEquatable { [JsonPropertyName("namespace")] - public readonly string Namespace { get; } + public string Namespace { get; } [JsonPropertyName("alias")] - public readonly string? Alias { get; } + public string? Alias { get; } [JsonIgnore] public bool HasAlias => Alias is not null; diff --git a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs index cc9c526c87..282c6d5afe 100644 --- a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs +++ b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs @@ -25,7 +25,7 @@ public sealed class DefaultSerializerSettingsProvider // https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#prod-DoubleStringCharacter // - it says « SourceCharacter [=any Unicode code point] but not one of " or \ or LineTerminator » // ...which isn't allowed in JSON in any context anyway - internal readonly JavaScriptEncoder HtmlSafeLessParaoidEncoder; + internal readonly JavaScriptEncoder HtmlSafeLessParanoidEncoder; private JsonSerializerOptions CreateSettings() { @@ -42,7 +42,7 @@ private JsonSerializerOptions CreateSettings() new DotvvmCustomPrimitiveTypeConverter() }, NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, - Encoder = HtmlSafeLessParaoidEncoder, + Encoder = HtmlSafeLessParanoidEncoder, MaxDepth = defaultMaxSerializationDepth }; } @@ -63,15 +63,12 @@ private DefaultSerializerSettingsProvider() var encoderSettings = new TextEncoderSettings(); encoderSettings.AllowRange(UnicodeRanges.All); encoderSettings.ForbidCharacters('>', '<'); - HtmlSafeLessParaoidEncoder = JavaScriptEncoder.Create(encoderSettings); - // JsonConvert.DefaultSettings = () => new JsonSerializerSettings() { MaxDepth = defaultMaxSerializationDepth }; + HtmlSafeLessParanoidEncoder = JavaScriptEncoder.Create(encoderSettings); Settings = CreateSettings(); SettingsHtmlUnsafe = new JsonSerializerOptions(Settings) { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; } - - // public static JsonSerializer CreateJsonSerializer() => JsonSerializer.Serialize( } } diff --git a/src/Framework/Framework/Configuration/DotvvmConfigurationSerializationResolver.cs b/src/Framework/Framework/Configuration/DotvvmConfigurationSerializationResolver.cs index 6542de6fc1..6de5a7d630 100644 --- a/src/Framework/Framework/Configuration/DotvvmConfigurationSerializationResolver.cs +++ b/src/Framework/Framework/Configuration/DotvvmConfigurationSerializationResolver.cs @@ -53,12 +53,6 @@ public override JsonTypeInfo GetTypeInfo(System.Type type, JsonSerializerOptions !object.Equals(value, def) && // null (value is not IEnumerable valE || def is not IEnumerable defE || !valE.Cast().SequenceEqual(defE.Cast())); } - // else if (property.Name == "compiledViewsAssemblies" && info.Type == typeof(DotvvmConfiguration)) - // { - // property.ShouldSerialize = (obj, value) => - // originalCondition(obj, value) && - // (value is not IEnumerable c || !new [] { "CompiledViews.dll" }.SequenceEqual(c)); - // } else { property.ShouldSerialize = (obj, value) => @@ -73,12 +67,6 @@ public override JsonTypeInfo GetTypeInfo(System.Type type, JsonSerializerOptions property.ShouldSerialize = (obj, value) => originalCondition(obj, value) && !(value is IEnumerable e && !e.Cast().Any()); } - - // if (type.GetMethod("ShouldSerialize" + property.Name, 0, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, Array.Empty()) is {} shouldSerializeMethod) - // { - // property.ShouldSerialize = (obj, value) => - // originalCondition(obj, value) && (bool)shouldSerializeMethod.Invoke(obj, Array.Empty()); - // } } return info; } diff --git a/src/Framework/Framework/Controls/DotvvmControlDebugJsonConverter.cs b/src/Framework/Framework/Controls/DotvvmControlDebugJsonConverter.cs index 175a026cdd..251186ab2b 100644 --- a/src/Framework/Framework/Controls/DotvvmControlDebugJsonConverter.cs +++ b/src/Framework/Framework/Controls/DotvvmControlDebugJsonConverter.cs @@ -4,50 +4,44 @@ using System.Text.Json.Serialization; using DotVVM.Framework.Binding.Expressions; using DotVVM.Framework.Configuration; +using DotVVM.Framework.ResourceManagement; namespace DotVVM.Framework.Controls { - internal class DotvvmControlDebugJsonConverter : JsonConverter - { - // public bool IncludeChildren { get; set; } = false; - // public DotvvmConfiguration? Configuration { get; set; } = null; + internal class DotvvmControlDebugJsonConverter() : GenericWriterJsonConverter((w, objInterface, options) => { + var obj = objInterface.Self; + w.WriteStartObject(); - public override DotvvmBindableObject? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => - throw new NotImplementedException("Deserializing dotvvm control from JSON is not supported."); - public override void Write(Utf8JsonWriter w, DotvvmBindableObject obj, JsonSerializerOptions options) + w.WritePropertyName("Control"); + w.WriteStringValue(obj.GetType().Name); + + w.WriteStartObject("Properties"); + foreach (var kvp in obj.Properties.OrderBy(p => (p.Key.DeclaringType.IsAssignableFrom(obj.GetType()), p.Key.Name))) { - w.WriteStartObject(); - - w.WritePropertyName("Control"); - w.WriteStringValue(obj.GetType().Name); - - w.WriteStartObject("Properties"); - foreach (var kvp in obj.Properties.OrderBy(p => (p.Key.DeclaringType.IsAssignableFrom(obj.GetType()), p.Key.Name))) - { - var p = kvp.Key; - var rawValue = kvp.Value; - var isAttached = !p.DeclaringType.IsAssignableFrom(obj.GetType()); - var name = isAttached ? p.DeclaringType.Name + "." + p.Name : p.Name; - if (rawValue is null) - w.WriteNull(name); - else if (rawValue is IBinding) - w.WriteString(name, rawValue.ToString()); - else - { - w.WritePropertyName(name); - JsonSerializer.Serialize(w, rawValue, options); - } - } - w.WriteEndObject(); - - if (obj is DotvvmControl control) + var p = kvp.Key; + var rawValue = kvp.Value; + var isAttached = !p.DeclaringType.IsAssignableFrom(obj.GetType()); + var name = isAttached ? p.DeclaringType.Name + "." + p.Name : p.Name; + if (rawValue is null) + w.WriteNull(name); + else if (rawValue is IBinding) + w.WriteString(name, rawValue.ToString()); + else { - w.WritePropertyName("LifecycleRequirements"); - w.WriteStringValue(control.LifecycleRequirements.ToString()); + w.WritePropertyName(name); + JsonSerializer.Serialize(w, rawValue, options); } + } + w.WriteEndObject(); - - w.WriteEndObject(); + if (obj is DotvvmControl control) + { + w.WritePropertyName("LifecycleRequirements"); + w.WriteStringValue(control.LifecycleRequirements.ToString()); } + + w.WriteEndObject(); + }) + { } } diff --git a/src/Framework/Framework/Controls/KnockoutHelper.cs b/src/Framework/Framework/Controls/KnockoutHelper.cs index c52a9bbf32..d618b8af02 100644 --- a/src/Framework/Framework/Controls/KnockoutHelper.cs +++ b/src/Framework/Framework/Controls/KnockoutHelper.cs @@ -485,7 +485,7 @@ public static string GetKnockoutBindingExpression(this DotvvmBindableObject obj, /// public static string MakeStringLiteral(string value, bool htmlSafe = true) { - var encoder = htmlSafe ? DefaultSerializerSettingsProvider.Instance.HtmlSafeLessParaoidEncoder : JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + var encoder = htmlSafe ? DefaultSerializerSettingsProvider.Instance.HtmlSafeLessParanoidEncoder : JavaScriptEncoder.UnsafeRelaxedJsonEscaping; if (value.Length < 64) { // try to allocate only the result string if it short enough diff --git a/src/Framework/Framework/Diagnostics/CompilationPageApiPresenter.cs b/src/Framework/Framework/Diagnostics/CompilationPageApiPresenter.cs index f0ce1f007e..f5c8310954 100644 --- a/src/Framework/Framework/Diagnostics/CompilationPageApiPresenter.cs +++ b/src/Framework/Framework/Diagnostics/CompilationPageApiPresenter.cs @@ -33,7 +33,7 @@ public async Task ProcessRequest(IDotvvmRequestContext context) } response.StatusCode = 500; - response.ContentType = "application/json"; + response.ContentType = "application/json; charset=utf-8"; await response.WriteAsync(JsonSerializer.Serialize(compilationService.GetFilesWithFailedCompilation(), DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe)); } } diff --git a/src/Framework/Framework/Diagnostics/DiagnosticsInformationSender.cs b/src/Framework/Framework/Diagnostics/DiagnosticsInformationSender.cs index a30fa787f6..3b2cf7c6c4 100644 --- a/src/Framework/Framework/Diagnostics/DiagnosticsInformationSender.cs +++ b/src/Framework/Framework/Diagnostics/DiagnosticsInformationSender.cs @@ -20,6 +20,9 @@ public DiagnosticsInformationSender(DiagnosticsServerConfiguration configuration this.configuration = configuration; } + public DiagnosticsInformationSenderState State => + configuration.GetFreshHostName() is {} && configuration.Port != 0 ? DiagnosticsInformationSenderState.Full : DiagnosticsInformationSenderState.Disconnected; + public async Task SendInformationAsync(DiagnosticsInformation information) { var hostname = configuration.GetFreshHostName(); diff --git a/src/Framework/Framework/Diagnostics/DiagnosticsRequestTracer.cs b/src/Framework/Framework/Diagnostics/DiagnosticsRequestTracer.cs index 7e22cdca6e..297bac754d 100644 --- a/src/Framework/Framework/Diagnostics/DiagnosticsRequestTracer.cs +++ b/src/Framework/Framework/Diagnostics/DiagnosticsRequestTracer.cs @@ -36,11 +36,18 @@ public Task TraceEvent(string eventName, IDotvvmRequestContext context) events.Add(CreateEventTiming(eventName)); return TaskUtils.GetCompletedTask(); } + + Memory ViewModelJson = Array.Empty(); - - public void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Lazy viewModelBuffer) + public void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Func viewModelBuffer) { - // TODO + if (informationSender.State >= DiagnosticsInformationSenderState.Full) + { + using (var stream = viewModelBuffer()) + { + ViewModelJson = stream.ReadToMemory(); + } + } } private EventTiming CreateEventTiming(string eventName) @@ -109,8 +116,8 @@ private ResponseDiagnostics BuildResponseDiagnostics(IDotvvmRequestContext reque StatusCode = request.HttpContext.Response.StatusCode, Headers = request.HttpContext.Response.Headers.Select(HttpHeaderItem.FromKeyValuePair) .ToList(), - // ViewModelJson = request.ViewModelJson?.GetValue("viewModel")?.ToString(), // TODO - // ViewModelDiff = request.ViewModelJson?.GetValue("viewModelDiff")?.ToString(), + ViewModelJson = StringUtils.Utf8Decode(this.ViewModelJson.Span), + // ViewModelDiff = request.ViewModelJson?.GetValue("viewModelDiff")?.ToString(), // TODO: how do we have diffs now? ResponseSize = ResponseSize?.realLength ?? -1, CompressedResponseSize = ResponseSize?.compressedLength ?? -1 }; diff --git a/src/Framework/Framework/Diagnostics/IDiagnosticsInformationSender.cs b/src/Framework/Framework/Diagnostics/IDiagnosticsInformationSender.cs index 3c8e533754..68be35d68d 100644 --- a/src/Framework/Framework/Diagnostics/IDiagnosticsInformationSender.cs +++ b/src/Framework/Framework/Diagnostics/IDiagnosticsInformationSender.cs @@ -7,6 +7,17 @@ namespace DotVVM.Framework.Diagnostics public interface IDiagnosticsInformationSender { Task SendInformationAsync(DiagnosticsInformation information); + DiagnosticsInformationSenderState State { get; } } -} \ No newline at end of file + public enum DiagnosticsInformationSenderState + { + /// No events are being sent, collection is unnecessary + Disconnected, + /// Only timing information is being collected, view models and other large objects are not neccessary to collect + TimingOnly, + /// All information is being collected + Full + } + +} diff --git a/src/Framework/Framework/Diagnostics/PerformanceWarningTracer.cs b/src/Framework/Framework/Diagnostics/PerformanceWarningTracer.cs index 02d831ea20..fc78f7b9b5 100644 --- a/src/Framework/Framework/Diagnostics/PerformanceWarningTracer.cs +++ b/src/Framework/Framework/Diagnostics/PerformanceWarningTracer.cs @@ -59,11 +59,11 @@ void WarnSlowRequest(TimeSpan totalElapsed) - public void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Lazy viewModelBuffer) + public void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Func viewModelBuffer) { if (viewModelSize >= config.BigViewModelBytes) { - WarnLargeViewModel(context, viewModelSize, viewModelBuffer.Value); + WarnLargeViewModel(context, viewModelSize, viewModelBuffer()); } } diff --git a/src/Framework/Framework/Hosting/DotvvmMetrics.cs b/src/Framework/Framework/Hosting/DotvvmMetrics.cs index efabc3943d..d07f47d704 100644 --- a/src/Framework/Framework/Hosting/DotvvmMetrics.cs +++ b/src/Framework/Framework/Hosting/DotvvmMetrics.cs @@ -45,10 +45,6 @@ public static class DotvvmMetrics public static readonly Histogram ViewModelSize = Meter.CreateHistogram("viewmodel_size_bytes", unit: "bytes", description: "Size of the result viewmodel JSON in bytes."); - /// Labeled by route=RouteName and request_type=Navigate/SpaNavigate/Command/StaticCommand - public static readonly Histogram ViewModelStringificationTime = - Meter.CreateHistogram("viewmodel_stringification_seconds", unit: "seconds", description: "Time it took to stringify the resulting JSON view model."); - /// Labeled by route=RouteName and request_type=Navigate/SpaNavigate/Command/StaticCommand public static readonly Histogram ViewModelSerializationTime = Meter.CreateHistogram("viewmodel_serialization_seconds", unit: "seconds", description: "Time it took to serialize view model to JSON objects."); diff --git a/src/Framework/Framework/Hosting/DotvvmPresenter.cs b/src/Framework/Framework/Hosting/DotvvmPresenter.cs index 9199baf18f..75a668fd63 100644 --- a/src/Framework/Framework/Hosting/DotvvmPresenter.cs +++ b/src/Framework/Framework/Hosting/DotvvmPresenter.cs @@ -343,6 +343,7 @@ public async Task ProcessStaticCommandRequest(IDotvvmRequestContext context) { postData = await JsonDocument.ParseAsync(requestBody); } + context.ReceivedViewModelJson = postData; // validate csrf token context.CsrfToken = postData.RootElement.GetProperty("$csrfToken"u8).GetString().NotNull("$csrfToken is required"); @@ -464,7 +465,7 @@ async Task RespondWithStaticCommandValidationFailure(ActionInfo action, IDotvvmR modelState = staticCommandModelState.Errors }); - context.HttpContext.Response.ContentType = "application/json"; + context.HttpContext.Response.ContentType = "application/json; charset=utf-8"; await context.HttpContext.Response.WriteAsync(result); throw new DotvvmInterruptRequestExecutionException(InterruptReason.ArgumentsValidationFailed, "Argument contain validation errors!"); } diff --git a/src/Framework/Framework/Hosting/DotvvmRequestContextExtensions.cs b/src/Framework/Framework/Hosting/DotvvmRequestContextExtensions.cs index b289cc4ae3..5757b921dc 100644 --- a/src/Framework/Framework/Hosting/DotvvmRequestContextExtensions.cs +++ b/src/Framework/Framework/Hosting/DotvvmRequestContextExtensions.cs @@ -147,7 +147,7 @@ public static void RedirectToRoutePermanent(this IDotvvmRequestContext context, internal static Task SetCachedViewModelMissingResponse(this IDotvvmRequestContext context) { context.HttpContext.Response.StatusCode = 200; - context.HttpContext.Response.ContentType = "application/json"; + context.HttpContext.Response.ContentType = "application/json; charset=utf-8"; return context.HttpContext.Response.WriteAsync(DefaultViewModelSerializer.GenerateMissingCachedViewModelResponse()); } @@ -165,7 +165,7 @@ public static void FailOnInvalidModelState(this IDotvvmRequestContext context) context.RouteLabel(), context.RequestTypeLabel() ); - context.HttpContext.Response.ContentType = "application/json"; + context.HttpContext.Response.ContentType = "application/json; charset=utf-8"; context.HttpContext.Response .Write(context.Services.GetRequiredService().SerializeModelState(context)); throw new DotvvmInterruptRequestExecutionException(InterruptReason.ModelValidationFailed, "The ViewModel contains validation errors!"); @@ -276,7 +276,7 @@ internal static async Task RejectRequest(this IDotvvmRequestContext context, str .GetRequiredService() .Warn(new DotvvmRuntimeWarning(msg)); context.HttpContext.Response.StatusCode = statusCode; - context.HttpContext.Response.ContentType = "text/plain"; + context.HttpContext.Response.ContentType = "text/plain; charset=utf-8"; await context.HttpContext.Response.WriteAsync(msg); throw new DotvvmInterruptRequestExecutionException(InterruptReason.RequestRejected, msg); } diff --git a/src/Framework/Framework/Hosting/ErrorPages/DotvvmErrorPageRenderer.cs b/src/Framework/Framework/Hosting/ErrorPages/DotvvmErrorPageRenderer.cs index 03b73fe91d..21abdf5111 100644 --- a/src/Framework/Framework/Hosting/ErrorPages/DotvvmErrorPageRenderer.cs +++ b/src/Framework/Framework/Hosting/ErrorPages/DotvvmErrorPageRenderer.cs @@ -46,7 +46,7 @@ private static async Task RenderFallbackMessage(IHttpContext context, Exception { try { - context.Response.ContentType = "text/plain"; + context.Response.ContentType = "text/plain; charset=utf-8"; using (var writer = new StreamWriter(context.Response.Body)) { await writer.WriteLineAsync("Error in DotVVM Application:"); diff --git a/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs b/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs index af26de7e0a..e336b79c94 100644 --- a/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs +++ b/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs @@ -18,6 +18,7 @@ using DotVVM.Framework.Compilation.ControlTree; using System.Text.Json.Serialization.Metadata; using System.Text.Encodings.Web; +using FastExpressionCompiler; namespace DotVVM.Framework.Hosting.ErrorPages { @@ -156,19 +157,13 @@ public string TransformText() return builder.ToString(); } - public void ObjectBrowser(object? obj) + internal static JsonNode SerializeObjectForBrowser(object? obj) { - if (obj is null) - { - WriteText("null"); - return; - } - var settings = new JsonSerializerOptions() { ReferenceHandler = ReferenceHandler.IgnoreCycles, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, Converters = { - new ReflectionTypeJsonConverter(), + new DebugReflectionTypeJsonConverter(), new ReflectionAssemblyJsonConverter(), new DotvvmTypeDescriptorJsonConverter(), new Controls.DotvvmControlDebugJsonConverter(), @@ -177,13 +172,45 @@ public void ObjectBrowser(object? obj) new UnsupportedTypeJsonConverterFactory(), }, TypeInfoResolver = new IgnoreUnsupportedResolver(), - // suppress any errors that occur during serialization (getters may throw exception, ...) - // Error = (sender, args) => { // TODO: how? - // args.ErrorContext.Handled = true; - // } }; - var jobject = JsonSerializer.SerializeToElement(obj, settings); - ObjectBrowser(JsonObject.Create(jobject)!); + return JsonSerializer.SerializeToNode(obj, settings)!; + } + + public void ObjectBrowser(object? obj) + { + if (obj is null) + { + WriteText("null"); + return; + } + + + try + { + switch (SerializeObjectForBrowser(obj)) + { + case JsonObject jobject: + ObjectBrowser(jobject); + break; + case JsonArray jarray: + ObjectBrowser(jarray); + break; + case var node: + WriteText(node.ToString()); + break; + }; + } + catch + { + try + { + WriteText(obj.ToString()); + } + catch + { + WriteText(""); + } + } } class IgnoreUnsupportedResolver: DefaultJsonTypeInfoResolver @@ -412,7 +439,7 @@ private void WriteLine(string textToAppend) class UnsupportedTypeJsonConverterFactory : JsonConverterFactory { public override bool CanConvert(Type typeToConvert) => - typeToConvert.IsDelegate() || typeof(MemberInfo).IsAssignableFrom(typeToConvert); + typeToConvert.IsDelegate() || typeof(ICustomAttributeProvider).IsAssignableFrom(typeToConvert); public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => (JsonConverter?)Activator.CreateInstance(typeof(Inner<>).MakeGenericType([ typeToConvert ])); @@ -424,7 +451,7 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions if (value is null) writer.WriteNullValue(); else if (value is Delegate) - writer.WriteStringValue(""); + writer.WriteStringValue($"[delegate {value.GetType().ToCode()}]"); else writer.WriteStringValue(value.ToString()); } diff --git a/src/Framework/Framework/Hosting/HttpRedirectService.cs b/src/Framework/Framework/Hosting/HttpRedirectService.cs index e46440d748..530d81b800 100644 --- a/src/Framework/Framework/Hosting/HttpRedirectService.cs +++ b/src/Framework/Framework/Hosting/HttpRedirectService.cs @@ -40,7 +40,7 @@ public void WriteRedirectResponse(IHttpContext httpContext, string url, int stat else { httpContext.Response.StatusCode = 200; - httpContext.Response.ContentType = "application/json"; + httpContext.Response.ContentType = "application/json; charset=utf-8"; httpContext.Response .WriteAsync(DefaultViewModelSerializer.GenerateRedirectActionResponse(url, replaceInHistory, allowSpaRedirect)) .GetAwaiter().GetResult(); diff --git a/src/Framework/Framework/Hosting/IDotvvmRequestContext.cs b/src/Framework/Framework/Hosting/IDotvvmRequestContext.cs index 5c68214c5b..75c23a54f2 100644 --- a/src/Framework/Framework/Hosting/IDotvvmRequestContext.cs +++ b/src/Framework/Framework/Hosting/IDotvvmRequestContext.cs @@ -118,11 +118,6 @@ public interface IDotvvmRequestContext CustomResponsePropertiesManager CustomResponseProperties { get; } } - public class ReceivedViewModelData - { - - } - public enum DotvvmRequestType { Unknown, diff --git a/src/Framework/Framework/Hosting/VisualStudioHelper.cs b/src/Framework/Framework/Hosting/VisualStudioHelper.cs index 3c3f26fe64..0c6b1afc70 100644 --- a/src/Framework/Framework/Hosting/VisualStudioHelper.cs +++ b/src/Framework/Framework/Hosting/VisualStudioHelper.cs @@ -38,10 +38,6 @@ public static JsonSerializerOptions GetSerializerOptions() { return new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - // ReferenceHandler = ReferenceHandler.IgnoreCycles, // doesn't work together with JsonObjectCreationHandling.Populate - // Error = (sender, args) => { // TODO: how? https://github.com/dotnet/runtime/issues/38049 - // args.ErrorContext.Handled = true; - // }, WriteIndented = true, Converters = { new ReflectionTypeJsonConverter(), @@ -53,7 +49,7 @@ public static JsonSerializerOptions GetSerializerOptions() new DataContextManipulationAttributeConverter() }, TypeInfoResolver = new DotvvmConfigurationSerializationResolver(), - Encoder = DefaultSerializerSettingsProvider.Instance.HtmlSafeLessParaoidEncoder + Encoder = DefaultSerializerSettingsProvider.Instance.HtmlSafeLessParanoidEncoder }; } diff --git a/src/Framework/Framework/ResourceManagement/ClientGlobalize/JQueryGlobalizeScriptCreator.cs b/src/Framework/Framework/ResourceManagement/ClientGlobalize/JQueryGlobalizeScriptCreator.cs index 18dfe5d5ed..22564be7ee 100644 --- a/src/Framework/Framework/ResourceManagement/ClientGlobalize/JQueryGlobalizeScriptCreator.cs +++ b/src/Framework/Framework/ResourceManagement/ClientGlobalize/JQueryGlobalizeScriptCreator.cs @@ -3,12 +3,10 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text; -using System.Threading.Tasks; -using DotVVM.Framework.Compilation.Javascript; using System.Text.Json.Nodes; using System.Text.Json; using DotVVM.Framework.Configuration; +using DotVVM.Framework.Controls; namespace DotVVM.Framework.ResourceManagement.ClientGlobalize { @@ -224,7 +222,7 @@ public static string BuildCultureInfoScript(CultureInfo ci) // Global variable Globalize = window.dotvvm_Globalize; } -Globalize.addCultureInfo({{JavascriptCompilationHelper.CompileConstant(ci.Name)}}, 'default', {{cultureJson}}); +Globalize.addCultureInfo({{KnockoutHelper.MakeStringLiteral(ci.Name)}}, 'default', {{cultureJson}}); }(this)); """; } diff --git a/src/Framework/Framework/ResourceManagement/ReflectionAssemblyJsonConverter.cs b/src/Framework/Framework/ResourceManagement/ReflectionAssemblyJsonConverter.cs index cb0c48c4b0..8cebd81729 100644 --- a/src/Framework/Framework/ResourceManagement/ReflectionAssemblyJsonConverter.cs +++ b/src/Framework/Framework/ResourceManagement/ReflectionAssemblyJsonConverter.cs @@ -2,14 +2,12 @@ using DotVVM.Framework.Compilation; using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Compilation.ControlTree.Resolved; -using DotVVM.Framework.Utils; +using FastExpressionCompiler; using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; -using System.Threading.Tasks; namespace DotVVM.Framework.ResourceManagement { @@ -50,6 +48,15 @@ public override void Write(Utf8JsonWriter writer, Type t, JsonSerializerOptions writer.WriteStringValue($"{t.FullName}, {t.Assembly.GetName().Name}"); } } + + /// Formats type as C# type identifier + public class DebugReflectionTypeJsonConverter(): GenericWriterJsonConverter( + (writer, value, options) => { + writer.WriteStringValue(value.ToCode()); + }) + { + } + public class DotvvmTypeDescriptorJsonConverter : JsonConverter where T: ITypeDescriptor { @@ -77,25 +84,16 @@ public override void Write(Utf8JsonWriter writer, T t, JsonSerializerOptions opt } } - public class DotvvmPropertyJsonConverter : JsonConverter - { - public override IControlAttributeDescriptor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => - throw new NotImplementedException(); - public override void Write(Utf8JsonWriter writer, IControlAttributeDescriptor value, JsonSerializerOptions options) - { + public class DotvvmPropertyJsonConverter() : GenericWriterJsonConverter( + (writer, value, options) => { writer.WriteStringValue(value.ToString()); - } + }) + { } - public class DataContextChangeAttributeConverter : JsonConverter + public class DataContextChangeAttributeConverter() : GenericWriterJsonConverter(WriteObjectReflection) { - public override DataContextChangeAttribute? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); - public override void Write(Utf8JsonWriter writer, DataContextChangeAttribute attribute, JsonSerializerOptions options) - { - WriteObjectReflction(writer, attribute, options); - } - - internal static void WriteObjectReflction(Utf8JsonWriter writer, object attribute, JsonSerializerOptions options) + internal static void WriteObjectReflection(Utf8JsonWriter writer, object attribute, JsonSerializerOptions options) { writer.WriteStartObject(); writer.WriteString("$type", attribute.GetType().ToString()); @@ -113,12 +111,22 @@ internal static void WriteObjectReflction(Utf8JsonWriter writer, object attribut } } - public class DataContextManipulationAttributeConverter : JsonConverter + public class DataContextManipulationAttributeConverter() : GenericWriterJsonConverter(DataContextChangeAttributeConverter.WriteObjectReflection) { - public override DataContextStackManipulationAttribute Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); - public override void Write(Utf8JsonWriter writer, DataContextStackManipulationAttribute value, JsonSerializerOptions options) + } + + public class GenericWriterJsonConverter(Action write) : JsonConverterFactory + { + public override bool CanConvert(Type typeToConvert) => typeof(T).IsAssignableFrom(typeToConvert); + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => + Activator.CreateInstance(typeof(Inner<>).MakeGenericType(typeof(T), typeToConvert), write) as JsonConverter; + + private class Inner(Action write) : JsonConverter { - DataContextChangeAttributeConverter.WriteObjectReflction(writer, value, options); + public override TActual Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + throw new NotImplementedException(); + public override void Write(Utf8JsonWriter writer, TActual value, JsonSerializerOptions options) => + write(writer, (T)(object)value!, options); } } } diff --git a/src/Framework/Framework/Runtime/Tracing/IRequestTracer.cs b/src/Framework/Framework/Runtime/Tracing/IRequestTracer.cs index 652f4510d6..4eeb8158c4 100644 --- a/src/Framework/Framework/Runtime/Tracing/IRequestTracer.cs +++ b/src/Framework/Framework/Runtime/Tracing/IRequestTracer.cs @@ -7,12 +7,18 @@ namespace DotVVM.Framework.Runtime.Tracing { public interface IRequestTracer { + /// Called multiple times per request at different phases. See for a list of possible values. Applications and other libraries may define additional events. Task TraceEvent(string eventName, IDotvvmRequestContext context); + + /// Called after the viewmodel is serialized. The initializes a stream which can read the serialized ViewModel. + /// The size of the serialized ViewModel in bytes (uncompressed). + /// When invoked, a new stream is created allowing to read the serialized ViewModel. The factory function must be invoked synchronously, but the Stream may be stored for further use and should be disposed when not needed anymore. + void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Func viewModelBuffer); - void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Lazy viewModelBuffer); - + /// Called when DotVVM is done with handling the request and it didn't throw an unhandled exception. If has been thrown, it has been already handled and this overload is called. Task EndRequest(IDotvvmRequestContext context); - + + /// Called when DotVVM is done with handling the request and it ended up throwing an exception (different than ) Task EndRequest(IDotvvmRequestContext context, Exception exception); } } diff --git a/src/Framework/Framework/Runtime/Tracing/NullRequestTracer.cs b/src/Framework/Framework/Runtime/Tracing/NullRequestTracer.cs index 7097863b2a..8022b5738d 100644 --- a/src/Framework/Framework/Runtime/Tracing/NullRequestTracer.cs +++ b/src/Framework/Framework/Runtime/Tracing/NullRequestTracer.cs @@ -25,7 +25,7 @@ public Task EndRequest(IDotvvmRequestContext context, Exception exception) return TaskUtils.GetCompletedTask(); } - public void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Lazy viewModelBuffer) + public void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Func viewModelBuffer) { } diff --git a/src/Framework/Framework/Runtime/Tracing/RequestTracingExtensions.cs b/src/Framework/Framework/Runtime/Tracing/RequestTracingExtensions.cs index 82bc879270..3fa4b53487 100644 --- a/src/Framework/Framework/Runtime/Tracing/RequestTracingExtensions.cs +++ b/src/Framework/Framework/Runtime/Tracing/RequestTracingExtensions.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; using DotVVM.Framework.Hosting; +using DotVVM.Framework.Utils; namespace DotVVM.Framework.Runtime.Tracing { @@ -17,12 +18,18 @@ public static async Task TracingEvent(this IEnumerable requestTr } } - public static void TracingSerialized(this IEnumerable requestTracers, IDotvvmRequestContext context, int viewModelSize, Func stream) + public static void TracingSerialized(this IEnumerable requestTracers, IDotvvmRequestContext context, int viewModelSize, MemoryStream stream) { + bool calledSynchronously = false; foreach (var tracer in requestTracers) { - tracer.ViewModelSerialized(context, viewModelSize, new Lazy(stream)); + tracer.ViewModelSerialized(context, viewModelSize, () => { + if (!calledSynchronously) // make sure we can optimize this later to use buffers from ArrayPool + throw new InvalidOperationException("The stream factory function is being invoked too late."); + return stream.CloneReadOnly(); + }); } + calledSynchronously = true; } public static async Task TracingEndRequest(this IEnumerable requestTracers, IDotvvmRequestContext context) diff --git a/src/Framework/Framework/Utils/JsonDiffWriter.cs b/src/Framework/Framework/Utils/JsonDiffWriter.cs index 4578d82a32..5bab7a9f4b 100644 --- a/src/Framework/Framework/Utils/JsonDiffWriter.cs +++ b/src/Framework/Framework/Utils/JsonDiffWriter.cs @@ -3,6 +3,8 @@ // using System.Text.Json; // using System.Diagnostics; // using System.Buffers; +// using DotVVM.Framework.ViewModel.Serialization; +// using System.Runtime.CompilerServices; // namespace DotVVM.Framework.Utils // { @@ -30,67 +32,158 @@ // var depth = reader.CurrentDepth; // while (reader.CurrentDepth >= depth) // { -// switch (reader.TokenType) -// { -// case JsonTokenType.False: -// case JsonTokenType.True: -// case JsonTokenType.Null: -// case JsonTokenType.String: -// case JsonTokenType.Number: { -// if (reader.HasValueSequence) -// writer.WriteRawValue(reader.ValueSequence); -// else -// writer.WriteRawValue(reader.ValueSpan); -// break; -// } -// case JsonTokenType.PropertyName: { -// var length = reader.HasValueSequence ? reader.ValueSequence.Length : reader.ValueSpan.Length; -// Span buffer = length <= 1024 ? stackalloc byte[(int)length] : new byte[length]; -// var realLength = reader.CopyString(buffer); -// writer.WritePropertyName(buffer.Slice(0, realLength)); -// break; -// } -// case JsonTokenType.StartArray: { -// writer.WriteStartArray(); -// break; -// } -// case JsonTokenType.EndArray: { -// writer.WriteEndArray(); -// break; -// } -// case JsonTokenType.StartObject: { -// writer.WriteStartObject(); -// break; -// } -// case JsonTokenType.EndObject: { -// writer.WriteEndObject(); -// break; -// } -// default: { -// throw new JsonException($"Unexpected token {reader.TokenType}."); -// } -// } +// CopyToken(ref reader, writer); // reader.Read(); // } // } + +// static void CopyToken(ref Utf8JsonReader reader, Utf8JsonWriter writer) +// { +// switch (reader.TokenType) +// { +// case JsonTokenType.False: +// case JsonTokenType.True: +// case JsonTokenType.Null: +// case JsonTokenType.String: +// case JsonTokenType.Number: { +// if (reader.HasValueSequence) +// writer.WriteRawValue(reader.ValueSequence); +// else +// writer.WriteRawValue(reader.ValueSpan); +// break; +// } +// case JsonTokenType.PropertyName: { +// var length = reader.HasValueSequence ? reader.ValueSequence.Length : reader.ValueSpan.Length; +// Span buffer = length <= 1024 ? stackalloc byte[(int)length] : new byte[length]; +// var realLength = reader.CopyString(buffer); +// writer.WritePropertyName(buffer.Slice(0, realLength)); +// break; +// } +// case JsonTokenType.StartArray: { +// writer.WriteStartArray(); +// break; +// } +// case JsonTokenType.EndArray: { +// writer.WriteEndArray(); +// break; +// } +// case JsonTokenType.StartObject: { +// writer.WriteStartObject(); +// break; +// } +// case JsonTokenType.EndObject: { +// writer.WriteEndObject(); +// break; +// } +// default: { +// throw new JsonException($"Unexpected token {reader.TokenType}."); +// } +// } +// } // public delegate bool? IncludePropertyDelegate(string typeId, ReadOnlySpan propertyName); // private readonly IncludePropertyDelegate? includePropertyOverride; + +// private Utf8JsonReader reader; +// private ReadOnlySequence remainingPreviousInput; + // private byte[]? nameBufferRented; // private Span nameBuffer; // private int nameBufferPosition; // private RefList lazyWriteStack; // private readonly Utf8JsonWriter writer; +// private RefList sourceElementStack; +// private RefList objectTypeIds; +// private JsonDiffState state; // private JsonDiffWriter( // IncludePropertyDelegate? includePropertyOverride, // Span nameBuffer, -// Utf8JsonWriter writer +// Utf8JsonWriter writer, +// JsonElement sourceElement // ) // { // this.includePropertyOverride = includePropertyOverride; // this.nameBuffer = nameBuffer; // this.writer = writer; +// this.sourceElementStack = new RefList(default); +// this.sourceElementStack.Enlarge(32); +// this.sourceElementStack.Add(sourceElement); +// this.objectTypeIds = new RefList(default); +// this.objectTypeIds.Enlarge(32); +// this.objectTypeIds.Add(default); +// } + +// public (int bytesRead, bool isDone) ContinueDiff(Span target, bool isFinalBlock) +// { +// var startPosition = reader.BytesConsumed; +// var position = startPosition; +// if (startPosition == 0) +// { +// reader = new Utf8JsonReader(target, isFinalBlock, default); +// } +// else +// { +// reader = new Utf8JsonReader(target, isFinalBlock, reader.CurrentState); +// } +// if (!reader.Read()) +// goto EndOfInput; + +// while (true) +// { +// if (state == JsonDiffState.Copying) +// { +// CopyToken(ref reader, writer); +// reader.Read(); +// } +// switch (reader.TokenType) +// { +// case JsonTokenType.StartObject: +// if (sourceElementStack.Last.ValueKind != JsonValueKind.Object) +// { +// CopyValue(ref reader, writer); +// state = JsonDiffState.Copying; +// } +// else +// { +// state = JsonDiffState.DiffObject; +// } +// break; +// case JsonTokenType.EndObject: +// break; +// case JsonTokenType.StartArray: +// break; +// case JsonTokenType.EndArray: +// break; +// case JsonTokenType.PropertyName: { +// var name = ReadName(ref reader); +// if (!reader.Read()) +// { +// state = JsonDiffState.DiffValue; +// goto EndOfInput; +// } +// break; +// } +// case JsonTokenType.Comment: +// break; +// case JsonTokenType.String: +// break; +// case JsonTokenType.Number: +// break; +// case JsonTokenType.True: +// break; +// case JsonTokenType.False: +// break; +// case JsonTokenType.Null: +// break; +// } +// } + +// return ((int)(reader.BytesConsumed - startPosition), true); + + +// EndOfInput: +// return ((int)(reader.BytesConsumed - startPosition), false); // } // Span ReadName(ref Utf8JsonReader reader) @@ -132,7 +225,7 @@ // { // lazyWriteStack.Add(0); // } - + // void DiffObject(in JsonElement source, ref Utf8JsonReader target) // { // AssertToken(ref target, JsonTokenType.StartObject); @@ -223,6 +316,13 @@ // return diff; // } +// enum JsonDiffState +// { +// DiffValue, +// DiffObject, +// Copying +// } + // ref struct RefList // { @@ -257,9 +357,19 @@ // buffer[count++] = item; // } +// public void Pop() +// { +// count--; +// if (RuntimeHelpers.IsReferenceOrContainsReferences()) +// buffer[count] = default!; +// } + // public void Clear() // { // count = 0; + +// if (RuntimeHelpers.IsReferenceOrContainsReferences()) +// buffer.Fill(default!); // } // public T? LastOrDefault() => count > 0 ? buffer[count - 1] : default; diff --git a/src/Framework/Framework/Utils/MultipleWriterStream.cs b/src/Framework/Framework/Utils/MultipleWriterStream.cs new file mode 100644 index 0000000000..f37770cff9 --- /dev/null +++ b/src/Framework/Framework/Utils/MultipleWriterStream.cs @@ -0,0 +1,133 @@ +// using System; +// using System.Collections.Generic; +// using System.IO; +// using System.Linq; +// using System.Threading; +// using System.Threading.Tasks; + +// namespace DotVVM.Framework.Utils +// { +// internal class MultipleWriterStream : Stream +// { +// private readonly Stream[] innerStreams; +// private readonly bool parallel; +// private long position; +// private Task?[] tasks; + +// public MultipleWriterStream(IEnumerable innerStreams, bool parallel) +// { +// this.innerStreams = innerStreams.ToArray(); +// if (this.innerStreams.Length > 512) +// throw new ArgumentException("Too many output streams."); +// this.tasks = new Task?[this.innerStreams.Length]; +// this.parallel = parallel; +// foreach (var stream in this.innerStreams) +// { +// if (!stream.CanWrite) +// throw new ArgumentException("All streams must be writable."); +// } +// } + +// public override bool CanRead => false; + +// public override bool CanSeek => false; + +// public override bool CanWrite => true; + +// public override long Length => position; + +// public override long Position { get => position; set => throw new NotSupportedException(); } + +// public override void Flush() +// { +// foreach (var stream in innerStreams) +// stream.Flush(); +// } +// public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); +// public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); +// public override void SetLength(long value) { } +// public override void Write(byte[] buffer, int offset, int count) +// { +// foreach (var stream in innerStreams) +// stream.Write(buffer, offset, count); +// } + +// public override void Write(ReadOnlySpan buffer) +// { +// foreach (var stream in innerStreams) +// stream.Write(buffer); +// position += buffer.Length; +// } + +// public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) +// { +// if (parallel) +// { +// bool done = true; +// for (int i = 0; i < innerStreams.Length; i++) +// { +// tasks[i] = innerStreams[i].WriteAsync(buffer, offset, count, cancellationToken); +// if (!tasks[i]!.IsCompleted) +// done = false; +// } +// return done ? Task.CompletedTask : Task.WhenAll(tasks); +// } +// else +// { +// for (int i = 0; i < innerStreams.Length; i++) +// { +// var task = innerStreams[i].WriteAsync(buffer, offset, count, cancellationToken); +// if (!task.IsCompleted) +// { +// if (i == innerStreams.Length - 1) +// return task; +// else +// return SerialAsyncCore(task, i + 1, buffer, offset, count, cancellationToken); +// } +// } +// return Task.CompletedTask; +// } + +// async Task SerialAsyncCore(Task firstResult, int startIndex, byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) +// { +// await firstResult; +// for (int i = startIndex; i < innerStreams.Length; i++) +// await innerStreams[i].WriteAsync(buffer, offset, count, cancellationToken); +// position += count; +// } +// } + +// public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) +// { +// if (!parallel) +// { +// return SerialCore(buffer, cancellationToken); +// } +// else +// { +// bool done = true; +// for (int i = 0; i < innerStreams.Length; i++) +// { +// var task = innerStreams[i].WriteAsync(buffer, cancellationToken); +// if (task.IsCompleted) +// tasks[i] = Task.CompletedTask; +// else +// { +// tasks[i] = task.AsTask(); +// done = false; +// } +// } +// return done ? default : new ValueTask(Task.WhenAll(tasks)); +// } + + +// async ValueTask SerialCore(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) +// { +// foreach (var stream in innerStreams) +// await stream.WriteAsync(buffer, cancellationToken); + +// position += buffer.Length; +// } +// } +// } +// } diff --git a/src/Framework/Framework/ViewModel/Serialization/ClientTypeId.cs b/src/Framework/Framework/ViewModel/Serialization/ClientTypeId.cs index 230a4b96cc..9f3ebab582 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ClientTypeId.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ClientTypeId.cs @@ -1,123 +1,153 @@ -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using DotVVM.Framework.Utils; +// using System; +// using System.Runtime.CompilerServices; +// using System.Runtime.InteropServices; +// using System.Text.Json; +// using DotVVM.Framework.Utils; -namespace DotVVM.Framework.ViewModel.Serialization -{ - [StructLayout(LayoutKind.Explicit)] - readonly struct ClientTypeId: IEquatable, IComparable - { - // first byte - [FieldOffset(0)] - readonly ulong a; - [FieldOffset(8)] - readonly ulong b; +// namespace DotVVM.Framework.ViewModel.Serialization +// { +// [StructLayout(LayoutKind.Explicit)] +// readonly struct ClientTypeId: IEquatable, IComparable +// { +// // first byte +// [FieldOffset(0)] +// readonly ulong a; +// [FieldOffset(8)] +// readonly ulong b; - [FieldOffset(0)] - readonly byte controlByte; - [FieldOffset(1)] - readonly byte dataByte1; - private ClientTypeId(ulong a, ulong b) - { - this.a = a; - this.b = b; - } +// [FieldOffset(0)] +// readonly byte controlByte; +// [FieldOffset(1)] +// readonly byte dataByte1; +// private ClientTypeId(ulong a, ulong b) +// { +// this.a = a; +// this.b = b; +// } - private ClientTypeId(bool isHash, ReadOnlySpan data) - { - if (data.Length > 15) throw new ArgumentException("Data too long"); - controlByte = (byte)(data.Length | (isHash ? 0x10 : 0)); -#if DotNetCore - data.CopyTo(MemoryMarshal.CreateSpan(ref dataByte1, 15)); -#else - unsafe - { - fixed (byte* ptr = &dataByte1) - { - data.CopyTo(new Span(ptr, 15)); - } - } -#endif - } +// private ClientTypeId(bool isHash, ReadOnlySpan data) +// { +// if (data.Length > 15) throw new ArgumentException("Data too long"); +// controlByte = (byte)(data.Length | (isHash ? 0x10 : 0)); +// #if DotNetCore +// data.CopyTo(MemoryMarshal.CreateSpan(ref dataByte1, 15)); +// #else +// unsafe +// { +// fixed (byte* ptr = &dataByte1) +// { +// data.CopyTo(new Span(ptr, 15)); +// } +// } +// #endif +// } - struct Utf8StringCtor {} - private ClientTypeId(ReadOnlySpan utf8Hash, Utf8StringCtor _) - { - if (utf8Hash.Length != 16) throw new ArgumentException("Hash must be 16 bytes long"); - controlByte = (byte)(12 | 0x10); -#if DotNetCore - System.Buffers.Text.Base64.DecodeFromUtf8(utf8Hash, MemoryMarshal.CreateSpan(ref dataByte1, 12), out var _, out var _); -#else - unsafe - { - fixed (byte* ptr = &dataByte1) - { - System.Buffers.Text.Base64.DecodeFromUtf8(utf8Hash, new Span(ptr, 12), out var _, out var _); - } - } -#endif - } +// struct Utf8StringCtor {} +// private ClientTypeId(ReadOnlySpan utf8Hash, Utf8StringCtor _) +// { +// if (utf8Hash.Length != 16) throw new ArgumentException("Hash must be 16 bytes long"); +// controlByte = (byte)(12 | 0x10); +// #if DotNetCore +// System.Buffers.Text.Base64.DecodeFromUtf8(utf8Hash, MemoryMarshal.CreateSpan(ref dataByte1, 12), out var _, out var _); +// #else +// unsafe +// { +// fixed (byte* ptr = &dataByte1) +// { +// System.Buffers.Text.Base64.DecodeFromUtf8(utf8Hash, new Span(ptr, 12), out var _, out var _); +// } +// } +// #endif +// } - public static ClientTypeId CreateHash(ReadOnlySpan data) => new ClientTypeId(true, data); - public static ClientTypeId CreateString(ReadOnlySpan data) => new ClientTypeId(false, data); - public static ClientTypeId Parse(ReadOnlySpan utf8) => - utf8.Length == 16 ? new ClientTypeId(utf8, default(Utf8StringCtor)) : new ClientTypeId(false, utf8); +// public static ClientTypeId CreateHash(ReadOnlySpan data) => new ClientTypeId(true, data); +// public static ClientTypeId CreateString(ReadOnlySpan data) => new ClientTypeId(false, data); +// public static ClientTypeId Parse(ReadOnlySpan utf8) => +// utf8.Length == 16 ? new ClientTypeId(utf8, default(Utf8StringCtor)) : new ClientTypeId(false, utf8); - byte Length => (byte)(controlByte & 0xf); - bool IsHash => ((controlByte >> 4) & 1) != 0; - bool IsEmpty => controlByte == 0; +// byte Length => (byte)(controlByte & 0xf); +// bool IsHash => ((controlByte >> 4) & 1) != 0; +// bool IsEmpty => controlByte == 0; - ReadOnlySpan Data => -#if DotNetCore - MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(in dataByte1), Length); -#else - throw new NotImplementedException(); -#endif +// ReadOnlySpan Data => +// #if DotNetCore +// MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(in dataByte1), Length); +// #else +// throw new NotImplementedException(); +// #endif - public void WriteJson(System.Text.Json.Utf8JsonWriter writer) - { - if (IsEmpty) writer.WriteNullValue(); - else if (IsHash) - writer.WriteBase64StringValue(Data); - else - writer.WriteStringValue(Data); - } - public void WriteJson(System.Text.Json.Utf8JsonWriter writer, ReadOnlySpan propertyName) - { - if (IsEmpty) writer.WriteNull(propertyName); - else if (IsHash) - writer.WriteBase64String(propertyName, Data); - else - writer.WriteString(propertyName, Data); - } +// public void WriteJson(Utf8JsonWriter writer) +// { +// if (IsEmpty) writer.WriteNullValue(); +// else if (IsHash) +// writer.WriteBase64StringValue(Data); +// else +// writer.WriteStringValue(Data); +// } +// public void WriteJson(Utf8JsonWriter writer, ReadOnlySpan propertyName) +// { +// if (IsEmpty) writer.WriteNull(propertyName); +// else if (IsHash) +// writer.WriteBase64String(propertyName, Data); +// else +// writer.WriteString(propertyName, Data); +// } - public static ClientTypeId ReadJson(ref System.Text.Json.Utf8JsonReader reader) - { - if (reader.TokenType == System.Text.Json.JsonTokenType.Null) return default; - if (reader.TokenType != System.Text.Json.JsonTokenType.String) throw new System.Text.Json.JsonException("Expected string"); - Span buffer = stackalloc byte[16]; - var readBytes = reader.CopyString(buffer); - return readBytes == 16 ? new ClientTypeId(buffer, default(Utf8StringCtor)) : new ClientTypeId(false, buffer.Slice(0, readBytes)); - } +// public static ClientTypeId ReadJson(ref Utf8JsonReader reader) +// { +// if (reader.TokenType == JsonTokenType.Null) return default; +// if (reader.TokenType != JsonTokenType.String) throw new JsonException("Expected string"); +// Span buffer = stackalloc byte[16]; +// var readBytes = reader.CopyString(buffer); +// return readBytes == 16 ? new ClientTypeId(buffer, default(Utf8StringCtor)) : new ClientTypeId(false, buffer.Slice(0, readBytes)); +// } - public override string ToString() - { - if (IsEmpty) return "[Empty]"; - if (IsHash) - return Convert.ToBase64String(Data -#if !DotNetCore - .ToArray() -#endif - ); - else - return StringUtils.Utf8Decode(Data); - } - public override int GetHashCode() => (a, b).GetHashCode(); - public override bool Equals(object? obj) => obj is ClientTypeId id && id.a == a && id.b == b; - public bool Equals(ClientTypeId other) => other.a == a && other.b == b; - public int CompareTo(ClientTypeId other) => a == other.a ? b.CompareTo(other.b) : a.CompareTo(other.a); - public static bool operator ==(ClientTypeId left, ClientTypeId right) => left.Equals(right); - public static bool operator !=(ClientTypeId left, ClientTypeId right) => !left.Equals(right); - } -} +// public static bool TryReadJson(ref Utf8JsonReader reader, out ClientTypeId output) +// { +// if (reader.TokenType == JsonTokenType.Null) +// { +// output = default; +// return true; +// } +// if (reader.TokenType != JsonTokenType.String) +// { +// output = default; +// return false; +// } +// const int maxLength = 16 * 6; // JsonConstants.MaxExpansionFactorWhileEscaping +// if (reader.GetValueLength() > maxLength) +// { +// output = default; +// return false; +// } +// Span buffer = stackalloc byte[maxLength]; +// var readBytes = reader.CopyString(buffer); +// if (readBytes > 16) +// { +// output = default; +// return false; +// } +// output = readBytes == 16 ? new ClientTypeId(buffer, default(Utf8StringCtor)) : new ClientTypeId(false, buffer.Slice(0, readBytes)); +// return true; +// } + +// public override string ToString() +// { +// if (IsEmpty) return "[Empty]"; +// if (IsHash) +// return Convert.ToBase64String(Data +// #if !DotNetCore +// .ToArray() +// #endif +// ); +// else +// return StringUtils.Utf8Decode(Data); +// } +// public override int GetHashCode() => (a, b).GetHashCode(); +// public override bool Equals(object? obj) => obj is ClientTypeId id && id.a == a && id.b == b; +// public bool Equals(ClientTypeId other) => other.a == a && other.b == b; +// public int CompareTo(ClientTypeId other) => a == other.a ? b.CompareTo(other.b) : a.CompareTo(other.a); +// public static bool operator ==(ClientTypeId left, ClientTypeId right) => left.Equals(right); +// public static bool operator !=(ClientTypeId left, ClientTypeId right) => !left.Equals(right); +// } +// } diff --git a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs index 59fa0ce5b7..b3e1292f82 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs @@ -80,19 +80,20 @@ public string SerializeViewModel(IDotvvmRequestContext context, object? commandR // context.ViewModelJson ??= new JObject(); // if (SendDiff && context.ReceivedViewModelJson?["viewModel"] is JObject receivedVM && context.ViewModelJson["viewModel"] is JObject responseVM) // { + // TODO: revive diffs // context.ViewModelJson["viewModelDiff"] = JsonUtils.Diff(receivedVM, responseVM, false, i => ShouldIncludeProperty(i.TypeId, i.Property)); // context.ViewModelJson.Remove("viewModel"); // } var requestTracers = context.Services.GetService>(); - requestTracers?.TracingSerialized(context, (int)utf8json.Length, () => utf8json.CloneReadOnly()); + requestTracers?.TracingSerialized(context, (int)utf8json.Length, utf8json); var result = StringUtils.Utf8Decode(utf8json.ToSpan()); var routeLabel = context.RouteLabel(); var requestType = context.RequestTypeLabel(); - DotvvmMetrics.ViewModelStringificationTime.Record(timer.ElapsedSeconds, routeLabel, requestType); + DotvvmMetrics.ViewModelSerializationTime.Record(timer.ElapsedSeconds, routeLabel, requestType); DotvvmMetrics.ViewModelSize.Record(utf8json.Length, routeLabel, requestType); - return result; + return result; // TODO: write Utf-8 directly } private bool? ShouldIncludeProperty(string typeId, string property) @@ -158,8 +159,6 @@ public string SerializeViewModel(IDotvvmRequestContext context, object? commandR /// public MemoryStream BuildViewModel(IDotvvmRequestContext context, object? commandResult, IEnumerable<(string name, string html)>? postbackUpdatedControls = null, bool serializeNewResources = false) { - var timer = ValueStopwatch.StartNew(); - (int, int) viewModelBodyPosition; var buffer = new MemoryStream(); @@ -228,8 +227,6 @@ public MemoryStream BuildViewModel(IDotvvmRequestContext context, object? comman writer.WriteEndObject(); } - DotvvmMetrics.ViewModelSerializationTime.Record(timer.ElapsedSeconds, context.RouteLabel(), context.RequestTypeLabel()); - return buffer; } diff --git a/src/Samples/Common/TextFileDiagnosticsInformationSender.cs b/src/Samples/Common/TextFileDiagnosticsInformationSender.cs index 549cf03941..d4dac7c0b1 100644 --- a/src/Samples/Common/TextFileDiagnosticsInformationSender.cs +++ b/src/Samples/Common/TextFileDiagnosticsInformationSender.cs @@ -16,6 +16,8 @@ public class TextFileDiagnosticsInformationSender : IDiagnosticsInformationSende private readonly string logFilePath; private readonly object locker = new object(); + public DiagnosticsInformationSenderState State => DiagnosticsInformationSenderState.TimingOnly; + public TextFileDiagnosticsInformationSender(DotvvmConfiguration config) { this.config = config; diff --git a/src/Tests/Runtime/ErrorPageTests.cs b/src/Tests/Runtime/ErrorPageTests.cs index a29ca9a234..fa108e3dcb 100644 --- a/src/Tests/Runtime/ErrorPageTests.cs +++ b/src/Tests/Runtime/ErrorPageTests.cs @@ -10,6 +10,13 @@ using DotVVM.Framework.Hosting; using DotVVM.Framework.Testing; using System.IO; +using DotVVM.Framework.Controls; +using CheckTestOutput; +using DotVVM.Framework.Compilation.ControlTree; +using DotVVM.Framework.Compilation; +using DotVVM.Framework.Compilation.ControlTree.Resolved; +using System.Reflection; +using System.Linq.Expressions; namespace DotVVM.Framework.Tests.Runtime { @@ -17,6 +24,7 @@ namespace DotVVM.Framework.Tests.Runtime public class ErrorPageTests { static DotvvmConfiguration config = DotvvmTestHelper.DefaultConfig; + OutputChecker check = new OutputChecker("testoutputs"); ErrorFormatter formatter = CreateFormatter(); BindingCompilationService bcs = config.ServiceProvider.GetService().WithoutInitialization(); IDotvvmRequestContext context = DotvvmTestHelper.CreateContext(config); @@ -59,5 +67,79 @@ public void InvalidBindingException() // the exception contains the property name StringAssert.Contains(tt, "DotVVM.Framework.Binding.Properties.KnockoutExpressionBindingProperty"); } + + [TestMethod] + public void SerializationDotvvmProperties() + { + var obj = new { + normal = DotvvmControl.IncludeInPageProperty, + interfaceType = (IControlAttributeDescriptor)DotvvmControl.IncludeInPageProperty, + alias = DotvvmPropertyTests.TestObject.AliasProperty, + withFallback = Button.EnabledProperty, + capability = Button.TextOrContentCapabilityProperty, + group = HtmlGenericControl.AttributesGroupDescriptor, + groupMember = HtmlGenericControl.AttributesGroupDescriptor.GetDotvvmProperty("class"), + }; + check.CheckJsonObject(ErrorPageTemplate.SerializeObjectForBrowser(obj)); + } + + [TestMethod] + public void SerializationReflectionType() + { + var obj = new { + plainType = typeof(string), + plainTypeObj = (object)typeof(string), + plainTypeInterface = (ICustomAttributeProvider)typeof(string), + nullableType = typeof(int?), + genericType = typeof(List), + + typeDescriptor = ResolvedTypeDescriptor.Create(typeof(string)), + typeDescriptorInterface = (ITypeDescriptor)ResolvedTypeDescriptor.Create(typeof(string)), + typeDescriptorObj = (object)ResolvedTypeDescriptor.Create(typeof(string)), + }; + check.CheckJsonObject(ErrorPageTemplate.SerializeObjectForBrowser(obj)); + } + + [TestMethod] + public void SerializationReflectionAssembly() + { + var obj = new { + assembly = typeof(string).Assembly, + assemblyObj = (object)typeof(string).Assembly, + assemblyInterface = (ICustomAttributeProvider)typeof(string).Assembly, + }; + check.CheckJsonObject(ErrorPageTemplate.SerializeObjectForBrowser(obj)); + } + + [TestMethod] + public void SerializationDelegates() + { + var obj = new { + func = new Func(() => 42), + funcObj = (object)new Func(() => 42), + dynamicMethod = Expression.Lambda>(Expression.Constant(42)).Compile(), + }; + check.CheckJsonObject(ErrorPageTemplate.SerializeObjectForBrowser(obj)); + } + + [TestMethod] + public void SerializationDotvvmControls() + { + var dataContext = DataContextStack.Create(typeof(string)); + var bindings = config.ServiceProvider.GetService(); + var control = new HtmlGenericControl("span") + .SetAttribute("data-test", "value") + .AddCssClass("my-class", bindings.Cache.CreateValueBinding("1 + 1 == 2", dataContext)); + var obj = new { + control = control, + controlBase = (DotvvmBindableObject)control, + controlBase2 = (DotvvmControl)control, + controlObj = (object)control, + controlInterface = (IDotvvmObjectLike)control, + controlInterface2 = (IControlWithHtmlAttributes)control, + controlInterface3 = (IObjectWithCapability)control, + }; + check.CheckJsonObject(ErrorPageTemplate.SerializeObjectForBrowser(obj)); + } } } diff --git a/src/Tests/Runtime/testoutputs/ErrorPageTests.SerializationDelegates.json b/src/Tests/Runtime/testoutputs/ErrorPageTests.SerializationDelegates.json new file mode 100644 index 0000000000..a098b34b1e --- /dev/null +++ b/src/Tests/Runtime/testoutputs/ErrorPageTests.SerializationDelegates.json @@ -0,0 +1,5 @@ +{ + "func": "[delegate System.Func]", + "funcObj": "[delegate System.Func]", + "dynamicMethod": "[delegate System.Func]" +} diff --git a/src/Tests/Runtime/testoutputs/ErrorPageTests.SerializationDotvvmControls.json b/src/Tests/Runtime/testoutputs/ErrorPageTests.SerializationDotvvmControls.json new file mode 100644 index 0000000000..4ee578a100 --- /dev/null +++ b/src/Tests/Runtime/testoutputs/ErrorPageTests.SerializationDotvvmControls.json @@ -0,0 +1,55 @@ +{ + "control": { + "Control": "HtmlGenericControl", + "Properties": { + "Attributes:data-test": "value", + "CssClasses:my-class": "{value: 1 + 1 == 2}" + }, + "LifecycleRequirements": "None" + }, + "controlBase": { + "Control": "HtmlGenericControl", + "Properties": { + "Attributes:data-test": "value", + "CssClasses:my-class": "{value: 1 + 1 == 2}" + }, + "LifecycleRequirements": "None" + }, + "controlBase2": { + "Control": "HtmlGenericControl", + "Properties": { + "Attributes:data-test": "value", + "CssClasses:my-class": "{value: 1 + 1 == 2}" + }, + "LifecycleRequirements": "None" + }, + "controlObj": { + "Control": "HtmlGenericControl", + "Properties": { + "Attributes:data-test": "value", + "CssClasses:my-class": "{value: 1 + 1 == 2}" + }, + "LifecycleRequirements": "None" + }, + "controlInterface": { + "Control": "HtmlGenericControl", + "Properties": { + "Attributes:data-test": "value", + "CssClasses:my-class": "{value: 1 + 1 == 2}" + }, + "LifecycleRequirements": "None" + }, + "controlInterface2": { + "Attributes": { + "data-test": "value" + } + }, + "controlInterface3": { + "Control": "HtmlGenericControl", + "Properties": { + "Attributes:data-test": "value", + "CssClasses:my-class": "{value: 1 + 1 == 2}" + }, + "LifecycleRequirements": "None" + } +} diff --git a/src/Tests/Runtime/testoutputs/ErrorPageTests.SerializationDotvvmProperties.json b/src/Tests/Runtime/testoutputs/ErrorPageTests.SerializationDotvvmProperties.json new file mode 100644 index 0000000000..9e145c402e --- /dev/null +++ b/src/Tests/Runtime/testoutputs/ErrorPageTests.SerializationDotvvmProperties.json @@ -0,0 +1,9 @@ +{ + "normal": "DotvvmControl.IncludeInPage", + "interfaceType": "DotvvmControl.IncludeInPage", + "alias": "TestObject.Alias", + "withFallback": "ButtonBase.Enabled", + "capability": "ButtonBase.TextOrContentCapability", + "group": "DotVVM.Framework.Compilation.ControlTree.DotvvmPropertyGroup", + "groupMember": "HtmlGenericControl.Attributes:class" +} diff --git a/src/Tests/Runtime/testoutputs/ErrorPageTests.SerializationReflectionAssembly.json b/src/Tests/Runtime/testoutputs/ErrorPageTests.SerializationReflectionAssembly.json new file mode 100644 index 0000000000..7725a8739d --- /dev/null +++ b/src/Tests/Runtime/testoutputs/ErrorPageTests.SerializationReflectionAssembly.json @@ -0,0 +1,5 @@ +{ + "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e", + "assemblyObj": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e", + "assemblyInterface": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e" +} diff --git a/src/Tests/Runtime/testoutputs/ErrorPageTests.SerializationReflectionType.json b/src/Tests/Runtime/testoutputs/ErrorPageTests.SerializationReflectionType.json new file mode 100644 index 0000000000..9900f3af1a --- /dev/null +++ b/src/Tests/Runtime/testoutputs/ErrorPageTests.SerializationReflectionType.json @@ -0,0 +1,10 @@ +{ + "plainType": "string", + "plainTypeObj": "string", + "plainTypeInterface": "System.String", + "nullableType": "int?", + "genericType": "System.Collections.Generic.List", + "typeDescriptor": "System.String", + "typeDescriptorInterface": "System.String", + "typeDescriptorObj": "System.String" +} diff --git a/src/Tracing/ApplicationInsights/ApplicationInsightsTracer.cs b/src/Tracing/ApplicationInsights/ApplicationInsightsTracer.cs index 13c9d7115c..f9cc255dd9 100644 --- a/src/Tracing/ApplicationInsights/ApplicationInsightsTracer.cs +++ b/src/Tracing/ApplicationInsights/ApplicationInsightsTracer.cs @@ -40,7 +40,7 @@ public Task EndRequest(IDotvvmRequestContext context, Exception exception) return TaskUtils.GetCompletedTask(); } - public void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Lazy viewModelBuffer) + public void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Func viewModelBuffer) { } } diff --git a/src/Tracing/MiniProfiler.Shared/MiniProfilerTracer.cs b/src/Tracing/MiniProfiler.Shared/MiniProfilerTracer.cs index ab2f4bb798..b7a2b52dd5 100644 --- a/src/Tracing/MiniProfiler.Shared/MiniProfilerTracer.cs +++ b/src/Tracing/MiniProfiler.Shared/MiniProfilerTracer.cs @@ -79,7 +79,7 @@ public Timing StepIf(string name, long minDuration, bool includeChildren = false return GetProfilerCurrent().StepIf(name, minDuration, includeChildren); } - public void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Lazy viewModelBuffer) + public void ViewModelSerialized(IDotvvmRequestContext context, int viewModelSize, Func viewModelBuffer) { } } From aad5dfc6e13a75de251b07bd06de60d82b27f26c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Wed, 24 Apr 2024 12:49:56 +0200 Subject: [PATCH 09/20] Remove Newtonsoft.Json.Linq usage from unit tests --- .../SwaggerFileGenerationTests.cs | 1 - .../Filters/AddTypeToModelSchemaFilter.cs | 1 - .../Expressions/BindingDebugJsonConverter.cs | 36 ++++++++++--------- .../Framework/Diagnostics/JsonSizeAnalyzer.cs | 1 - src/Samples/Api.Common/Model/Order.cs | 1 - src/Samples/Api.Common/Model/OrderItem.cs | 3 +- .../NestedGridViewInlineEditingViewModel.cs | 1 - .../GridView/RenamedPrimaryKeyViewModel.cs | 1 - .../Binding/StaticCommandExecutorTests.cs | 1 - src/Tests/ControlTests/CommandTests.cs | 1 - .../ConfigurationSerializationTests.cs | 18 +++++----- .../ControlTree/ServerSideStyleTests.cs | 1 - src/Tests/Runtime/RuntimeErrorTests.cs | 1 - 13 files changed, 30 insertions(+), 37 deletions(-) diff --git a/src/Api/Swashbuckle.AspNetCore.Tests/SwaggerFileGenerationTests.cs b/src/Api/Swashbuckle.AspNetCore.Tests/SwaggerFileGenerationTests.cs index 3dd2cf4616..81e2dfc802 100644 --- a/src/Api/Swashbuckle.AspNetCore.Tests/SwaggerFileGenerationTests.cs +++ b/src/Api/Swashbuckle.AspNetCore.Tests/SwaggerFileGenerationTests.cs @@ -17,7 +17,6 @@ using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; diff --git a/src/Api/Swashbuckle.AspNetCore/Filters/AddTypeToModelSchemaFilter.cs b/src/Api/Swashbuckle.AspNetCore/Filters/AddTypeToModelSchemaFilter.cs index 8065dc07db..8bf4296a39 100644 --- a/src/Api/Swashbuckle.AspNetCore/Filters/AddTypeToModelSchemaFilter.cs +++ b/src/Api/Swashbuckle.AspNetCore/Filters/AddTypeToModelSchemaFilter.cs @@ -8,7 +8,6 @@ using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Writers; -using Newtonsoft.Json; using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; diff --git a/src/Framework/Framework/Binding/Expressions/BindingDebugJsonConverter.cs b/src/Framework/Framework/Binding/Expressions/BindingDebugJsonConverter.cs index c236ae50d1..d006b64d56 100644 --- a/src/Framework/Framework/Binding/Expressions/BindingDebugJsonConverter.cs +++ b/src/Framework/Framework/Binding/Expressions/BindingDebugJsonConverter.cs @@ -2,28 +2,30 @@ using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; +using DotVVM.Framework.ResourceManagement; namespace DotVVM.Framework.Binding.Expressions { - internal class BindingDebugJsonConverter: JsonConverter - { - public override IBinding Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => - throw new NotImplementedException("Deserializing dotvvm bindings from JSON is not supported."); - public override void Write(Utf8JsonWriter writer, IBinding obj, JsonSerializerOptions options) + internal class BindingDebugJsonConverter(bool detailed): GenericWriterJsonConverter((writer, obj, options) => { + if (detailed) + { + writer.WriteStartObject(); + writer.WriteString("ToString"u8, obj.ToString()); + var props = (obj as ICloneableBinding)?.GetAllComputedProperties() ?? Enumerable.Empty(); + foreach (var p in props) + { + var name = p.GetType().Name; + writer.WritePropertyName(name); + JsonSerializer.Serialize(writer, p, options); + } + writer.WriteEndObject(); + } + else { writer.WriteStringValue(obj?.ToString()); - - // w.WriteStartObject(); - // w.WritePropertyName("ToString"); - // w.WriteValue(obj.ToString()); - // var props = (obj as ICloneableBinding)?.GetAllComputedProperties() ?? Enumerable.Empty(); - // foreach (var p in props) - // { - // var name = p.GetType().Name; - // w.WritePropertyName(name); - // serializer.Serialize(w, p); - // } - // w.WriteEndObject(); } + }) + { + public BindingDebugJsonConverter() : this(false) { } } } diff --git a/src/Framework/Framework/Diagnostics/JsonSizeAnalyzer.cs b/src/Framework/Framework/Diagnostics/JsonSizeAnalyzer.cs index 43042706f5..5a0245b915 100644 --- a/src/Framework/Framework/Diagnostics/JsonSizeAnalyzer.cs +++ b/src/Framework/Framework/Diagnostics/JsonSizeAnalyzer.cs @@ -7,7 +7,6 @@ using DotVVM.Framework.Utils; using DotVVM.Framework.ViewModel.Serialization; using FastExpressionCompiler; -// using Newtonsoft.Json.Linq; namespace DotVVM.Framework.Diagnostics { diff --git a/src/Samples/Api.Common/Model/Order.cs b/src/Samples/Api.Common/Model/Order.cs index 134e433e76..01187de269 100644 --- a/src/Samples/Api.Common/Model/Order.cs +++ b/src/Samples/Api.Common/Model/Order.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Newtonsoft.Json; namespace DotVVM.Samples.BasicSamples.Api.Common.Model { diff --git a/src/Samples/Api.Common/Model/OrderItem.cs b/src/Samples/Api.Common/Model/OrderItem.cs index bbd174ecbd..f5e8e1bc39 100644 --- a/src/Samples/Api.Common/Model/OrderItem.cs +++ b/src/Samples/Api.Common/Model/OrderItem.cs @@ -1,5 +1,4 @@ -using Newtonsoft.Json; - + namespace DotVVM.Samples.BasicSamples.Api.Common.Model { public class OrderItem diff --git a/src/Samples/Common/ViewModels/ControlSamples/GridView/NestedGridViewInlineEditingViewModel.cs b/src/Samples/Common/ViewModels/ControlSamples/GridView/NestedGridViewInlineEditingViewModel.cs index 767bc0071f..2f7b0aa10b 100644 --- a/src/Samples/Common/ViewModels/ControlSamples/GridView/NestedGridViewInlineEditingViewModel.cs +++ b/src/Samples/Common/ViewModels/ControlSamples/GridView/NestedGridViewInlineEditingViewModel.cs @@ -6,7 +6,6 @@ using DotVVM.Framework.Compilation.ControlTree; using DotVVM.Framework.Controls; using DotVVM.Framework.ViewModel; -using Newtonsoft.Json; namespace DotVVM.Samples.BasicSamples.ViewModels.ControlSamples.GridView { diff --git a/src/Samples/Common/ViewModels/ControlSamples/GridView/RenamedPrimaryKeyViewModel.cs b/src/Samples/Common/ViewModels/ControlSamples/GridView/RenamedPrimaryKeyViewModel.cs index 9804139407..bd37e900e8 100644 --- a/src/Samples/Common/ViewModels/ControlSamples/GridView/RenamedPrimaryKeyViewModel.cs +++ b/src/Samples/Common/ViewModels/ControlSamples/GridView/RenamedPrimaryKeyViewModel.cs @@ -4,7 +4,6 @@ using System.Text; using System.Threading.Tasks; using DotVVM.Framework.Controls; -using Newtonsoft.Json; namespace DotVVM.Samples.BasicSamples.ViewModels.ControlSamples.GridView { diff --git a/src/Tests/Binding/StaticCommandExecutorTests.cs b/src/Tests/Binding/StaticCommandExecutorTests.cs index 10fe3884a6..fc417ff1a6 100644 --- a/src/Tests/Binding/StaticCommandExecutorTests.cs +++ b/src/Tests/Binding/StaticCommandExecutorTests.cs @@ -14,7 +14,6 @@ using DotVVM.Framework.ViewModel; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json.Linq; using static DotVVM.Framework.Testing.DotvvmTestHelper; namespace DotVVM.Framework.Tests.Binding diff --git a/src/Tests/ControlTests/CommandTests.cs b/src/Tests/ControlTests/CommandTests.cs index a130b2049e..27a0856871 100644 --- a/src/Tests/ControlTests/CommandTests.cs +++ b/src/Tests/ControlTests/CommandTests.cs @@ -2,7 +2,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using DotVVM.Framework.Testing; using System.Threading.Tasks; -using Newtonsoft.Json; using Microsoft.Extensions.DependencyInjection; using DotVVM.Framework.ViewModel; diff --git a/src/Tests/Runtime/ConfigurationSerializationTests.cs b/src/Tests/Runtime/ConfigurationSerializationTests.cs index f502e6d0fe..25af7626a4 100644 --- a/src/Tests/Runtime/ConfigurationSerializationTests.cs +++ b/src/Tests/Runtime/ConfigurationSerializationTests.cs @@ -11,8 +11,10 @@ using DotVVM.Framework.ResourceManagement; using DotVVM.Framework.Testing; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json.Linq; using Microsoft.Extensions.DependencyInjection; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Encodings.Web; namespace DotVVM.Framework.Tests.Runtime { @@ -43,19 +45,19 @@ void checkConfig(DotvvmConfiguration config, bool includeProperties = false, str Console.WriteLine(serialized); - var jobject = JObject.Parse(serialized); - void removeTestStuff(JToken token) + var jobject = JsonNode.Parse(serialized).AsObject(); + void removeTestStuff(JsonNode token) { - if (token is JObject obj) - foreach (var testControl in obj.Properties().Where(p => p.Name.Contains(".Tests.")).ToArray()) - testControl.Remove(); + if (token is JsonObject obj) + foreach (var testControl in obj.Where(p => p.Key.Contains(".Tests.")).ToArray()) + obj.Remove(testControl.Key); } removeTestStuff(jobject["properties"]); removeTestStuff(jobject["propertyGroups"]); removeTestStuff(jobject["capabilities"]); removeTestStuff(jobject["controls"]); - jobject["assemblies"]?.Parent.Remove(); // there are user specific paths - check.CheckString(jobject.ToString(), checkName, fileExtension, memberName, sourceFilePath); + jobject.Remove("assemblies"); // there are user specific paths + check.CheckString(jobject.ToJsonString(new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, WriteIndented = true }), checkName, fileExtension, memberName, sourceFilePath); } [TestMethod] diff --git a/src/Tests/Runtime/ControlTree/ServerSideStyleTests.cs b/src/Tests/Runtime/ControlTree/ServerSideStyleTests.cs index 286da08142..dfb54409a4 100644 --- a/src/Tests/Runtime/ControlTree/ServerSideStyleTests.cs +++ b/src/Tests/Runtime/ControlTree/ServerSideStyleTests.cs @@ -18,7 +18,6 @@ using DotVVM.Framework.ResourceManagement; using DotVVM.Framework.ViewModel; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json.Linq; namespace DotVVM.Framework.Tests.Runtime.ControlTree { diff --git a/src/Tests/Runtime/RuntimeErrorTests.cs b/src/Tests/Runtime/RuntimeErrorTests.cs index 9d17903928..aabc23d5bc 100644 --- a/src/Tests/Runtime/RuntimeErrorTests.cs +++ b/src/Tests/Runtime/RuntimeErrorTests.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json; using DotVVM.Framework.Configuration; using DotVVM.Framework.Hosting; using DotVVM.Framework.ResourceManagement; From 1bafb79f6a21b4d1e975d73e23f93c7a93cee07b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Wed, 24 Apr 2024 22:30:43 +0200 Subject: [PATCH 10/20] STJ migration: Add support for field serialization for ValueTuple --- ...urceViewModelValidationMetadataProvider.cs | 8 +-- .../Metadata/PropertyDisplayMetadata.cs | 4 +- ...urceViewModelValidationMetadataProvider.cs | 7 ++- .../ViewModel/DefaultPropertySerialization.cs | 2 +- .../Core/ViewModel/IPropertySerialization.cs | 2 +- .../Javascript/JsViewModelPropertyAdjuster.cs | 2 +- .../Serialization/ViewModelJsonConverter.cs | 1 - .../Serialization/ViewModelPropertyMap.cs | 16 ++++- .../ViewModelSerializationMap.cs | 35 ++++++----- .../ViewModelSerializationMapper.cs | 58 ++++++++++++------- ...buteViewModelValidationMetadataProvider.cs | 2 +- .../Validation/IValidationRuleTranslator.cs | 2 +- .../IViewModelValidationMetadataProvider.cs | 2 +- .../Validation/ValidationErrorPathExpander.cs | 2 +- .../ViewModelValidationRuleTranslator.cs | 5 +- .../Validation/ViewModelValidator.cs | 3 +- src/Tests/ViewModel/SerializerTests.cs | 7 ++- 17 files changed, 101 insertions(+), 57 deletions(-) diff --git a/src/AutoUI/Core/Metadata/ResourceViewModelValidationMetadataProvider.cs b/src/AutoUI/Core/Metadata/ResourceViewModelValidationMetadataProvider.cs index 96614e638e..70b5d2ea60 100644 --- a/src/AutoUI/Core/Metadata/ResourceViewModelValidationMetadataProvider.cs +++ b/src/AutoUI/Core/Metadata/ResourceViewModelValidationMetadataProvider.cs @@ -16,7 +16,7 @@ namespace DotVVM.AutoUI.Metadata public class ResourceViewModelValidationMetadataProvider : IViewModelValidationMetadataProvider { private readonly IViewModelValidationMetadataProvider baseValidationMetadataProvider; - private readonly ConcurrentDictionary> cache = new(); + private readonly ConcurrentDictionary cache = new(); private readonly ResourceManager errorMessages; private static readonly FieldInfo internalErrorMessageField; @@ -43,7 +43,7 @@ static ResourceViewModelValidationMetadataProvider() /// /// Gets validation attributes for the specified property. /// - public IEnumerable GetAttributesForProperty(PropertyInfo property) + public IEnumerable GetAttributesForProperty(MemberInfo property) { return cache.GetOrAdd(property, GetAttributesForPropertyCore); } @@ -51,7 +51,7 @@ public IEnumerable GetAttributesForProperty(PropertyInfo pr /// /// Determines validation attributes for the specified property and loads missing error messages from the resource file. /// - private List GetAttributesForPropertyCore(PropertyInfo property) + private ValidationAttribute[] GetAttributesForPropertyCore(MemberInfo property) { // process all validation attributes var results = new List(); @@ -73,7 +73,7 @@ private List GetAttributesForPropertyCore(PropertyInfo prop } } - return results; + return results.Count == 0 ? Array.Empty() : results.ToArray(); } private bool HasDefaultErrorMessage(ValidationAttribute attribute) diff --git a/src/DynamicData/DynamicData/Metadata/PropertyDisplayMetadata.cs b/src/DynamicData/DynamicData/Metadata/PropertyDisplayMetadata.cs index 0b2f2da2d6..cfc276f9cc 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 PropertyInfo PropertyInfo { get; set; } + public MemberInfo PropertyInfo { get; set; } public string DisplayName { get; set; } @@ -28,4 +28,4 @@ public class PropertyDisplayMetadata public StyleAttribute Styles { get; set; } public bool IsEditAllowed { get; set; } } -} \ No newline at end of file +} diff --git a/src/DynamicData/DynamicData/Metadata/ResourceViewModelValidationMetadataProvider.cs b/src/DynamicData/DynamicData/Metadata/ResourceViewModelValidationMetadataProvider.cs index 595fc5470d..96d53cbacd 100644 --- a/src/DynamicData/DynamicData/Metadata/ResourceViewModelValidationMetadataProvider.cs +++ b/src/DynamicData/DynamicData/Metadata/ResourceViewModelValidationMetadataProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; +using System.Linq; using System.Reflection; using System.Resources; using System.Threading; @@ -40,8 +41,12 @@ public ResourceViewModelValidationMetadataProvider(Type errorMessagesResourceFil /// /// Gets validation attributes for the specified property. /// - public IEnumerable GetAttributesForProperty(PropertyInfo property) + public IEnumerable GetAttributesForProperty(MemberInfo member) { + if (member is not PropertyInfo property) + { + return []; + } return cache.GetOrAdd(new PropertyInfoCulturePair(CultureInfo.CurrentUICulture, property), GetAttributesForPropertyCore); } diff --git a/src/Framework/Core/ViewModel/DefaultPropertySerialization.cs b/src/Framework/Core/ViewModel/DefaultPropertySerialization.cs index ae6d73a24a..9fcf9a9480 100644 --- a/src/Framework/Core/ViewModel/DefaultPropertySerialization.cs +++ b/src/Framework/Core/ViewModel/DefaultPropertySerialization.cs @@ -8,7 +8,7 @@ public class DefaultPropertySerialization : IPropertySerialization { static readonly Type? JsonPropertyNJ = Type.GetType("Newtonsoft.Json.JsonPropertyAttribute, Newtonsoft.Json"); static readonly PropertyInfo? JsonPropertyNJPropertyName = JsonPropertyNJ?.GetProperty("PropertyName"); - public string ResolveName(PropertyInfo propertyInfo) + public string ResolveName(MemberInfo propertyInfo) { var bindAttribute = propertyInfo.GetCustomAttribute(); if (bindAttribute != null) diff --git a/src/Framework/Core/ViewModel/IPropertySerialization.cs b/src/Framework/Core/ViewModel/IPropertySerialization.cs index c7f91b4873..accb2bf1cb 100644 --- a/src/Framework/Core/ViewModel/IPropertySerialization.cs +++ b/src/Framework/Core/ViewModel/IPropertySerialization.cs @@ -4,6 +4,6 @@ namespace DotVVM.Framework.ViewModel { public interface IPropertySerialization { - string ResolveName(PropertyInfo propertyInfo); + string ResolveName(MemberInfo propertyInfo); } } diff --git a/src/Framework/Framework/Compilation/Javascript/JsViewModelPropertyAdjuster.cs b/src/Framework/Framework/Compilation/Javascript/JsViewModelPropertyAdjuster.cs index 1b60fd096f..98efbea6fa 100644 --- a/src/Framework/Framework/Compilation/Javascript/JsViewModelPropertyAdjuster.cs +++ b/src/Framework/Framework/Compilation/Javascript/JsViewModelPropertyAdjuster.cs @@ -81,7 +81,7 @@ protected override void DefaultVisit(JsNode node) memberAccess.MemberName = propertyMap.Name; } } - else if (member is FieldInfo) + else if (member is FieldInfo && propAnnotation.SerializationMap?.IsAvailableOnClient() != true) throw new NotSupportedException($"Cannot translate field '{member}' to Javascript"); if (targetAnnotation is { IsControl: true } && diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs index e03d35b8a0..2372c999fa 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs @@ -32,7 +32,6 @@ public ViewModelJsonConverter(IViewModelSerializationMapper viewModelSerializati public static bool CanConvertType(Type type) => !ReflectionUtils.IsEnumerable(type) && ReflectionUtils.IsComplexType(type) && - !ReflectionUtils.IsTupleLike(type) && !ReflectionUtils.IsJsonDom(type) && type != typeof(object); diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelPropertyMap.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelPropertyMap.cs index 3dee1ac2d2..fc191a9af9 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelPropertyMap.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelPropertyMap.cs @@ -11,7 +11,7 @@ namespace DotVVM.Framework.ViewModel.Serialization { public class ViewModelPropertyMap { - public ViewModelPropertyMap(PropertyInfo propertyInfo, string name, ProtectMode viewModelProtection, Type type, bool transferToServer, bool transferAfterPostback, bool transferFirstRequest, bool populate) + public ViewModelPropertyMap(MemberInfo propertyInfo, string name, ProtectMode viewModelProtection, Type type, bool transferToServer, bool transferAfterPostback, bool transferFirstRequest, bool populate) { PropertyInfo = propertyInfo; Name = name; @@ -23,7 +23,8 @@ public ViewModelPropertyMap(PropertyInfo propertyInfo, string name, ProtectMode Populate = populate; } - public PropertyInfo PropertyInfo { get; set; } + /// The serialized property, or in rare cases the serialized field (when declared in ValueTuple`? or when explicitly marked with [Bind] attribute). + public MemberInfo PropertyInfo { get; set; } /// Property name, as seen in the serialized JSON and client-side. Note that it will be different than `PropertyInfo.Name`, if `[Bind(Name = X)]` or `[JsonPropertyName(X)]` is used. public string Name { get; set; } @@ -33,6 +34,7 @@ public ViewModelPropertyMap(PropertyInfo propertyInfo, string name, ProtectMode public ProtectMode ViewModelProtection { get; set; } + /// Type of the property public Type Type { get; set; } public Direction BindDirection { get; set; } = Direction.None; @@ -67,6 +69,16 @@ public bool IsFullyTransferred() { return TransferToServer && TransferToClient; } + + /// Returns the runtime property value using reflection + public object? GetValue(object obj) + { + return PropertyInfo switch { + PropertyInfo p => p.GetValue(obj), + FieldInfo f => f.GetValue(obj), + _ => throw new NotSupportedException() + }; + } public override string ToString() { diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs index 57f0948ada..b072977559 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs @@ -107,7 +107,7 @@ public override void ResetFunctions() /// /// Creates the constructor for this object. /// - private Expression CallConstructor(Expression services, Dictionary propertyVariables, bool throwImmediately = false) + private Expression CallConstructor(Expression services, Dictionary propertyVariables, bool throwImmediately = false) { if (constructorFactory != null) return Invoke(Constant(constructorFactory), services); @@ -142,9 +142,9 @@ private Expression CallConstructor(Expression services, Dictionary @@ -184,20 +184,20 @@ public ReaderDelegate CreateReaderFactory() var propertyVars = Properties .Where(p => p.TransferToServer) .ToDictionary( - p => p.PropertyInfo, + p => p, p => Expression.Variable(p.Type, "prop_" + p.Name) ); // If we have constructor property or if we have { get; init; } property, we always create new instance var alwaysCallConstructor = Properties.Any(p => p.TransferToServer && ( p.ConstructorParameter is {} || - p.PropertyInfo.IsInitOnly())); + (p.PropertyInfo as PropertyInfo)?.IsInitOnly() == true)); // We don't want to clone IDotvvmViewModel automatically, because the user is likely to register this specific instance somewhere if (alwaysCallConstructor && typeof(IDotvvmViewModel).IsAssignableFrom(Type) && Constructor is {} && !SerialiationMapperAttributeHelper.IsJsonConstructor(Constructor)) { var cloneReason = - Properties.FirstOrDefault(p => p.TransferToServer && p.PropertyInfo.IsInitOnly()) is {} initProperty + Properties.FirstOrDefault(p => p.TransferToServer && (p.PropertyInfo as PropertyInfo)?.IsInitOnly() == true) is {} initProperty ? $"init-only property {initProperty.Name} is transferred client → server" : Properties.FirstOrDefault(p => p.TransferToServer && p.ConstructorParameter is {}) is {} ctorProperty ? $"property {ctorProperty.Name} must be injected into constructor parameter {ctorProperty.ConstructorParameter!.Name}" : "internal bug"; @@ -223,8 +223,8 @@ public ReaderDelegate CreateReaderFactory() Type.IsValueType ? Constant(true) : NotEqual(value, Constant(null)), Block( propertyVars - .Where(p => p.Key.GetMethod is not null) - .Select(p => Expression.Assign(p.Value, Expression.Property(value, p.Key))) + .Where(p => p.Key.PropertyInfo is not PropertyInfo { GetMethod: null }) + .Select(p => Expression.Assign(p.Value, MemberAccess(value, p.Key))) ) )); } @@ -243,7 +243,7 @@ public ReaderDelegate CreateReaderFactory() { continue; } - var propertyVar = propertyVars[property.PropertyInfo]; + var propertyVar = propertyVars[property]; var existingValue = property.Populate ? @@ -357,8 +357,8 @@ public ReaderDelegate CreateReaderFactory() { var propertySettingExpressions = Properties - .Where(p => p is { PropertyInfo.SetMethod: not null, ConstructorParameter: null, TransferToServer: true }) - .Select(p => Assign(Property(value, p.PropertyInfo), propertyVars[p.PropertyInfo])) + .Where(p => p is { ConstructorParameter: null, TransferToServer: true, PropertyInfo: PropertyInfo { SetMethod: not null } or FieldInfo { IsInitOnly: false } }) + .Select(p => Assign(MemberAccess(value, p), propertyVars[p])) .ToList(); if (propertySettingExpressions.Any()) @@ -377,6 +377,15 @@ public ReaderDelegate CreateReaderFactory() return ex.CompileFast(flags: CompilerFlags.ThrowOnNotSupportedExpression | CompilerFlags.EnableDelegateDebugInfo); } + Expression MemberAccess(Expression obj, ViewModelPropertyMap property) + { + if (property.PropertyInfo is PropertyInfo pi) + return Property(obj, pi); + if (property.PropertyInfo is FieldInfo fi) + return Field(obj, fi); + throw new NotSupportedException(); + } + /// /// Creates the writer factory. /// @@ -405,7 +414,7 @@ public WriterDelegate CreateWriterFactory() var property = Properties[propertyIndex]; var endPropertyLabel = Expression.Label("end_property_" + property.Name); - if (property.TransferToClient && property.PropertyInfo.GetMethod != null) + if (property.TransferToClient && property.PropertyInfo is not PropertyInfo { GetMethod: null }) { if (property.TransferFirstRequest != property.TransferAfterPostback) { @@ -424,7 +433,7 @@ public WriterDelegate CreateWriterFactory() } // (object)value.{property.PropertyInfo.Name} - var prop = Expression.Property(value, property.PropertyInfo); + var prop = MemberAccess(value, property); var writeEV = property.ViewModelProtection == ProtectMode.EncryptData || property.ViewModelProtection == ProtectMode.SignData; diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs index 571a5bb686..57d868a913 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs @@ -75,6 +75,14 @@ protected virtual ViewModelSerializationMap CreateMap() if (type.GetConstructor(Type.EmptyTypes) is {} emptyCtor) return emptyCtor; + if (ReflectionUtils.IsTupleLike(type)) + { + var ctors = type.GetConstructors(); + if (ctors.Length == 1) + return ctors[0]; + else + throw new NotSupportedException($"Type {type.FullName} is a tuple-like type, but it has {ctors.Length} constructors."); + } return GetRecordConstructor(type); } @@ -115,11 +123,30 @@ protected virtual ViewModelSerializationMap CreateMap() protected virtual IEnumerable GetProperties(Type type, MethodBase? constructor) { var ctorParams = constructor?.GetParameters().ToDictionary(p => p.Name.NotNull(), StringComparer.OrdinalIgnoreCase); - var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + var properties = type.GetAllMembers(BindingFlags.Public | BindingFlags.Instance) + .Where(m => m is PropertyInfo or FieldInfo) + .ToArray(); Array.Sort(properties, (a, b) => StringComparer.Ordinal.Compare(a.Name, b.Name)); - foreach (var property in properties) + foreach (MemberInfo property in properties) { - if (SerialiationMapperAttributeHelper.IsJsonIgnore(property)) continue; + var bindAttribute = property.GetCustomAttribute(); + var include = !SerialiationMapperAttributeHelper.IsJsonIgnore(property) && bindAttribute is not { Direction: Direction.None }; + if (property is FieldInfo) + { + // fields are ignored by default, unless marked with [Bind(not None)], [JsonInclude] or defined in ValueTuple<...> + include = include || + !(bindAttribute is null or { Direction: Direction.None }) || + property.IsDefined(typeof(JsonIncludeAttribute)) || + (property.DeclaringType.IsGenericType && property.DeclaringType.FullName.StartsWith("System.ValueTuple`")); + } + if (!include) continue; + + var (propertyType, canGet, canSet) = property switch { + PropertyInfo p => (p.PropertyType, p.GetMethod is { IsPublic: true }, p.SetMethod is { IsPublic: true }), + FieldInfo f => (f.FieldType, true, !f.IsInitOnly && !f.IsLiteral), + _ => throw new NotSupportedException() + }; var ctorParam = ctorParams?.GetValueOrDefault(property.Name); @@ -127,22 +154,21 @@ protected virtual IEnumerable GetProperties(Type type, Met property, propertySerialization.ResolveName(property), ProtectMode.None, - property.PropertyType, - transferToServer: ctorParam is {} || IsSetterSupported(property), - transferAfterPostback: property.GetMethod != null && property.GetMethod.IsPublic, - transferFirstRequest: property.GetMethod != null && property.GetMethod.IsPublic, - populate: (ViewModelJsonConverter.CanConvertType(property.PropertyType) || property.PropertyType == typeof(object)) && property.GetMethod != null + propertyType, + transferToServer: ctorParam is {} || canSet, + transferAfterPostback: canGet, + transferFirstRequest: canGet, + populate: (ViewModelJsonConverter.CanConvertType(propertyType) || propertyType == typeof(object)) && canGet ); propertyMap.ConstructorParameter = ctorParam; propertyMap.JsonConverter = GetJsonConverter(property); - propertyMap.AllowDynamicDispatch = property.PropertyType.IsAbstract || property.PropertyType == typeof(object); + propertyMap.AllowDynamicDispatch = propertyType.IsAbstract || propertyType == typeof(object); foreach (ISerializationInfoAttribute attr in property.GetCustomAttributes().OfType()) { attr.SetOptions(propertyMap); } - var bindAttribute = property.GetCustomAttribute(); if (bindAttribute != null) { propertyMap.Bind(bindAttribute.Direction); @@ -169,18 +195,8 @@ protected virtual IEnumerable GetProperties(Type type, Met yield return propertyMap; } } - /// - /// Returns whether DotVVM serialization supports setter of given property. - /// - private static bool IsSetterSupported(PropertyInfo property) - { - // support all properties of KeyValuePair<,> - if (property.DeclaringType!.IsGenericType && property.DeclaringType.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) return true; - - return property.SetMethod != null && property.SetMethod.IsPublic; - } - protected virtual JsonConverter? GetJsonConverter(PropertyInfo property) + protected virtual JsonConverter? GetJsonConverter(MemberInfo property) { var converterType = property.GetCustomAttribute()?.ConverterType; if (converterType == null) diff --git a/src/Framework/Framework/ViewModel/Validation/AttributeViewModelValidationMetadataProvider.cs b/src/Framework/Framework/ViewModel/Validation/AttributeViewModelValidationMetadataProvider.cs index c13b793022..650043fdb6 100644 --- a/src/Framework/Framework/ViewModel/Validation/AttributeViewModelValidationMetadataProvider.cs +++ b/src/Framework/Framework/ViewModel/Validation/AttributeViewModelValidationMetadataProvider.cs @@ -6,7 +6,7 @@ namespace DotVVM.Framework.ViewModel.Validation { public class AttributeViewModelValidationMetadataProvider : IViewModelValidationMetadataProvider { - public IEnumerable GetAttributesForProperty(PropertyInfo property) + public IEnumerable GetAttributesForProperty(MemberInfo property) { return property.GetCustomAttributes(true); } diff --git a/src/Framework/Framework/ViewModel/Validation/IValidationRuleTranslator.cs b/src/Framework/Framework/ViewModel/Validation/IValidationRuleTranslator.cs index a176f6fa90..eee22d5956 100644 --- a/src/Framework/Framework/ViewModel/Validation/IValidationRuleTranslator.cs +++ b/src/Framework/Framework/ViewModel/Validation/IValidationRuleTranslator.cs @@ -10,6 +10,6 @@ namespace DotVVM.Framework.ViewModel.Validation { public interface IValidationRuleTranslator { - IEnumerable TranslateValidationRules(PropertyInfo property, IEnumerable validationAttributes); + IEnumerable TranslateValidationRules(MemberInfo property, IEnumerable validationAttributes); } } diff --git a/src/Framework/Framework/ViewModel/Validation/IViewModelValidationMetadataProvider.cs b/src/Framework/Framework/ViewModel/Validation/IViewModelValidationMetadataProvider.cs index 2554ba81f3..c38b5f9a37 100644 --- a/src/Framework/Framework/ViewModel/Validation/IViewModelValidationMetadataProvider.cs +++ b/src/Framework/Framework/ViewModel/Validation/IViewModelValidationMetadataProvider.cs @@ -7,7 +7,7 @@ namespace DotVVM.Framework.ViewModel.Validation public interface IViewModelValidationMetadataProvider { - IEnumerable GetAttributesForProperty(PropertyInfo property); + IEnumerable GetAttributesForProperty(MemberInfo property); } } diff --git a/src/Framework/Framework/ViewModel/Validation/ValidationErrorPathExpander.cs b/src/Framework/Framework/ViewModel/Validation/ValidationErrorPathExpander.cs index f2157a56c8..2b2d58c96e 100644 --- a/src/Framework/Framework/ViewModel/Validation/ValidationErrorPathExpander.cs +++ b/src/Framework/Framework/ViewModel/Validation/ValidationErrorPathExpander.cs @@ -126,7 +126,7 @@ private int Expand(object? viewModel, string pathPrefix, ValidationErrorPathExpa var map = viewModelSerializationMapper.GetMap(viewModel.GetType()); foreach (var property in map.Properties.Where(p => p.TransferToServer)) { - var value = property.PropertyInfo.GetValue(viewModel); + var value = property.GetValue(viewModel); if (value == null) continue; diff --git a/src/Framework/Framework/ViewModel/Validation/ViewModelValidationRuleTranslator.cs b/src/Framework/Framework/ViewModel/Validation/ViewModelValidationRuleTranslator.cs index 56ddfbffe9..014cce45d4 100644 --- a/src/Framework/Framework/ViewModel/Validation/ViewModelValidationRuleTranslator.cs +++ b/src/Framework/Framework/ViewModel/Validation/ViewModelValidationRuleTranslator.cs @@ -11,8 +11,9 @@ public class ViewModelValidationRuleTranslator : IValidationRuleTranslator /// /// Gets the validation rules. /// - public virtual IEnumerable TranslateValidationRules(PropertyInfo property, IEnumerable validationAttributes) + public virtual IEnumerable TranslateValidationRules(MemberInfo property, IEnumerable validationAttributes) { + var propertyType = property.GetResultType(); var addEnforceClientFormat = true; foreach (var attribute in validationAttributes) { @@ -58,7 +59,7 @@ public virtual IEnumerable TranslateValidationR yield return validationRule; } // enforce client format by default - if (addEnforceClientFormat && (property.PropertyType.IsNullable() && property.PropertyType.UnwrapNullableType().IsNumericType() || property.PropertyType.UnwrapNullableType().IsDateOrTimeType())) + if (addEnforceClientFormat && (propertyType.IsNullable() && propertyType.UnwrapNullableType().IsNumericType() || propertyType.UnwrapNullableType().IsDateOrTimeType())) { var enforceClientFormatAttr = new DotvvmClientFormatAttribute(); diff --git a/src/Framework/Framework/ViewModel/Validation/ViewModelValidator.cs b/src/Framework/Framework/ViewModel/Validation/ViewModelValidator.cs index ad7e0388cd..513e0713c3 100644 --- a/src/Framework/Framework/ViewModel/Validation/ViewModelValidator.cs +++ b/src/Framework/Framework/ViewModel/Validation/ViewModelValidator.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Reflection; using DotVVM.Framework.Configuration; using DotVVM.Framework.Utils; using DotVVM.Framework.ViewModel.Serialization; @@ -68,7 +69,7 @@ private IEnumerable ValidateViewModel(object? viewMode var map = viewModelSerializationMapper.GetMap(viewModel.GetType()); foreach (var property in map.Properties.Where(p => p.TransferToServer)) { - var value = property.PropertyInfo.GetValue(viewModel); + var value = property.GetValue(viewModel); // validate the property if (property.ValidationRules.Any()) diff --git a/src/Tests/ViewModel/SerializerTests.cs b/src/Tests/ViewModel/SerializerTests.cs index 6b2e4d8f06..54c491c704 100644 --- a/src/Tests/ViewModel/SerializerTests.cs +++ b/src/Tests/ViewModel/SerializerTests.cs @@ -25,7 +25,7 @@ namespace DotVVM.Framework.Tests.ViewModel public class SerializerTests { static ViewModelJsonConverter jsonConverter = DotvvmTestHelper.DefaultConfig.ServiceProvider.GetRequiredService(); - static STJ.JsonSerializerOptions jsonOptions = new STJ.JsonSerializerOptions(DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe) { + static JsonSerializerOptions jsonOptions = new JsonSerializerOptions(DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe) { Converters = { jsonConverter }, WriteIndented = true }; @@ -229,8 +229,9 @@ public void SupportTuples() }; var (obj2, json) = SerializeAndDeserialize(obj, isPostback: true); - Assert.AreEqual("""{"Item1":9,"Item2":8,"Item3":7,"Item4":6}""", json["P1"].ToJsonString(new JsonSerializerOptions { WriteIndented = false })); - Assert.AreEqual("", json["P2"].ToString()); + Assert.AreEqual("""{"$type":"pUM6BN1XpGhNArrP","Item1":9,"Item2":8,"Item3":7,"Item4":6}""", json["P1"].ToJsonString(new JsonSerializerOptions { WriteIndented = false })); + Assert.AreEqual("""{"$type":"VnTd1CsMIOOD62hn","Key":3,"Value":4}""", json["P3"][0].ToJsonString(new JsonSerializerOptions { WriteIndented = false })); + Assert.AreEqual("""{"$type":"wAhAjOT9J9ARTgbN","Item1":5,"Item2":6,"Item3":7,"Item4":8}""", json["P2"].ToJsonString(new JsonSerializerOptions { WriteIndented = false })); Assert.AreEqual(obj.P1, obj2.P1); Assert.AreEqual(obj.P2, obj2.P2); From fd1f83ac73a29092dd63cf09d19dffce148c74a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Thu, 25 Apr 2024 19:24:37 +0200 Subject: [PATCH 11/20] STJ migration: ignore DotvvmConfiguration deserialization tests Just to avoid balooning this monster PR. This specific feature is also likely to be removed, depending on the answer from VS Extension guys. --- src/Tests/Routing/RouteSerializationTests.cs | 1 + .../Runtime/DotvvmCompilationExceptionSerializationTests.cs | 1 + src/Tests/Runtime/ResourceManagerTests.cs | 2 ++ src/Tests/ViewModel/JsonDiffTests.cs | 2 ++ 4 files changed, 6 insertions(+) diff --git a/src/Tests/Routing/RouteSerializationTests.cs b/src/Tests/Routing/RouteSerializationTests.cs index f8fb80eb88..b5caa0802c 100644 --- a/src/Tests/Routing/RouteSerializationTests.cs +++ b/src/Tests/Routing/RouteSerializationTests.cs @@ -17,6 +17,7 @@ namespace DotVVM.Framework.Tests.Routing public class RouteSerializationTests { [TestMethod] + [Ignore("DotvvmConfiguration deserialization is not currently implemented")] public void RouteTable_Deserialization() { DotvvmTestHelper.EnsureCompiledAssemblyCache(); diff --git a/src/Tests/Runtime/DotvvmCompilationExceptionSerializationTests.cs b/src/Tests/Runtime/DotvvmCompilationExceptionSerializationTests.cs index 118e844480..6a55b935c9 100644 --- a/src/Tests/Runtime/DotvvmCompilationExceptionSerializationTests.cs +++ b/src/Tests/Runtime/DotvvmCompilationExceptionSerializationTests.cs @@ -12,6 +12,7 @@ namespace DotVVM.Framework.Tests.Runtime public class DotvvmCompilationExceptionSerializationTests { [TestMethod] + [Ignore("DotvvmCompilationException deserialization is not currently implemented")] public void DotvvmCompilationException_SerializationAndDeserialization_WorksCorrectly() { var compilationException = diff --git a/src/Tests/Runtime/ResourceManagerTests.cs b/src/Tests/Runtime/ResourceManagerTests.cs index eb345389da..3b6f1b8178 100644 --- a/src/Tests/Runtime/ResourceManagerTests.cs +++ b/src/Tests/Runtime/ResourceManagerTests.cs @@ -56,6 +56,7 @@ public void ResourceManager_DependentResources_Css() /// Verifies that the default configuration populated with contents from the JSON file is merged correctly. /// [TestMethod] + [Ignore("DotvvmConfiguration deserialization is not currently implemented")] public void ResourceManager_ConfigurationDeserialization() { //define @@ -100,6 +101,7 @@ public void ResourceManager_ConfigurationDeserialization() } [TestMethod] + [Ignore("DotvvmConfiguration deserialization is not currently implemented")] public void ResourceManager_ConfigurationOldDeserialization() { var json = string.Format(@" diff --git a/src/Tests/ViewModel/JsonDiffTests.cs b/src/Tests/ViewModel/JsonDiffTests.cs index 977da704bb..5cbb544a21 100644 --- a/src/Tests/ViewModel/JsonDiffTests.cs +++ b/src/Tests/ViewModel/JsonDiffTests.cs @@ -36,6 +36,7 @@ public void JsonDiff_SimpleTest() } [TestMethod] + [Ignore("DotvvmConfiguration deserialization is not currently implemented")] public void JsonDiff_Configuration_AddingResources() { var config = ApplyPatches( @@ -50,6 +51,7 @@ public void JsonDiff_Configuration_AddingResources() } [TestMethod] + [Ignore("DotvvmConfiguration deserialization is not currently implemented")] public void JsonDiff_Configuration_AddingRoute() { var config = ApplyPatches( From c90af837ec54edbdd40af5e56e171770e86307a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 26 Apr 2024 00:07:08 +0200 Subject: [PATCH 12/20] STJ: add proper support for property shadowing It is neccessary in order to support IGridViewDataSet type hierarchy. The issue didn't occur before, because we didn't map interface correctly, ignoring inherited properties. It didn't matter, because we never serialized them. Now we can end up serializing interface if DynamicDispatch is disabled. The shadowing allows redefining a property with another property of the same .NET Name with a compatible type. --- src/Framework/Core/ViewModel/BindAttribute.cs | 6 +- .../Serialization/ViewModelJsonConverter.cs | 12 ++- .../Serialization/ViewModelPropertyMap.cs | 2 +- .../ViewModelSerializationMap.cs | 14 +-- .../ViewModelSerializationMapper.cs | 38 +++++++- src/Tests/ViewModel/SerializerTests.cs | 89 ++++++++++++++++++- .../ViewModelSerializationMapperTests.cs | 88 ++++++++++++++++-- 7 files changed, 222 insertions(+), 27 deletions(-) diff --git a/src/Framework/Core/ViewModel/BindAttribute.cs b/src/Framework/Core/ViewModel/BindAttribute.cs index 01797114b7..112da2bdf2 100644 --- a/src/Framework/Core/ViewModel/BindAttribute.cs +++ b/src/Framework/Core/ViewModel/BindAttribute.cs @@ -7,7 +7,7 @@ namespace DotVVM.Framework.ViewModel /// /// Specifies the binding direction. /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] public class BindAttribute : Attribute { @@ -24,8 +24,8 @@ public class BindAttribute : Attribute public bool? _allowDynamicDispatch; /// /// When true, DotVVM serializer will select the JSON converter based on the runtime type, instead of deciding it ahead of time. - /// This essentially enables serialization of properties defined derived types, but does not enable derive type deserialization. - /// By default, dynamic dispatch is enabled for abstract types (including interfaces). + /// This essentially enables serialization of properties defined derived types, but does not enable derive type deserialization, unless an instance of the correct type is prepopulated into the property. + /// By default, dynamic dispatch is enabled for abstract types (including interfaces and System.Object). /// public bool AllowDynamicDispatch { get => _allowDynamicDispatch ?? false; set => _allowDynamicDispatch = value; } diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs index 2372c999fa..dabbf5e1d6 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs @@ -3,8 +3,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using DotVVM.Framework.Configuration; -using System.Reflection; using DotVVM.Framework.Utils; using System.Security; using System.Diagnostics; @@ -12,9 +10,7 @@ using System.IO; using System.Text.Json.Nodes; using System.Text.Json.Serialization; -using System.Threading.Tasks; -using System.Security.Cryptography.X509Certificates; -using DotVVM.Framework.Controls; +using FastExpressionCompiler; namespace DotVVM.Framework.ViewModel.Serialization { @@ -69,6 +65,8 @@ public class VMConverter(ViewModelJsonConverter factory): JsonConverter, I { if (state is null) throw new ArgumentNullException(nameof(state), "DotvvmSerializationState must be created before calling the ViewModelJsonConverter."); + if (typeof(T) != typeToConvert) + throw new ArgumentException("typeToConvert must be the same as T", nameof(typeToConvert)); if (reader.TokenType == JsonTokenType.Null) { @@ -103,13 +101,13 @@ static void ReadObjectStart(ref Utf8JsonReader reader) { if (reader.TokenType == JsonTokenType.None) reader.Read(); if (reader.TokenType != JsonTokenType.StartObject) - throw new JsonException($"Expected StartObject token, but reader.TokenType={reader.TokenType}"); + throw new JsonException($"Cannot deserialize '{typeof(T).ToCode()}': Expected StartObject token, but reader.TokenType = {reader.TokenType}"); reader.Read(); } static void ReadEndObject(ref Utf8JsonReader reader) { if (reader.TokenType != JsonTokenType.EndObject) - throw new JsonException($"Expected EndObject token, but reader.TokenType={reader.TokenType}"); + throw new JsonException($"Expected EndObject token, but reader.TokenType = {reader.TokenType}"); } public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelPropertyMap.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelPropertyMap.cs index fc191a9af9..53f38fa145 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelPropertyMap.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelPropertyMap.cs @@ -46,7 +46,7 @@ public ViewModelPropertyMap(MemberInfo propertyInfo, string name, ProtectMode vi public bool TransferFirstRequest { get; set; } /// When true, an existing object in this property will be preserved during deserialization. A new object will only be created if the property is null, or if we need to call the constructor to set some properties. public bool Populate { get; set; } - /// If true, DotVVM serializer will use JSON converter for the runtime type, instead of resolving one statically + /// If true, DotVVM serializer will use JSON converter for the runtime type, instead of resolving one statically. Affects mostly serialization, but also deserialization into an existing instance. public bool AllowDynamicDispatch { get; set; } /// List of validation rules (~= validation attributes) on this property. Includes rules which can't be run client-side diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs index b072977559..d585be850d 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs @@ -62,13 +62,13 @@ internal ViewModelSerializationMap(Type type, IEnumerable private void ValidatePropertyMap() { - var hashset = new HashSet(); + var dict = new Dictionary(); foreach (var propertyMap in Properties) { - if (!hashset.Add(propertyMap.Name)) + if (!dict.TryAdd(propertyMap.Name, propertyMap)) { - throw new InvalidOperationException($"Detected member shadowing on property \"{propertyMap.Name}\" " + - $"while building serialization map for \"{Type.ToCode()}\""); + 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."); } } } @@ -673,7 +673,7 @@ private Expression DeserializePropertyValue(ViewModelPropertyMap property, Expre if (this.viewModelJsonConverter.CanConvert(type)) { var defaultConverter = this.viewModelJsonConverter.CreateConverter(type); - if (property.AllowDynamicDispatch) + if (property.AllowDynamicDispatch && !type.IsSealed) { return Call( JsonSerializationCodegenFragments.DeserializeViewModelDynamicMethod.MakeGenericMethod(type), @@ -728,7 +728,7 @@ private Expression GetSerializeExpression(ViewModelPropertyMap property, Express } if (this.viewModelJsonConverter.CanConvert(value.Type)) { - if (property.AllowDynamicDispatch) + if (property.AllowDynamicDispatch && !value.Type.IsSealed) { // TODO: ?? // return Call( @@ -745,7 +745,7 @@ private Expression GetSerializeExpression(ViewModelPropertyMap property, Express } } - return Call(JsonSerializationCodegenFragments.SerializeValueMethod.MakeGenericMethod(value.Type), writer, jsonOptions, value, Constant(property.AllowDynamicDispatch)); + return Call(JsonSerializationCodegenFragments.SerializeValueMethod.MakeGenericMethod(value.Type), writer, jsonOptions, value, Constant(property.AllowDynamicDispatch && !value.Type.IsSealed)); } } diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs index 57d868a913..cfc5a8cce7 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs @@ -11,6 +11,7 @@ using System.Text.Json.Serialization; using System.Text.Json; using DotVVM.Framework.Compilation.Javascript; +using FastExpressionCompiler; namespace DotVVM.Framework.ViewModel.Serialization { @@ -117,6 +118,40 @@ protected virtual ViewModelSerializationMap CreateMap() static Type unwrapByRef(Type t) => t.IsByRef ? t.GetElementType()! : t; } + protected virtual MemberInfo[] ResolveShadowing(Type type, MemberInfo[] members) + { + var shadowed = new Dictionary(); + foreach (var member in members) + { + if (shadowed.TryAdd(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."); + var (inherited, replacing) = member.DeclaringType!.IsAssignableFrom(previous.DeclaringType!) ? (member, previous) : (previous, member); + var inheritedType = inherited.GetResultType(); + var replacingType = replacing.GetResultType(); + + // collections are special case, since everything is serialized as array, we don't have to care about the actual type, only the element type + // this is neccessary for IGridViewDataSet hierarchy to work... + while (ReflectionUtils.IsCollection(inheritedType) && ReflectionUtils.IsCollection(replacingType)) + { + inheritedType = ReflectionUtils.GetEnumerableType(inheritedType) ?? typeof(object); + replacingType = ReflectionUtils.GetEnumerableType(replacingType) ?? typeof(object); + } + + if (inheritedType.IsAssignableFrom(replacingType)) + { + shadowed[member.Name] = replacing; + } + else + { + throw new InvalidOperationException($"Detected forbidden member shadowing of '{inherited.DeclaringType.ToCode(stripNamespace: true)}.{inherited.Name}: {inherited.GetResultType().ToCode(stripNamespace: true)}' by '{replacing.DeclaringType.ToCode(stripNamespace: true)}.{replacing.Name}: {replacing.GetResultType().ToCode(stripNamespace: true)}' while building serialization map for '{type.ToCode(stripNamespace: true)}'"); + } + } + return shadowed.Values.ToArray(); + } + /// /// Gets the properties of the specified type. /// @@ -127,6 +162,7 @@ protected virtual IEnumerable GetProperties(Type type, Met var properties = type.GetAllMembers(BindingFlags.Public | BindingFlags.Instance) .Where(m => m is PropertyInfo or FieldInfo) .ToArray(); + properties = ResolveShadowing(type, properties); Array.Sort(properties, (a, b) => StringComparer.Ordinal.Compare(a.Name, b.Name)); foreach (MemberInfo property in properties) { @@ -138,7 +174,7 @@ protected virtual IEnumerable GetProperties(Type type, Met include = include || !(bindAttribute is null or { Direction: Direction.None }) || property.IsDefined(typeof(JsonIncludeAttribute)) || - (property.DeclaringType.IsGenericType && property.DeclaringType.FullName.StartsWith("System.ValueTuple`")); + (type.IsGenericType && type.FullName.StartsWith("System.ValueTuple`")); } if (!include) continue; diff --git a/src/Tests/ViewModel/SerializerTests.cs b/src/Tests/ViewModel/SerializerTests.cs index 54c491c704..67b57fe27f 100644 --- a/src/Tests/ViewModel/SerializerTests.cs +++ b/src/Tests/ViewModel/SerializerTests.cs @@ -409,7 +409,7 @@ public void SupportsSignedDictionary() CollectionAssert.Contains(obj2.SignedDictionary, new KeyValuePair("a", "x")); CollectionAssert.Contains(obj2.SignedDictionary, new KeyValuePair("b", "y")); Assert.AreEqual(obj.SignedDictionary.Count, obj2.SignedDictionary.Count); - Assert.IsNotNull(json["SignedDictionary"]); + XAssert.IsType(json["SignedDictionary"]); } [TestMethod] @@ -578,6 +578,52 @@ public class ViewModelWithUnmatchedConstuctorProperty2 public ViewModelWithUnmatchedConstuctorProperty2(TestViewModelWithByteArray x) { } } + + [TestMethod] + public void PropertyShadowing() + { + var obj = new TestViewModelWithPropertyShadowing.Inner { + EnumerableToList = ["x", "y"], + ObjectToList = ["z" ], + InterfaceToInteger = 5, + ObjectToInteger = 6, + ShadowedByField = 7 + }; + + var (obj2, json) = SerializeAndDeserialize(obj); + XAssert.Equal(obj.EnumerableToList, obj2.EnumerableToList); + XAssert.Equal(obj.ObjectToList, obj2.ObjectToList); + XAssert.Equal(obj.InterfaceToInteger, obj2.InterfaceToInteger); + XAssert.Equal(obj.ObjectToInteger, obj2.ObjectToInteger); + XAssert.Equal(obj.ShadowedByField, obj2.ShadowedByField); + XAssert.IsType(json["EnumerableToList"]); + } + [TestMethod] + public void PropertyShadowing_BaseTypeDeserialized() + { + var obj = new TestViewModelWithPropertyShadowing.Inner { + EnumerableToList = ["x", "y"], + ObjectToList = ["z" ], + InterfaceToInteger = 5, + ObjectToInteger = 6, + ShadowedByField = 7 + }; + // Serialized Inner but deserializes the base type + var (obj2Box, json) = SerializeAndDeserialize>(new() { Value = obj }); + var obj2 = obj2Box.Value; + json = json["Value"].AsObject(); + XAssert.Equal(typeof(TestViewModelWithPropertyShadowing), obj2.GetType()); + + XAssert.Equal(obj.EnumerableToList, obj2.EnumerableToList); + XAssert.IsType(obj2.ObjectToList); + XAssert.Null(obj2.InterfaceToInteger); + XAssert.Equal(6d, XAssert.IsType(obj2.ObjectToInteger)); + XAssert.Equal(7d, XAssert.IsType(obj2.ShadowedByField)); + XAssert.Equal(5, (int)json["InterfaceToInteger"]); + XAssert.Equal(6, (int)json["ObjectToInteger"]); + XAssert.Equal(7, (double)json["ShadowedByField"]); + XAssert.IsType(json["EnumerableToList"]); + } } public class DataNode @@ -767,4 +813,45 @@ public class TestViewModelWithDateTimes public DateOnly DateOnly { get; set; } public TimeOnly TimeOnly { get; set; } } + + public class TestViewModelWithPropertyShadowing + { + public object ObjectToInteger { get; set; } + [JsonIgnore] // does not "inherit" to shadowed property + public IComparable InterfaceToInteger { get; set; } + + public IEnumerable EnumerableToList { get; set; } + public object ObjectToList { get; set; } + + public object ShadowedByField { get; set; } + + public class Inner: TestViewModelWithPropertyShadowing + { + public new int ObjectToInteger { get; set; } = 123; + public new int InterfaceToInteger { get; set; } = 1234; + + public new List EnumerableToList { get; set; } = [ "A", "B" ]; + public new List ObjectToList { get; set; } = [ "C", "D" ]; + + public new double ShadowedByField { get; set; } = 12345; + } + } + + class DynamicDispatchVMContainer + { + [Bind(AllowDynamicDispatch = true)] + public TStatic Value { get; set; } + } + + class StaticDispatchVMContainer + { + [Bind(AllowDynamicDispatch = false)] + public TStatic Value { get; set; } + } + + class DefaultDispatchVMContainer + { + [Bind(AllowDynamicDispatch = false)] + public TStatic Value { get; set; } + } } diff --git a/src/Tests/ViewModel/ViewModelSerializationMapperTests.cs b/src/Tests/ViewModel/ViewModelSerializationMapperTests.cs index b477ef73db..d67d2d9f30 100644 --- a/src/Tests/ViewModel/ViewModelSerializationMapperTests.cs +++ b/src/Tests/ViewModel/ViewModelSerializationMapperTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Text.Json.Serialization; using DotVVM.Framework.Configuration; @@ -10,6 +11,7 @@ using FastExpressionCompiler; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; +using NJ=Newtonsoft.Json; namespace DotVVM.Framework.Tests.ViewModel { @@ -33,25 +35,74 @@ public void ViewModelSerializationMapper_Name_JsonPropertyVsBindAttribute() } [TestMethod] - public void ViewModelSerializationMapper_Name_MemberShadowing() + public void ViewModelSerializationMapper_Name_NewtonsoftJsonAttributes() { + // we still respect NJ attributes var mapper = DotvvmTestHelper.DefaultConfig.ServiceProvider.GetRequiredService(); + var map = mapper.GetMap(typeof(NewtonsoftJsonAttributes)); - var exception = XAssert.ThrowsAny(() => mapper.GetMap(typeof(MemberShadowingViewModelB))); + XAssert.DoesNotContain("Ignored", map.Properties.Select(p => p.Name)); + Assert.AreEqual("new_name", map.Property("RenamedProperty").Name); + } + + [DataTestMethod] + [DataRow(typeof(MemberShadowingViewModelB), "Property1", "List", "List>")] + [DataRow(typeof(MemberShadowingViewModelC), "Property1", "List", "object")] + [DataRow(typeof(MemberShadowingViewModelD), "Property2", "ViewModelSerializationMapperTests.JsonPropertyVsBindAttribute", "object")] + [DataRow(typeof(MemberShadowingViewModelE), "Property2", "ViewModelSerializationMapperTests.JsonPropertyVsBindAttribute", "TestViewModelWithBind")] + public void ViewModelSerializationMapper_Name_MemberShadowing(Type type, string prop, string t1, string t2) + { + var mapper = DotvvmTestHelper.DefaultConfig.ServiceProvider.GetRequiredService(); + + var exception = XAssert.ThrowsAny(() => mapper.GetMap(type)); + XAssert.IsType(exception.GetBaseException()); + XAssert.Equal($"Detected forbidden member shadowing of 'ViewModelSerializationMapperTests.MemberShadowingViewModelA.{prop}: {t1}' by '{type.ToCode(stripNamespace: true)}.{prop}: {t2}' while building serialization map for '{type.ToCode(stripNamespace: true)}'", exception.GetBaseException().Message); + } + + [TestMethod] + public void ViewModelSerializationMapper_Name_NameConflictAttributes() + { + var mapper = DotvvmTestHelper.DefaultConfig.ServiceProvider.GetRequiredService(); + var exception = XAssert.ThrowsAny(() => mapper.GetMap(typeof(NameConflictAttributes))); + XAssert.IsType(exception.GetBaseException()); + Assert.AreEqual("Serialization map for 'DotVVM.Framework.Tests.ViewModel.ViewModelSerializationMapperTests.NameConflictAttributes' has a name conflict between a property 'Name' and property 'MyProperty' — both are named 'Name' in JSON.", exception.GetBaseException().Message); + } + + [TestMethod] + public void ViewModelSerializationMapper_Name_NameConflictFieldProperty() + { + var mapper = DotvvmTestHelper.DefaultConfig.ServiceProvider.GetRequiredService(); + var exception = XAssert.ThrowsAny(() => mapper.GetMap(typeof(NameConflictFieldProperty))); XAssert.IsType(exception.GetBaseException()); - XAssert.Equal($"Detected member shadowing on property \"{nameof(MemberShadowingViewModelB.Property)}\" while building serialization map for \"{typeof(MemberShadowingViewModelB).ToCode()}\"", exception.GetBaseException().Message); + Assert.AreEqual("Serialization map for 'DotVVM.Framework.Tests.ViewModel.ViewModelSerializationMapperTests.NameConflictFieldProperty' has a name conflict between a property 'Name' and field 'MyField' — both are named 'Name' in JSON.", exception.GetBaseException().Message); } public class MemberShadowingViewModelA { - public object Property { get; set; } + public List Property1 { get; set; } + public JsonPropertyVsBindAttribute Property2 { get; set; } } public class MemberShadowingViewModelB : MemberShadowingViewModelA { -#pragma warning disable CS0108 // Member hides inherited member; missing new keyword - public string Property { get; set; } -#pragma warning restore CS0108 // Member hides inherited member; missing new keyword + // different type + public new List> Property1 { get; set; } + } + + public class MemberShadowingViewModelC : MemberShadowingViewModelA + { + // more generic + public new object Property1 { get; set; } + } + public class MemberShadowingViewModelD : MemberShadowingViewModelA + { + // more generic + public new object Property2 { get; set; } + } + public class MemberShadowingViewModelE : MemberShadowingViewModelA + { + // different type + public new TestViewModelWithBind Property2 { get; set; } } public class JsonPropertyVsBindAttribute @@ -77,7 +128,30 @@ public class JsonPropertyVsBindAttribute [Bind()] [JsonPropertyName("jsonProperty3")] public string BindWithoutNameJsonPropertyWithName { get; set; } + } + public class NewtonsoftJsonAttributes + { + [NJ.JsonIgnore] + public bool Ignored { get; set; } + + [NJ.JsonProperty("new_name")] + public string RenamedProperty { get; set; } + } + + public class NameConflictAttributes + { + [Bind(Name = "Name")] + public string MyProperty { get; set; } + + public string Name { get; set; } + } + + public class NameConflictFieldProperty + { + public string Name { get; set; } + [Bind(Name = "Name")] + public string MyField; } } From 63d84712679997b5ea26c0732914749d15c48cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 26 Apr 2024 11:40:43 +0200 Subject: [PATCH 13/20] STJ: test for static dispatch interface serialization --- src/Tests/ViewModel/SerializerTests.cs | 84 ++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/Tests/ViewModel/SerializerTests.cs b/src/Tests/ViewModel/SerializerTests.cs index 67b57fe27f..ceca3d41c6 100644 --- a/src/Tests/ViewModel/SerializerTests.cs +++ b/src/Tests/ViewModel/SerializerTests.cs @@ -243,6 +243,40 @@ public void SupportTuples() Assert.AreEqual("default", obj2.P4.b.ClientToServer); } + [TestMethod] + public void SupportTuplesPopulate() + { + var obj = new TestViewModelWithTuples() { + P4 = (10, new TestViewModelWithBind { P1 = "1" }), + P5 = new (new TestViewModelWithBind { P1 = "2" }, new TestViewModelWithBind { P1 = "3" }), + P6 = new(new TestViewModelWithBind { P1 = "4" }, 5) + }; + var json = Serialize(obj, out var _, isPostback: true); + var obj2 = new TestViewModelWithTuples() { + P4 = (0, new TestViewModelWithBind()), + P5 = new (new TestViewModelWithBind(), new TestViewModelWithBind()), + P6 = new (new TestViewModelWithBind(), 0) + }; + var originalInstances = ( + P4: obj2.P4.b, + P5Key: obj2.P5.Key, + P5Value: obj2.P5.Value, + P6a: obj2.P6.Item1 + ); + var objPopulated = PopulateViewModel(json, obj2); + Assert.AreEqual(10, objPopulated.P4.a); + Assert.AreEqual("1", objPopulated.P4.b.P1); + Assert.AreEqual("2", objPopulated.P5.Key.P1); + Assert.AreEqual("3", objPopulated.P5.Value.P1); + Assert.AreEqual("4", objPopulated.P6.Item1.P1); + Assert.AreEqual(5, objPopulated.P6.Item2); + Assert.AreSame(objPopulated, obj2); + Assert.AreSame(originalInstances.P4, objPopulated.P4.b); + Assert.AreSame(originalInstances.P5Key, objPopulated.P5.Key); + Assert.AreSame(originalInstances.P5Value, objPopulated.P5.Value); + Assert.AreSame(originalInstances.P6a, objPopulated.P6.Item1); + } + [TestMethod] public void DoesNotCloneSettableRecord() { @@ -598,6 +632,7 @@ public void PropertyShadowing() XAssert.Equal(obj.ShadowedByField, obj2.ShadowedByField); XAssert.IsType(json["EnumerableToList"]); } + [TestMethod] public void PropertyShadowing_BaseTypeDeserialized() { @@ -624,6 +659,53 @@ public void PropertyShadowing_BaseTypeDeserialized() XAssert.Equal(7, (double)json["ShadowedByField"]); XAssert.IsType(json["EnumerableToList"]); } + + [TestMethod] + public void InterfaceDeserialization_Error() + { + var obj = new VMWithInterface(); + var ex = XAssert.ThrowsAny(() => SerializeAndDeserialize(new StaticDispatchVMContainer { Value = obj })); + XAssert.Contains("Can not deserialize DotVVM.Framework.Tests.ViewModel.SerializerTests.IVMInterface1 because it's abstract.", ex.Message); + } + + [TestMethod] + public void InterfaceSerialization_Static() + { + var obj = new VMWithInterface() { + Property1 = "x", + Property2 = "y", + Property3 = "z" + }; + var jsonStr = Serialize(new StaticDispatchVMContainer { Value = obj }, out var _, false); + Console.WriteLine(jsonStr); + var json = JsonNode.Parse(jsonStr).AsObject()["Value"].AsObject(); + XAssert.DoesNotContain("Property3", json); + XAssert.Equal("x", (string)json["Property1"]); + XAssert.Equal("y", (string)json["Property2"]); + + var obj2 = new StaticDispatchVMContainer { Value = new VMWithInterface() }; + var obj2Populated = PopulateViewModel(jsonStr, obj2); + Assert.AreSame(obj2, obj2Populated); + XAssert.Equal(obj.Property1, obj2.Value.Property1); + XAssert.Equal(obj.Property2, obj2.Value.Property2); + } + + class VMWithInterface: IVMInterface1 + { + public string Property1 { get; set; } + public string Property2 { get; set; } + public string Property3 { get; set; } + } + + interface IVMInterface1: IVMInterface2 + { + string Property1 { get; set; } + } + interface IVMInterface2 + { + string Property2 { get; set; } + } + } public class DataNode @@ -669,6 +751,8 @@ public class TestViewModelWithTuples public (int a, int b, int c, int d) P2 { get; set; } = (1, 2, 3, 4); public List> P3 { get; set; } = new List>(); public (int a, TestViewModelWithBind b) P4 { get; set; } = (1, new TestViewModelWithBind()); + public KeyValuePair P5 { get; set; } + public Tuple P6 { get; set; } } public class TestViewModelWithBind From e469e0082a032f98b9e49047d19ac69807e129a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 26 Apr 2024 13:51:31 +0200 Subject: [PATCH 14/20] STJ: Fix dynamic dispatch in deserialization --- .../ViewModelSerializationMap.cs | 15 +++++------ src/Tests/ViewModel/SerializerTests.cs | 25 ++++++++++++++++++- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs index d585be850d..36f9acc504 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs @@ -813,27 +813,28 @@ 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, ViewModelJsonConverter.VMConverter defaultConverter, DotvvmSerializationState state) where TVM: class { if (reader.TokenType == JsonTokenType.Null) return default; - if (existingValue is null && defaultConverter is null) + if (existingValue is null) { - throw new Exception($"Cannot deserialize {typeof(TVM).ToCode()} dynamically, originalValue is null and type is abstract."); + return defaultConverter.Read(ref reader, typeof(TVM), options, state); } - if (defaultConverter is {} && (existingValue is null || existingValue.GetType() == typeof(TVM))) + var realType = existingValue?.GetType() ?? typeof(TVM); + if (defaultConverter is {} && realType == typeof(TVM)) { return populate && existingValue is {} ? defaultConverter.Populate(ref reader, options, existingValue, state) : defaultConverter.Read(ref reader, typeof(TVM), options, state); } - var converter = factory.GetConverterCached(typeof(TVM)); - return populate ? (TVM?)converter.PopulateUntyped(ref reader, typeof(TVM), existingValue, options, state) - : (TVM?)converter.ReadUntyped(ref reader, typeof(TVM), options, state); + var converter = factory.GetConverterCached(realType); + return populate ? (TVM?)converter.PopulateUntyped(ref reader, realType, existingValue, options, state) + : (TVM?)converter.ReadUntyped(ref reader, realType, options, state); } public static readonly MethodInfo ReadEncryptedValueMethod = typeof(JsonSerializationCodegenFragments).GetMethod(nameof(ReadEncryptedValue), BindingFlags.NonPublic | BindingFlags.Static).NotNull(); diff --git a/src/Tests/ViewModel/SerializerTests.cs b/src/Tests/ViewModel/SerializerTests.cs index ceca3d41c6..1330cb8331 100644 --- a/src/Tests/ViewModel/SerializerTests.cs +++ b/src/Tests/ViewModel/SerializerTests.cs @@ -690,6 +690,29 @@ public void InterfaceSerialization_Static() XAssert.Equal(obj.Property2, obj2.Value.Property2); } + [TestMethod] + public void InterfaceSerialization_Dynamic() + { + var obj = new VMWithInterface() { + Property1 = "x", + Property2 = "y", + Property3 = "z" + }; + var jsonStr = Serialize(new DefaultDispatchVMContainer { Value = obj }, out var _, false); + Console.WriteLine(jsonStr); + var json = JsonNode.Parse(jsonStr).AsObject()["Value"].AsObject(); + XAssert.Equal("x", (string)json["Property1"]); + XAssert.Equal("y", (string)json["Property2"]); + XAssert.Equal("z", (string)json["Property3"]); + + var obj2 = new DefaultDispatchVMContainer { Value = new VMWithInterface() }; + var obj2Populated = PopulateViewModel(jsonStr, obj2); + Assert.AreSame(obj2, obj2Populated); + XAssert.Equal(obj.Property1, obj2.Value.Property1); + XAssert.Equal(obj.Property2, obj2.Value.Property2); + XAssert.Equal(obj.Property3, ((VMWithInterface)obj2.Value).Property3); + } + class VMWithInterface: IVMInterface1 { public string Property1 { get; set; } @@ -935,7 +958,7 @@ class StaticDispatchVMContainer class DefaultDispatchVMContainer { - [Bind(AllowDynamicDispatch = false)] + [Bind(Name = "Value")] // make sure that the attribute presence does not affect the default behavior public TStatic Value { get; set; } } } From 8c4bcbf943c3a1189ef879082accd265651fa8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 26 Apr 2024 13:51:51 +0200 Subject: [PATCH 15/20] Add Strykker configuration --- src/.config/dotnet-tools.json | 6 +++ src/DotVVM.Stryker.sln | 70 +++++++++++++++++++++++++++++++++++ src/stryker-config.json | 24 ++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 src/DotVVM.Stryker.sln create mode 100644 src/stryker-config.json diff --git a/src/.config/dotnet-tools.json b/src/.config/dotnet-tools.json index 762a929601..38ba1500bb 100644 --- a/src/.config/dotnet-tools.json +++ b/src/.config/dotnet-tools.json @@ -13,6 +13,12 @@ "commands": [ "sign" ] + }, + "dotnet-stryker": { + "version": "4.0.4", + "commands": [ + "dotnet-stryker" + ] } } } \ No newline at end of file diff --git a/src/DotVVM.Stryker.sln b/src/DotVVM.Stryker.sln new file mode 100644 index 0000000000..ebd4ace11c --- /dev/null +++ b/src/DotVVM.Stryker.sln @@ -0,0 +1,70 @@ + +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/stryker-config.json b/src/stryker-config.json new file mode 100644 index 0000000000..d6b98b4484 --- /dev/null +++ b/src/stryker-config.json @@ -0,0 +1,24 @@ +{ + "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" + } +} From dcba751490177046451452eb3cc082fa34983a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 26 Apr 2024 16:09:55 +0200 Subject: [PATCH 16/20] STJ: remove custom DateOnly/TimeOnly converters Not needed anymore, the types are natively supported by the serialized --- .../StaticCommandExecutionPlanSerializer.cs | 6 ++-- .../DefaultSerializerSettingsProvider.cs | 2 -- .../Framework/Utils/SystemTextJsonUtils.cs | 22 ++++++++++++--- .../Serialization/DotvvmByteArrayConverter.cs | 2 +- .../Serialization/DotvvmDateOnlyConverter.cs | 27 ------------------ .../DotvvmDictionaryConverter.cs | 28 ++++++++----------- .../Serialization/DotvvmObjectConverter.cs | 1 + .../Serialization/DotvvmTimeOnlyConverter.cs | 28 ------------------- src/Tests/ViewModel/SerializerTests.cs | 20 ++++++++++++- 9 files changed, 54 insertions(+), 82 deletions(-) delete mode 100644 src/Framework/Framework/ViewModel/Serialization/DotvvmDateOnlyConverter.cs delete mode 100644 src/Framework/Framework/ViewModel/Serialization/DotvvmTimeOnlyConverter.cs diff --git a/src/Framework/Framework/Compilation/Binding/StaticCommandExecutionPlanSerializer.cs b/src/Framework/Framework/Compilation/Binding/StaticCommandExecutionPlanSerializer.cs index 3e7e09b440..8311cf8529 100644 --- a/src/Framework/Framework/Compilation/Binding/StaticCommandExecutionPlanSerializer.cs +++ b/src/Framework/Framework/Compilation/Binding/StaticCommandExecutionPlanSerializer.cs @@ -140,7 +140,7 @@ public static StaticCommandInvocationPlan DeserializePlan(ref Utf8JsonReader jso method = method.MakeGenericMethod(generics); } - var methodParameters = method.GetParameters(); + ParameterInfo?[] methodParameters = method.GetParameters(); if (!method.IsStatic) { methodParameters = [ null, ..methodParameters ]; @@ -153,11 +153,11 @@ public static StaticCommandInvocationPlan DeserializePlan(ref Utf8JsonReader jso args[i] = type switch { StaticCommandParameterType.Argument or StaticCommandParameterType.Inject => - new StaticCommandParameterPlan(type, json.TokenType == JsonTokenType.Null ? paramType : Type.GetType(json.GetString())), + new StaticCommandParameterPlan(type, json.TokenType == JsonTokenType.Null ? paramType : Type.GetType(json.GetString()!)), StaticCommandParameterType.Constant => new StaticCommandParameterPlan(type, JsonSerializer.Deserialize(ref json, paramType)), StaticCommandParameterType.DefaultValue => - new StaticCommandParameterPlan(type, methodParameters[i].DefaultValue), + new StaticCommandParameterPlan(type, methodParameters[i]!.DefaultValue), StaticCommandParameterType.Invocation => new StaticCommandParameterPlan(type, DeserializePlan(ref json)), _ => throw new NotSupportedException(type.ToString()) diff --git a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs index 282c6d5afe..1a446d5ffb 100644 --- a/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs +++ b/src/Framework/Framework/Configuration/DefaultSerializerSettingsProvider.cs @@ -33,8 +33,6 @@ private JsonSerializerOptions CreateSettings() { Converters = { new DotvvmDateTimeConverter(), - new DotvvmDateOnlyConverter(), - new DotvvmTimeOnlyConverter(), new DotvvmObjectConverter(), new DotvvmEnumConverter(), new DotvvmDictionaryConverter(), diff --git a/src/Framework/Framework/Utils/SystemTextJsonUtils.cs b/src/Framework/Framework/Utils/SystemTextJsonUtils.cs index 378147080b..bdd61fc195 100644 --- a/src/Framework/Framework/Utils/SystemTextJsonUtils.cs +++ b/src/Framework/Framework/Utils/SystemTextJsonUtils.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Text.Json; namespace DotVVM.Framework.Utils @@ -76,12 +77,19 @@ public static IEnumerable EnumerateStringArray(this JsonElement json) } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void AssertToken(this in Utf8JsonReader reader, JsonTokenType type) { if (reader.TokenType != type) - { - throw new JsonException($"Expected token of type {type}, but got {reader.TokenType}."); - } + ThrowUnexpectedToken(in reader, type); + } + + static void ThrowUnexpectedToken(in Utf8JsonReader reader, JsonTokenType expected) + { + var value = reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName or JsonTokenType.Number + ? $" (\"{StringUtils.Utf8Decode(reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan.ToArray())}\")" + : ""; + throw new JsonException($"Expected token of type {expected}, but got {reader.TokenType}{value} at position {reader.BytesConsumed}."); } public static void AssertRead(this ref Utf8JsonReader reader, JsonTokenType type) @@ -90,10 +98,16 @@ public static void AssertRead(this ref Utf8JsonReader reader, JsonTokenType type AssertRead(ref reader); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void AssertRead(this ref Utf8JsonReader reader) { if (!reader.Read()) - throw new JsonException($"Expected end of stream."); + ThrowUnexpectedEndOfStream(in reader); + } + + public static void ThrowUnexpectedEndOfStream(in Utf8JsonReader reader) + { + throw new JsonException($"Unexpected end of stream at position {reader.BytesConsumed}."); } public static string? ReadString(this ref Utf8JsonReader reader) diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmByteArrayConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmByteArrayConverter.cs index 0a5c66bf31..582df374f4 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmByteArrayConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmByteArrayConverter.cs @@ -24,7 +24,7 @@ public class DotvvmByteArrayConverter : JsonConverter switch (reader.TokenType) { case JsonTokenType.Number: - list.Add((byte)reader.GetUInt16()); + list.Add(checked((byte)reader.GetUInt16())); break; case JsonTokenType.EndArray: return list.ToArray(); diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmDateOnlyConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmDateOnlyConverter.cs deleted file mode 100644 index d6b07b7c9d..0000000000 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmDateOnlyConverter.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace DotVVM.Framework.ViewModel.Serialization -{ - public class DotvvmDateOnlyConverter : JsonConverter - { - public override void Write(Utf8JsonWriter writer, DateOnly date, JsonSerializerOptions options) - { - var dateWithoutTimezone = new DateOnly(date.Year, date.Month, date.Day); - writer.WriteStringValue(dateWithoutTimezone.ToString("O", CultureInfo.InvariantCulture)); // TODO: utf8 - } - - public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.String - && DateOnly.TryParseExact(reader.GetString(), "O", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)) - { - return date; - } - - throw new Exception("The value specified in the JSON could not be converted to DateTime!"); - } - } -} diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs index b55e7ba2aa..61e44cf926 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs @@ -46,44 +46,40 @@ public override void Write(Utf8JsonWriter json, TDictionary value, JsonSerialize } public override TDictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType != JsonTokenType.StartArray) - throw new JsonException($"Expected StartArray, but got {reader.TokenType}."); - reader.Read(); + reader.AssertRead(JsonTokenType.StartArray); var dict = new Dictionary(); while (reader.TokenType != JsonTokenType.EndArray) { - if (reader.TokenType != JsonTokenType.StartObject) - throw new JsonException($"Expected StartObject, but got {reader.TokenType}."); - reader.Read(); + reader.AssertRead(JsonTokenType.StartObject); (K key, V value) item = default; + bool hasKey = false, hasValue = false; while (reader.TokenType != JsonTokenType.EndObject) { - if (reader.TokenType != JsonTokenType.PropertyName) - throw new JsonException($"Expected PropertyName, but got {reader.TokenType}."); + reader.AssertToken(JsonTokenType.PropertyName); if (reader.ValueTextEquals("Key"u8)) { - reader.Read(); + reader.AssertRead(); item.key = SystemTextJsonUtils.Deserialize(ref reader, options)!; - reader.Read(); + hasKey = true; } else if (reader.ValueTextEquals("Value"u8)) { - reader.Read(); + reader.AssertRead(); item.value = SystemTextJsonUtils.Deserialize(ref reader, options)!; - reader.Read(); + hasValue = true; } else { - reader.Read(); + reader.AssertRead(); reader.Skip(); - reader.Read(); } + reader.AssertRead(); } + if (!hasKey || !hasValue) throw new JsonException("Missing Key or Value property in dictionary item."); dict.Add(item.key!, item.value); - reader.Read(); + reader.AssertRead(JsonTokenType.EndObject); } - reader.Read(); if (dict is TDictionary result) return result; diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmObjectConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmObjectConverter.cs index 3275b1f800..3c73ec1b9e 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmObjectConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmObjectConverter.cs @@ -6,6 +6,7 @@ namespace DotVVM.Framework.ViewModel.Serialization { + /// Mimicks Newtonsoft.Json behavior for System.Object - number -> double, string -> string, true/false -> bool, otherwise JsonElement public class DotvvmObjectConverter : JsonConverter { public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options) diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmTimeOnlyConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmTimeOnlyConverter.cs deleted file mode 100644 index 0636c52bc6..0000000000 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmTimeOnlyConverter.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace DotVVM.Framework.ViewModel.Serialization -{ - public class DotvvmTimeOnlyConverter : JsonConverter - { - public override void Write(Utf8JsonWriter writer, TimeOnly date, JsonSerializerOptions options) - { - var dateWithoutTimezone = new TimeOnly(date.Ticks); - writer.WriteStringValue(dateWithoutTimezone.ToString("O", CultureInfo.InvariantCulture)); // TODO: utf8 - } - - public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.String - && TimeOnly.TryParseExact(reader.GetString(), "O", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)) - { - return date; - } - - throw new Exception("The value specified in the JSON could not be converted to DateTime!"); - } - - } -} diff --git a/src/Tests/ViewModel/SerializerTests.cs b/src/Tests/ViewModel/SerializerTests.cs index 1330cb8331..e17e41bdac 100644 --- a/src/Tests/ViewModel/SerializerTests.cs +++ b/src/Tests/ViewModel/SerializerTests.cs @@ -468,11 +468,29 @@ public void SupportsDateTime() Assert.AreEqual(obj.TimeOnly, obj2.TimeOnly); Assert.AreEqual("2000-01-01", json["DateOnly"].GetValue()); - Assert.AreEqual("15:00:00.0000000", json["TimeOnly"].GetValue()); + Assert.AreEqual("15:00:00", json["TimeOnly"].GetValue()); Assert.AreEqual("2000-01-01T15:00:00", json["DateTime1"].GetValue()); Assert.AreEqual("2000-01-01T15:00:00", json["DateTime2"].GetValue()); Assert.AreEqual("2000-01-01T15:00:00", json["DateTime3"].GetValue()); } + + [TestMethod] + public void SupportsDateTime_MicrosecondPrecision() + { + var obj = new TestViewModelWithDateTimes() { + DateTime1 = new DateTime(2000, 1, 2, 15, 16, 17).AddMilliseconds(123.456), + TimeOnly = new TimeOnly(15, 16, 17).Add(TimeSpan.FromMilliseconds(123.456)) + }; + var (obj2, json) = SerializeAndDeserialize(obj); + Console.WriteLine(json); + Assert.AreEqual(obj.DateTime1, obj2.DateTime1); + Assert.AreEqual(DateTimeKind.Unspecified, obj2.DateTime1.Kind); + Assert.AreEqual(obj.TimeOnly, obj2.TimeOnly); + Assert.AreEqual(obj.TimeOnly.Ticks, obj2.TimeOnly.Ticks); + + Assert.AreEqual("2000-01-02T15:16:17.123456", json["DateTime1"].GetValue()); + Assert.AreEqual("15:16:17.1234560", json["TimeOnly"].GetValue()); + } [TestMethod] public void SupportsEnums() From 3b3a36525e425c1f6bc6f34c57663976873c895e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 26 Apr 2024 19:19:18 +0200 Subject: [PATCH 17/20] STJ: support custom converters for view models --- src/DotVVM.Stryker.sln | 70 --------------- .../Metadata/PropertyDisplayMetadata.cs | 2 +- .../DotVVMServiceCollectionExtensions.cs | 1 + .../Hosting/StaticCommandExecutor.cs | 4 +- .../CustomPrimitiveTypeJsonConverter.cs | 4 +- .../DefaultViewModelSerializer.cs | 37 +++----- .../DotvvmJsonOptionsProvider.cs | 47 ++++++++++ .../Serialization/IDotvvmJsonConverter.cs | 22 +++++ .../Serialization/IViewModelSerializer.cs | 1 - .../Serialization/ViewModelJsonConverter.cs | 35 ++++---- .../ViewModelSerializationMap.cs | 85 ++++++++++--------- .../ViewModelSerializationMapper.cs | 18 ++-- .../Validation/ValidationErrorFactory.cs | 10 ++- src/Tests/ViewModel/SerializerTests.cs | 82 +++++++++++++++++- src/stryker-config.json | 24 ------ 15 files changed, 248 insertions(+), 194 deletions(-) delete mode 100644 src/DotVVM.Stryker.sln create mode 100644 src/Framework/Framework/ViewModel/Serialization/DotvvmJsonOptionsProvider.cs create mode 100644 src/Framework/Framework/ViewModel/Serialization/IDotvvmJsonConverter.cs delete mode 100644 src/stryker-config.json 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 class DotvvmCustomPrimitiveTypeConverter : JsonConverterFactory 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 ViewModelJsonConverter(IViewModelSerializationMapper viewModelSerializati !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 @@ internal ViewModelSerializationMap(Type type, IEnumerable private void ValidatePropertyMap() { - var dict = new Dictionary(); + 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 @@ public ReaderDelegate CreateReaderFactory() 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" - } -} From 5fa835ece1573ef198c500e2aac39ef613e4ff58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Sat, 27 Apr 2024 12:33:21 +0200 Subject: [PATCH 18/20] STJ: remove redundant code, small code review fixes --- src/.config/dotnet-tools.json | 8 +- src/Framework/Core/DotVVM.Core.csproj | 2 +- .../Framework/Utils/ExpressionUtils.cs | 39 -- .../Framework/Utils/FunctionalExtensions.cs | 4 - .../Framework/Utils/JsonDiffWriter.cs | 383 ------------------ .../Framework/Utils/JsonPatchWriter.cs | 205 ---------- .../Framework/Utils/MultipleWriterStream.cs | 133 ------ .../Framework/Utils/ReadOnlyMemoryStream.cs | 2 +- .../Framework/Utils/SystemTextJsonHacks.cs | 69 ---- .../ViewModel/Serialization/ClientTypeId.cs | 153 ------- .../DefaultViewModelSerializer.cs | 26 +- ...VVM.Samples.BasicSamples.Api.Common.csproj | 2 +- src/Tests/Runtime/ResourceManagerTests.cs | 3 +- 13 files changed, 6 insertions(+), 1023 deletions(-) delete mode 100644 src/Framework/Framework/Utils/JsonDiffWriter.cs delete mode 100644 src/Framework/Framework/Utils/JsonPatchWriter.cs delete mode 100644 src/Framework/Framework/Utils/MultipleWriterStream.cs delete mode 100644 src/Framework/Framework/Utils/SystemTextJsonHacks.cs delete mode 100644 src/Framework/Framework/ViewModel/Serialization/ClientTypeId.cs diff --git a/src/.config/dotnet-tools.json b/src/.config/dotnet-tools.json index 38ba1500bb..d41b636bf6 100644 --- a/src/.config/dotnet-tools.json +++ b/src/.config/dotnet-tools.json @@ -13,12 +13,6 @@ "commands": [ "sign" ] - }, - "dotnet-stryker": { - "version": "4.0.4", - "commands": [ - "dotnet-stryker" - ] } } -} \ No newline at end of file +} diff --git a/src/Framework/Core/DotVVM.Core.csproj b/src/Framework/Core/DotVVM.Core.csproj index 9d1f019448..75f2fa9530 100644 --- a/src/Framework/Core/DotVVM.Core.csproj +++ b/src/Framework/Core/DotVVM.Core.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/Framework/Framework/Utils/ExpressionUtils.cs b/src/Framework/Framework/Utils/ExpressionUtils.cs index d2202ac20d..6f366beb12 100644 --- a/src/Framework/Framework/Utils/ExpressionUtils.cs +++ b/src/Framework/Framework/Utils/ExpressionUtils.cs @@ -22,45 +22,6 @@ public static Expression While(Expression condition, Expression body) Expression.IfThenElse(condition, body, Expression.Goto(brkLabel)), brkLabel); } - public static Expression Foreach(Expression collection, ParameterExpression item, Expression body) - { - var genericType = collection.Type.IsGenericType ? collection.Type.GetGenericArguments()[0] : typeof(object); - if (collection.Type.IsArray || collection.Type == typeof(string) || genericType == typeof(Span<>) || genericType == typeof(ReadOnlySpan<>)) - return ForeachIndexer(collection, item, body); - - throw new NotImplementedException(); - } - - static Expression ForeachIndexer(Expression collection, ParameterExpression item, Expression body) - { - var block = new List(); - var vars = new List(); - ParameterExpression collectionP; - if (collection is ParameterExpression) - collectionP = (ParameterExpression)collection; - else - { - collectionP = Expression.Parameter(collection.Type); - block.Add(Expression.Assign(collectionP, collection)); - vars.Add(collectionP); - } - var indexP = Expression.Parameter(typeof(int)); - vars.Add(indexP); - block.Add(Expression.Assign(indexP, Expression.Constant(0))); - - var loop = While( - Expression.LessThan(indexP, Expression.Property(collectionP, "Length")), - Expression.Block( - new[] { item }, - Expression.Assign(item, Index(collectionP, indexP)), - body, - Expression.PostIncrementAssign(indexP) - ) - ); - block.Add(loop); - return Expression.Block(vars, block); - } - static Expression Index(Expression list, Expression index) { if (list.Type.IsArray) diff --git a/src/Framework/Framework/Utils/FunctionalExtensions.cs b/src/Framework/Framework/Utils/FunctionalExtensions.cs index d8fec648ed..7dd383c860 100644 --- a/src/Framework/Framework/Utils/FunctionalExtensions.cs +++ b/src/Framework/Framework/Utils/FunctionalExtensions.cs @@ -104,10 +104,6 @@ public static T NotNull([NotNull] this T? target, string message = "Unexpecte where T : class => target ?? throw new Exception(message); - // public static T NotNull([NotNull] this T? target, string message = "Unexpected null value.") - // where T : struct => - // target ?? throw new Exception(message); - public static SortedDictionary ToSorted(this IDictionary d, IComparer? c = null) where K: notnull => new(d, c ?? Comparer.Default); diff --git a/src/Framework/Framework/Utils/JsonDiffWriter.cs b/src/Framework/Framework/Utils/JsonDiffWriter.cs deleted file mode 100644 index 5bab7a9f4b..0000000000 --- a/src/Framework/Framework/Utils/JsonDiffWriter.cs +++ /dev/null @@ -1,383 +0,0 @@ -// using System.Linq; -// using System; -// using System.Text.Json; -// using System.Diagnostics; -// using System.Buffers; -// using DotVVM.Framework.ViewModel.Serialization; -// using System.Runtime.CompilerServices; - -// namespace DotVVM.Framework.Utils -// { -// ref struct JsonDiffWriter -// { -// static void AssertToken(ref Utf8JsonReader reader, JsonTokenType expected) -// { -// if (reader.TokenType != expected) -// throw new JsonException($"Expected {expected} but got {reader.TokenType}."); -// } -// static void CopyValue(ref Utf8JsonReader reader, Utf8JsonWriter writer) -// { -// Debug.Assert(reader.TokenType != JsonTokenType.PropertyName); - -// if (reader.TokenType is not JsonTokenType.StartArray and not JsonTokenType.StartObject) -// { -// if (reader.HasValueSequence) -// writer.WriteRawValue(reader.ValueSequence); -// else -// writer.WriteRawValue(reader.ValueSpan); - -// return; -// } - -// var depth = reader.CurrentDepth; -// while (reader.CurrentDepth >= depth) -// { -// CopyToken(ref reader, writer); -// reader.Read(); -// } -// } - -// static void CopyToken(ref Utf8JsonReader reader, Utf8JsonWriter writer) -// { -// switch (reader.TokenType) -// { -// case JsonTokenType.False: -// case JsonTokenType.True: -// case JsonTokenType.Null: -// case JsonTokenType.String: -// case JsonTokenType.Number: { -// if (reader.HasValueSequence) -// writer.WriteRawValue(reader.ValueSequence); -// else -// writer.WriteRawValue(reader.ValueSpan); -// break; -// } -// case JsonTokenType.PropertyName: { -// var length = reader.HasValueSequence ? reader.ValueSequence.Length : reader.ValueSpan.Length; -// Span buffer = length <= 1024 ? stackalloc byte[(int)length] : new byte[length]; -// var realLength = reader.CopyString(buffer); -// writer.WritePropertyName(buffer.Slice(0, realLength)); -// break; -// } -// case JsonTokenType.StartArray: { -// writer.WriteStartArray(); -// break; -// } -// case JsonTokenType.EndArray: { -// writer.WriteEndArray(); -// break; -// } -// case JsonTokenType.StartObject: { -// writer.WriteStartObject(); -// break; -// } -// case JsonTokenType.EndObject: { -// writer.WriteEndObject(); -// break; -// } -// default: { -// throw new JsonException($"Unexpected token {reader.TokenType}."); -// } -// } -// } - -// public delegate bool? IncludePropertyDelegate(string typeId, ReadOnlySpan propertyName); -// private readonly IncludePropertyDelegate? includePropertyOverride; - -// private Utf8JsonReader reader; -// private ReadOnlySequence remainingPreviousInput; - -// private byte[]? nameBufferRented; -// private Span nameBuffer; -// private int nameBufferPosition; -// private RefList lazyWriteStack; -// private readonly Utf8JsonWriter writer; -// private RefList sourceElementStack; -// private RefList objectTypeIds; -// private JsonDiffState state; - -// private JsonDiffWriter( -// IncludePropertyDelegate? includePropertyOverride, -// Span nameBuffer, -// Utf8JsonWriter writer, -// JsonElement sourceElement -// ) -// { -// this.includePropertyOverride = includePropertyOverride; -// this.nameBuffer = nameBuffer; -// this.writer = writer; -// this.sourceElementStack = new RefList(default); -// this.sourceElementStack.Enlarge(32); -// this.sourceElementStack.Add(sourceElement); -// this.objectTypeIds = new RefList(default); -// this.objectTypeIds.Enlarge(32); -// this.objectTypeIds.Add(default); -// } - -// public (int bytesRead, bool isDone) ContinueDiff(Span target, bool isFinalBlock) -// { -// var startPosition = reader.BytesConsumed; -// var position = startPosition; -// if (startPosition == 0) -// { -// reader = new Utf8JsonReader(target, isFinalBlock, default); -// } -// else -// { -// reader = new Utf8JsonReader(target, isFinalBlock, reader.CurrentState); -// } -// if (!reader.Read()) -// goto EndOfInput; - -// while (true) -// { -// if (state == JsonDiffState.Copying) -// { -// CopyToken(ref reader, writer); -// reader.Read(); -// } -// switch (reader.TokenType) -// { -// case JsonTokenType.StartObject: -// if (sourceElementStack.Last.ValueKind != JsonValueKind.Object) -// { -// CopyValue(ref reader, writer); -// state = JsonDiffState.Copying; -// } -// else -// { -// state = JsonDiffState.DiffObject; -// } -// break; -// case JsonTokenType.EndObject: -// break; -// case JsonTokenType.StartArray: -// break; -// case JsonTokenType.EndArray: -// break; -// case JsonTokenType.PropertyName: { -// var name = ReadName(ref reader); -// if (!reader.Read()) -// { -// state = JsonDiffState.DiffValue; -// goto EndOfInput; -// } -// break; -// } -// case JsonTokenType.Comment: -// break; -// case JsonTokenType.String: -// break; -// case JsonTokenType.Number: -// break; -// case JsonTokenType.True: -// break; -// case JsonTokenType.False: -// break; -// case JsonTokenType.Null: -// break; -// } -// } - -// return ((int)(reader.BytesConsumed - startPosition), true); - - -// EndOfInput: -// return ((int)(reader.BytesConsumed - startPosition), false); -// } - -// Span ReadName(ref Utf8JsonReader reader) -// { -// var length = reader.CopyString(nameBuffer.Slice(nameBufferPosition)); -// if (length == 0) -// throw new JsonException("Empty property name."); -// if (length < nameBuffer.Length) -// { -// return nameBuffer.Slice(nameBufferPosition, length); -// } -// var newBuffer = ArrayPool.Shared.Rent(length); -// nameBuffer.CopyTo(newBuffer); -// if (nameBufferRented is {}) -// ArrayPool.Shared.Return(nameBufferRented); -// nameBuffer = newBuffer; -// nameBufferRented = newBuffer; -// return ReadName(ref reader); -// } - -// void WriteoutLazyStack() -// { -// // foreach (var x in lazyWriteStack.AsSpan()) -// // { -// // if (x <= 0) -// // { -// // writer.Write TODO -// // } -// // } -// } - -// void AddPropertyToStack(ReadOnlySpan propertyName) -// { -// nameBufferPosition = nameBufferPosition + propertyName.Length; -// lazyWriteStack.Add(nameBufferPosition); -// } - -// void AddArrayToStack() -// { -// lazyWriteStack.Add(0); -// } - -// void DiffObject(in JsonElement source, ref Utf8JsonReader target) -// { -// AssertToken(ref target, JsonTokenType.StartObject); -// if (source.ValueKind != JsonValueKind.Object) -// { -// CopyValue(ref target, writer); -// return; -// } - -// string? typeId = null; -// target.Read(); -// // var typeId = target.TryGetValue("$type", out var t) ? t.Value() : null; - -// while (target.TokenType != JsonTokenType.EndObject) -// { -// AssertToken(ref target, JsonTokenType.PropertyName); -// var propertyName = ReadName(ref target); - -// if (propertyName[0] == '$') -// { -// if (propertyName.SequenceEqual("$type"u8)) -// { -// typeId = target.GetString(); -// } -// } -// else -// { -// if (typeId is {} && includePropertyOverride is {}) -// { -// var include = includePropertyOverride(typeId, propertyName); -// if (include == true) -// { -// writer.WritePropertyName(propertyName); -// CopyValue(ref target, writer); -// continue; -// } -// else if (include == false) -// { -// continue; -// } -// } -// } - -// if (!source.TryGetProperty(propertyName, out var sourceValue)) -// { -// writer.WritePropertyName(propertyName); -// CopyValue(ref target, writer); -// continue; -// } - -// if (sourceValue.ValueKind == JsonValueKind.Object && target.TokenType == JsonTokenType.StartObject) -// { -// writer.WritePropertyName(propertyName); -// DiffObject(sourceValue, ref target); -// } -// else if (sourceValue.ValueKind == JsonValueKind.Array && target.TokenType == JsonTokenType.StartArray) -// { -// writer.WritePropertyName(propertyName); -// DiffArray(sourceValue, ref target); -// } -// else if -// } -// foreach (var item in target) -// { -// if (sourceItem.Type == JTokenType.Array) -// { -// var sourceArr = (JArray)sourceItem; -// var subchanged = false; -// var arrDiff = Diff(sourceArr, (JArray)item.Value, out subchanged, nullOnRemoved); -// if (subchanged) -// { -// diff[item.Key] = arrDiff; -// } -// } -// else if (!JToken.DeepEquals(sourceItem, item.Value)) -// { -// diff[item.Key] = item.Value; -// } -// } - -// if (nullOnRemoved) -// { -// foreach (var item in source) -// { -// if (target[item.Key] == null) diff[item.Key] = JValue.CreateNull(); -// } -// } -// return diff; -// } - -// enum JsonDiffState -// { -// DiffValue, -// DiffObject, -// Copying -// } - - -// ref struct RefList -// { -// Span buffer; -// T[]? rented; -// int count; - -// public RefList(Span initialCapacity) -// { -// this.buffer = initialCapacity; -// this.rented = null; -// this.count = 0; -// } - -// public void Enlarge(int newCapacity) -// { -// if (newCapacity <= buffer.Length) -// return; - -// var newBuffer = ArrayPool.Shared.Rent(Math.Max(newCapacity, buffer.Length * 2)); -// buffer.CopyTo(newBuffer); -// if (rented is {}) -// ArrayPool.Shared.Return(rented); -// rented = newBuffer; -// buffer = newBuffer; -// } - -// public void Add(T item) -// { -// if (count == buffer.Length) -// Enlarge(buffer.Length + 8); -// buffer[count++] = item; -// } - -// public void Pop() -// { -// count--; -// if (RuntimeHelpers.IsReferenceOrContainsReferences()) -// buffer[count] = default!; -// } - -// public void Clear() -// { -// count = 0; - -// if (RuntimeHelpers.IsReferenceOrContainsReferences()) -// buffer.Fill(default!); -// } - -// public T? LastOrDefault() => count > 0 ? buffer[count - 1] : default; - -// public ref T this[int index] => ref buffer[index]; -// public ref T Last => ref buffer[count - 1]; - -// public Span AsSpan() => buffer.Slice(0, count); -// } -// } -// } diff --git a/src/Framework/Framework/Utils/JsonPatchWriter.cs b/src/Framework/Framework/Utils/JsonPatchWriter.cs deleted file mode 100644 index 9ef23da61e..0000000000 --- a/src/Framework/Framework/Utils/JsonPatchWriter.cs +++ /dev/null @@ -1,205 +0,0 @@ -// using System.Linq; -// using System; -// using System.Text.Json; -// using System.Diagnostics; -// using System.Buffers; -// using System.Collections.Generic; - -// namespace DotVVM.Framework.Utils -// { -// ref struct JsonPatchWriter -// { -// static void CopyValue(ref Utf8JsonReader reader, Utf8JsonWriter writer) -// { -// Debug.Assert(reader.TokenType != JsonTokenType.PropertyName); - -// if (reader.TokenType is not JsonTokenType.StartArray and not JsonTokenType.StartObject) -// { -// if (reader.HasValueSequence) -// writer.WriteRawValue(reader.ValueSequence); -// else -// writer.WriteRawValue(reader.ValueSpan); - -// return; -// } - -// var depth = reader.CurrentDepth; -// while (reader.CurrentDepth >= depth) -// { -// switch (reader.TokenType) -// { -// case JsonTokenType.False: -// case JsonTokenType.True: -// case JsonTokenType.Null: -// case JsonTokenType.String: -// case JsonTokenType.Number: { -// if (reader.HasValueSequence) -// writer.WriteRawValue(reader.ValueSequence); -// else -// writer.WriteRawValue(reader.ValueSpan); -// break; -// } -// case JsonTokenType.PropertyName: { -// var length = reader.HasValueSequence ? reader.ValueSequence.Length : reader.ValueSpan.Length; -// Span buffer = length <= 1024 ? stackalloc byte[(int)length] : new byte[length]; -// var realLength = reader.CopyString(buffer); -// writer.WritePropertyName(buffer.Slice(0, realLength)); -// break; -// } -// case JsonTokenType.StartArray: { -// writer.WriteStartArray(); -// break; -// } -// case JsonTokenType.EndArray: { -// writer.WriteEndArray(); -// break; -// } -// case JsonTokenType.StartObject: { -// writer.WriteStartObject(); -// break; -// } -// case JsonTokenType.EndObject: { -// writer.WriteEndObject(); -// break; -// } -// default: { -// throw new JsonException($"Unexpected token {reader.TokenType}."); -// } -// } -// reader.Read(); -// } -// } - -// private readonly Utf8JsonWriter writer; -// private List patchStack; -// private Span nameBuffer; -// private byte[] nameBufferRented; - -// private JsonPatchWriter( -// Utf8JsonWriter writer, -// JsonElement patch, -// Span nameBuffer -// ) -// { -// this.writer = writer; -// } - -// Span ReadName(ref Utf8JsonReader reader) -// { -// var length = reader.CopyString(nameBuffer); -// if (length < nameBuffer.Length) -// { -// return nameBuffer.Slice(length); -// } -// var newBuffer = ArrayPool.Shared.Rent(length); -// nameBuffer.CopyTo(newBuffer); -// if (nameBufferRented is {}) -// ArrayPool.Shared.Return(nameBufferRented); -// nameBuffer = newBuffer; -// nameBufferRented = newBuffer; -// return ReadName(ref reader); -// } - -// private void Patch(ref Utf8JsonReader original, JsonElement patchValue) -// { -// var patchKind = patchValue.ValueKind; -// if (patchKind == JsonValueKind.Object && original.TokenType == JsonTokenType.StartObject) -// { -// PatchObject(ref original, patchValue); -// } -// else if (patchKind == JsonValueKind.Array && original.TokenType == JsonTokenType.StartArray) -// { -// PatchArray(ref original, patchValue); -// } -// else -// { -// patchValue.WriteTo(writer); -// } -// } - -// void PatchObject(ref Utf8JsonReader original, JsonElement patch) -// { -// original.AssertToken(JsonTokenType.StartObject); -// if (patch.ValueKind != JsonValueKind.Object) -// { -// patch.WriteTo(writer); -// return; -// } -// writer.WriteStartObject(); -// original.Read(); - -// var patchedProperties = 0; -// while (original.TokenType == JsonTokenType.PropertyName) -// { -// var propertyName = ReadName(ref original); -// original.Read(); -// writer.WritePropertyName(propertyName); - -// if (!patch.TryGetProperty(propertyName, out var patchValue)) -// { -// CopyValue(ref original, writer); -// continue; -// } - -// patchedProperties += 1; - -// Patch(ref original, patchValue); -// } -// original.AssertToken(JsonTokenType.EndObject); - -// var remainingProperties = -patchedProperties; -// foreach (var p in patch.EnumerateObject()) -// { -// remainingProperties += 1; -// } -// if (remainingProperties > 0) -// { -// throw new JsonException("Patching failed"); -// } - -// writer.WriteEndObject(); -// } - -// void PatchArray(ref Utf8JsonReader original, JsonElement patch) -// { -// using var patchEnumerator = patch.EnumerateArray(); -// original.AssertRead(JsonTokenType.StartArray); -// writer.WriteStartArray(); - -// while (original.TokenType != JsonTokenType.EndArray) -// { -// if (!patchEnumerator.MoveNext()) -// { -// while (original.TokenType != JsonTokenType.EndArray) -// { -// original.Skip(); -// original.Read(); -// } -// } - -// var patchKind = patchEnumerator.Current.ValueKind; -// var tokenType = original.TokenType; -// if (patchKind == JsonValueKind.Object && tokenType == JsonTokenType.StartObject) -// { -// PatchObject(ref original, patchEnumerator.Current); -// } -// else if (patchKind == JsonValueKind.Array && tokenType == JsonTokenType.StartArray) -// { -// PatchArray(ref original, patchEnumerator.Current); -// } -// else -// { -// patchEnumerator.Current.WriteTo(writer); -// } -// original.Read(); -// } - -// while (patchEnumerator.MoveNext()) -// { -// patchEnumerator.Current.WriteTo(writer); -// } - -// writer.WriteEndArray(); -// } -// } -// } diff --git a/src/Framework/Framework/Utils/MultipleWriterStream.cs b/src/Framework/Framework/Utils/MultipleWriterStream.cs deleted file mode 100644 index f37770cff9..0000000000 --- a/src/Framework/Framework/Utils/MultipleWriterStream.cs +++ /dev/null @@ -1,133 +0,0 @@ -// using System; -// using System.Collections.Generic; -// using System.IO; -// using System.Linq; -// using System.Threading; -// using System.Threading.Tasks; - -// namespace DotVVM.Framework.Utils -// { -// internal class MultipleWriterStream : Stream -// { -// private readonly Stream[] innerStreams; -// private readonly bool parallel; -// private long position; -// private Task?[] tasks; - -// public MultipleWriterStream(IEnumerable innerStreams, bool parallel) -// { -// this.innerStreams = innerStreams.ToArray(); -// if (this.innerStreams.Length > 512) -// throw new ArgumentException("Too many output streams."); -// this.tasks = new Task?[this.innerStreams.Length]; -// this.parallel = parallel; -// foreach (var stream in this.innerStreams) -// { -// if (!stream.CanWrite) -// throw new ArgumentException("All streams must be writable."); -// } -// } - -// public override bool CanRead => false; - -// public override bool CanSeek => false; - -// public override bool CanWrite => true; - -// public override long Length => position; - -// public override long Position { get => position; set => throw new NotSupportedException(); } - -// public override void Flush() -// { -// foreach (var stream in innerStreams) -// stream.Flush(); -// } -// public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); -// public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); -// public override void SetLength(long value) { } -// public override void Write(byte[] buffer, int offset, int count) -// { -// foreach (var stream in innerStreams) -// stream.Write(buffer, offset, count); -// } - -// public override void Write(ReadOnlySpan buffer) -// { -// foreach (var stream in innerStreams) -// stream.Write(buffer); -// position += buffer.Length; -// } - -// public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) -// { -// if (parallel) -// { -// bool done = true; -// for (int i = 0; i < innerStreams.Length; i++) -// { -// tasks[i] = innerStreams[i].WriteAsync(buffer, offset, count, cancellationToken); -// if (!tasks[i]!.IsCompleted) -// done = false; -// } -// return done ? Task.CompletedTask : Task.WhenAll(tasks); -// } -// else -// { -// for (int i = 0; i < innerStreams.Length; i++) -// { -// var task = innerStreams[i].WriteAsync(buffer, offset, count, cancellationToken); -// if (!task.IsCompleted) -// { -// if (i == innerStreams.Length - 1) -// return task; -// else -// return SerialAsyncCore(task, i + 1, buffer, offset, count, cancellationToken); -// } -// } -// return Task.CompletedTask; -// } - -// async Task SerialAsyncCore(Task firstResult, int startIndex, byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) -// { -// await firstResult; -// for (int i = startIndex; i < innerStreams.Length; i++) -// await innerStreams[i].WriteAsync(buffer, offset, count, cancellationToken); -// position += count; -// } -// } - -// public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) -// { -// if (!parallel) -// { -// return SerialCore(buffer, cancellationToken); -// } -// else -// { -// bool done = true; -// for (int i = 0; i < innerStreams.Length; i++) -// { -// var task = innerStreams[i].WriteAsync(buffer, cancellationToken); -// if (task.IsCompleted) -// tasks[i] = Task.CompletedTask; -// else -// { -// tasks[i] = task.AsTask(); -// done = false; -// } -// } -// return done ? default : new ValueTask(Task.WhenAll(tasks)); -// } - - -// async ValueTask SerialCore(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) -// { -// foreach (var stream in innerStreams) -// await stream.WriteAsync(buffer, cancellationToken); - -// position += buffer.Length; -// } -// } -// } -// } diff --git a/src/Framework/Framework/Utils/ReadOnlyMemoryStream.cs b/src/Framework/Framework/Utils/ReadOnlyMemoryStream.cs index 5f3ca10b04..f46b887bbe 100644 --- a/src/Framework/Framework/Utils/ReadOnlyMemoryStream.cs +++ b/src/Framework/Framework/Utils/ReadOnlyMemoryStream.cs @@ -1,6 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// stolen from https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/IO/ReadOnlyMemoryStream.cs +// From https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/IO/ReadOnlyMemoryStream.cs using System.Threading; using System.Threading.Tasks; diff --git a/src/Framework/Framework/Utils/SystemTextJsonHacks.cs b/src/Framework/Framework/Utils/SystemTextJsonHacks.cs deleted file mode 100644 index 64dd476e8a..0000000000 --- a/src/Framework/Framework/Utils/SystemTextJsonHacks.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Buffers; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; -using DotVVM.Framework.Routing; - -namespace DotVVM.Framework.Utils -{ - static class SystemTextJsonHacks - { - public static void Populate(T obj, string input, JsonSerializerOptions options) - where T: class - { - if (obj == null) throw new ArgumentNullException(nameof(obj)); - - options = new JsonSerializerOptions(options); - options.TypeInfoResolver = new Resolver(options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver(), obj); - - var length = StringUtils.Utf8.GetByteCount(input) + 6; - var bytes = ArrayPool.Shared.Rent(length); - try - { - """{"X":"""u8.CopyTo(bytes.AsSpan().Slice(0, 5)); - StringUtils.Utf8Encode(input.AsSpan(), bytes.AsSpan(5)); - bytes[length - 1] = (byte)'}'; - var reader = new Utf8JsonReader(bytes.AsSpan().Slice(0, length)); - var result = JsonSerializer.Deserialize>(ref reader, options)?.X; - if (!object.ReferenceEquals(result, obj)) - { - throw new InvalidOperationException("The object was not populated correctly."); - } - } - finally - { - ArrayPool.Shared.Return(bytes); - } - } - - class PopulateClass - { - [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] - public T X { get; init; } = default!; - } - - class Resolver: IJsonTypeInfoResolver - { - private readonly IJsonTypeInfoResolver inner; - private readonly T populateInstance; - - public Resolver(IJsonTypeInfoResolver inner, T populateInstance) - { - this.inner = inner; - this.populateInstance = populateInstance; - } - public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) - { - var info = inner.GetTypeInfo(type, options); - if (info?.Type == typeof(PopulateClass)) - { - info.CreateObject = () => { - return new PopulateClass { X = populateInstance }; - }; - } - return info; - } - } - } -} diff --git a/src/Framework/Framework/ViewModel/Serialization/ClientTypeId.cs b/src/Framework/Framework/ViewModel/Serialization/ClientTypeId.cs deleted file mode 100644 index 9f3ebab582..0000000000 --- a/src/Framework/Framework/ViewModel/Serialization/ClientTypeId.cs +++ /dev/null @@ -1,153 +0,0 @@ -// using System; -// using System.Runtime.CompilerServices; -// using System.Runtime.InteropServices; -// using System.Text.Json; -// using DotVVM.Framework.Utils; - -// namespace DotVVM.Framework.ViewModel.Serialization -// { -// [StructLayout(LayoutKind.Explicit)] -// readonly struct ClientTypeId: IEquatable, IComparable -// { -// // first byte -// [FieldOffset(0)] -// readonly ulong a; -// [FieldOffset(8)] -// readonly ulong b; - -// [FieldOffset(0)] -// readonly byte controlByte; -// [FieldOffset(1)] -// readonly byte dataByte1; -// private ClientTypeId(ulong a, ulong b) -// { -// this.a = a; -// this.b = b; -// } - -// private ClientTypeId(bool isHash, ReadOnlySpan data) -// { -// if (data.Length > 15) throw new ArgumentException("Data too long"); -// controlByte = (byte)(data.Length | (isHash ? 0x10 : 0)); -// #if DotNetCore -// data.CopyTo(MemoryMarshal.CreateSpan(ref dataByte1, 15)); -// #else -// unsafe -// { -// fixed (byte* ptr = &dataByte1) -// { -// data.CopyTo(new Span(ptr, 15)); -// } -// } -// #endif -// } - -// struct Utf8StringCtor {} -// private ClientTypeId(ReadOnlySpan utf8Hash, Utf8StringCtor _) -// { -// if (utf8Hash.Length != 16) throw new ArgumentException("Hash must be 16 bytes long"); -// controlByte = (byte)(12 | 0x10); -// #if DotNetCore -// System.Buffers.Text.Base64.DecodeFromUtf8(utf8Hash, MemoryMarshal.CreateSpan(ref dataByte1, 12), out var _, out var _); -// #else -// unsafe -// { -// fixed (byte* ptr = &dataByte1) -// { -// System.Buffers.Text.Base64.DecodeFromUtf8(utf8Hash, new Span(ptr, 12), out var _, out var _); -// } -// } -// #endif -// } - -// public static ClientTypeId CreateHash(ReadOnlySpan data) => new ClientTypeId(true, data); -// public static ClientTypeId CreateString(ReadOnlySpan data) => new ClientTypeId(false, data); -// public static ClientTypeId Parse(ReadOnlySpan utf8) => -// utf8.Length == 16 ? new ClientTypeId(utf8, default(Utf8StringCtor)) : new ClientTypeId(false, utf8); - -// byte Length => (byte)(controlByte & 0xf); -// bool IsHash => ((controlByte >> 4) & 1) != 0; -// bool IsEmpty => controlByte == 0; - -// ReadOnlySpan Data => -// #if DotNetCore -// MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(in dataByte1), Length); -// #else -// throw new NotImplementedException(); -// #endif - -// public void WriteJson(Utf8JsonWriter writer) -// { -// if (IsEmpty) writer.WriteNullValue(); -// else if (IsHash) -// writer.WriteBase64StringValue(Data); -// else -// writer.WriteStringValue(Data); -// } -// public void WriteJson(Utf8JsonWriter writer, ReadOnlySpan propertyName) -// { -// if (IsEmpty) writer.WriteNull(propertyName); -// else if (IsHash) -// writer.WriteBase64String(propertyName, Data); -// else -// writer.WriteString(propertyName, Data); -// } - -// public static ClientTypeId ReadJson(ref Utf8JsonReader reader) -// { -// if (reader.TokenType == JsonTokenType.Null) return default; -// if (reader.TokenType != JsonTokenType.String) throw new JsonException("Expected string"); -// Span buffer = stackalloc byte[16]; -// var readBytes = reader.CopyString(buffer); -// return readBytes == 16 ? new ClientTypeId(buffer, default(Utf8StringCtor)) : new ClientTypeId(false, buffer.Slice(0, readBytes)); -// } - -// public static bool TryReadJson(ref Utf8JsonReader reader, out ClientTypeId output) -// { -// if (reader.TokenType == JsonTokenType.Null) -// { -// output = default; -// return true; -// } -// if (reader.TokenType != JsonTokenType.String) -// { -// output = default; -// return false; -// } -// const int maxLength = 16 * 6; // JsonConstants.MaxExpansionFactorWhileEscaping -// if (reader.GetValueLength() > maxLength) -// { -// output = default; -// return false; -// } -// Span buffer = stackalloc byte[maxLength]; -// var readBytes = reader.CopyString(buffer); -// if (readBytes > 16) -// { -// output = default; -// return false; -// } -// output = readBytes == 16 ? new ClientTypeId(buffer, default(Utf8StringCtor)) : new ClientTypeId(false, buffer.Slice(0, readBytes)); -// return true; -// } - -// public override string ToString() -// { -// if (IsEmpty) return "[Empty]"; -// if (IsHash) -// return Convert.ToBase64String(Data -// #if !DotNetCore -// .ToArray() -// #endif -// ); -// else -// return StringUtils.Utf8Decode(Data); -// } -// public override int GetHashCode() => (a, b).GetHashCode(); -// public override bool Equals(object? obj) => obj is ClientTypeId id && id.a == a && id.b == b; -// public bool Equals(ClientTypeId other) => other.a == a && other.b == b; -// public int CompareTo(ClientTypeId other) => a == other.a ? b.CompareTo(other.b) : a.CompareTo(other.a); -// public static bool operator ==(ClientTypeId left, ClientTypeId right) => left.Equals(right); -// public static bool operator !=(ClientTypeId left, ClientTypeId right) => !left.Equals(right); -// } -// } diff --git a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs index 5a89ac829a..0c99aa9674 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs @@ -219,21 +219,6 @@ public MemoryStream BuildViewModel(IDotvvmRequestContext context, object? comman return buffer; } - static ReadOnlySpan TrimJsonObject(ReadOnlySpan json) - { - if (json[0] == '[' && json[json.Length - 1] == ']') - { - json = json.Slice(1, json.Length - 2); - json = TrimStart(TrimEnd(json)); - } - // Trim { and } - if (json.Length < 2 || json[0] != '{' || json[json.Length - 1] != '}') - throw new InvalidOperationException("Internal bug."); - json = json.Slice(1, json.Length - 2); - // trim (ASCII) whitespace from end - return TrimEnd(json); - } - static ReadOnlySpan TrimStart(ReadOnlySpan json) { while (json.Length > 0 && char.IsWhiteSpace((char)json[0])) @@ -407,7 +392,7 @@ public void PopulateViewModel(IDotvvmRequestContext context, ReadOnlyMemoryall runtime; build; native; contentfiles; analyzers - + diff --git a/src/Tests/Runtime/ResourceManagerTests.cs b/src/Tests/Runtime/ResourceManagerTests.cs index 3b6f1b8178..e5f130afc0 100644 --- a/src/Tests/Runtime/ResourceManagerTests.cs +++ b/src/Tests/Runtime/ResourceManagerTests.cs @@ -111,8 +111,7 @@ public void ResourceManager_ConfigurationOldDeserialization() 'stylesheets': {{ 'newResource': {{ 'url': 'test' }} }} }} }}", ResourceConstants.GlobalizeResourceName); - var configuration = DotvvmTestHelper.CreateConfiguration(); - SystemTextJsonHacks.Populate(configuration, json.Replace("'", "\""), DefaultSerializerSettingsProvider.Instance.Settings); + var configuration = JsonSerializer.Deserialize(json.Replace("'", "\""), DefaultSerializerSettingsProvider.Instance.Settings); Assert.IsTrue(configuration.Resources.FindResource(ResourceConstants.GlobalizeResourceName) is ScriptResource); Assert.IsTrue(configuration.Resources.FindResource("newResource") is StylesheetResource); From abf5ce9b5f8f10ca375081515ef9aa114f4e7317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Thu, 23 May 2024 19:20:58 +0200 Subject: [PATCH 19/20] STJ migration: apply code review suggestions --- .../Metadata/PropertyDisplayMetadata.cs | 2 +- .../Serialization/DotvvmEnumConverter.cs | 11 +- .../ViewModelSerializationMap.cs | 161 ++++++++++-------- .../ViewModelSerializationMapper.cs | 1 - src/Samples/Api.Common/Model/Order.cs | 1 + 5 files changed, 93 insertions(+), 83 deletions(-) diff --git a/src/DynamicData/DynamicData/Metadata/PropertyDisplayMetadata.cs b/src/DynamicData/DynamicData/Metadata/PropertyDisplayMetadata.cs index d4b8113fb1..0b2f2da2d6 100644 --- a/src/DynamicData/DynamicData/Metadata/PropertyDisplayMetadata.cs +++ b/src/DynamicData/DynamicData/Metadata/PropertyDisplayMetadata.cs @@ -28,4 +28,4 @@ public class PropertyDisplayMetadata public StyleAttribute Styles { get; set; } public bool IsEditAllowed { get; set; } } -} +} \ No newline at end of file diff --git a/src/Framework/Framework/ViewModel/Serialization/DotvvmEnumConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmEnumConverter.cs index 201f1d1f38..933e72fb63 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmEnumConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmEnumConverter.cs @@ -23,9 +23,6 @@ public class DotvvmEnumConverter : JsonConverterFactory static MethodInfo CreateConverterGenericMethod = (MethodInfo)MethodFindingHelper.GetMethodFromExpression(() => default(DotvvmEnumConverter)!.CreateConverter()); public JsonConverter CreateConverter() where TEnum : unmanaged, Enum { - // if (!ReflectionUtils.EnumInfo.HasEnumMemberField) - // return (JsonConverter)new JsonStringEnumConverter().CreateConverter(typeof(TEnum), options)!; - var underlyingType = Enum.GetUnderlyingType(typeof(TEnum)); var isFlags = typeof(TEnum).IsDefined(typeof(FlagsAttribute), false); var isSigned = underlyingType == typeof(sbyte) || underlyingType == typeof(short) || underlyingType == typeof(int) || underlyingType == typeof(long); @@ -59,10 +56,12 @@ public class DotvvmEnumConverter : JsonConverterFactory var maxNameLen = fieldList.Max(x => x.Name.Length); var nameToEnum = new (TEnum Value, byte[] Name)[maxNameLen + 1][]; - foreach (var field in fieldList.GroupBy(x => x.Name.Length)) // TODO: do we want to allow the client to send the duplicate names? + // index enum names by length, then sort them by name + // the names in enumToName are already deduplicated, each value is represented by the shortest name + foreach (var field in enumToName.GroupBy(x => x.Value.Length)) { - var array = field.ToArray(); - Array.Sort(array, (a, b) => a.Name.AsSpan().SequenceCompareTo(b.Name.AsSpan())); + var array = field.Select(f => (f.Key, f.Value)).ToArray(); + Array.Sort(array, (a, b) => a.Value.AsSpan().SequenceCompareTo(b.Value.AsSpan())); nameToEnum[field.Key] = array; } diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs index baf49ad7dc..b35bf442d4 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs @@ -16,6 +16,7 @@ using System.Text.Json.Serialization; using System.Text.Json.Nodes; using DotVVM.Framework.Compilation.Binding; +using DotVVM.Framework.Compilation.Javascript; namespace DotVVM.Framework.ViewModel.Serialization { @@ -165,9 +166,9 @@ private Expression CallConstructor(Expression services, Dictionary throwImmediately ? throw new Exception(message) - : Expression.Throw(Expression.New( + : Throw(New( typeof(Exception).GetConstructor(new [] { typeof(string) })!, - Expression.Constant(message) + Constant(message) ), this.Type); } @@ -177,21 +178,21 @@ private Expression CallConstructor(Expression services, Dictionary CreateReaderFactory() { var block = new List(); - var reader = Expression.Parameter(typeof(Utf8JsonReader).MakeByRefType(), "reader"); - var jsonOptions = Expression.Parameter(typeof(JsonSerializerOptions), "jsonOptions"); - var value = Expression.Parameter(typeof(T), "value"); - var allowPopulate = Expression.Parameter(typeof(bool), "allowPopulate"); - var encryptedValuesReader = Expression.Parameter(typeof(EncryptedValuesReader), "encryptedValuesReader"); - var state = Expression.Parameter(typeof(DotvvmSerializationState), "state"); - var currentProperty = Expression.Variable(typeof(string), "currentProperty"); - var readerTmp = Expression.Variable(typeof(Utf8JsonReader), "readerTmp"); + var reader = Parameter(typeof(Utf8JsonReader).MakeByRefType(), "reader"); + var jsonOptions = Parameter(typeof(JsonSerializerOptions), "jsonOptions"); + var value = Parameter(typeof(T), "value"); + var allowPopulate = Parameter(typeof(bool), "allowPopulate"); + var encryptedValuesReader = Parameter(typeof(EncryptedValuesReader), "encryptedValuesReader"); + var state = Parameter(typeof(DotvvmSerializationState), "state"); + var currentProperty = Variable(typeof(string), "currentProperty"); + var readerTmp = Variable(typeof(Utf8JsonReader), "readerTmp"); // we first read all values into local variables and only then we either call the constructor or set the properties on the object var propertyVars = Properties .Where(p => p.TransferToServer) .ToDictionary( p => p, - p => Expression.Variable(p.Type, "prop_" + p.Name) + p => Variable(p.Type, "prop_" + p.Name) ); // If we have constructor property or if we have { get; init; } property, we always create new instance @@ -230,13 +231,13 @@ public ReaderDelegate CreateReaderFactory() Block( propertyVars .Where(p => p.Key.PropertyInfo is not PropertyInfo { GetMethod: null }) - .Select(p => Expression.Assign(p.Value, MemberAccess(value, p.Key))) + .Select(p => Assign(p.Value, MemberAccess(value, p.Key))) ) )); } // add current object to encrypted values, this is needed because one property can potentially contain more objects (is a collection) - block.Add(Expression.Call(encryptedValuesReader, nameof(EncryptedValuesReader.Nest), Type.EmptyTypes)); + block.Add(Call(encryptedValuesReader, nameof(EncryptedValuesReader.Nest), Type.EmptyTypes)); var propertiesSwitch = new List<(string fieldName, Expression readExpression)>(); @@ -257,7 +258,7 @@ public ReaderDelegate CreateReaderFactory() Constant(null, typeof(object)); // when suppressed, we read from the standard properties, because the object is nested in the - var isEVSuppressed = Expression.Property(encryptedValuesReader, "Suppressed"); + var isEVSuppressed = Property(encryptedValuesReader, "Suppressed"); var readEncrypted = property.ViewModelProtection == ProtectMode.EncryptData || property.ViewModelProtection == ProtectMode.SignData; @@ -267,21 +268,21 @@ public ReaderDelegate CreateReaderFactory() // encryptedValuesReader.Suppress() // value.{property} = ({property.Type})Deserialize(serializer, encryptedValuesReader.ReadValue({propertyIndex}), {property}, (object)value.{PropertyInfo}); // encryptedValuesReader.EndSuppress() - Expression readEncryptedValue = Expression.Block( - Expression.Assign( + Expression readEncryptedValue = Block( + Assign( readerTmp, - Call(JsonSerializationCodegenFragments.ReadEncryptedValueMethod, Call(encryptedValuesReader, "ReadValue", Type.EmptyTypes, Expression.Constant(propertyIndex))) + Call(JsonSerializationCodegenFragments.ReadEncryptedValueMethod, Call(encryptedValuesReader, "ReadValue", Type.EmptyTypes, Constant(propertyIndex))) ), - Expression.Call(encryptedValuesReader, "Suppress", Type.EmptyTypes), + Call(encryptedValuesReader, "Suppress", Type.EmptyTypes), - Expression.Assign( + Assign( propertyVar, DeserializePropertyValue(property, readerTmp, propertyVar, jsonOptions, state)) ); - readEncryptedValue = Expression.TryFinally( + readEncryptedValue = TryFinally( readEncryptedValue, - Expression.Call(encryptedValuesReader, "EndSuppress", Type.EmptyTypes) + Call(encryptedValuesReader, "EndSuppress", Type.EmptyTypes) ); // if (!encryptedValuesReader.Suppressed) @@ -321,7 +322,7 @@ public ReaderDelegate CreateReaderFactory() body = IfThenElse( isEVSuppressed, body, - Expression.Call(JsonSerializationCodegenFragments.IgnoreValueMethod, reader) + Call(JsonSerializationCodegenFragments.IgnoreValueMethod, reader) ); } @@ -380,8 +381,7 @@ public ReaderDelegate CreateReaderFactory() var ex = Lambda>( 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(); + return ex.CompileFast(flags: CompilerFlags.ThrowOnNotSupportedExpression); } Expression MemberAccess(Expression obj, ViewModelPropertyMap property) @@ -419,7 +419,7 @@ public WriterDelegate CreateWriterFactory() for (int propertyIndex = 0; propertyIndex < Properties.Length; propertyIndex++) { var property = Properties[propertyIndex]; - var endPropertyLabel = Expression.Label("end_property_" + property.Name); + var endPropertyLabel = Label("end_property_" + property.Name); if (property.TransferToClient && property.PropertyInfo is not PropertyInfo { GetMethod: null }) { @@ -433,10 +433,10 @@ public WriterDelegate CreateWriterFactory() Expression condition = isPostback; if (property.TransferAfterPostback) { - condition = Expression.Not(condition); + condition = Not(condition); } - block.Add(Expression.IfThen(condition, Expression.Goto(endPropertyLabel))); + block.Add(IfThen(condition, Goto(endPropertyLabel))); } // (object)value.{property.PropertyInfo.Name} @@ -449,7 +449,7 @@ public WriterDelegate CreateWriterFactory() { // encryptedValuesWriter.WriteValue({propertyIndex}, (object)value.{property.PropertyInfo.Name}); block.Add( - Expression.Call(encryptedValuesWriter, nameof(EncryptedValuesWriter.WriteValue), Type.EmptyTypes, Expression.Constant(propertyIndex), Expression.Convert(prop, typeof(object)))); + Call(encryptedValuesWriter, nameof(EncryptedValuesWriter.WriteValue), Type.EmptyTypes, Constant(propertyIndex), Convert(prop, typeof(object)))); } @@ -463,61 +463,60 @@ public WriterDelegate CreateWriterFactory() if (writeEV) { // encryptedValuesWriter.Suppress(); - propertyBlock.Add(Expression.Call(encryptedValuesWriter, nameof(EncryptedValuesWriter.Suppress), Type.EmptyTypes)); + propertyBlock.Add(Call(encryptedValuesWriter, nameof(EncryptedValuesWriter.Suppress), Type.EmptyTypes)); } else { // encryptedValuesWriter.Nest({propertyIndex}); - propertyBlock.Add(Expression.Call(encryptedValuesWriter, nameof(EncryptedValuesWriter.Nest), Type.EmptyTypes, Expression.Constant(propertyIndex))); + propertyBlock.Add(Call(encryptedValuesWriter, nameof(EncryptedValuesWriter.Nest), Type.EmptyTypes, Constant(propertyIndex))); } } // writer.WritePropertyName({property.Name}); - propertyBlock.Add(Expression.Call(writer, nameof(Utf8JsonWriter.WritePropertyName), Type.EmptyTypes, - Expression.Constant(property.Name))); + propertyBlock.Add(Call(writer, nameof(Utf8JsonWriter.WritePropertyName), Type.EmptyTypes, + Constant(property.Name))); // serializer.Serialize(serializer, writer, {property}, (object)value.{property.PropertyInfo.Name}); propertyBlock.Add(GetSerializeExpression(property, writer, prop, jsonOptions, dotvvmState)); - Expression propertyFinally = Expression.Default(typeof(void)); + Expression propertyFinally = Default(typeof(void)); if (checkEV) { if (writeEV) { // encryptedValuesWriter.EndSuppress(); - propertyFinally = Expression.Call(encryptedValuesWriter, nameof(EncryptedValuesWriter.EndSuppress), Type.EmptyTypes); + propertyFinally = Call(encryptedValuesWriter, nameof(EncryptedValuesWriter.EndSuppress), Type.EmptyTypes); } // encryption is worthless if the property is not being transferred both ways // therefore ClearEmptyNest throws exception if the property contains encrypted values else if (!property.IsFullyTransferred()) { // encryptedValuesWriter.ClearEmptyNest(); - propertyFinally = Expression.Call(encryptedValuesWriter, nameof(EncryptedValuesWriter.ClearEmptyNest), Type.EmptyTypes); + propertyFinally = Call(encryptedValuesWriter, nameof(EncryptedValuesWriter.ClearEmptyNest), Type.EmptyTypes); } else { // encryptedValuesWriter.End(); - propertyFinally = Expression.Call(encryptedValuesWriter, nameof(EncryptedValuesWriter.End), Type.EmptyTypes); + propertyFinally = Call(encryptedValuesWriter, nameof(EncryptedValuesWriter.End), Type.EmptyTypes); } } block.Add( - Expression.TryFinally( - Expression.Block(propertyBlock), + TryFinally( + Block(propertyBlock), propertyFinally ) ); } } - block.Add(Expression.Label(endPropertyLabel)); + block.Add(Label(endPropertyLabel)); } // compile the expression 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(); + return ex.CompileFast(flags: CompilerFlags.ThrowOnNotSupportedExpression); } /// @@ -592,17 +591,10 @@ private Expression CallPropertyConverterWrite(JsonConverter converter, Expressio private Expression? TryDeserializePrimitive(Expression reader, Type type) { // Utf8JsonReader readerTest = default; - // readerTest.CopyString( if (type == typeof(bool)) return Call(reader, "GetBoolean", Type.EmptyTypes); if (type == typeof(byte)) return Call(reader, "GetByte", Type.EmptyTypes); - // if (type == typeof(byte[])) - // return Call(reader, "GetBytesFromBase64", Type.EmptyTypes); - // if (type == typeof(DateTime)) - // return Call(reader, "GetDateTime", Type.EmptyTypes); - // if (type == typeof(DateTimeOffset)) - // return Call(reader, "GetDateTimeOffset", Type.EmptyTypes); if (type == typeof(decimal)) return Call(reader, "GetDecimal", Type.EmptyTypes); if (type == typeof(double)) @@ -729,26 +721,38 @@ private Expression GetSerializeExpression(ViewModelPropertyMap property, Express { return CallPropertyConverterWrite(converter, writer, value, jsonOptions, dotvvmState); } - if (TrySerializePrimitive(writer, value) is {} primite) + if (TrySerializePrimitive(writer, value) is {} primitive) { - return primite; + return primitive; } if (this.viewModelJsonConverter.CanConvert(value.Type)) { if (property.AllowDynamicDispatch && !value.Type.IsSealed) { - // TODO: ?? - // return Call( - // JsonSerializationCodegenFragments.DeserializeViewModelDynamicMethod.MakeGenericMethod(value.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 - // dotvvmState); // DotvvmSerializationState state + if (value.Type.IsAbstract) + { + // Always doing dynamic dispatch to an unknown type + return Call( + (MethodInfo)MethodFindingHelper.GetMethodFromExpression(() => + JsonSerializer.Serialize(default(Utf8JsonWriter)!, null, default(JsonSerializerOptions)!)), + writer, + Convert(value, typeof(object)), + jsonOptions + ); + } + else + { + // We use cached converter for T, if value.GetType() == T + var defaultConverter = this.viewModelJsonConverter.GetConverter(value.Type); + return Call( + JsonSerializationCodegenFragments.SerializeViewModelDynamicMethod.MakeGenericMethod(value.Type), + writer, jsonOptions, value, Constant(defaultConverter), dotvvmState); + } } else { - var defaultConverter = this.viewModelJsonConverter.GetConverter(value.Type); - return CallPropertyConverterWrite(defaultConverter, writer, value, jsonOptions, dotvvmState); + var viewModelConverter = this.viewModelJsonConverter.GetConverter(value.Type); + return CallPropertyConverterWrite(viewModelConverter, writer, value, jsonOptions, dotvvmState); } } @@ -791,6 +795,28 @@ private static void SerializeValue(Utf8JsonWriter writer, JsonSerializer } } + public static readonly MethodInfo SerializeViewModelDynamicMethod = typeof(JsonSerializationCodegenFragments).GetMethod(nameof(SerializeViewModelDynamic), BindingFlags.NonPublic | BindingFlags.Static).NotNull(); + private static void SerializeViewModelDynamic(Utf8JsonWriter writer, JsonSerializerOptions options, TVM? value, IDotvvmJsonConverter defaultConverter, DotvvmSerializationState state) + where TVM: class + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + var type = value.GetType(); + if (defaultConverter is {} && type == typeof(TVM)) + { + defaultConverter.Write(writer, value, options, state); + return; + } + else + { + JsonSerializer.Serialize(writer, value, type, options); + } + } + public static readonly MethodInfo DeserializeValueStaticMethod = typeof(JsonSerializationCodegenFragments).GetMethod(nameof(DeserializeValueStatic), BindingFlags.NonPublic | BindingFlags.Static).NotNull(); private static TValue? DeserializeValueStatic(ref Utf8JsonReader reader, JsonSerializerOptions options) { @@ -817,7 +843,7 @@ private static void SerializeValue(Utf8JsonWriter writer, JsonSerializer } return (TValue?)JsonSerializer.Deserialize(ref reader, type!, options); - } + null!} 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, IDotvvmJsonConverter defaultConverter, DotvvmSerializationState state) @@ -869,20 +895,5 @@ static void IgnoreValue(ref Utf8JsonReader reader) } reader.Read(); } - - // private static TValue? DeserializeViewModel(ref Utf8JsonReader reader, ViewModelJsonConverter.VMConverter converter, JsonSerializerOptions options, DotvvmSerializationState state, TValue existingValue, bool allowPopulate) - // { - // if (reader.TokenType == JsonTokenType.Null) - // return default; - - // if (allowPopulate) - // { - // return converter.Populate(ref reader, options, existingValue, state); - // } - // else - // { - // return converter.Read(ref reader, typeof(TValue), options, state); - // } - // } } } diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs index 63e8483e0e..17738372c3 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs @@ -51,7 +51,6 @@ public class ViewModelSerializationMapper : IViewModelSerializationMapper protected virtual ViewModelSerializationMap CreateMap(Type type) => (ViewModelSerializationMap)CreateMapGenericMethod.MakeGenericMethod(type).Invoke(this, Array.Empty())!; static MethodInfo CreateMapGenericMethod = - // typeof(ViewModelSerializationMapper).GetMethod(nameof(CreateMap), 1, BindingFlags.NonPublic | BindingFlags.Instance, null, [], [])!; (MethodInfo)MethodFindingHelper.GetMethodFromExpression(() => default(ViewModelSerializationMapper)!.CreateMap()); protected virtual ViewModelSerializationMap CreateMap() { diff --git a/src/Samples/Api.Common/Model/Order.cs b/src/Samples/Api.Common/Model/Order.cs index 01187de269..134e433e76 100644 --- a/src/Samples/Api.Common/Model/Order.cs +++ b/src/Samples/Api.Common/Model/Order.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Newtonsoft.Json; namespace DotVVM.Samples.BasicSamples.Api.Common.Model { From f7fa2e74b05de482610ee33743247205f79d7934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Thu, 23 May 2024 19:36:29 +0200 Subject: [PATCH 20/20] STJ: bit more tests for enum converter --- .../Serialization/ViewModelSerializationMap.cs | 2 +- src/Tests/ViewModel/SerializerTests.cs | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs index b35bf442d4..6172a3ce49 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMap.cs @@ -843,7 +843,7 @@ private static void SerializeViewModelDynamic(Utf8JsonWriter writer, JsonSe } return (TValue?)JsonSerializer.Deserialize(ref reader, type!, options); - null!} + } 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, IDotvvmJsonConverter defaultConverter, DotvvmSerializationState state) diff --git a/src/Tests/ViewModel/SerializerTests.cs b/src/Tests/ViewModel/SerializerTests.cs index f0056a812b..54bf53fe11 100644 --- a/src/Tests/ViewModel/SerializerTests.cs +++ b/src/Tests/ViewModel/SerializerTests.cs @@ -533,17 +533,27 @@ public void SupportsEnums() [DataRow(TestViewModelWithEnums.ByteEnum.A, "'A'", true)] [DataRow(TestViewModelWithEnums.ByteEnum.B, "'B'", true)] [DataRow(TestViewModelWithEnums.ByteEnum.C, "'C'", true)] + [DataRow((TestViewModelWithEnums.ByteEnum)45, "45", false)] + [DataRow(TestViewModelWithEnums.Int16Enum.A, "'A'", true)] + [DataRow(TestViewModelWithEnums.Int16Enum.B, "'B'", true)] + [DataRow((TestViewModelWithEnums.Int16Enum)(-6), "-6", false)] [DataRow(TestViewModelWithEnums.EnumMemberEnum.A, "'member-a'", true)] [DataRow(TestViewModelWithEnums.DuplicateNameEnum.A, "'A'", true)] [DataRow(TestViewModelWithEnums.DuplicateNameEnum.B, "'A'", true)] [DataRow(TestViewModelWithEnums.DuplicateNameEnum.C, "'C'", true)] [DataRow(TestViewModelWithEnums.DuplicateNameEnum.DAndAlsoLonger, "'D'", true)] + [DataRow((TestViewModelWithEnums.DuplicateNameEnum)3, "3", false)] [DataRow(TestViewModelWithEnums.Int32FlagsEnum.ABC, "'a+b+c'", true)] [DataRow(TestViewModelWithEnums.Int32FlagsEnum.A | TestViewModelWithEnums.Int32FlagsEnum.BCD, "'b+c+d,a'", true)] + [DataRow(TestViewModelWithEnums.Int32FlagsEnum.Everything, "'everything'", true)] [DataRow((TestViewModelWithEnums.Int32FlagsEnum)2356543, "2356543", false)] [DataRow((TestViewModelWithEnums.Int32FlagsEnum)0, "0", true)] [DataRow((TestViewModelWithEnums.UInt64FlagsEnum)0, "0", true)] [DataRow(TestViewModelWithEnums.UInt64FlagsEnum.F1 | TestViewModelWithEnums.UInt64FlagsEnum.F2 | TestViewModelWithEnums.UInt64FlagsEnum.F64, "'F64,F2,F1'", true)] + [DataRow(TestViewModelWithEnums.UInt64FlagsEnum.F64, "'F64'", true)] + [DataRow((TestViewModelWithEnums.UInt64FlagsEnum)12 | TestViewModelWithEnums.UInt64FlagsEnum.F64, "9223372036854775820", false)] + [DataRow((TestViewModelWithEnums.UInt64FlagsEnum)ulong.MaxValue, "18446744073709551615", false)] + [DataRow((TestViewModelWithEnums.UInt64FlagsEnum)0, "0", true)] public void TestEnumSerialization(object enumValue, string serializedValue, bool canDeserialize) { var json = JsonSerializer.Serialize(enumValue, DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe); @@ -958,11 +968,11 @@ public enum Int32FlagsEnum : int } [Flags] - public enum UInt64FlagsEnum: long + public enum UInt64FlagsEnum: ulong { F1 = 1, F2 = 2, - F64 = 1L << 63, + F64 = 1UL << 63, } }