Skip to content

Latest commit

 

History

History
471 lines (360 loc) · 15.2 KB

README.md

File metadata and controls

471 lines (360 loc) · 15.2 KB

Reflection

NuGet

Why

With the development of .NET, there is an increasing need for AOT Native in many applications. However, reflection and dynamic code pose obstacles to AOT deployment. Source generators can effectively this issue. For example, System.Json.Text use SourceGenerator to handle object serialization. However, these implementations are specific to individual businesses and cannot be easily generalized.

SourceReflection aims to provide a more universal solution, offering AOTable Reflection support to more developers without the need for repetitive source generator implementation.

Supports

Supports the following types

  • Class
  • Record
  • Struct (ref struct is not supports)
  • Enum
  • Array
  • GenericType(just handle known types)

Supports the the following members

  • Field
  • Property
  • Indexer
  • Method
  • Constructor

Unsupports

  • Generic type definition
  • Generic method *

Currently, support for generic type definition is not yet available. The main issue lies in the handling of MarkGenericType. I am currently experimenting with more effective approaches, but there is no specific plan at the moment.

Similar to generic types, there are also generic methods. The issue lies in MarkGenericMethod. However, it can currently handle some specific cases, such as situations where the generic type can be inferred.

Adapters

  • System.Text.Json Adapter, supports AOT without JsonSerializerContext

Installing Reflection

Install-Package SourceGeneration.Reflection -Version 1.0.0-beta2.241113.1
dotnet add package SourceGeneration.Reflection --version 1.0.0-beta2.241113.1

Source Reflection

SourceReflection requires using an attribute to mark the type

  • Adding [SourceReflectionAttribute] to type.
  • Adding SourceReflectionTypeAttribute] to assembly, it is possible to specify types from other assemblies
[assembly: SourceReflectionType<object>]
[assembly: SourceReflectionType(typeof(Enumerable))]

[SourceReflection]
public class Goods { }

Basic

Add [SourceReflectionAttribute] to your class

using SourceGeneration.Reflection;

[SourceReflection]
public class Goods
{
    private int Id { get; set; }
    public string Name { get; private set; }
    public double Price { get; set; }

    internal void Discount(double discount)
    {
        Price = Price * discount;
    }
}
using SourceGeneration.Reflection;

// Get TypeInfo
var type = SourceReflector.GetType(typeof(Goods));

// Get default ConstructorInfo and create a instance
var goods = (Goods)type.GetConstructor([]).Invoke([]);

// Get PropertyInfo and set value
type.GetProperty("Id").SetValue(goods, 1); // private property
type.GetProperty("Name").SetValue(goods, "book"); // private property setter
type.GetProperty("Price").SetValue(goods, 3.14); // public property

// Output book
Console.WriteLine(goods.Name);
// Output 1
Console.WriteLine(type.GetProperty("Id").GetValue(goods));
// Output 3.14
Console.WriteLine(goods.Price);

// Get MethodInfo and invoke
type.GetMethod("Discount").Invoke(goods, [0.5]);
// Output 1.57
Console.WriteLine(goods.Price);

Enum

[SourceReflection]
public enum TestEnum { A, B }
var type = SourceReflector.GetType(typeof(TestEnum));
Assert.IsTrue(type.IsEnum);
Assert.AreEqual("A", type.DeclaredFields[0].Name);
Assert.AreEqual("B", type.DeclaredFields[1].Name);
Assert.AreEqual(0, type.DeclaredFields[0].GetValue(null));
Assert.AreEqual(1, type.DeclaredFields[1].GetValue(null));

Array

The usage of MarkArrayType is similar to that of Runtime reflection.

[assembly: SourceReflectionType(typeof(int))]

SourceTypeInfo type = SourceReflector.GetType<int>();
SourceTypeInfo arrayType = type.MarkArrayType();

int[] array = [1, 2];
Assert.AreEqual(2, arrayType.GetRequriedProperty("Length").GetValue(array));

arrayType.GetMethod("Set")!.Invoke(array, [0, 2]);
Assert.AreEqual(2, arrayType.GetMethod("Get")!.Invoke(array, [0]));

Or adding SourceReflectionTypeAttribute

[assembly: SourceReflectionType(typeof(int[]))]

Nullable Annotation

SourceReflection supports nullable annotations, nullable annotations can be obtained for fields, properties, method return values, and parameters. It includes:

  • SourceNullableAnnotation.Annotated
  • SourceNullableAnnotation.NotAnnotated
  • SourceNullableAnnotation.None, Indicates that nullable is disabled in the current context.
[SourceReflection]
public class NullableAnnotationTestObject
{
    public string? Nullable { get; set; }
    public string NotNullable { get; set; } = null!;

    #nullable disable

    public string DisableNullable { get; set; }
}
var type = SourceReflector.GetType<NullableAnnotationTestObject>()!;

