Skip to content

Commit

Permalink
Merge pull request #1799 from riganti/system.text.json
Browse files Browse the repository at this point in the history
Use System.Text.Json as a backend for view model serialization
  • Loading branch information
exyi authored Nov 3, 2024
2 parents 06964d7 + 20019bf commit 8cf46af
Show file tree
Hide file tree
Showing 252 changed files with 5,866 additions and 3,007 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,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 @@ -127,6 +127,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
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
]
}
}
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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.5" />
</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
Original file line number Diff line number Diff line change
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; }

/// <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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ namespace DotVVM.Framework.ViewModel
{
public interface IPropertySerialization
{
string ResolveName(PropertyInfo propertyInfo);
string ResolveName(MemberInfo propertyInfo);
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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) { }
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 static ParametrizedCode CreateJsPostbackInvocation(string id, bool? needs
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
18 changes: 18 additions & 0 deletions src/Framework/Framework/CallerArgumentExpressionAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Runtime.CompilerServices
{
#if !NET5_0_OR_GREATER
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class CallerArgumentExpressionAttribute : Attribute
{
public CallerArgumentExpressionAttribute(string parameterName)
{
ParameterName = parameterName;
}

public string ParameterName { get; }
}
#endif
}
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit 8cf46af

Please sign in to comment.