BunnyTail.MemberAccessor

June 7, 2026 · View on GitHub

NuGet

AOT-safe source-generated member accessor for .NET. A reflection-free alternative for property get/set, constructor invocation, and member enumeration.

Support Matrix

FeatureSupportedNotes
classFull support
structBoxed instance required for IAccessor.SetValue; typed CreateSetter<T> returns null (see Struct Support)
record (class)Treated as class
record structTreated as struct
Open generic (Foo<T>)On-demand closed-type instantiation
Closed generic pre-registration[TypedAccessor(typeof(Foo<int>))]
Inherited propertiesFlattened from base classes
Public instance propertiesRead/write; static, non-public and indexers are ignored
Read-only propertiesSetter returns null
Constructor accessorArity 0–4; AOT-safe; generic types supported
Same-arity constructor overloadsResolved by argument type at runtime (see Constructor Accessor)
IAccessorFactory.MembersIReadOnlyList<MemberDescriptor> (public instance properties only)
static membersNot yet supported
Non-public membersPublic only
FieldsProperties only
init-only propertiesReadable; init setters are treated as read-only (CanWrite = false, typed setter returns null)

Reference

Add reference to BunnyTail.MemberAccessor to csproj.

  <ItemGroup>
    <PackageReference Include="BunnyTail.MemberAccessor" Version="1.2.0" />
  </ItemGroup>

MemberAccessor

Source

using BunnyTail.MemberAccessor;

[GenerateAccessor]
public class Data
{
    public int Id { get; set; }

    public string Name { get; set; } = default!;
}
using BunnyTail.MemberAccessor;

var accessorFactory = AccessorRegistry.FindFactory<Data>();
var getter = accessorFactory.CreateGetter<int>(nameof(Data.Id));
var setter = accessorFactory.CreateSetter<int>(nameof(Data.Id));

var data = new Data();
setter(data, 123);
var id = getter(data);

Member Enumeration

var factory = AccessorRegistry.FindFactory<Data>();
foreach (var member in factory.Members)
{
    Console.WriteLine($"{member.Name}: {member.Type} CanRead={member.CanRead} CanWrite={member.CanWrite}");
}

Constructor Accessor

var ctor = AccessorRegistry.FindConstructor<Data>();
var instance = ctor.Create();          // parameterless
var instance2 = ctor.Create<int>(42); // 1-arg constructor

Constructor accessors are available for generic types as well (closed types are pre-registered with [TypedAccessor], others are created on demand):

var ctor = AccessorRegistry.FindConstructor<GenericHolder<int>>();
var instance = ctor.Create<int>(42);

When a type declares multiple constructors with the same arity, the matching constructor is selected at runtime by the argument type. Pass the exact parameter type as the type argument:

// class Sample { Sample(int v); Sample(string v); }
var ctor = AccessorRegistry.FindConstructor<Sample>();
var a = ctor.Create(42);      // -> Sample(int)
var b = ctor.Create("text");  // -> Sample(string)

If no constructor matches the supplied argument type, NotSupportedException is thrown.

Struct Support

[GenerateAccessor]
public struct Point { public int X { get; set; } public int Y { get; set; } }

var accessor = AccessorRegistry.FindAccessor<Point>();
object boxed = new Point { X = 1, Y = 2 };
accessor.SetValue(boxed, "X", 10); // modifies boxed instance

Note: For value types, the typed IAccessorFactory<T>.CreateSetter<TProperty> returns null, because a delegate void(T, TProperty) would receive a copy of the struct and could not mutate the caller's value. Use IAccessor.SetValue with a boxed instance to modify a struct.

Benchmark

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7171/25H2/2025Update/HudsonValley2)
AMD Ryzen 9 5900X 3.70GHz, 1 CPU, 24 logical and 12 physical cores
.NET SDK 10.0.100
  [Host]    : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3
  MediumRun : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3