Assert.AreEqual(SourceNullableAnnotation.Annotated, type.GetProperty("Nullable").NullableAnnotation);
Assert.AreEqual(SourceNullableAnnotation.NotAnnotated, type.GetProperty("NotNullable").NullableAnnotation);
Assert.AreEqual(SourceNullableAnnotation.None, type.GetProperty("DisableNullable").NullableAnnotation);

Required Member

[SourceReflection]
public class RequiredMemberTestObject
{
    public required int Property { get; set; } = 1;
    public required int Field = 1;
}
var type = SourceReflector.GetType(typeof(RequiredMemberTestObject));
Assert.IsTrue(type.GetProperty("Property").IsRequired);
Assert.IsTrue(type.GetProperty("Field").IsRequired);

InitOnly Property

[SourceReflection]
public class InitOnlyPropertyTestObject
{
    public required int Property { get; init; }
}
var type = SourceReflector.GetType(typeof(InitOnlyPropertyTestObject));
Assert.IsTrue(type.GetProperty("Property").IsInitOnly);

Create Instance

The SourceReflector.CreateInstance method has almost the same functionality and features as the System.Activator.CreateInstance method. It supports parameter matching and parameter default values.

[SourceReflection]
public class CreateInstanceTestObject
{
    public CreateInstanceTestObject() { }
    public CreateInstanceTestObject(byte a, string? c = "abc") { }
    internal CreateInstanceTestObject(int a, int b) { }
    protected CreateInstanceTestObject(long a, int c = 1, string? c = "abc") { }
}
var o1 = SourceReflector.CreateInstance<CreateInstanceTestObject>(); // Call the first constructor.
var o2 = SourceReflector.CreateInstance<CreateInstanceTestObject>((byte)1); // Call the second constructor.
var o3 = SourceReflector.CreateInstance<CreateInstanceTestObject>(1, 2); // Call the third constructor.
var o4 = SourceReflector.CreateInstance<CreateInstanceTestObject>(1L); // Call the fourth constructor.
//or use non-generic method
var o5 = SourceReflector.CreateInstance(typeof(CreateInstanceTestObject), 1); // Call the fourth constructor.
var o6 = SourceReflector.CreateInstance(typeof(CreateInstanceTestObject), 1, 2, "abc"); // Call the fourth constructor.

Generic Definition

Currently, support for generic type definition is not yet available. The main issue lies in the handling of MarkGenericType. I am currently experimenting with more effective approaches, but there is no specific plan at the moment.

[assembly: SourceReflectionType(typeof(List<>))]

[SourceReflection]
public class GenericTypeDefinitionTestObject<T> { }
//Can not generate generic type definition info
Assert.IsNull(SourceReflector.GetType<List<>>());
Assert.IsNull(SourceReflector.GetType<GenericTypeDefinitionTestObject<>>());

Generic Type

Currently, support for generic type definition is not yet available. The main issue lies in the handling of MarkGenericType. The source generation can handle handle known types.

[assembly: SourceReflectionType(typeof(List<string>))]

var type = SourceReflector.GetRequiredType<List<string>>();
List<string> list = ["a", "b"];
type.GetMethod("Add")!.Invoke(list, ["c"]);
Assert.AreEqual(3, type.GetProperty("Count")!.GetValue(list));

Generic Method

For generic methods with inferable types, they can be called using source generation

[SourceReflection]
public class GenericMethodTestObject
{
    public T Invoke0<T>() => default!;
    public T Invoke1<T>(T t) => t;
    public T Invoke2<T>(T t) where T : ICloneable => t;
    public T Invoke3<T>(T t) where T : unmanaged => t;
    public T Invoke4<T>(T t) where T : notnull => t;
    public T Invoke5<T>(T t) where T : ICloneable, IComparable => t;
    public T Invoke6<T, K>(T t, K k) where T : ICloneable where K : IComparable => t;
    public T[] InvokeArray1<T>(T[] t) => t;
}
SourceTypeInfo type = SourceReflector.GetRequiredType<GenericMethodTestObject>();
GenericMethodTestObject instance = new();

// Success
type.GetMethod("Invoke1").Invoke(instance, [1]);
type.GetMethod("Invoke2").Invoke(instance, [1]);
type.GetMethod("Invoke4").Invoke(instance, [1]);
type.GetMethod("Invoke5").Invoke(instance, [1]);
type.GetMethod("Invoke6").Invoke(instance, [1, 2]);

//Error
type.GetMethod("Invoke0").Invoke(instance, []);
type.GetMethod("Invoke3").Invoke(instance, [1]);
type.GetMethod("InvokeArray1").Invoke(instance, [new int[] { 1 }]);

When the generic type cannot be inferred, the only option is to use runtime reflection through MarkGenericMethod, which will not be supported in AOT compilation.

type.GetMethod("Invoke0").MethodInfo.MarkGenericMethod([]).Invoke(instance);

Even if the type can be inferred, this approach has its drawbacks. If the internal implementation of the method has type checks on the generic parameters, the result may not meet expectations.

