Skip to content

Commit

Permalink
STJ migration: Add support for field serialization for ValueTuple
Browse files Browse the repository at this point in the history
  • Loading branch information
exyi committed Apr 24, 2024
1 parent 1dc58d7 commit 81350aa
Show file tree
Hide file tree
Showing 17 changed files with 101 additions and 57 deletions.
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 @@ -7,7 +7,7 @@ namespace DotVVM.Framework.Controls.DynamicData.Metadata
public class PropertyDisplayMetadata
{

public PropertyInfo PropertyInfo { get; set; }
public MemberInfo PropertyInfo { get; set; }

public string DisplayName { get; set; }

Expand All @@ -28,4 +28,4 @@ public class PropertyDisplayMetadata
public StyleAttribute Styles { get; set; }
public bool IsEditAllowed { get; set; }
}
}
}
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class DefaultPropertySerialization : IPropertySerialization
{
static readonly Type? JsonPropertyNJ = Type.GetType("Newtonsoft.Json.JsonPropertyAttribute, Newtonsoft.Json");
static readonly PropertyInfo? JsonPropertyNJPropertyName = JsonPropertyNJ?.GetProperty("PropertyName");
public string ResolveName(PropertyInfo propertyInfo)
public string ResolveName(MemberInfo propertyInfo)
{
var bindAttribute = propertyInfo.GetCustomAttribute<BindAttribute>();
if (bindAttribute != null)
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
Expand Up @@ -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 } &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public ViewModelJsonConverter(IViewModelSerializationMapper viewModelSerializati
public static bool CanConvertType(Type type) =>
!ReflectionUtils.IsEnumerable(type) &&
ReflectionUtils.IsComplexType(type) &&
!ReflectionUtils.IsTupleLike(type) &&
!ReflectionUtils.IsJsonDom(type) &&
type != typeof(object);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace DotVVM.Framework.ViewModel.Serialization
{
public class ViewModelPropertyMap
{
public ViewModelPropertyMap(PropertyInfo propertyInfo, string name, ProtectMode viewModelProtection, Type type, bool transferToServer, bool transferAfterPostback, bool transferFirstRequest, bool populate)
public ViewModelPropertyMap(MemberInfo propertyInfo, string name, ProtectMode viewModelProtection, Type type, bool transferToServer, bool transferAfterPostback, bool transferFirstRequest, bool populate)
{
PropertyInfo = propertyInfo;
Name = name;
Expand All @@ -23,7 +23,8 @@ public ViewModelPropertyMap(PropertyInfo propertyInfo, string name, ProtectMode
Populate = populate;
}

public PropertyInfo PropertyInfo { get; set; }
/// <summary> The serialized property, or in rare cases the serialized field (when declared in ValueTuple`? or when explicitly marked with [Bind] attribute). </summary>
public MemberInfo PropertyInfo { get; set; }

/// <summary> 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. </summary>
public string Name { get; set; }
Expand All @@ -33,6 +34,7 @@ public ViewModelPropertyMap(PropertyInfo propertyInfo, string name, ProtectMode

public ProtectMode ViewModelProtection { get; set; }

/// <summary> Type of the property </summary>
public Type Type { get; set; }

public Direction BindDirection { get; set; } = Direction.None;
Expand Down Expand Up @@ -67,6 +69,16 @@ public bool IsFullyTransferred()
{
return TransferToServer && TransferToClient;
}

/// <summary> Returns the runtime property value using reflection </summary>
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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public override void ResetFunctions()
/// <summary>
/// Creates the constructor for this object.
/// </summary>
private Expression CallConstructor(Expression services, Dictionary<PropertyInfo, ParameterExpression> propertyVariables, bool throwImmediately = false)
private Expression CallConstructor(Expression services, Dictionary<ViewModelPropertyMap, ParameterExpression> propertyVariables, bool throwImmediately = false)
{
if (constructorFactory != null)
return Invoke(Constant(constructorFactory), services);
Expand Down Expand Up @@ -142,9 +142,9 @@ private Expression CallConstructor(Expression services, Dictionary<PropertyInfo,
if (!prop.TransferToServer)
throw new Exception($"Can not deserialize {Type.ToCode()}, property {prop.Name} is not transferred to server, but it's used in constructor.");

Debug.Assert(propertyVariables.ContainsKey(prop.PropertyInfo));
Debug.Assert(propertyVariables.ContainsKey(prop));

return propertyVariables[prop.PropertyInfo];
return propertyVariables[prop];
}).ToArray();
return Constructor switch {
ConstructorInfo c =>
Expand Down Expand Up @@ -184,20 +184,20 @@ public ReaderDelegate<T> CreateReaderFactory()
var propertyVars = Properties
.Where(p => p.TransferToServer)
.ToDictionary(
p => p.PropertyInfo,
p => p,
p => Expression.Variable(p.Type, "prop_" + p.Name)
);

// If we have constructor property or if we have { get; init; } property, we always create new instance
var alwaysCallConstructor = Properties.Any(p => p.TransferToServer && (
p.ConstructorParameter is {} ||
p.PropertyInfo.IsInitOnly()));
(p.PropertyInfo as PropertyInfo)?.IsInitOnly() == true));

// We don't want to clone IDotvvmViewModel automatically, because the user is likely to register this specific instance somewhere
if (alwaysCallConstructor && typeof(IDotvvmViewModel).IsAssignableFrom(Type) && Constructor is {} && !SerialiationMapperAttributeHelper.IsJsonConstructor(Constructor))
{
var cloneReason =
Properties.FirstOrDefault(p => p.TransferToServer && p.PropertyInfo.IsInitOnly()) is {} initProperty
Properties.FirstOrDefault(p => p.TransferToServer && (p.PropertyInfo as PropertyInfo)?.IsInitOnly() == true) is {} initProperty
? $"init-only property {initProperty.Name} is transferred client → server" :
Properties.FirstOrDefault(p => p.TransferToServer && p.ConstructorParameter is {}) is {} ctorProperty
? $"property {ctorProperty.Name} must be injected into constructor parameter {ctorProperty.ConstructorParameter!.Name}" : "internal bug";
Expand All @@ -223,8 +223,8 @@ p.ConstructorParameter is {} ||
Type.IsValueType ? Constant(true) : NotEqual(value, Constant(null)),
Block(
propertyVars
.Where(p => p.Key.GetMethod is not null)
.Select(p => Expression.Assign(p.Value, Expression.Property(value, p.Key)))
.Where(p => p.Key.PropertyInfo is not PropertyInfo { GetMethod: null })
.Select(p => Expression.Assign(p.Value, MemberAccess(value, p.Key)))
)
));
}
Expand All @@ -243,7 +243,7 @@ p.ConstructorParameter is {} ||
{
continue;
}
var propertyVar = propertyVars[property.PropertyInfo];
var propertyVar = propertyVars[property];

var existingValue =
property.Populate ?
Expand Down Expand Up @@ -357,8 +357,8 @@ p.ConstructorParameter is {} ||
{
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())
Expand All @@ -377,6 +377,15 @@ p.ConstructorParameter is {} ||
return ex.CompileFast(flags: CompilerFlags.ThrowOnNotSupportedExpression | CompilerFlags.EnableDelegateDebugInfo);
}

Expression MemberAccess(Expression obj, ViewModelPropertyMap property)
{
if (property.PropertyInfo is PropertyInfo pi)
return Property(obj, pi);
if (property.PropertyInfo is FieldInfo fi)
return Field(obj, fi);
throw new NotSupportedException();
}

/// <summary>
/// Creates the writer factory.
/// </summary>
Expand Down Expand Up @@ -405,7 +414,7 @@ public WriterDelegate<T> CreateWriterFactory()
var property = Properties[propertyIndex];
var endPropertyLabel = Expression.Label("end_property_" + property.Name);

if (property.TransferToClient && property.PropertyInfo.GetMethod != null)
if (property.TransferToClient && property.PropertyInfo is not PropertyInfo { GetMethod: null })
{
if (property.TransferFirstRequest != property.TransferAfterPostback)
{
Expand All @@ -424,7 +433,7 @@ public WriterDelegate<T> CreateWriterFactory()
}

// (object)value.{property.PropertyInfo.Name}
var prop = Expression.Property(value, property.PropertyInfo);
var prop = MemberAccess(value, property);

var writeEV = property.ViewModelProtection == ProtectMode.EncryptData ||
property.ViewModelProtection == ProtectMode.SignData;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ protected virtual ViewModelSerializationMap<T> CreateMap<T>()

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);
}

Expand Down Expand Up @@ -115,34 +123,52 @@ where c.GetParameters().Select(p => p.Name).SequenceEqual(d.GetParameters().Sele
protected virtual IEnumerable<ViewModelPropertyMap> GetProperties(Type type, MethodBase? constructor)
{
var ctorParams = constructor?.GetParameters().ToDictionary(p => p.Name.NotNull(), StringComparer.OrdinalIgnoreCase);
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);

var properties = type.GetAllMembers(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m is PropertyInfo or FieldInfo)
.ToArray();
Array.Sort(properties, (a, b) => StringComparer.Ordinal.Compare(a.Name, b.Name));
foreach (var property in properties)
foreach (MemberInfo property in properties)
{
if (SerialiationMapperAttributeHelper.IsJsonIgnore(property)) continue;
var bindAttribute = property.GetCustomAttribute<BindAttribute>();
var include = !SerialiationMapperAttributeHelper.IsJsonIgnore(property) && bindAttribute is not { Direction: Direction.None };
if (property is FieldInfo)
{
// fields are ignored by default, unless marked with [Bind(not None)], [JsonInclude] or defined in ValueTuple<...>
include = include ||
!(bindAttribute is null or { Direction: Direction.None }) ||
property.IsDefined(typeof(JsonIncludeAttribute)) ||
(property.DeclaringType.IsGenericType && property.DeclaringType.FullName.StartsWith("System.ValueTuple`"));
}
if (!include) continue;

var (propertyType, canGet, canSet) = property switch {
PropertyInfo p => (p.PropertyType, p.GetMethod is { IsPublic: true }, p.SetMethod is { IsPublic: true }),
FieldInfo f => (f.FieldType, true, !f.IsInitOnly && !f.IsLiteral),
_ => throw new NotSupportedException()
};

var ctorParam = ctorParams?.GetValueOrDefault(property.Name);

var propertyMap = new ViewModelPropertyMap(
property,
propertySerialization.ResolveName(property),
ProtectMode.None,
property.PropertyType,
transferToServer: ctorParam is {} || IsSetterSupported(property),
transferAfterPostback: property.GetMethod != null && property.GetMethod.IsPublic,
transferFirstRequest: property.GetMethod != null && property.GetMethod.IsPublic,
populate: (ViewModelJsonConverter.CanConvertType(property.PropertyType) || property.PropertyType == typeof(object)) && property.GetMethod != null
propertyType,
transferToServer: ctorParam is {} || canSet,
transferAfterPostback: canGet,
transferFirstRequest: canGet,
populate: (ViewModelJsonConverter.CanConvertType(propertyType) || propertyType == typeof(object)) && canGet
);
propertyMap.ConstructorParameter = ctorParam;
propertyMap.JsonConverter = GetJsonConverter(property);
propertyMap.AllowDynamicDispatch = property.PropertyType.IsAbstract || property.PropertyType == typeof(object);
propertyMap.AllowDynamicDispatch = propertyType.IsAbstract || propertyType == typeof(object);

foreach (ISerializationInfoAttribute attr in property.GetCustomAttributes().OfType<ISerializationInfoAttribute>())
{
attr.SetOptions(propertyMap);
}

var bindAttribute = property.GetCustomAttribute<BindAttribute>();
if (bindAttribute != null)
{
propertyMap.Bind(bindAttribute.Direction);
Expand All @@ -169,18 +195,8 @@ protected virtual IEnumerable<ViewModelPropertyMap> GetProperties(Type type, Met
yield return propertyMap;
}
}
/// <summary>
/// Returns whether DotVVM serialization supports setter of given property.
/// </summary>
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<JsonConverterAttribute>()?.ConverterType;
if (converterType == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace DotVVM.Framework.ViewModel.Validation
{
public class AttributeViewModelValidationMetadataProvider : IViewModelValidationMetadataProvider
{
public IEnumerable<ValidationAttribute> GetAttributesForProperty(PropertyInfo property)
public IEnumerable<ValidationAttribute> GetAttributesForProperty(MemberInfo property)
{
return property.GetCustomAttributes<ValidationAttribute>(true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ namespace DotVVM.Framework.ViewModel.Validation
{
public interface IValidationRuleTranslator
{
IEnumerable<ViewModelPropertyValidationRule> TranslateValidationRules(PropertyInfo property, IEnumerable<ValidationAttribute> validationAttributes);
IEnumerable<ViewModelPropertyValidationRule> TranslateValidationRules(MemberInfo property, IEnumerable<ValidationAttribute> validationAttributes);
}
}
Loading

0 comments on commit 81350aa

Please sign in to comment.