Skip to content

Commit f10d4f0

Browse files
authored
Merge pull request #60 from IvanMurzak/feature/ignorable-pointer-members
feat: Ignorable pointer members
2 parents 7385f4a + 9062e6c commit f10d4f0

4 files changed

Lines changed: 214 additions & 4 deletions

File tree

ReflectorNet.Tests/ReflectorNet.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<ImplicitUsings>disable</ImplicitUsings>
77
<Nullable>enable</Nullable>
88
<IsPackable>false</IsPackable>
9+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
910
</PropertyGroup>
1011

1112
<ItemGroup>

ReflectorNet.Tests/src/Converter/Reflection/Base/BaseReflectionConverterTests.cs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,177 @@ public void ClearReflectionCache_ShouldClearBothCaches()
207207
Assert.Equal(0, propCache.Count);
208208
}
209209

210+
#region Pointer Access Tests
211+
212+
// Test entity with pointer fields and properties
213+
private unsafe class TestEntityWithPointers
214+
{
215+
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value
216+
public int PublicField;
217+
public int* PointerField;
218+
public void* VoidPointerField;
219+
#pragma warning restore CS0649
220+
public int PublicProp { get; set; }
221+
public unsafe int* PointerProp { get; set; }
222+
public unsafe void* VoidPointerProp { get; set; }
223+
}
224+
225+
// Converter that allows pointer access (default behavior)
226+
private class PointerAllowedConverter : BaseReflectionConverter<TestEntityWithPointers>
227+
{
228+
public override bool AllowPointerFieldsAccess => true;
229+
public override bool AllowPointerPropertiesAccess => true;
230+
231+
protected override SerializedMember InternalSerialize(
232+
Reflector reflector, object? obj, Type type, string? name, bool recursive,
233+
BindingFlags flags, int depth, Logs? logs, ILogger? logger, SerializationContext? context)
234+
=> new SerializedMember();
235+
236+
protected override bool SetValue(
237+
Reflector reflector, ref object? instance, Type type, JsonElement? element,
238+
int depth, Logs? logs, ILogger? logger)
239+
=> true;
240+
241+
public override bool SetField(
242+
Reflector reflector, ref object? instance, Type type, FieldInfo field,
243+
SerializedMember? member, int depth, Logs? logs, BindingFlags flags, ILogger? logger)
244+
=> true;
245+
246+
public override bool SetProperty(
247+
Reflector reflector, ref object? instance, Type type, PropertyInfo prop,
248+
SerializedMember? member, int depth, Logs? logs, BindingFlags flags, ILogger? logger)
249+
=> true;
250+
}
251+
252+
// Converter that disallows pointer access
253+
private class PointerDisallowedConverter : BaseReflectionConverter<TestEntityWithPointers>
254+
{
255+
public override bool AllowPointerFieldsAccess => false;
256+
public override bool AllowPointerPropertiesAccess => false;
257+
258+
protected override SerializedMember InternalSerialize(
259+
Reflector reflector, object? obj, Type type, string? name, bool recursive,
260+
BindingFlags flags, int depth, Logs? logs, ILogger? logger, SerializationContext? context)
261+
=> new SerializedMember();
262+
263+
protected override bool SetValue(
264+
Reflector reflector, ref object? instance, Type type, JsonElement? element,
265+
int depth, Logs? logs, ILogger? logger)
266+
=> true;
267+
268+
public override bool SetField(
269+
Reflector reflector, ref object? instance, Type type, FieldInfo field,
270+
SerializedMember? member, int depth, Logs? logs, BindingFlags flags, ILogger? logger)
271+
=> true;
272+
273+
public override bool SetProperty(
274+
Reflector reflector, ref object? instance, Type type, PropertyInfo prop,
275+
SerializedMember? member, int depth, Logs? logs, BindingFlags flags, ILogger? logger)
276+
=> true;
277+
}
278+
279+
[Fact]
280+
public void AllowPointerFieldsAccess_WhenTrue_ShouldIncludePointerFields()
281+
{
282+
// Arrange
283+
var converter = new PointerAllowedConverter();
284+
var reflector = new Reflector();
285+
var type = typeof(TestEntityWithPointers);
286+
var flags = BindingFlags.Public | BindingFlags.Instance;
287+
288+
// Act
289+
var fields = converter.GetSerializableFields(reflector, type, flags)?.ToList();
290+
291+
// Assert
292+
Assert.NotNull(fields);
293+
Assert.Equal(3, fields.Count); // PublicField, PointerField, VoidPointerField
294+
Assert.Contains(fields, f => f.Name == nameof(TestEntityWithPointers.PublicField));
295+
Assert.Contains(fields, f => f.Name == nameof(TestEntityWithPointers.PointerField));
296+
Assert.Contains(fields, f => f.Name == nameof(TestEntityWithPointers.VoidPointerField));
297+
}
298+
299+
[Fact]
300+
public void AllowPointerFieldsAccess_WhenFalse_ShouldExcludePointerFields()
301+
{
302+
// Arrange
303+
var converter = new PointerDisallowedConverter();
304+
var reflector = new Reflector();
305+
var type = typeof(TestEntityWithPointers);
306+
var flags = BindingFlags.Public | BindingFlags.Instance;
307+
308+
// Act
309+
var fields = converter.GetSerializableFields(reflector, type, flags)?.ToList();
310+
311+
// Assert
312+
Assert.NotNull(fields);
313+
Assert.Single(fields); // Only PublicField
314+
Assert.Contains(fields, f => f.Name == nameof(TestEntityWithPointers.PublicField));
315+
Assert.DoesNotContain(fields, f => f.Name == nameof(TestEntityWithPointers.PointerField));
316+
Assert.DoesNotContain(fields, f => f.Name == nameof(TestEntityWithPointers.VoidPointerField));
317+
}
318+
319+
[Fact]
320+
public void AllowPointerPropertiesAccess_WhenTrue_ShouldIncludePointerProperties()
321+
{
322+
// Arrange
323+
var converter = new PointerAllowedConverter();
324+
var reflector = new Reflector();
325+
var type = typeof(TestEntityWithPointers);
326+
var flags = BindingFlags.Public | BindingFlags.Instance;
327+
328+
// Act
329+
var props = converter.GetSerializableProperties(reflector, type, flags)?.ToList();
330+
331+
// Assert
332+
Assert.NotNull(props);
333+
Assert.Equal(3, props.Count); // PublicProp, PointerProp, VoidPointerProp
334+
Assert.Contains(props, p => p.Name == nameof(TestEntityWithPointers.PublicProp));
335+
Assert.Contains(props, p => p.Name == nameof(TestEntityWithPointers.PointerProp));
336+
Assert.Contains(props, p => p.Name == nameof(TestEntityWithPointers.VoidPointerProp));
337+
}
338+
339+
[Fact]
340+
public void AllowPointerPropertiesAccess_WhenFalse_ShouldExcludePointerProperties()
341+
{
342+
// Arrange
343+
var converter = new PointerDisallowedConverter();
344+
var reflector = new Reflector();
345+
var type = typeof(TestEntityWithPointers);
346+
var flags = BindingFlags.Public | BindingFlags.Instance;
347+
348+
// Act
349+
var props = converter.GetSerializableProperties(reflector, type, flags)?.ToList();
350+
351+
// Assert
352+
Assert.NotNull(props);
353+
Assert.Single(props); // Only PublicProp
354+
Assert.Contains(props, p => p.Name == nameof(TestEntityWithPointers.PublicProp));
355+
Assert.DoesNotContain(props, p => p.Name == nameof(TestEntityWithPointers.PointerProp));
356+
Assert.DoesNotContain(props, p => p.Name == nameof(TestEntityWithPointers.VoidPointerProp));
357+
}
358+
359+
[Fact]
360+
public void AllowPointerFieldsAccess_DefaultValue_ShouldBeTrue()
361+
{
362+
// Arrange
363+
var converter = new TestableReflectionConverter();
364+
365+
// Assert
366+
Assert.True(converter.AllowPointerFieldsAccess);
367+
}
368+
369+
[Fact]
370+
public void AllowPointerPropertiesAccess_DefaultValue_ShouldBeTrue()
371+
{
372+
// Arrange
373+
var converter = new TestableReflectionConverter();
374+
375+
// Assert
376+
Assert.True(converter.AllowPointerPropertiesAccess);
377+
}
378+
379+
#endregion
380+
210381
// Helper methods to access private caches via reflection
211382
private LruCache<(Type, BindingFlags), FieldInfo[]> GetFieldCache(object converter)
212383
{

ReflectorNet/src/Converter/IReflectionConverter.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ public interface IReflectionConverter
4343
/// </summary>
4444
bool AllowCascadePropertiesConversion { get; }
4545

46+
/// <summary>
47+
/// Gets a value indicating whether this converter should access pointer fields.
48+
/// When true, the converter can access pointer fields.
49+
/// When false, pointer-type fields are excluded from serialization while non-pointer fields remain accessible.
50+
/// </summary>
51+
bool AllowPointerFieldsAccess { get; }
52+
53+
/// <summary>
54+
/// Gets a value indicating whether this converter should access pointer properties.
55+
/// When true, the converter can access pointer properties.
56+
/// When false, pointer-type properties are excluded from serialization while non-pointer public properties remain accessible.
57+
/// </summary>
58+
bool AllowPointerPropertiesAccess { get; }
59+
4660
int SerializationPriority(Type type, ILogger? logger = null);
4761

4862
object? Deserialize(

ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.cs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,20 @@ public void ClearReflectionCache(ILogger? logger = null)
103103
/// </summary>
104104
public virtual bool AllowCascadePropertiesConversion => true;
105105

106+
/// <summary>
107+
/// Gets a value indicating whether this converter should include pointer-type fields during serialization.
108+
/// When true, pointer-type fields may be accessed and serialized.
109+
/// When false, pointer-type fields are excluded from serialization while non-pointer public fields remain accessible.
110+
/// </summary>
111+
public virtual bool AllowPointerFieldsAccess => true;
112+
113+
/// <summary>
114+
/// Gets a value indicating whether this converter should include pointer-type properties during serialization.
115+
/// When true, pointer-type properties may be accessed and serialized.
116+
/// When false, pointer-type properties are excluded from serialization while non-pointer public properties remain accessible.
117+
/// </summary>
118+
public virtual bool AllowPointerPropertiesAccess => true;
119+
106120
/// <summary>
107121
/// Calculates the priority score for this converter when handling the specified type.
108122
/// Higher scores indicate stronger compatibility and preference for handling the type.
@@ -172,10 +186,15 @@ public virtual int SerializationPriority(Type type, ILogger? logger = null)
172186
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
173187
ILogger? logger = null)
174188
{
175-
return objType.GetFields(flags)
189+
var result = objType.GetFields(flags)
190+
.Where(field => field.IsPublic)
176191
.Where(field => field.GetCustomAttribute<ObsoleteAttribute>() == null)
177-
.Where(field => field.GetCustomAttribute<NonSerializedAttribute>() == null)
178-
.Where(field => field.IsPublic);
192+
.Where(field => field.GetCustomAttribute<NonSerializedAttribute>() == null);
193+
194+
if (!AllowPointerFieldsAccess)
195+
result = result.Where(field => !field.FieldType.IsPointer);
196+
197+
return result;
179198
}
180199

181200
/// <summary>
@@ -217,10 +236,15 @@ public virtual int SerializationPriority(Type type, ILogger? logger = null)
217236
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
218237
ILogger? logger = null)
219238
{
220-
return objType.GetProperties(flags)
239+
var result = objType.GetProperties(flags)
221240
.Where(prop => prop.CanRead)
222241
.Where(prop => prop.GetCustomAttribute<ObsoleteAttribute>() == null)
223242
.Where(prop => prop.GetIndexParameters().Length == 0); // Filter out indexer properties
243+
244+
if (!AllowPointerPropertiesAccess)
245+
result = result.Where(prop => !prop.PropertyType.IsPointer);
246+
247+
return result;
224248
}
225249

226250
public virtual IEnumerable<string> GetAdditionalSerializableFields(

0 commit comments

Comments
 (0)