public class GenericMethodTestObject
{
    public string Invoke1<T>(T value) => typeof(T).Name;
    public string Invoke2<T>(T value) where T : ICloneable => typeof(T).Name;
}
var type = SourceReflector.GetRequiredType<GenericMethodTestInferObject>();
GenericMethodTestInferObject instance = new();
Assert.AreEqual("Object", type.GetMethod("Invoke1")!.Invoke(instance, [1]));
Assert.AreEqual("ICloneable", type.GetMethod("Invoke2")!.Invoke(instance, ["a"]));

Without SourceReflectionAttribute

You can also without using SourceReflectionAttribute for reflection

public class Goods
{
    private int Id { get; set; }
    public string Name { get; private set; }
    public double Price { get; set; }

    internal void Discount(double discount)
    {
        Price = Price * discount;
    }
}
// Get TypeInfo and allow Runtime Reflection
var type = SourceReflector.GetType(typeof(Goods), true);

var goods = (Goods)type.GetConstructor([]).Invoke([]);
type.GetProperty("Id").SetValue(goods, 1); // private property
type.GetProperty("Name").SetValue(goods, "book"); // private property setter
type.GetProperty("Price").SetValue(goods, 3.14); // public property
type.GetMethod("Discount").Invoke(goods, [0.5]);

It can work properly after AOT compilation. DynamicallyAccessedMembers allows tools to understand which members are being accessed during the execution of a program.

Use other attribute to mark SourceReflection

You can create a custom attribute to indicate to the source generator which types need to be reflected.

Edit your project .csproj

<!-- define your Attribute -->
<PropertyGroup>
  <DisplaySourceReflectionAttribute>System.ComponentModel.DataAnnotations.DisplayAttribute</DisplaySourceReflectionAttribute>
</PropertyGroup>

<!-- set property visible  -->
<!-- property name must be endswith 'SourceReflectionAttribute'  -->
<ItemGroup>
  <CompilerVisibleProperty Include="DisplaySourceReflectionAttribute" />
</ItemGroup>

Now you can use the DisplayAttribute to inform the source generator that you need to reflect it.

[System.ComponentModel.DataAnnotations.DisplayAttribute]
public class Goods
{
    private int Id { get; set; }
    public string Name { get; private set; }
    public double Price { get; set; }
}

System.Text.Json Adapter

Supports AOT without JsonSerializerContext, System.Text.Json already provides a complete solution for AOT compilation, but in most cases, besides JSON serialization, there there are still many places where reflection is needed. Although different solutions can be selected for different scenarios, it may also result of more models or the marking of more attributes. SourceReflection can simplify this for JSON serialization.

Install-Package SourceGeneration.Reflection.SystemTextJson -Version 1.0.0-beta2.240523.1
dotnet add package SourceGeneration.Reflection.SystemTextJson --version 1.0.0-beta2.240523.1
var options = new JsonSerializerOptions
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver().WithSourceReflection(),
};

var json = JsonSerializer.Serialize(new Goods(), options);
var goods = JsonSerializer.Deserialize<Model>(json, options);

[SourceReflection]
public class Goods
{
    private int Id { get; set; }
    public string Name { get; private set; }
    public double Price { get; set; }
}

Performance & Optimization

SourceReflection generates alternative reflection-based method invocations through source generator, for example:

// code generated
new SourcePropertyInfo()
{
    Name = "Name",
    FieldType = typeof(string),
    GetValue = instance => ((Goods)instance).Name,
    SetValue = (instance, value) => ((Goods)instance).Name = (string)value,
    //Other properties init
}

For public and internal members, this approach is used, while for protected and private members, runtime reflection is still used to accomplish it.

You can use SourceReflection get the runtime MemberInfo, the code like this:

public class SourcePropertyInfo
{
    private PropertyInfo? _propertyInfo;
    private Func<object, object?>? _getMethod;

    public PropertyInfo PropertyInfo
    {
        get => _propertyInfo ??= typeof(Goods).GetProperty(BindingFlags.NonPublic | BindingFlags.Instance, "Id");
    }

    public Func<object, object?> GetValue
    {
        get => _getMethod ??= PropertyInfo.GetValue;
        init => _getMethod = value;
    }
}

SourceReflection uses lazy evaluation, which means that reflection is only performed and the result is cached when you first retrieve it. You don't need to worry about whether the user has marked an object with the SourceReflectionAttribute. You can use the SourceReflection to retrieve metadata or invoke methods in a generic way regardless of whether the attribute is used. SourceReflection globally caching all objects (Type, FieldInfo, PropertyInfo, MethodInfo, ConstructorInfo) in a static cache.

Samples

  • Basic example demonstrates some basic uses of SourceReflection.

  • Sytem.Text.Json Adapter example demonstrates how to uses SourceReflection for JsonSerializer .

  • CsvExporter is a csv file export sample.

  • AutoMapper is a object-object mapper sample.

  • CustomLibrary example demonstrates how to use SourceReflection to publish your NuGet package and propagate your attributes.