ILAccess.Fody provides functionality similar to the UnsafeAccessor introduced in .NET 8, but supports older .NET platforms. It is a Fody weaver that injects IL at compile-time, enabling access to private or internal members without runtime reflection. This results in faster access and compile-time safety compared to traditional reflection-based approaches.
-
Include the
FodyandILAccess.FodyNuGet packages with aPrivateAssets="all"attribute on their<PackageReference />items. InstallingFodyexplicitly is needed to enable weaving.<PackageReference Include="Fody" Version="..." PrivateAssets="all" /> <PackageReference Include="ILAccess.Fody" Version="..." PrivateAssets="all" />
-
If you already have a
FodyWeavers.xmlfile in the root directory of your project, add the<ILAccess />tag there. This file will be created on the first build if it doesn't exist:<?xml version="1.0" encoding="utf-8" ?> <Weavers> <ILAccess /> </Weavers>
See Fody usage for general guidelines, and Fody Configuration for additional options.
You can use ILAccessor to access private fields, methods, or constructors — similar to UnsafeAccessor since .NET 8.
public class TestModel
{
private static int _staticValue = 42;
private int _value;
private TestModel(int value) => _value = value;
private string GetMessage(int code)
=> $"Current value: {_value}, code: {code}";
private static string GetStaticMessage(int code)
=> $"Current static value: {_staticValue}, code: {code}";
}
public static class Accessors
{
[ILAccessor(ILAccessorKind.Field, Name = "_value")]
public static extern ref int Value(this TestModel instance);
[ILAccessor(ILAccessorKind.StaticField, Name = "_staticValue")]
public static extern ref int StaticValue(TestModel instance);
[ILAccessor(ILAccessorKind.Method, Name = "GetMessage")]
public static extern string GetMessage(this TestModel instance, int code);
[ILAccessor(ILAccessorKind.StaticMethod, Name = "GetStaticMessage")]
public static extern string GetStaticMessage(TestModel? instance, int code);
[ILAccessor(ILAccessorKind.Constructor)]
public static extern TestModel Ctor(int x);
}
internal class Program
{
private static void Main(string[] args)
{
var model = Ctor(100);
ref var value = ref model.Value();
Console.WriteLine($"_value: {value}");
value += 50;
Console.WriteLine($"_value updated: {value}");
ref var staticValue = ref StaticValue(model);
Console.WriteLine($"_staticValue: {staticValue}");
staticValue += 10;
Console.WriteLine($"_staticValue updated: {staticValue}");
var message = model.GetMessage(7);
Console.WriteLine($"GetMessage: {message}");
var staticMessage = GetStaticMessage(null, 7);
Console.WriteLine($"GetStaticMessage: {staticMessage}");
Console.Read();
}
}The stub methods in the TestModel are replaced at compile-time with injected IL instructions that directly access the target members.
Below is an example of what the generated IL looks like after weaving:
.method public hidebysig static int32& Value(class ILAccess.Example.TestModel 'instance') cil managed
{
IL_0000: ldarg.0 // 'instance'
IL_0001: ldflda int32 ILAccess.Example.TestModel::_value
IL_0006: ret
}
.method public hidebysig static int32& StaticValue(class ILAccess.Example.TestModel 'instance') cil managed
{
IL_0000: ldsflda int32 ILAccess.Example.TestModel::_staticValue
IL_0005: ret
}
.method public hidebysig static string GetMessage(class ILAccess.Example.TestModel 'instance', int32 code) cil managed
{
IL_0000: ldarg.0 // 'instance'
IL_0001: ldarg.1 // code
IL_0002: callvirt instance string ILAccess.Example.TestModel::GetMessage(int32)
IL_0007: ret
}
.method public hidebysig static string GetStaticMessage(class ILAccess.Example.TestModel 'instance', int32 code) cil managed
{
IL_0000: ldarg.1 // code
IL_0001: call string ILAccess.Example.TestModel::GetStaticMessage(int32)
IL_0006: ret
}
.method public hidebysig static class ILAccess.Example.TestModel Ctor(int32 x) cil managed
{
IL_0000: ldarg.0 // x
IL_0001: newobj instance void ILAccess.Example.TestModel::.ctor(int32)
IL_0006: ret
}
These injected method bodies effectively make private and static members accessible in a strongly-typed, reflection-free way.
| Feature | Reflection | UnsafeAccessor | ILAccess.Fody |
|---|---|---|---|
| Performance | Slow 🐌 | Fast 🚀 | Fast 🚀 |
| Works before .NET 8 | ✅ | ❌ | ✅ |
| Compile-time validation | ❌ | ❌ | ✅ |
| AOT | Partly supported |
✅ | ✅ |
- Add more test cases.
- Add more compile-time validation and diagnostic messages.
MIT License — see LICENSE for details.