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/.config/dotnet-tools.json b/src/.config/dotnet-tools.json index 762a929601..d41b636bf6 100644 --- a/src/.config/dotnet-tools.json +++ b/src/.config/dotnet-tools.json @@ -15,4 +15,4 @@ ] } } -} \ No newline at end of file +} 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/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/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/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/DotVVM.Core.csproj b/src/Framework/Core/DotVVM.Core.csproj index 59d4af116a..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/Core/ViewModel/BindAttribute.cs b/src/Framework/Core/ViewModel/BindAttribute.cs index 354416c024..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 { @@ -21,6 +21,17 @@ public class BindAttribute : Attribute /// public string? Name { get; set; } + 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, 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; } + + /// See + 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..9fcf9a9480 100644 --- a/src/Framework/Core/ViewModel/DefaultPropertySerialization.cs +++ b/src/Framework/Core/ViewModel/DefaultPropertySerialization.cs @@ -1,11 +1,14 @@ -using System.Reflection; -using Newtonsoft.Json; +using System; +using System.Reflection; +using System.Text.Json.Serialization; namespace DotVVM.Framework.ViewModel { public class DefaultPropertySerialization : IPropertySerialization { - public string ResolveName(PropertyInfo propertyInfo) + static readonly Type? JsonPropertyNJ = Type.GetType("Newtonsoft.Json.JsonPropertyAttribute, Newtonsoft.Json"); + static readonly PropertyInfo? JsonPropertyNJPropertyName = JsonPropertyNJ?.GetProperty("PropertyName"); + public string ResolveName(MemberInfo propertyInfo) { var bindAttribute = propertyInfo.GetCustomAttribute(); if (bindAttribute != null) @@ -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?.PropertyName)) + return jsonPropertyAttribute!.Name!; + } + + if (JsonPropertyNJ is not null) + { + var jsonPropertyNJAttribute = propertyInfo.GetCustomAttribute(JsonPropertyNJ); + if (jsonPropertyNJAttribute is not null) { - return jsonPropertyAttribute!.PropertyName!; + var name = (string?)JsonPropertyNJPropertyName!.GetValue(jsonPropertyNJAttribute); + if (!string.IsNullOrEmpty(name)) + { + return name; + } } } 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/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..d006b64d56 100644 --- a/src/Framework/Framework/Binding/Expressions/BindingDebugJsonConverter.cs +++ b/src/Framework/Framework/Binding/Expressions/BindingDebugJsonConverter.cs @@ -1,32 +1,31 @@ using System; using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.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 bool CanConvert(Type objectType) => - typeof(IBinding).IsAssignableFrom(objectType); - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => - throw new NotImplementedException("Deserializing dotvvm bindings from JSON is not supported."); - public override void WriteJson(JsonWriter w, object? valueObj, JsonSerializer serializer) + internal class BindingDebugJsonConverter(bool detailed): GenericWriterJsonConverter((writer, obj, options) => { + if (detailed) { - var obj = valueObj; - w.WriteValue(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(); + 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()); + } + }) + { + public BindingDebugJsonConverter() : this(false) { } } } 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..8d68009242 100644 --- a/src/Framework/Framework/Binding/Expressions/CommandBindingExpression.cs +++ b/src/Framework/Framework/Binding/Expressions/CommandBindingExpression.cs @@ -2,19 +2,14 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Linq.Expressions; -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; using DotVVM.Framework.Runtime.Filters; using DotVVM.Framework.Utils; using FastExpressionCompiler; -using Newtonsoft.Json; namespace DotVVM.Framework.Binding.Expressions { @@ -149,7 +144,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..8311cf8529 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); } @@ -77,34 +78,57 @@ public static byte[] EncryptJson(JToken json, IViewModelProtector protector) } public static string[] GetEncryptionPurposes() { - return new[] { - "StaticCommand", - }; + 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))) @@ -112,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(); + ParameterInfo?[] methodParameters = method.GetParameters(); + 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/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/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/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/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/DotvvmViewCompilationServiceExtensions.cs b/src/Framework/Framework/Compilation/DotvvmViewCompilationServiceExtensions.cs index 3d5a95919c..32147a7b11 100644 --- a/src/Framework/Framework/Compilation/DotvvmViewCompilationServiceExtensions.cs +++ b/src/Framework/Framework/Compilation/DotvvmViewCompilationServiceExtensions.cs @@ -11,7 +11,8 @@ public static class DotvvmViewCompilationServiceExtensions public static Task Precompile(this ViewCompilationConfiguration compilationConfiguration, DotvvmConfiguration config, IStartupTracer startupTracer) { return Task.Run(async () => { - var compilationService = config.ServiceProvider.GetService(); + var compilationService = config.ServiceProvider.GetRequiredService(); + if (compilationConfiguration.BackgroundCompilationDelay != null) { await Task.Delay(compilationConfiguration.BackgroundCompilationDelay.Value); diff --git a/src/Framework/Framework/Compilation/Javascript/Ast/JsLiteral.cs b/src/Framework/Framework/Compilation/Javascript/Ast/JsLiteral.cs index 20c0c9f4a6..18ac10a5a8 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,15 @@ public sealed class JsLiteral : JsExpression /// public string LiteralValue { - get => JavascriptCompilationHelper.CompileConstant(Value); - set => Value = JsonConvert.DeserializeObject(value, DefaultSerializerSettingsProvider.Instance.Settings); + // 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; } 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..c514cb4646 100644 --- a/src/Framework/Framework/Compilation/Javascript/JavascriptCompilationHelper.cs +++ b/src/Framework/Framework/Compilation/Javascript/JavascriptCompilationHelper.cs @@ -1,24 +1,24 @@ using System; +using System.Globalization; using System.Linq; +using System.Text.Json; 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) + int i => i.ToString(CultureInfo.InvariantCulture).DotvvmInternString(trySystemIntern: false), + string s => KnockoutHelper.MakeStringLiteral(s, htmlSafe), + _ => 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..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 } && @@ -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 (!ReflectionUtils.IsPrimitiveType(vmAnnotation.Type) && vmAnnotation.Type != typeof(void) && !ReflectionUtils.IsCollection(vmAnnotation.Type)) + 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..17a01c5e3b 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 + public readonly struct NamespaceImport: IEquatable { - [JsonProperty("namespace")] - public readonly string Namespace; - [JsonProperty("alias")] - public readonly string? Alias; + [JsonPropertyName("namespace")] + public string Namespace { get; } + [JsonPropertyName("alias")] + 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 d66ed10b8e..1a446d5ffb 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 HtmlSafeLessParanoidEncoder; - 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 DotvvmObjectConverter(), + new DotvvmEnumConverter(), new DotvvmDictionaryConverter(), new DotvvmByteArrayConverter(), new DotvvmCustomPrimitiveTypeConverter() }, + NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, + Encoder = HtmlSafeLessParanoidEncoder, MaxDepth = defaultMaxSerializationDepth }; } @@ -51,10 +58,15 @@ public static DefaultSerializerSettingsProvider Instance private DefaultSerializerSettingsProvider() { - JsonConvert.DefaultSettings = () => new JsonSerializerSettings() { MaxDepth = defaultMaxSerializationDepth }; + var encoderSettings = new TextEncoderSettings(); + encoderSettings.AllowRange(UnicodeRanges.All); + encoderSettings.ForbidCharacters('>', '<'); + HtmlSafeLessParanoidEncoder = JavaScriptEncoder.Create(encoderSettings); Settings = CreateSettings(); + SettingsHtmlUnsafe = new JsonSerializerOptions(Settings) + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; } - - public static JsonSerializer CreateJsonSerializer() => JsonSerializer.Create(Instance.Settings); } } 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..6f664f4347 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()) { } /// @@ -247,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/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(); - } - if (property.PropertyType == typeof(DotvvmGlobalFeatureFlag) && prop is object) + var info = base.GetTypeInfo(type, options); + object? defaults = null; + if (type == typeof(DotvvmSecurityConfiguration) || + type == typeof(DotvvmRuntimeConfiguration) || + type == typeof(DotvvmCompilationPageConfiguration) || + type == typeof(DotvvmPerfWarningsConfiguration) || + type == typeof(DotvvmDiagnosticsConfiguration) || + type == typeof(DotvvmMarkupConfiguration) || + type == typeof(DotvvmExperimentalFeaturesConfiguration) || + type == typeof(ViewCompilationConfiguration)) { - // ignore defaults for brevity - property.ShouldSerialize = o => - !(prop.GetValue(o) is DotvvmGlobalFeatureFlag flag) || flag.Enabled; + defaults = Activator.CreateInstance(type, nonPublic: true); } - - if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType) && property.PropertyType != typeof(string) && prop is object) + else if (type == typeof(DotvvmConfiguration)) { - property.TypeNameHandling = TypeNameHandling.None; - var originalCondition = property.ShouldSerialize; - property.ShouldSerialize = o => - originalCondition?.Invoke(o) != false && - (!(prop.GetValue(o) is IEnumerable c) || c.Cast().Any()); + defaults = new DotvvmConfiguration(new EmptyServiceProvider()) { DefaultCulture = null! }; } - - if (prop is object && prop.Name == "CompiledViewsAssemblies" && prop.DeclaringType == typeof(DotvvmConfiguration)) + 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.ShouldSerialize = o => - !(prop.GetValue(o) is IEnumerable c) || !new [] { "CompiledViews.dll" }.SequenceEqual(c); + 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 + { + 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()); + } } + 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/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/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..251186ab2b 100644 --- a/src/Framework/Framework/Controls/DotvvmControlDebugJsonConverter.cs +++ b/src/Framework/Framework/Controls/DotvvmControlDebugJsonConverter.cs @@ -1,57 +1,47 @@ 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; +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 bool CanConvert(Type objectType) => - typeof(DotvvmBindableObject).IsAssignableFrom(objectType); - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => - throw new NotImplementedException("Deserializing dotvvm control from JSON is not supported."); - public override void WriteJson(JsonWriter w, object? valueObj, JsonSerializer serializer) + 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))) { - if (valueObj is null) - { - w.WriteNull(); - return; - } - var obj = (DotvvmBindableObject)valueObj; - w.WriteStartObject(); - - w.WritePropertyName("Control"); - w.WriteValue(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); - - 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.WriteValue(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/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..877a117065 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)) { @@ -59,11 +58,11 @@ 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()) { - 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..d618b8af02 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.HtmlSafeLessParanoidEncoder : 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.AsSpan(), 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..9f426fa708 100644 --- a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs +++ b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs @@ -58,7 +58,9 @@ public static IServiceCollection RegisterDotVVMServices(IServiceCollection servi services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); @@ -91,7 +93,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(); @@ -142,12 +144,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/Diagnostics/CompilationPageApiPresenter.cs b/src/Framework/Framework/Diagnostics/CompilationPageApiPresenter.cs index f721fc868c..f5c8310954 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 { @@ -33,8 +33,8 @@ public async Task ProcessRequest(IDotvvmRequestContext context) } response.StatusCode = 500; - response.ContentType = "application/json"; - await response.WriteAsync(JsonConvert.SerializeObject(compilationService.GetFilesWithFailedCompilation())); + 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 4e446222d0..3b2cf7c6c4 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 { @@ -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(); @@ -31,11 +34,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..297bac754d 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; @@ -35,6 +36,19 @@ 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, Func viewModelBuffer) + { + if (informationSender.State >= DiagnosticsInformationSenderState.Full) + { + using (var stream = viewModelBuffer()) + { + ViewModelJson = stream.ReadToMemory(); + } + } + } private EventTiming CreateEventTiming(string eventName) { @@ -87,30 +101,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 +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(), - 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/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/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/JsonSizeAnalyzer.cs b/src/Framework/Framework/Diagnostics/JsonSizeAnalyzer.cs index 5e176771be..5a0245b915 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.Compilation.Binding; using DotVVM.Framework.Utils; using DotVVM.Framework.ViewModel.Serialization; using FastExpressionCompiler; -using Newtonsoft.Json.Linq; namespace DotVVM.Framework.Diagnostics { @@ -19,62 +20,89 @@ 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(ReadOnlySpan json, Type? rootViewModelType) + { + 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 analyzeToken(JToken token) + AtomicSizeProfile analyzeNode(ref Utf8JsonReader json, Type? type) { - switch (token.Type) + switch (json.TokenType) { - case JTokenType.Object: - return new (InclusiveSize: analyzeObject((JObject)token), ExclusiveSize: 2); - case JTokenType.Array: { + 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); - foreach (var item in (JArray)token) + while (json.TokenType != JsonTokenType.EndArray) { - r += analyzeToken(item); + 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 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: + 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); - 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); + default: { + Debug.Assert(false, $"Unexpected token type {json.TokenType}"); + var start = json.TokenStartIndex; + json.Skip(); + return new((int)(json.BytesConsumed - start)); + } } } - int analyzeObject(JObject j) + int analyzeObject(ref Utf8JsonReader json, Type? type) { - var type = ((string?)j.Property("$type")?.Value)?.Apply(viewModelMapper.GetMapByTypeId); + 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 totalSize = new AtomicSizeProfile(0); - foreach (var prop in j.Properties()) + var startIndex = json.TokenStartIndex; + var exclusiveSize = 2; + + json.AssertRead(JsonTokenType.StartObject); + while (json.TokenType == JsonTokenType.PropertyName) { - var propSize = analyzeToken(prop.Value); - props[prop.Name] = propSize; + 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); + } - totalSize += propSize; - totalSize += 4 + prop.Name.Length; // 2 for the quotes, 1 for :, 1 for , + var propertyMap = typeMap?.Properties.FirstOrDefault(p => p.Name == propName); + + 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 classSize = new ClassSizeProfile(totalSize, props); + 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; @@ -83,10 +111,13 @@ int analyzeObject(JObject j) { results[typeName] = classSize; } - return totalSize.InclusiveSize; + return inclusiveSize; } + + if (json.TokenType == JsonTokenType.None) + json.AssertRead(JsonTokenType.None); - var totalSize = analyzeObject(json); + var totalSize = analyzeObject(ref json, rootViewModelType); return new JsonSizeProfile(results, totalSize); } @@ -122,7 +153,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/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..fc78f7b9b5 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,14 +56,22 @@ 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, Func viewModelBuffer) { - if (context.ViewModelJson is null) - return; + if (viewModelSize >= config.BigViewModelBytes) + { + WarnLargeViewModel(context, viewModelSize, viewModelBuffer()); + } + } + void WarnLargeViewModel(IDotvvmRequestContext context, int viewModelSize, Stream viewModelBuffer) + { try { - var vmAnalysis = jsonSizeAnalyzer.Analyze(context.ViewModelJson); + var vmAnalysis = jsonSizeAnalyzer.Analyze(viewModelBuffer.ReadToMemory().Span, context.ViewModel?.GetType()); var topClasses = vmAnalysis.Classes @@ -101,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/DotVVM.Framework.csproj b/src/Framework/Framework/DotVVM.Framework.csproj index 9bafd8d706..1388c874dd 100644 --- a/src/Framework/Framework/DotVVM.Framework.csproj +++ b/src/Framework/Framework/DotVVM.Framework.csproj @@ -9,6 +9,8 @@ DotVVM true enable + + true @@ -45,18 +47,16 @@ - - - - - - + + + - + + @@ -70,7 +70,7 @@ - + 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 dee997e54a..75a668fd63 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.ReadToMemoryAsync(); } 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,23 @@ 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); } + context.ReceivedViewModelJson = postData; // 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).GetBytesFromBase64(); + var arguments = postData.RootElement.GetProperty("args"u8); var executionPlan = StaticCommandExecutor.DecryptPlan(command); var actionInfo = new ActionInfo( @@ -392,6 +388,7 @@ public async Task ProcessStaticCommandRequest(IDotvvmRequestContext context) } finally { + postData?.Dispose(); // returns pooled byte buffers StaticCommandExecutor.DisposeServices(context); } } @@ -463,14 +460,12 @@ 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"; + 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/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..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,12 +165,9 @@ 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 - .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!"); } } @@ -279,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 6b52195d8e..e336b79c94 100644 --- a/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs +++ b/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs @@ -7,13 +7,18 @@ 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; +using System.Text.Encodings.Web; +using FastExpressionCompiler; namespace DotVVM.Framework.Hosting.ErrorPages { @@ -152,6 +157,25 @@ public string TransformText() return builder.ToString(); } + internal static JsonNode SerializeObjectForBrowser(object? obj) + { + var settings = new JsonSerializerOptions() { + ReferenceHandler = ReferenceHandler.IgnoreCycles, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, + Converters = { + new DebugReflectionTypeJsonConverter(), + new ReflectionAssemblyJsonConverter(), + new DotvvmTypeDescriptorJsonConverter(), + new Controls.DotvvmControlDebugJsonConverter(), + new BindingDebugJsonConverter(), + new DotvvmPropertyJsonConverter(), + new UnsupportedTypeJsonConverterFactory(), + }, + TypeInfoResolver = new IgnoreUnsupportedResolver(), + }; + return JsonSerializer.SerializeToNode(obj, settings)!; + } + public void ObjectBrowser(object? obj) { if (obj is null) @@ -160,28 +184,69 @@ public void ObjectBrowser(object? obj) return; } - var settings = new JsonSerializerSettings() { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - DefaultValueHandling = DefaultValueHandling.Ignore, - Converters = { - new ReflectionTypeJsonConverter(), - new ReflectionAssemblyJsonConverter(), - new DotvvmTypeDescriptorJsonConverter(), - new Controls.DotvvmControlDebugJsonConverter(), - new IgnoreStuffJsonConverter(), - new BindingDebugJsonConverter(), - new DotvvmPropertyJsonConverter() - }, - // suppress any errors that occur during serialization (getters may throw exception, ...) - Error = (sender, args) => { - args.ErrorContext.Handled = true; + + 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()); } - }; - var jobject = JObject.FromObject(obj, JsonSerializer.Create(settings)); - ObjectBrowser(jobject); + catch + { + WriteText(""); + } + } } - public void ObjectBrowser(JArray arr) + 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(JsonArray arr) { if (arr.Count == 0) { @@ -198,13 +263,17 @@ public void ObjectBrowser(JArray arr)
"); foreach (var p in arr) { - if (p is JObject) + if (p is JsonObject pObj) + { + ObjectBrowser(pObj); + } + else if (p is JsonArray pArr) { - ObjectBrowser((JObject)p); + ObjectBrowser(pArr); } - else if (p is JArray) + else if (p is null) { - ObjectBrowser((JArray)p); + WriteText("null"); } else { @@ -215,7 +284,7 @@ public void ObjectBrowser(JArray arr) } } - public void ObjectBrowser(JObject obj) + public void ObjectBrowser(JsonObject obj) { if (obj.Count == 0) { @@ -238,17 +307,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(new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping })); } Write("
"); } @@ -367,15 +436,25 @@ private void WriteLine(string textToAppend) builder.AppendLine(); } - class IgnoreStuffJsonConverter : JsonConverter + class UnsupportedTypeJsonConverterFactory : JsonConverterFactory { - public override bool CanConvert(Type objectType) => - objectType.IsDelegate(); - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => - throw new NotImplementedException(); - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override bool CanConvert(Type typeToConvert) => + typeToConvert.IsDelegate() || typeof(ICustomAttributeProvider).IsAssignableFrom(typeToConvert); + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => + (JsonConverter?)Activator.CreateInstance(typeof(Inner<>).MakeGenericType([ typeToConvert ])); + + class Inner : JsonConverter { - writer.WriteValue(""); + 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($"[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 d1aef7f12f..530d81b800 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; @@ -41,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 47d066a117..75c23a54f2 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. /// 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..a8fd04c97c 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, IDotvvmJsonOptionsProvider jsonOptions, IViewModelProtector viewModelProtector, IStaticCommandArgumentValidator validator, DotvvmConfiguration configuration) { this.serviceLoader = serviceLoader; this.viewModelProtector = viewModelProtector; @@ -32,30 +31,33 @@ public StaticCommandExecutor(IStaticCommandServiceLoader serviceLoader, IViewMod this.configuration = configuration; if (configuration.ExperimentalFeatures.UseDotvvmSerializationForStaticCommandArguments.Enabled) { - this.jsonDeserializer = DefaultSerializerSettingsProvider.CreateJsonSerializer(); + this.jsonOptions = jsonOptions.ViewModelJsonOptions; } else { - this.jsonDeserializer = JsonSerializer.Create(); + this.jsonOptions = new JsonSerializerOptions(DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe) { + WriteIndented = configuration.Debug + }; } } #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, - 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 +72,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..0c6b1afc70 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,26 @@ 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, + 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.HtmlSafeLessParanoidEncoder + }; } 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..22564be7ee 100644 --- a/src/Framework/Framework/ResourceManagement/ClientGlobalize/JQueryGlobalizeScriptCreator.cs +++ b/src/Framework/Framework/ResourceManagement/ClientGlobalize/JQueryGlobalizeScriptCreator.cs @@ -1,12 +1,12 @@ -using Newtonsoft.Json.Linq; using DotVVM.Framework.Utils; using System; 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 { @@ -22,83 +22,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 +131,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 +142,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 +162,33 @@ 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 { + // 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, + ["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 +198,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 +207,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({{KnockoutHelper.MakeStringLiteral(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..8cebd81729 100644 --- a/src/Framework/Framework/ResourceManagement/ReflectionAssemblyJsonConverter.cs +++ b/src/Framework/Framework/ResourceManagement/ReflectionAssemblyJsonConverter.cs @@ -1,106 +1,132 @@ -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 FastExpressionCompiler; using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Threading.Tasks; +using System.Text.Json; +using System.Text.Json.Serialization; 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 + + /// Formats type as C# type identifier + public class DebugReflectionTypeJsonConverter(): GenericWriterJsonConverter( + (writer, value, options) => { + writer.WriteStringValue(value.ToCode()); + }) { - public override bool CanConvert(Type objectType) => typeof(ITypeDescriptor).IsAssignableFrom(objectType); + } - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + public class DotvvmTypeDescriptorJsonConverter : JsonConverter + where T: ITypeDescriptor + { + 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() : GenericWriterJsonConverter( + (writer, value, options) => { + writer.WriteStringValue(value.ToString()); + }) + { + } + + public class DataContextChangeAttributeConverter() : GenericWriterJsonConverter(WriteObjectReflection) { - 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) => - throw new NotImplementedException(); - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + internal static void WriteObjectReflection(Utf8JsonWriter writer, object attribute, JsonSerializerOptions options) { - if (value is null) + 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() : GenericWriterJsonConverter(DataContextChangeAttributeConverter.WriteObjectReflection) + { + } + + 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 + { + 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/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/Runtime/Tracing/IRequestTracer.cs b/src/Framework/Framework/Runtime/Tracing/IRequestTracer.cs index 1f28c9bb62..4eeb8158c4 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; @@ -6,10 +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); + /// 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 e46ad0de92..8022b5738d 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, Func 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..3fa4b53487 100644 --- a/src/Framework/Framework/Runtime/Tracing/RequestTracingExtensions.cs +++ b/src/Framework/Framework/Runtime/Tracing/RequestTracingExtensions.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.IO; using System.Runtime.CompilerServices; using System.Threading.Tasks; using DotVVM.Framework.Hosting; +using DotVVM.Framework.Utils; namespace DotVVM.Framework.Runtime.Tracing { @@ -16,6 +18,20 @@ public static async Task TracingEvent(this IEnumerable requestTr } } + 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, () => { + 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) { foreach (var tracer in requestTracers) 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/System.Index.cs b/src/Framework/Framework/System.Index.cs new file mode 100644 index 0000000000..1985b076b2 --- /dev/null +++ b/src/Framework/Framework/System.Index.cs @@ -0,0 +1,166 @@ +// 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 + /// + /// + internal 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/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/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..c6912aaf32 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,33 @@ 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) => +#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); 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..6f366beb12 100644 --- a/src/Framework/Framework/Utils/ExpressionUtils.cs +++ b/src/Framework/Framework/Utils/ExpressionUtils.cs @@ -22,6 +22,14 @@ public static Expression While(Expression condition, Expression body) Expression.IfThenElse(condition, body, Expression.Goto(brkLabel)), brkLabel); } + 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/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..0802829c7d --- /dev/null +++ b/src/Framework/Framework/Utils/MemoryUtils.cs @@ -0,0 +1,52 @@ +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 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; + + public static Memory ReadToMemory(this Stream stream) + { + using var buffer = new MemoryStream(); + stream.CopyTo(buffer); + return buffer.ToMemory(); + } + public static async Task> ReadToMemoryAsync(this Stream stream) + { + using var buffer = new MemoryStream(); + 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..f46b887bbe --- /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. +// 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/Utils/ReflectionUtils.cs b/src/Framework/Framework/Utils/ReflectionUtils.cs index 91d6c2aa00..bc6aa0870e 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 { @@ -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); @@ -569,7 +576,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..bbc7a46dea 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,41 @@ 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/SystemTextJsonUtils.cs b/src/Framework/Framework/Utils/SystemTextJsonUtils.cs new file mode 100644 index 0000000000..bdd61fc195 --- /dev/null +++ b/src/Framework/Framework/Utils/SystemTextJsonUtils.cs @@ -0,0 +1,207 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +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()!; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AssertToken(this in Utf8JsonReader reader, JsonTokenType type) + { + if (reader.TokenType != type) + 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) + { + AssertToken(in reader, type); + AssertRead(ref reader); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AssertRead(this ref Utf8JsonReader reader) + { + if (!reader.Read()) + 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) + { + 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; + } + + 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) + { + if (reader.TokenType == JsonTokenType.String) + { + return reader.GetString()!; + } + if (reader.TokenType == JsonTokenType.Number) + { + return reader.GetDouble(); + } + if (reader.TokenType == JsonTokenType.True) + { + return BoxingUtils.True; + } + if (reader.TokenType == JsonTokenType.False) + { + return BoxingUtils.False; + } + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + return JsonElement.ParseValue(ref reader); + } + + 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/CustomPrimitiveTypeJsonConverter.cs b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs index 7529850f24..72258c88cf 100644 --- a/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/CustomPrimitiveTypeJsonConverter.cs @@ -1,59 +1,58 @@ 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)) ?? throw new InvalidOperationException($"The type {typeof(T)} is not a custom primitive type!"); + 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 str = reader.TokenType is JsonTokenType.String ? reader.GetString() : + reader.HasValueSequence ? StringUtils.Utf8Decode(reader.ValueSequence.ToArray()) : + StringUtils.Utf8Decode(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..0c99aa9674 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs @@ -10,13 +10,18 @@ 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; +using DotVVM.Framework.Runtime.Tracing; +using Microsoft.Extensions.DependencyInjection; namespace DotVVM.Framework.ViewModel.Serialization { @@ -35,46 +40,49 @@ public record SerializationException(bool Serialize, Type? ViewModelType, string private readonly IViewModelSerializationMapper viewModelMapper; private readonly IViewModelServerCache viewModelServerCache; private readonly IViewModelTypeMetadataSerializer viewModelTypeMetadataSerializer; - + private readonly IDotvvmJsonOptionsProvider jsonOptions; + private readonly ILogger? logger; public bool SendDiff { get; set; } = true; - public Formatting JsonFormatting { get; set; } - /// /// 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, IDotvvmJsonOptionsProvider jsonOptions, ILogger? logger) { this.viewModelProtector = protector; - this.JsonFormatting = configuration.Debug ? Formatting.Indented : Formatting.None; this.viewModelMapper = serializationMapper; this.viewModelServerCache = viewModelServerCache; this.viewModelTypeMetadataSerializer = viewModelTypeMetadataSerializer; + this.jsonOptions = jsonOptions; + this.logger = logger; } /// /// 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) + // { + // 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); + var result = StringUtils.Utf8Decode(utf8json.ToSpan()); - 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.HttpContext.SetItem("dotvvm-viewmodel-size-bytes", result.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.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) @@ -98,148 +106,222 @@ public string SerializeViewModel(IDotvvmRequestContext context) bool IsPostBack(IDotvvmRequestContext c) => c.RequestType is DotvvmRequestType.Command or DotvvmRequestType.StaticCommand; - /// - /// Builds the view model for the client. - /// - public void BuildViewModel(IDotvvmRequestContext context, object? commandResult) + (int vmStart, int vmEnd) WriteViewModelJson(Utf8JsonWriter writer, IDotvvmRequestContext context, DotvvmSerializationState state) { - 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(); - try - { - serializer.Serialize(writer, context.ViewModel); - } - catch (Exception ex) - { - throw new SerializationException(true, context.ViewModel!.GetType(), writer.Path, ex); - } - var viewModelToken = writer.Token.NotNull(); + var converter = jsonOptions.GetRootViewModelConverter(context.ViewModel!.GetType()); - string? viewModelCacheId = null; - if (context.Configuration.ExperimentalFeatures.ServerSideViewModelCache.IsEnabledForRoute(context.Route?.RouteName)) - { - viewModelCacheId = viewModelServerCache.StoreViewModel(context, (JObject)viewModelToken); - } + 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, jsonOptions.ViewModelJsonOptions, state, wrapObject: false); + + writer.Flush(); + var vmEnd = (int)writer.BytesCommitted; // persist CSRF token if (context.CsrfToken is object) - viewModelToken["$csrfToken"] = context.CsrfToken; + writer.WriteString("$csrfToken"u8, context.CsrfToken); // persist encrypted values - if (viewModelConverter.EncryptedValues.Count > 0) - viewModelToken["$encryptedValues"] = viewModelProtector.Protect(viewModelConverter.EncryptedValues.ToString(Formatting.None), context); + 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)); - // 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; - } + writer.WriteEndObject(); - if (context.RequestType is DotvvmRequestType.Command or DotvvmRequestType.SpaNavigate) - { - result["action"] = "successfulCommand"; - } - else + 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) + { + (int, int) viewModelBodyPosition; + + var buffer = new MemoryStream(); + using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions { + Indented = jsonOptions.ViewModelJsonOptions.WriteIndented, + Encoder = jsonOptions.ViewModelJsonOptions.Encoder, + //SkipValidation = true, // for the hack with WriteRawValue + })) { - result["renderedResources"] = JArray.FromObject(context.ResourceManager.GetNamedResourcesInOrder().Select(r => r.Name)); - } + using var state = DotvvmSerializationState.Create(context.IsPostBack, context.Services); + writer.WriteStartObject(); + + writer.WritePropertyName("viewModel"u8); + try + { + viewModelBodyPosition = WriteViewModelJson(writer, context, state); + } + 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) + { + writer.WriteString("resultIdFragment"u8, context.ResultIdFragment); + } - if (commandResult != null) result["commandResult"] = WriteCommandData(commandResult, serializer, "the command result"); - AddCustomPropertiesIfAny(context, serializer, result); + if (context.RequestType is DotvvmRequestType.Command or DotvvmRequestType.SpaNavigate) + { + writer.WriteString("action"u8, "successfulCommand"u8); + } + else + { + writer.WriteStartArray("renderedResources"u8); + foreach (var resource in context.ResourceManager.GetNamedResourcesInOrder()) + writer.WriteStringValue(resource.Name); + writer.WriteEndArray(); + } - result["typeMetadata"] = SerializeTypeMetadata(context, viewModelConverter); + if (commandResult != null) + { + writer.WritePropertyName("commandResult"u8); + WriteCommandData(commandResult, writer, buffer); + } + AddCustomPropertiesIfAny(context, writer, buffer); - context.ViewModelJson = result; + if (postbackUpdatedControls is not null) + { + AddPostBackUpdatedControls(context, writer, postbackUpdatedControls); + } - DotvvmMetrics.ViewModelSerializationTime.Record(timer.ElapsedSeconds, context.RouteLabel(), context.RequestTypeLabel()); + if (serializeNewResources) + { + AddNewResources(context, writer); + } + + SerializeTypeMetadata(context, writer, state.UsedSerializationMaps); + writer.WriteEndObject(); + } + + return buffer; } - private JObject SerializeTypeMetadata(IDotvvmRequestContext context, ViewModelJsonConverter viewModelJsonConverter) + static ReadOnlySpan TrimStart(ReadOnlySpan json) { - var knownTypeIds = context.ReceivedViewModelJson?["knownTypeMetadata"]?.Values().WhereNotNull().ToImmutableHashSet(); - return viewModelTypeMetadataSerializer.SerializeTypeMetadata(viewModelJsonConverter.UsedSerializationMaps, knownTypeIds); + while (json.Length > 0 && char.IsWhiteSpace((char)json[0])) + json = json.Slice(1); + return json; } - public void AddNewResources(IDotvvmRequestContext context) + static ReadOnlySpan TrimEnd(ReadOnlySpan json) { - 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; + while (json.Length > 0 && char.IsWhiteSpace((char)json[json.Length - 1])) + json = json.Slice(0, json.Length - 1); + return json; } - public string BuildStaticCommandResponse(IDotvvmRequestContext context, object? result, string[]? knownTypeMetadata = null) + + private void SerializeTypeMetadata(IDotvvmRequestContext context, Utf8JsonWriter writer, IEnumerable usedSerializationMaps) { - var timer = ValueStopwatch.StartNew(); + var knownTypeIds = context.ReceivedViewModelJson?.RootElement.GetPropertyOrNull("knownTypeMetadata"u8)?.EnumerateArray().Select(e => e.GetString()).WhereNotNull().ToHashSet(StringComparer.OrdinalIgnoreCase); + viewModelTypeMetadataSerializer.SerializeTypeMetadata(usedSerializationMaps, writer, "typeMetadata"u8, knownTypeIds); + } + + private void AddNewResources(IDotvvmRequestContext context, Utf8JsonWriter writer) + { + 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(); + } - 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"); + public ReadOnlyMemory BuildStaticCommandResponse(IDotvvmRequestContext context, object? result, string[]? knownTypeMetadata = null) + { + var timer = ValueStopwatch.StartNew(); - 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 = jsonOptions.ViewModelJsonOptions.WriteIndented, Encoder = jsonOptions.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, jsonOptions.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 +330,9 @@ public JObject BuildResourcesJson(IDotvvmRequestContext context, Func @@ -261,12 +341,12 @@ public JObject BuildResourcesJson(IDotvvmRequestContext context, Func @@ -275,21 +355,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" + }, jsonOptions.PlainJsonOptions); } @@ -297,49 +377,81 @@ 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; + ReadOnlyMemory? cachedViewModel = null; + 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 = root.GetProperty("viewModelDiff"u8); + cachedViewModel = viewModelServerCache.TryRestoreViewModel(context, viewModelCacheId, viewModelElement); } 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) ]); } - else viewModelConverter = new ViewModelJsonConverter(IsPostBack(context), viewModelMapper, context.Services); + else + { + readEncryptedValues = new JsonObject(); + } + + 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((cachedViewModel ?? 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."); + + if (cachedViewModel is null) + { + // skip to the "viewModel" property + 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 = jsonOptions.GetRootViewModelConverter(context.ViewModel.GetType()); + var newVM = converter.PopulateUntyped(ref reader, context.ViewModel.GetType(), context.ViewModel, jsonOptions.ViewModelJsonOptions, state); + 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 +459,32 @@ public void PopulateViewModel(IDotvvmRequestContext context, string serializedPo } catch (Exception ex) { - throw new SerializationException(false, context.ViewModel?.GetType(), reader.Path, ex); + var documentSlice = (cachedViewModel ?? 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, readEncryptedValues: new JsonObject()); + return JsonSerializer.Deserialize(a, t, jsonOptions.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 +505,23 @@ 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; } } } diff --git a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelServerCache.cs b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelServerCache.cs index 332517940a..f84ba92b9d 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelServerCache.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelServerCache.cs @@ -1,13 +1,15 @@ using System; +using System.Buffers; 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 +22,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 ReadOnlyMemory TryRestoreViewModel(IDotvvmRequestContext context, string viewModelCacheId, JsonElement viewModelDiffToken) { var cachedData = viewModelStore.Retrieve(viewModelCacheId); var routeLabel = new KeyValuePair("route", context.Route!.RouteName); @@ -41,30 +43,53 @@ public JObject TryRestoreViewModel(IDotvvmRequestContext context, string viewMod DotvvmMetrics.ViewModelCacheHit.Add(1, routeLabel); DotvvmMetrics.ViewModelCacheBytesLoaded.Add(cachedData.Length, routeLabel); - var result = UnpackViewModel(cachedData); - JsonUtils.Patch(result, viewModelDiffToken); - return result; - } - - protected virtual byte[] PackViewModel(JObject viewModelToken) - { - using (var ms = new MemoryStream()) - using (var bsonWriter = new BsonDataWriter(ms)) + var unpacked = UnpackViewModel(cachedData); + var unpackedBuffer = ArrayPool.Shared.Rent(unpacked.length + 2); + try { - viewModelToken.WriteTo(bsonWriter); - bsonWriter.Flush(); + 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)'}'; - return ms.ToArray(); + 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); } } - protected virtual JObject UnpackViewModel(byte[] cachedData) + protected virtual byte[] PackViewModel(Stream data) { - using (var ms = new MemoryStream(cachedData)) - using (var bsonReader = new BsonDataReader(ms)) + var output = new MemoryStream(); + using (var compressed = new System.IO.Compression.DeflateStream(output, System.IO.Compression.CompressionLevel.Fastest, leaveOpen: true)) { - return (JObject)JToken.ReadFrom(bsonReader); + 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 data, int length) UnpackViewModel(byte[] cachedData) + { + 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/DotvvmByteArrayConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmByteArrayConverter.cs index 4df3b330c0..582df374f4 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(checked((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 deleted file mode 100644 index f7a138e4da..0000000000 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmDateOnlyConverter.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Globalization; -using Newtonsoft.Json; - -namespace DotVVM.Framework.ViewModel.Serialization -{ - public class DotvvmDateOnlyConverter : JsonConverter - { - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - 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)); - } - } - - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - 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)) - { - return date; - } - - throw new JsonSerializationException("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..61e44cf926 100644 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmDictionaryConverter.cs @@ -1,86 +1,94 @@ 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) + reader.AssertRead(JsonTokenType.StartArray); + var dict = new Dictionary(); + while (reader.TokenType != JsonTokenType.EndArray) { - dict[keyProp.GetValue(item)!] = valueProp.GetValue(item); + reader.AssertRead(JsonTokenType.StartObject); + (K key, V value) item = default; + bool hasKey = false, hasValue = false; + while (reader.TokenType != JsonTokenType.EndObject) + { + reader.AssertToken(JsonTokenType.PropertyName); + + if (reader.ValueTextEquals("Key"u8)) + { + reader.AssertRead(); + item.key = SystemTextJsonUtils.Deserialize(ref reader, options)!; + hasKey = true; + } + else if (reader.ValueTextEquals("Value"u8)) + { + reader.AssertRead(); + item.value = SystemTextJsonUtils.Deserialize(ref reader, options)!; + hasValue = true; + } + else + { + reader.AssertRead(); + reader.Skip(); + } + reader.AssertRead(); + } + if (!hasKey || !hasValue) throw new JsonException("Missing Key or Value property in dictionary item."); + dict.Add(item.key!, item.value); + reader.AssertRead(JsonTokenType.EndObject); } - return dict; + + 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..933e72fb63 --- /dev/null +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmEnumConverter.cs @@ -0,0 +1,391 @@ +using System; +using System.Buffers; +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.Compilation.Javascript; +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 = (MethodInfo)MethodFindingHelper.GetMethodFromExpression(() => default(DotvvmEnumConverter)!.CreateConverter()); + public JsonConverter CreateConverter() where TEnum : unmanaged, Enum + { + 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][]; + // 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.Select(f => (f.Key, f.Value)).ToArray(); + Array.Sort(array, (a, b) => a.Value.AsSpan().SequenceCompareTo(b.Value.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; + byte[]? rentedBuffer = null; + try + { + Span buffer = valueLength < 512 ? stackalloc byte[valueLength] : (rentedBuffer = ArrayPool.Shared.Rent(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); + } + finally + { + if (rentedBuffer is {}) + ArrayPool.Shared.Return(rentedBuffer); + } + } + } + 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/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/DotvvmObjectConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmObjectConverter.cs new file mode 100644 index 0000000000..3c73ec1b9e --- /dev/null +++ b/src/Framework/Framework/ViewModel/Serialization/DotvvmObjectConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using DotVVM.Framework.Utils; + +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) + { + 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/DotvvmTimeOnlyConverter.cs b/src/Framework/Framework/ViewModel/Serialization/DotvvmTimeOnlyConverter.cs deleted file mode 100644 index 2a03f596c3..0000000000 --- a/src/Framework/Framework/ViewModel/Serialization/DotvvmTimeOnlyConverter.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Globalization; -using Newtonsoft.Json; - -namespace DotVVM.Framework.ViewModel.Serialization -{ - public class DotvvmTimeOnlyConverter : JsonConverter - { - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - 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)); - } - } - - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - 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)) - { - return date; - } - - throw new JsonSerializationException("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/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/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..ebd5c1d3dd 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,14 @@ namespace DotVVM.Framework.ViewModel.Serialization { public interface IViewModelSerializer { - void BuildViewModel(IDotvvmRequestContext context, object? commandResult); + 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..3b2423de40 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); + ReadOnlyMemory 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..c6fb2c3324 --- /dev/null +++ b/src/Framework/Framework/ViewModel/Serialization/SerialiationMapperAttributeHelper.cs @@ -0,0 +1,29 @@ +using System; +using System.Reflection; +using STJ = System.Text.Json.Serialization; + +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? 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..58185679ee 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelJsonConverter.cs @@ -3,57 +3,33 @@ 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 FastExpressionCompiler; 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 : 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) && - !ReflectionUtils.IsTupleLike(type) && + !ReflectionUtils.IsJsonDom(type) && + !type.IsDefined(typeof(JsonConverterAttribute), true) && type != typeof(object); /// @@ -64,101 +40,222 @@ 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) => (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 IDotvvmJsonConverter GetDotvvmConverter(Type type) => + converterCache.GetOrAdd(type, t => (IDotvvmJsonConverter)CreateConverterReally(t)); + internal JsonConverter GetConverter(Type type) => + (JsonConverter)GetDotvvmConverter(type); + + public class VMConverter(ViewModelJsonConverter factory): JsonConverter, IDotvvmJsonConverter { - 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."); + if (typeof(T) != typeToConvert) + throw new ArgumentException("typeToConvert must be the same as T", nameof(typeToConvert)); - // 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($"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}"); + } - 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, bool wrapObject = 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 + { + 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 + { + // 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, typeof(T), value, options, DotvvmSerializationState.Current!); + public T Populate(ref Utf8JsonReader reader, Type typeToConvert, T value, JsonSerializerOptions options, 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, 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); } + } - /// - /// 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..53f38fa145 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelPropertyMap.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelPropertyMap.cs @@ -3,15 +3,15 @@ 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 { 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,9 +23,10 @@ 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 `[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. @@ -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; @@ -44,6 +46,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. 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 public List ValidationRules { get; } = new(); @@ -65,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 ebc8e1ede5..6172a3ce49 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,27 @@ 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; +using DotVVM.Framework.Compilation.Javascript; 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 JsonSerializerOptions jsonOptions; + 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 +38,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,56 +47,77 @@ 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, JsonSerializerOptions jsonOptions, DotvvmConfiguration configuration) { + this.jsonOptions = jsonOptions; 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(); + var dict = new Dictionary(capacity: Properties.Length); foreach (var propertyMap in Properties) { - if (!hashset.Add(propertyMap.Name)) + if (!dict.ContainsKey(propertyMap.Name)) + { + dict.Add(propertyMap.Name, propertyMap); + } + else { - 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."); } } } - public void ResetFunctions() + public abstract void ResetFunctions(); + public abstract void SetConstructorUntyped(Func constructor); + + } + public sealed class ViewModelSerializationMap : ViewModelSerializationMap + { + public ViewModelSerializationMap(IEnumerable properties, MethodBase? constructor, JsonSerializerOptions jsonOptions, DotvvmConfiguration configuration): + base(typeof(T), properties, constructor, jsonOptions, 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. /// - private Expression CallConstructor(Expression services, Dictionary propertyVariables, bool throwImmediately = false) + 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) { @@ -119,9 +149,9 @@ private Expression CallConstructor(Expression services, Dictionary @@ -136,83 +166,80 @@ 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); } /// /// 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 encryptedValuesReader = Expression.Parameter(typeof(EncryptedValuesReader), "encryptedValuesReader"); - var servicesParameter = Expression.Parameter(typeof(IServiceProvider), "services"); - var value = Expression.Variable(Type, "value"); - var currentProperty = Expression.Variable(typeof(string), "currentProperty"); - var readerTmp = Expression.Variable(typeof(JsonReader), "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.PropertyInfo, - p => Expression.Variable(p.Type, "prop_" + p.Name) + p => p, + p => 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 {} && !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 + 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"; 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( 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 => 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)); - - // 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)); + block.Add(Call(encryptedValuesReader, nameof(EncryptedValuesReader.Nest), Type.EmptyTypes)); - 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 @@ -223,7 +250,7 @@ public ReaderDelegate CreateReaderFactory() { continue; } - var propertyVar = propertyVars[property.PropertyInfo]; + var propertyVar = propertyVars[property]; var existingValue = property.Populate ? @@ -231,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; @@ -241,35 +268,26 @@ 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, - ExpressionUtils.Replace( - (EncryptedValuesReader ev) => ev.ReadValue(propertyIndex).CreateReader(), - encryptedValuesReader).OptimizeConstants() + Call(JsonSerializationCodegenFragments.ReadEncryptedValueMethod, Call(encryptedValuesReader, "ReadValue", Type.EmptyTypes, Constant(propertyIndex))) ), - Expression.Call(encryptedValuesReader, "Suppress", Type.EmptyTypes), - Expression.Assign( + Call(encryptedValuesReader, "Suppress", Type.EmptyTypes), + + 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( + readEncryptedValue = TryFinally( readEncryptedValue, - Expression.Call(encryptedValuesReader, "EndSuppress", Type.EmptyTypes) + 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 +295,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)) + 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) @@ -369,8 +364,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()) @@ -380,131 +375,53 @@ 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); + var ex = Lambda>( + Block(typeof(T), [ currentProperty, readerTmp, ..propertyVars.Values ], block).OptimizeConstants(), + reader, jsonOptions, value, allowPopulate, encryptedValuesReader, state); return ex.CompileFast(flags: CompilerFlags.ThrowOnNotSupportedExpression); } - 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) + Expression MemberAccess(Expression obj, ViewModelPropertyMap property) { - 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); - } - } + if (property.PropertyInfo is PropertyInfo pi) + return Property(obj, pi); + if (property.PropertyInfo is FieldInfo fi) + return Field(obj, fi); + throw new NotSupportedException(); } - /// 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"); - // writer.WriteStartObject(); - block.Add(Expression.Call(writer, nameof(JsonWriter.WriteStartObject), Type.EmptyTypes)); - - // encryptedValuesWriter.Nest(); - block.Add(Expression.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()))); - } + // curly brackets are used for variables and methods from the scope of this factory method + // 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++) { var property = Properties[propertyIndex]; - var endPropertyLabel = Expression.Label("end_property_" + property.Name); + var endPropertyLabel = 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) { @@ -516,14 +433,14 @@ 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} - var prop = Expression.Property(value, property.PropertyInfo); + var prop = MemberAccess(value, property); var writeEV = property.ViewModelProtection == ProtectMode.EncryptData || property.ViewModelProtection == ProtectMode.SignData; @@ -532,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)))); } @@ -546,65 +463,59 @@ 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(JsonWriter.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, serializer)); + 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)); } - // 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 = Expression.Lambda( - Expression.Block(new[] { value }, block).OptimizeConstants(), writer, valueParam, serializer, encryptedValuesWriter, isPostback); + var ex = Lambda>( + Block(new[] { value }, block).OptimizeConstants(), writer, value, jsonOptions, requireTypeField, encryptedValuesWriter, dotvvmState); return ex.CompileFast(flags: CompilerFlags.ThrowOnNotSupportedExpression); } @@ -621,6 +532,368 @@ 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, jsonOptions); + return property.JsonConverter; + } + + 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 IDotvvmJsonConverter) + { + // 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, Constant(type), jsonOptions, dotvvmState); + else + return Call(Constant(converter), "Populate", Type.EmptyTypes, reader, Constant(type), existingValue, jsonOptions, dotvvmState); + } + else + { + var read = Call(Constant(converter), "Read", Type.EmptyTypes, reader, 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, bool requireTypeField = true, bool wrapObject = true) + if (converter is IDotvvmJsonConverter) + { + return Call(Constant(converter), nameof(IDotvvmJsonConverter.Write), Type.EmptyTypes, writer, value, jsonOptions, dotvvmState, Constant(true), Constant(true)); + } + else + { + return Call(Constant(converter), nameof(IDotvvmJsonConverter.Write), Type.EmptyTypes, writer, value, jsonOptions); + } + } + + private Expression? TryDeserializePrimitive(Expression reader, Type type) + { + // Utf8JsonReader readerTest = default; + if (type == typeof(bool)) + return Call(reader, "GetBoolean", Type.EmptyTypes); + if (type == typeof(byte)) + return Call(reader, "GetByte", 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 writer, 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(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(writer, "WriteNumberValue", Type.EmptyTypes, value); + if (type == typeof(short) || type == typeof(ushort) || type == typeof(sbyte) || type == typeof(byte)) + return Call(writer, "WriteNumberValue", Type.EmptyTypes, Convert(value, typeof(int))); + if (type == typeof(string) || type == typeof(Guid)) // TODO: datetime too? + return Call(writer, "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, type, reader, jsonOptions, dotvvmState, property.Populate ? existingValue : null); + } + + if (TryDeserializePrimitive(reader, type) is {} primitive) + { + return primitive; + } + + var converter = this.jsonOptions.GetConverter(type); + if (!converter.CanConvert(type)) + { + 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(converter), // ViewModelJsonConverter.VMConverter? defaultConverter + dotvvmState); // DotvvmSerializationState state + } + else + { + 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 CallPropertyConverterRead(converter, type, reader, jsonOptions, dotvvmState, property.Populate ? existingValue : null); + } + } + + 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 {} primitive) + { + return primitive; + } + if (this.viewModelJsonConverter.CanConvert(value.Type)) + { + if (property.AllowDynamicDispatch && !value.Type.IsSealed) + { + 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 viewModelConverter = this.viewModelJsonConverter.GetConverter(value.Type); + return CallPropertyConverterWrite(viewModelConverter, writer, value, jsonOptions, dotvvmState); + } + } + + return Call(JsonSerializationCodegenFragments.SerializeValueMethod.MakeGenericMethod(value.Type), writer, jsonOptions, value, Constant(property.AllowDynamicDispatch && !value.Type.IsSealed)); + } } + 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 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) + { + 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.GetDotvvmConverter(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, IDotvvmJsonConverter defaultConverter, DotvvmSerializationState state) + where TVM: class + { + if (reader.TokenType == JsonTokenType.Null) + return default; + + if (existingValue is null) + { + return defaultConverter.Read(ref reader, typeof(TVM), options, state); + } + + var realType = existingValue?.GetType() ?? typeof(TVM); + if (defaultConverter is {} && realType == typeof(TVM)) + { + return populate && existingValue is {} + ? defaultConverter.Populate(ref reader, typeof(TVM), existingValue, options, state) + : defaultConverter.Read(ref reader, typeof(TVM), options, state); + } + + var converter = factory.GetDotvvmConverter(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(); + 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(); + } + } } diff --git a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs index 9e29efd0ad..17738372c3 100644 --- a/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs +++ b/src/Framework/Framework/ViewModel/Serialization/ViewModelSerializationMapper.cs @@ -3,11 +3,15 @@ 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; +using DotVVM.Framework.Compilation.Javascript; +using FastExpressionCompiler; namespace DotVVM.Framework.ViewModel.Serialization { @@ -20,31 +24,41 @@ 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) + 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)); } 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 = + (MethodInfo)MethodFindingHelper.GetMethodFromExpression(() => default(ViewModelSerializationMapper)!.CreateMap()); + 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, jsonOptions.ViewModelJsonOptions, configuration); } protected virtual MethodBase? GetConstructor(Type type) @@ -52,17 +66,25 @@ 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) 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); } @@ -97,17 +119,74 @@ protected virtual ViewModelSerializationMap CreateMap(Type type) 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.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."); + 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. /// 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(); + properties = ResolveShadowing(type, properties); Array.Sort(properties, (a, b) => StringComparer.Ordinal.Compare(a.Name, b.Name)); - foreach (var property in properties) + foreach (MemberInfo property in properties) { - if (property.IsDefined(typeof(JsonIgnoreAttribute))) 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)) || + (type.IsGenericType && type.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); @@ -115,24 +194,28 @@ 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.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 = propertyMap.JsonConverter is null && (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); + 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(); @@ -155,22 +238,16 @@ 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) { + 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..a29931a037 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)) + else if (type == typeof(object) || ReflectionUtils.IsJsonDom(type)) { - 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/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/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..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())); + 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/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/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/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/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/Hosting.Owin/DotVVM.Framework.Hosting.Owin.csproj b/src/Framework/Hosting.Owin/DotVVM.Framework.Hosting.Owin.csproj index 87454e0506..fa6717917c 100644 --- a/src/Framework/Hosting.Owin/DotVVM.Framework.Hosting.Owin.csproj +++ b/src/Framework/Hosting.Owin/DotVVM.Framework.Hosting.Owin.csproj @@ -27,8 +27,8 @@ - - + + 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/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/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/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..b0b7573be2 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/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/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/DotVVM.Samples.ApplicationInsights.Owin.csproj b/src/Samples/ApplicationInsights.Owin/DotVVM.Samples.ApplicationInsights.Owin.csproj index 85d3e57047..ade018c851 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 @@ - + @@ -50,9 +50,7 @@ - - - + 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/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/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/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/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/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/Samples/MiniProfiler.Owin/DotVVM.Samples.MiniProfiler.Owin.csproj b/src/Samples/MiniProfiler.Owin/DotVVM.Samples.MiniProfiler.Owin.csproj index e56bedeaa5..eb0454fe2c 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 @@ -41,8 +41,6 @@ - - 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 1b85ac7325..6fe4fce94e 100644 --- a/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj +++ b/src/Samples/Owin/DotVVM.Samples.BasicSamples.Owin.csproj @@ -28,9 +28,11 @@ - - - + + + + + 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 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"); diff --git a/src/Tests/Binding/JavascriptCompilationTests.cs b/src/Tests/Binding/JavascriptCompilationTests.cs index d387558513..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 { @@ -104,8 +105,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()")] @@ -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/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..fc417ff1a6 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; @@ -13,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 @@ -43,7 +43,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/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/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..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; @@ -25,14 +24,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/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 @@
- + 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..b5caa0802c 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 { @@ -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(); @@ -30,9 +31,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..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 { @@ -31,7 +33,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,19 +43,21 @@ 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"); - var jobject = JObject.Parse(serialized); - void removeTestStuff(JToken token) + Console.WriteLine(serialized); + + var jobject = JsonNode.Parse(serialized).AsObject(); + void removeTestStuff(JsonNode token) { - if (token is object) - foreach (var testControl in ((JObject)token).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] @@ -189,6 +193,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/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/DotvvmCompilationExceptionSerializationTests.cs b/src/Tests/Runtime/DotvvmCompilationExceptionSerializationTests.cs index 8ab2d15b1e..6a55b935c9 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 { @@ -12,15 +12,16 @@ namespace DotVVM.Framework.Tests.Runtime public class DotvvmCompilationExceptionSerializationTests { [TestMethod] + [Ignore("DotvvmCompilationException deserialization is not currently implemented")] public void DotvvmCompilationException_SerializationAndDeserialization_WorksCorrectly() { var compilationException = 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/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/ResourceManagerTests.cs b/src/Tests/Runtime/ResourceManagerTests.cs index 5f39d14bbb..e5f130afc0 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 { @@ -55,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 @@ -71,9 +73,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 && @@ -100,6 +101,7 @@ public void ResourceManager_ConfigurationDeserialization() } [TestMethod] + [Ignore("DotvvmConfiguration deserialization is not currently implemented")] public void ResourceManager_ConfigurationOldDeserialization() { var json = string.Format(@" @@ -109,8 +111,7 @@ public void ResourceManager_ConfigurationOldDeserialization() 'stylesheets': {{ 'newResource': {{ 'url': 'test' }} }} }} }}", ResourceConstants.GlobalizeResourceName); - var configuration = DotvvmTestHelper.CreateConfiguration(); - JsonConvert.PopulateObject(json.Replace("'", "\""), configuration); + 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); @@ -121,7 +122,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/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; 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/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/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..5cbb544a21 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,14 +27,16 @@ 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] + [Ignore("DotvvmConfiguration deserialization is not currently implemented")] public void JsonDiff_Configuration_AddingResources() { var config = ApplyPatches( @@ -49,6 +51,7 @@ public void JsonDiff_Configuration_AddingResources() } [TestMethod] + [Ignore("DotvvmConfiguration deserialization is not currently implemented")] public void JsonDiff_Configuration_AddingRoute() { var config = ApplyPatches( @@ -56,9 +59,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 +69,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..54bf53fe11 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 JsonSerializerOptions jsonOptions = new 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, typeof(T), existingValue, jsonOptions, 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,11 @@ public void SupportTuples() } ) }; - var obj2 = SerializeAndDeserialize(obj, isPostback: true).vm; + var (obj2, json) = SerializeAndDeserialize(obj, isPostback: true); + + 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); @@ -240,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() { @@ -389,7 +426,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 +443,144 @@ 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")); + XAssert.IsType(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", 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() + { + 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.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); + 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] @@ -466,6 +640,160 @@ 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"]); + } + + [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); + } + + [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; } + public string Property2 { get; set; } + public string Property3 { get; set; } + } + + interface IVMInterface1: IVMInterface2 + { + string Property1 { get; set; } + } + 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 @@ -511,13 +839,15 @@ 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 { [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 +913,158 @@ 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: ulong + { + F1 = 1, + F2 = 2, + F64 = 1UL << 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; } + } + + 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; + } + } + + [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)] + public TStatic Value { get; set; } + } + + class StaticDispatchVMContainer + { + [Bind(AllowDynamicDispatch = false)] + public TStatic Value { get; set; } + } + + class DefaultDispatchVMContainer + { + [Bind(Name = "Value")] // make sure that the attribute presence does not affect the default behavior + public TStatic Value { get; set; } + } } diff --git a/src/Tests/ViewModel/ViewModelSerializationMapperTests.cs b/src/Tests/ViewModel/ViewModelSerializationMapperTests.cs index 419181cddb..d67d2d9f30 100644 --- a/src/Tests/ViewModel/ViewModelSerializationMapperTests.cs +++ b/src/Tests/ViewModel/ViewModelSerializationMapperTests.cs @@ -1,13 +1,17 @@ using System; using System.Collections.Generic; +using System.Linq; 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; +using NJ=Newtonsoft.Json; namespace DotVVM.Framework.Tests.ViewModel { @@ -18,10 +22,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); @@ -34,28 +35,74 @@ public void ViewModelSerializationMapper_Name_JsonPropertyVsBindAttribute() } [TestMethod] - public void ViewModelSerializationMapper_Name_MemberShadowing() + public void ViewModelSerializationMapper_Name_NewtonsoftJsonAttributes() { - 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()}\""); + // we still respect NJ attributes + var mapper = DotvvmTestHelper.DefaultConfig.ServiceProvider.GetRequiredService(); + var map = mapper.GetMap(typeof(NewtonsoftJsonAttributes)); + + 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()); + 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 @@ -69,20 +116,42 @@ 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; } + } + + 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; } } 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/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/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 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/ApplicationInsightsTracer.cs b/src/Tracing/ApplicationInsights/ApplicationInsightsTracer.cs index 094bd39c3a..f9cc255dd9 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, Func viewModelBuffer) + { + } } } 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 @@ - + diff --git a/src/Tracing/MiniProfiler.Shared/MiniProfilerTracer.cs b/src/Tracing/MiniProfiler.Shared/MiniProfilerTracer.cs index 196889ae62..b7a2b52dd5 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, Func viewModelBuffer) + { + } } -} \ No newline at end of file +}