Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use System.Text.Json as a backend for view model serialization #1799

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/main.yml
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/.config/dotnet-tools.json
Expand Up @@ -15,4 +15,4 @@
]
}
}
}
}
Expand Up @@ -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;

Expand Down
Expand Up @@ -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;

Expand Down
Expand Up @@ -16,7 +16,7 @@ namespace DotVVM.AutoUI.Metadata
public class ResourceViewModelValidationMetadataProvider : IViewModelValidationMetadataProvider
{
private readonly IViewModelValidationMetadataProvider baseValidationMetadataProvider;
private readonly ConcurrentDictionary<PropertyInfo, List<ValidationAttribute>> cache = new();
private readonly ConcurrentDictionary<MemberInfo, ValidationAttribute[]> cache = new();
private readonly ResourceManager errorMessages;
private static readonly FieldInfo internalErrorMessageField;

Expand All @@ -43,15 +43,15 @@ static ResourceViewModelValidationMetadataProvider()
/// <summary>
/// Gets validation attributes for the specified property.
/// </summary>
public IEnumerable<ValidationAttribute> GetAttributesForProperty(PropertyInfo property)
public IEnumerable<ValidationAttribute> GetAttributesForProperty(MemberInfo property)
{
return cache.GetOrAdd(property, GetAttributesForPropertyCore);
}

/// <summary>
/// Determines validation attributes for the specified property and loads missing error messages from the resource file.
/// </summary>
private List<ValidationAttribute> GetAttributesForPropertyCore(PropertyInfo property)
private ValidationAttribute[] GetAttributesForPropertyCore(MemberInfo property)
{
// process all validation attributes
var results = new List<ValidationAttribute>();
Expand All @@ -73,7 +73,7 @@ private List<ValidationAttribute> GetAttributesForPropertyCore(PropertyInfo prop
}
}

return results;
return results.Count == 0 ? Array.Empty<ValidationAttribute>() : results.ToArray();
}

private bool HasDefaultErrorMessage(ValidationAttribute attribute)
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Expand Up @@ -20,7 +20,7 @@
</PropertyGroup>

<PropertyGroup Label="Building">
<LangVersion>11.0</LangVersion>
<LangVersion>12.0</LangVersion>
<!-- Disable warning for missing XML doc comments. -->
<NoWarn>$(NoWarn);CS1591;CS1573</NoWarn>
<Deterministic>true</Deterministic>
Expand Down
Expand Up @@ -28,4 +28,4 @@ public class PropertyDisplayMetadata
public StyleAttribute Styles { get; set; }
public bool IsEditAllowed { get; set; }
}
}
}
Expand Up @@ -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;
Expand Down Expand Up @@ -40,8 +41,12 @@ public ResourceViewModelValidationMetadataProvider(Type errorMessagesResourceFil
/// <summary>
/// Gets validation attributes for the specified property.
/// </summary>
public IEnumerable<ValidationAttribute> GetAttributesForProperty(PropertyInfo property)
public IEnumerable<ValidationAttribute> GetAttributesForProperty(MemberInfo member)
{
if (member is not PropertyInfo property)
{
return [];
}
return cache.GetOrAdd(new PropertyInfoCulturePair(CultureInfo.CurrentUICulture, property), GetAttributesForPropertyCore);
}

Expand Down
2 changes: 1 addition & 1 deletion src/Framework/Core/DotVVM.Core.csproj
Expand Up @@ -16,7 +16,7 @@
<EmbeddedResource Include="compiler\resources\**\*" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Text.Json" Version="8.0.3" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
<PackageReference Include="System.ComponentModel.Annotations" Version="4.3.0" />
Expand Down
13 changes: 12 additions & 1 deletion src/Framework/Core/ViewModel/BindAttribute.cs
Expand Up @@ -7,7 +7,7 @@ namespace DotVVM.Framework.ViewModel
/// <summary>
/// Specifies the binding direction.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class BindAttribute : Attribute
{

Expand All @@ -21,6 +21,17 @@ public class BindAttribute : Attribute
/// </summary>
public string? Name { get; set; }

public bool? _allowDynamicDispatch;
/// <summary>
/// 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).
/// </summary>
public bool AllowDynamicDispatch { get => _allowDynamicDispatch ?? false; set => _allowDynamicDispatch = value; }
tomasherceg marked this conversation as resolved.
Show resolved Hide resolved

/// <summary> See <see cref="AllowDynamicDispatch" /> </summary>
public bool AllowsDynamicDispatch(bool defaultValue) => _allowDynamicDispatch ?? defaultValue;


/// <summary>
/// Initializes a new instance of the <see cref="BindAttribute"/> class.
Expand Down
29 changes: 21 additions & 8 deletions 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<BindAttribute>();
if (bindAttribute != null)
Expand All @@ -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<JsonPropertyNameAttribute>();
if (!string.IsNullOrEmpty(jsonPropertyAttribute?.Name))
{
// use JsonProperty name if Bind attribute is not present or doesn't specify it
var jsonPropertyAttribute = propertyInfo.GetCustomAttribute<JsonPropertyAttribute>();
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;
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Framework/Core/ViewModel/IPropertySerialization.cs
Expand Up @@ -4,6 +4,6 @@ namespace DotVVM.Framework.ViewModel
{
public interface IPropertySerialization
{
string ResolveName(PropertyInfo propertyInfo);
string ResolveName(MemberInfo propertyInfo);
}
}
@@ -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
{
Expand Down
2 changes: 1 addition & 1 deletion src/Framework/Framework/Binding/DotvvmProperty.cs
Expand Up @@ -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
{
Expand Down
@@ -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<IBinding>((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<IBinding>();
// 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<IBinding>();
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) { }
}
}
Expand Up @@ -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 <see cref="GetProperty(Type, ErrorHandlingMode)" /> is invoked. </summary>
[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<TValue> where TValue : class
Expand Down
Expand Up @@ -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
{
Expand Down Expand Up @@ -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<object[]> command, string id)
Expand Down
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions 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
{
Expand Down Expand Up @@ -111,14 +111,14 @@ public ValueOrBinding<T2> UpCast<T2>()
/// <summary> Returns a Javascript (knockout) expression representing this value or this binding. </summary>
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)
);

/// <summary> 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. </summary>
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)
);

Expand Down
Expand Up @@ -12,7 +12,6 @@
using DotVVM.Framework.Compilation.Javascript;
using DotVVM.Framework.Controls;
using DotVVM.Framework.Utils;
using Newtonsoft.Json;

public static class ValueOrBindingExtensions
{
Expand Down
Expand Up @@ -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)
{
Expand Down