From 26188cd08aea4690aff46ebf0423723797f655d9 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 9 Aug 2025 12:12:40 +0300 Subject: [PATCH] Target net8, add trimming support --- .../ValveKeyValue/DefaultObjectReflector.cs | 6 +- ValveKeyValue/ValveKeyValue/FieldMember.cs | 3 + ValveKeyValue/ValveKeyValue/IObjectMember.cs | 3 + .../ValveKeyValue/IObjectReflector.cs | 4 +- ValveKeyValue/ValveKeyValue/KVSerializer.cs | 5 +- ValveKeyValue/ValveKeyValue/ObjectCopier.cs | 111 ++++++++++++------ ValveKeyValue/ValveKeyValue/PropertyMember.cs | 3 + ValveKeyValue/ValveKeyValue/Trimming.cs | 17 +++ .../ValveKeyValue/ValveKeyValue.csproj | 3 +- 9 files changed, 114 insertions(+), 41 deletions(-) create mode 100644 ValveKeyValue/ValveKeyValue/Trimming.cs diff --git a/ValveKeyValue/ValveKeyValue/DefaultObjectReflector.cs b/ValveKeyValue/ValveKeyValue/DefaultObjectReflector.cs index de0501d0..a9f6a7c6 100644 --- a/ValveKeyValue/ValveKeyValue/DefaultObjectReflector.cs +++ b/ValveKeyValue/ValveKeyValue/DefaultObjectReflector.cs @@ -1,15 +1,15 @@ +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace ValveKeyValue { sealed class DefaultObjectReflector : IObjectReflector { - IEnumerable IObjectReflector.GetMembers(object @object) + IEnumerable IObjectReflector.GetMembers([DynamicallyAccessedMembers(Trimming.Properties)] Type objectType, object @object) { + Require.NotNull(objectType, nameof(objectType)); Require.NotNull(@object, nameof(@object)); - var objectType = @object.GetType(); - if (IsValueTupleType(objectType)) { var fields = objectType.GetFields(BindingFlags.Instance | BindingFlags.Public); diff --git a/ValveKeyValue/ValveKeyValue/FieldMember.cs b/ValveKeyValue/ValveKeyValue/FieldMember.cs index 52bd5e0d..f7294527 100644 --- a/ValveKeyValue/ValveKeyValue/FieldMember.cs +++ b/ValveKeyValue/ValveKeyValue/FieldMember.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace ValveKeyValue @@ -20,6 +21,8 @@ public FieldMember(FieldInfo fieldInfo, object @object) public string Name => fieldInfo.Name; + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2073", Justification = "FieldType")] + [DynamicallyAccessedMembers(Trimming.Properties)] public Type MemberType => fieldInfo.FieldType; public object Value diff --git a/ValveKeyValue/ValveKeyValue/IObjectMember.cs b/ValveKeyValue/ValveKeyValue/IObjectMember.cs index 65347efb..b266ddcc 100644 --- a/ValveKeyValue/ValveKeyValue/IObjectMember.cs +++ b/ValveKeyValue/ValveKeyValue/IObjectMember.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace ValveKeyValue { interface IObjectMember @@ -6,6 +8,7 @@ interface IObjectMember string Name { get; } + [DynamicallyAccessedMembers(Trimming.Properties)] Type MemberType { get; } object Value { get; set; } diff --git a/ValveKeyValue/ValveKeyValue/IObjectReflector.cs b/ValveKeyValue/ValveKeyValue/IObjectReflector.cs index 633076e7..cbb6199a 100644 --- a/ValveKeyValue/ValveKeyValue/IObjectReflector.cs +++ b/ValveKeyValue/ValveKeyValue/IObjectReflector.cs @@ -1,7 +1,9 @@ +using System.Diagnostics.CodeAnalysis; + namespace ValveKeyValue { interface IObjectReflector { - IEnumerable GetMembers(object @object); + IEnumerable GetMembers([DynamicallyAccessedMembers(Trimming.Properties)] Type objectType, object @object); } } diff --git a/ValveKeyValue/ValveKeyValue/KVSerializer.cs b/ValveKeyValue/ValveKeyValue/KVSerializer.cs index 72db7638..ce0d5ef0 100644 --- a/ValveKeyValue/ValveKeyValue/KVSerializer.cs +++ b/ValveKeyValue/ValveKeyValue/KVSerializer.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using ValveKeyValue.Abstraction; using ValveKeyValue.Deserialization; using ValveKeyValue.Deserialization.KeyValues1; @@ -52,7 +53,7 @@ public KVDocument Deserialize(Stream stream, KVSerializerOptions options = null) /// Options to use that can influence the deserialization process. /// A instance representing the KeyValues structure in the stream. /// The type of object to deserialize.; - public TObject Deserialize(Stream stream, KVSerializerOptions options = null) + public TObject Deserialize<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TObject>(Stream stream, KVSerializerOptions options = null) { Require.NotNull(stream, nameof(stream)); @@ -91,7 +92,7 @@ public void Serialize(Stream stream, KVDocument data, KVSerializerOptions option /// The top-level object name /// Options to use that can influence the serialization process. /// The type of object to serialize. - public void Serialize(Stream stream, TData data, string name, KVSerializerOptions options = null) + public void Serialize<[DynamicallyAccessedMembers(Trimming.Properties)] TData>(Stream stream, TData data, string name, KVSerializerOptions options = null) { Require.NotNull(stream, nameof(stream)); diff --git a/ValveKeyValue/ValveKeyValue/ObjectCopier.cs b/ValveKeyValue/ValveKeyValue/ObjectCopier.cs index 2c3bd983..aac1c4c8 100644 --- a/ValveKeyValue/ValveKeyValue/ObjectCopier.cs +++ b/ValveKeyValue/ValveKeyValue/ObjectCopier.cs @@ -1,23 +1,26 @@ using System.Collections; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; -using System.Runtime.Serialization; namespace ValveKeyValue { // TODO: Migrate to IVisitationListener static class ObjectCopier { - public static TObject MakeObject(KVObject keyValueObject) + public static TObject MakeObject<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TObject>(KVObject keyValueObject) => MakeObject(keyValueObject, new DefaultObjectReflector()); - public static object MakeObject(Type objectType, KVObject keyValueObject, IObjectReflector reflector) + public static object MakeObject( + [DynamicallyAccessedMembers(Trimming.Properties)] Type objectType, KVObject keyValueObject, IObjectReflector reflector) => InvokeGeneric(nameof(MakeObject), objectType, new object[] { keyValueObject, reflector }); - public static TObject MakeObject(KVObject keyValueObject, IObjectReflector reflector) + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2062", Justification = "If the lookup value type exists at runtime then it should have enough for us to introspect.")] + public static TObject MakeObject<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TObject>(KVObject keyValueObject, IObjectReflector reflector) { Require.NotNull(keyValueObject, nameof(keyValueObject)); Require.NotNull(reflector, nameof(reflector)); @@ -43,8 +46,8 @@ public static TObject MakeObject(KVObject keyValueObject, IObjectReflec // The object must remain boxed until it is fully initiallized, as this is the only way // that we can build a struct due to the nature of struct copying. - var typedObject = FormatterServices.GetUninitializedObject(typeof(TObject)); - CopyObject(keyValueObject, typedObject, reflector); + var typedObject = RuntimeHelpers.GetUninitializedObject(typeof(TObject)); + CopyObject(keyValueObject, typeof(TObject), typedObject, reflector); return (TObject)typedObject; } else if (TryConvertValueTo(keyValueObject.Name, keyValueObject.Value, out var converted)) @@ -58,10 +61,18 @@ public static TObject MakeObject(KVObject keyValueObject, IObjectReflec } } - public static KVObject FromObject(Type objectType, object managedObject, string topLevelName) + public static KVObject FromObject( + [DynamicallyAccessedMembers(Trimming.Properties)] Type objectType, + object managedObject, + string topLevelName) => FromObjectCore(objectType, managedObject, topLevelName, new DefaultObjectReflector(), new HashSet()); - static KVObject FromObjectCore(Type objectType, object managedObject, string topLevelName, IObjectReflector reflector, HashSet visitedObjects) + static KVObject FromObjectCore( + [DynamicallyAccessedMembers(Trimming.Properties)] Type objectType, + object managedObject, + string topLevelName, + IObjectReflector reflector, + HashSet visitedObjects) { if (managedObject == null) { @@ -76,7 +87,12 @@ static KVObject FromObjectCore(Type objectType, object managedObject, string top return new KVObject(topLevelName, transformedValue); } - static KVValue ConvertObjectToValue(Type objectType, object managedObject, IObjectReflector reflector, HashSet visitedObjects) + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "If the IDictionary's value object already exists at runtime then its properties will too.")] + static KVValue ConvertObjectToValue( + [DynamicallyAccessedMembers(Trimming.Properties)] Type objectType, + object managedObject, + IObjectReflector reflector, + HashSet visitedObjects) { if (!objectType.IsValueType && objectType != typeof(string) && !visitedObjects.Add(managedObject)) { @@ -108,7 +124,7 @@ static KVValue ConvertObjectToValue(Type objectType, object managedObject, IObje var counter = 0; foreach (var child in (IEnumerable)managedObject) { - var childKVObject = CopyObject(child, counter.ToString(), reflector, visitedObjects); + var childKVObject = FromObjectCore(child.GetType(), child, counter.ToString(), reflector, visitedObjects); childObjects.Add(childKVObject); counter++; @@ -116,30 +132,27 @@ static KVValue ConvertObjectToValue(Type objectType, object managedObject, IObje } else { - foreach (var member in reflector.GetMembers(managedObject).OrderBy(p => p.Name, StringComparer.InvariantCulture)) + foreach (var member in reflector.GetMembers(objectType, managedObject).OrderBy(p => p.Name, StringComparer.InvariantCulture)) { if (!member.MemberType.IsValueType && member.Value is null) { continue; } - childObjects.Add(CopyObject(member.Value, member.Name, reflector, visitedObjects)); + childObjects.Add(FromObjectCore(member.Value.GetType(), member.Value, member.Name, reflector, visitedObjects)); } } return childObjects; } - static KVObject CopyObject(object @object, string name, IObjectReflector reflector, HashSet visitedObjects) - => FromObjectCore(@object.GetType(), @object, name, reflector, visitedObjects); - - static void CopyObject(KVObject kv, object obj, IObjectReflector reflector) + static void CopyObject(KVObject kv, [DynamicallyAccessedMembers(Trimming.Properties)] Type objectType, object obj, IObjectReflector reflector) { Require.NotNull(kv, nameof(kv)); Require.NotNull(obj, nameof(obj)); Require.NotNull(reflector, nameof(reflector)); - var members = reflector.GetMembers(obj).ToDictionary(m => m.Name, m => m, StringComparer.OrdinalIgnoreCase); + var members = reflector.GetMembers(objectType, obj).ToDictionary(m => m.Name, m => m, StringComparer.OrdinalIgnoreCase); foreach (var item in kv.Children) { @@ -148,7 +161,8 @@ static void CopyObject(KVObject kv, object obj, IObjectReflector reflector) continue; } - member.Value = MakeObject(member.MemberType, item, reflector); + var convertedValue = MakeObject(member.MemberType, item, reflector); + member.Value = convertedValue; } } @@ -208,11 +222,18 @@ static bool IsLookupWithStringKey(Type type, out Type valueType) return true; } - static object MakeLookup(Type valueType, IEnumerable items, IObjectReflector reflector) + static object MakeLookup( + [DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] Type valueType, + IEnumerable items, + IObjectReflector reflector) => InvokeGeneric(nameof(MakeLookupCore), valueType, new object[] { items, reflector }); - static ILookup MakeLookupCore(IEnumerable items, IObjectReflector reflector) - => items.ToLookup(kv => kv.Name, kv => ConvertValue(kv.Value, reflector)); + static ILookup MakeLookupCore<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TValue>(IEnumerable items, IObjectReflector reflector) + { + TValue valueConversionFunc(KVObject kv) => ConvertValue(kv.Value, reflector); + + return items.ToLookup(kv => kv.Name, valueConversionFunc); + } static readonly Dictionary> EnumerableBuilders = new() { @@ -223,7 +244,13 @@ static ILookup MakeLookupCore(IEnumerable item [typeof(ObservableCollection<>)] = (type, values, reflector) => InvokeGeneric(nameof(MakeObservableCollection), type.GetGenericArguments()[0], new object[] { values, reflector }), }; - static bool ConstructTypedEnumerable(Type type, object[] values, IObjectReflector reflector, out object typedEnumerable) + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "If our T[] array exists then so much the element T.")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "If our T[] array exists then so much the element T.")] + static bool ConstructTypedEnumerable( + Type type, + object[] values, + IObjectReflector reflector, + out object typedEnumerable) { object listObject = null; @@ -274,6 +301,9 @@ static bool IsConstructibleEnumerableType(Type type) return false; } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060", Justification = "Analysis cannot follow MakeGenericMethod. All callers validated manually.")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2111", Justification = "Analysis cannot follow MakeGenericMethod. All callers validated manually.")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Analysis cannot follow MakeGenericMethod. All callers validated manually.")] static object InvokeGeneric(string methodName, Type genericType, params object[] parameters) { var method = typeof(ObjectCopier) @@ -292,18 +322,22 @@ static object InvokeGeneric(string methodName, Type genericType, params object[] } } - static List MakeList(object[] items, IObjectReflector reflector) + static List MakeList<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TElement>(object[] items, IObjectReflector reflector) { - return items.Select(i => ConvertValue(i, reflector)) - .ToList(); + var list = new List(capacity: items.Length); + foreach (var item in items) + { + list.Add(ConvertValue(item, reflector)); + } + return list; } - static Collection MakeCollection(object[] items, IObjectReflector reflector) + static Collection MakeCollection<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TElement>(object[] items, IObjectReflector reflector) { return new Collection(MakeList(items, reflector)); } - static ObservableCollection MakeObservableCollection(object[] items, IObjectReflector reflector) + static ObservableCollection MakeObservableCollection<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TElement>(object[] items, IObjectReflector reflector) { return new ObservableCollection(MakeList(items, reflector)); } @@ -334,20 +368,25 @@ static bool IsDictionary(Type type) return true; } - static object MakeDictionary(Type type, KVObject kv, IObjectReflector reflector) + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060", Justification = "Analysis cannot follow MakeGenericMethod but we should be clear by here anyway.")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Analysis cannot follow MakeGenericMethod but we should be clear by here anyway.")] + static object MakeDictionary( + [DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] Type type, + KVObject kv, + IObjectReflector reflector) { var dictionary = Activator.CreateInstance(type); var genericArguments = type.GetGenericArguments(); - typeof(ObjectCopier) - .GetMethod(nameof(FillDictionary), BindingFlags.Static | BindingFlags.NonPublic) - .MakeGenericMethod(genericArguments) + var method = typeof(ObjectCopier) + .GetMethod(nameof(FillDictionary), BindingFlags.Static | BindingFlags.NonPublic); + method.MakeGenericMethod(genericArguments) .Invoke(null, new[] { dictionary, kv, reflector }); return dictionary; } - static void FillDictionary(Dictionary dictionary, KVObject kv, IObjectReflector reflector) + static void FillDictionary<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TKey, [DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TValue>(Dictionary dictionary, KVObject kv, IObjectReflector reflector) { foreach (var item in kv.Children) { @@ -363,9 +402,13 @@ static void FillDictionary(Dictionary dictionary, KV } } - static TValue ConvertValue(object value, IObjectReflector reflector) => (TValue)ConvertValue(value, typeof(TValue), reflector); + static TValue ConvertValue<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TValue>(object value, IObjectReflector reflector) + => (TValue)ConvertValue(value, typeof(TValue), reflector); - static object ConvertValue(object value, Type valueType, IObjectReflector reflector) + static object ConvertValue( + object value, + [DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] Type valueType, + IObjectReflector reflector) { if (value is KVCollectionValue collectionValue) { diff --git a/ValveKeyValue/ValveKeyValue/PropertyMember.cs b/ValveKeyValue/ValveKeyValue/PropertyMember.cs index 2f3576ed..6b67cbfa 100644 --- a/ValveKeyValue/ValveKeyValue/PropertyMember.cs +++ b/ValveKeyValue/ValveKeyValue/PropertyMember.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace ValveKeyValue @@ -21,6 +22,8 @@ public PropertyMember(PropertyInfo propertyInfo, object @object) string IObjectMember.Name => PropertyAttribute?.PropertyName ?? propertyInfo.Name; + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2073", Justification = "PropertyType")] + [DynamicallyAccessedMembers(Trimming.Properties)] Type IObjectMember.MemberType => propertyInfo.PropertyType; object IObjectMember.Value diff --git a/ValveKeyValue/ValveKeyValue/Trimming.cs b/ValveKeyValue/ValveKeyValue/Trimming.cs new file mode 100644 index 00000000..d2aa8ff0 --- /dev/null +++ b/ValveKeyValue/ValveKeyValue/Trimming.cs @@ -0,0 +1,17 @@ +using System.Diagnostics.CodeAnalysis; + +namespace ValveKeyValue +{ + internal static class Trimming + { + public const DynamicallyAccessedMemberTypes Constructors = + DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | + DynamicallyAccessedMemberTypes.PublicConstructors | + DynamicallyAccessedMemberTypes.NonPublicConstructors; + + public const DynamicallyAccessedMemberTypes Properties = + DynamicallyAccessedMemberTypes.PublicProperties | + DynamicallyAccessedMemberTypes.NonPublicProperties | + DynamicallyAccessedMemberTypes.PublicFields; + } +} diff --git a/ValveKeyValue/ValveKeyValue/ValveKeyValue.csproj b/ValveKeyValue/ValveKeyValue/ValveKeyValue.csproj index 9d107569..5d020a92 100644 --- a/ValveKeyValue/ValveKeyValue/ValveKeyValue.csproj +++ b/ValveKeyValue/ValveKeyValue/ValveKeyValue.csproj @@ -1,6 +1,6 @@ - netstandard2.1 + net8.0 10.0 Valve KeyValue Library Library to parse and write Valve KeyValue formats @@ -10,6 +10,7 @@ steam valve keyvalues keyvalue kv kv3 csgo dota2 tf2 true ValveKeyValue.snk + true