Job=MediumRun  IterationCount=15  LaunchCount=2  
WarmupCount=10  
MethodMeanErrorStdDevMinMaxP90Code SizeGen0Allocated
DirectGetter0.2243 ns0.0064 ns0.0095 ns0.2138 ns0.2538 ns0.2375 ns10 B--
PropertyGetter20.6895 ns0.5456 ns0.8166 ns19.6389 ns22.6418 ns21.8329 ns3,019 B0.001424 B
PropertyGetterCashed8.9811 ns0.2230 ns0.3338 ns8.5007 ns9.7118 ns9.3515 ns3,278 B0.001424 B
AccessorGetter10.6687 ns0.2781 ns0.4163 ns9.9247 ns11.7124 ns11.1563 ns3,219 B0.001424 B
AccessorGetterCached2.3157 ns0.0976 ns0.1461 ns2.0956 ns2.5933 ns2.4920 ns174 B0.001424 B
ExpressionGetter1.3618 ns0.0267 ns0.0392 ns1.2959 ns1.4362 ns1.4167 ns54 B--
GeneratorGetter0.2304 ns0.0055 ns0.0082 ns0.2172 ns0.2518 ns0.2416 ns76 B--
DirectSetter0.2291 ns0.0066 ns0.0099 ns0.2145 ns0.2458 ns0.2427 ns28 B--
PropertySetter19.3523 ns0.6403 ns0.9584 ns17.8336 ns21.3628 ns20.3991 ns8,536 B0.001424 B
PropertySetterCashed11.1574 ns0.2706 ns0.4051 ns10.5017 ns11.5931 ns11.5931 ns8,736 B0.001424 B
AccessorSetter10.5961 ns0.2128 ns0.3120 ns10.1118 ns11.3181 ns11.0217 ns3,238 B0.001424 B
AccessorSetterCached2.2665 ns0.1085 ns0.1623 ns1.9878 ns2.5154 ns2.4811 ns191 B0.001424 B
ExpressionSetter1.4610 ns0.0427 ns0.0599 ns1.3909 ns1.6234 ns1.5634 ns57 B--
GeneratorSetter0.5057 ns0.0181 ns0.0259 ns0.4630 ns0.5806 ns0.5321 ns85 B--

Type Scenarios

Cached reflection (PropertyInfo) compared with the generated typed accessor (CreateGetter<T> / CreateSetter<T>) across property types (int / string), a value type (struct), a large class, and a generic type. The generated accessor is allocation-free and stays roughly constant regardless of the member type or the declaring-type kind.

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8524/25H2/2025Update/HudsonValley2)
AMD Ryzen 9 5900X 3.70GHz, 1 CPU, 24 logical and 12 physical cores
.NET SDK 10.0.300
  [Host]    : .NET 10.0.8 (10.0.8, 10.0.826.23019), X64 RyuJIT x86-64-v3
  MediumRun : .NET 10.0.8 (10.0.8, 10.0.826.23019), X64 RyuJIT x86-64-v3

Job=MediumRun  IterationCount=15  LaunchCount=2  
WarmupCount=10  
MethodMeanErrorStdDevMinMaxP90Gen0Allocated
IntGetReflection11.2540 ns0.5883 ns0.8805 ns10.1597 ns13.0197 ns12.3929 ns0.001424 B
IntGetGenerator0.2751 ns0.0052 ns0.0074 ns0.2604 ns0.2916 ns0.2840 ns--
IntSetReflection13.5897 ns0.6378 ns0.9147 ns12.6522 ns16.0574 ns15.0835 ns0.001424 B
IntSetGenerator0.2780 ns0.0108 ns0.0158 ns0.2614 ns0.3194 ns0.3024 ns--
StringGetReflection7.5344 ns0.1788 ns0.2620 ns7.0083 ns8.1777 ns7.9011 ns--
StringGetGenerator0.3142 ns0.0236 ns0.0354 ns0.2700 ns0.3841 ns0.3574 ns--
StringSetReflection12.0234 ns0.3071 ns0.4597 ns11.3507 ns13.1734 ns12.6080 ns--
StringSetGenerator0.2908 ns0.0139 ns0.0203 ns0.2281 ns0.3274 ns0.3096 ns--
StructGetReflection10.7348 ns0.3399 ns0.4765 ns9.9749 ns11.7202 ns11.3857 ns0.001424 B
StructGetGenerator0.3123 ns0.0291 ns0.0435 ns0.2205 ns0.3754 ns0.3677 ns--
LargeGetReflection10.3250 ns0.3926 ns0.5631 ns9.7004 ns11.8913 ns11.1991 ns0.001424 B
LargeGetGenerator0.2837 ns0.0225 ns0.0315 ns0.2514 ns0.3526 ns0.3394 ns--
LargeSetReflection13.8748 ns0.8492 ns1.2448 ns12.8041 ns17.1278 ns16.1829 ns0.001424 B
LargeSetGenerator0.2960 ns0.0183 ns0.0274 ns0.2682 ns0.3671 ns0.3377 ns--
GenericGetReflection11.0396 ns0.6136 ns0.8995 ns10.0449 ns13.5371 ns12.5900 ns0.001424 B
GenericGetGenerator0.2951 ns0.0177 ns0.0265 ns0.2712 ns0.3638 ns0.3351 ns--
GenericSetReflection13.9884 ns0.4783 ns0.7010 ns12.9126 ns15.8474 ns14.8361 ns0.001424 B
GenericSetGenerator0.2836 ns0.0035 ns0.0049 ns0.2683 ns0.2925 ns0.2884 ns--