/PrivateProxy

Source Generator and .NET 8 UnsafeAccessor based high-performance strongly-typed private accessor for unit testing and runtime.

Primary LanguageC#MIT LicenseMIT

PrivateProxy

GitHub Actions Releases

Source Generator and .NET 8 UnsafeAccessor based high-performance strongly-typed private accessor for unit testing and runtime.

[GeneratePrivateProxy(typeof(TargetType))] generates accessor proxy.

using PrivateProxy;

public class Sample
{
    int _field1;
    int PrivateAdd(int x, int y) => x + y;
}

[GeneratePrivateProxy(typeof(Sample))]
public partial struct SampleProxy;
// Source Generator generate this type
partial struct SampleProxy(Sample target)
{
    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_field1")]
    static extern ref int ___field1__(Sample target);

    [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "PrivateAdd")]
    static extern int __PrivateAdd__(Sample target, int x, int y);

    public ref int _field1 => ref ___field1__(target);
    public int PrivateAdd(int x, int y) => __PrivateAdd__(target, x, y);
}

public static class SamplePrivateProxyExtensions
{
    public static SampleProxy AsPrivateProxy(this Sample target)
    {
        return new SampleProxy(target);
    }
}
// You can access like this.
var sample = new SampleProxy();
sample.AsPrivateProxy()._field1 = 10;

Generated code is fully typed, you can access private filed via IntelliSense and when private field was changed, can check compiler error.

image

  • No performance penalty, it can be used not only for unit testing but also for runtime
  • No runtime dependency(all codes are included in source generator)
  • Private accessors are strongly-typed
  • Supports both instance, static and fields, properties, methods
  • Supports ref, out, in, and ref readonly method parameters
  • Supports readonly field and property
  • Supports ref return
  • Supports mutable struct

For example, this is the mutable struct and static, ref return sample.

using PrivateProxy;

public struct MutableStructSample
{
    int _counter;
    void Increment() => _counter++;

    // static and ref sample
    static ref int GetInstanceCounter(ref MutableStructSample sample) => ref sample._counter;
}

// use ref partial struct
[GeneratePrivateProxy(typeof(MutableStructSample))]
public ref partial struct MutableStructSampleProxy;
var sample = new MutableStructSample();
var proxy = sample.AsPrivateProxy();
proxy.Increment();
proxy.Increment();
proxy.Increment();

// call private static method.
ref var counter = ref MutableStructSampleProxy.GetInstanceCounter(ref sample);

Console.WriteLine(counter); // 3
counter = 9999;
Console.WriteLine(proxy._counter); // 9999

Installation

This library is distributed via NuGet, minimum requirement is .NET 8 and C# 12.

PM> Install-Package PrivateProxy

Package provides only analyzer and generated code does not dependent any other libraries.

Supported Members

GeneratePrivateProxy target type and member

public class/* struct */ SupportTarget
{
    // field
    private int field;
    private readonly int readOnlyField;
    
    // property
    private int Property { get; set; }
    private int GetOnlyProperty { get; }
    public int GetOnlyPrivateProperty { private get; set; }
    public int SetOnlyPrivateProperty { get; private set; }
    private int SetOnlyProperty { set => field = value; }
    private ref int RefGetOnlyProperty => ref field;
    private ref readonly int RefReadOnlyGetOnlyProperty => ref field;

    // method
    private void VoidMethod() => { }
    private int ReturnMethod() => field;
    private int ParameterMethod(int x, int y) => x + y;
    private void RefOutInMethod(in int x, ref int y, out int z, ref readonly int xyz) { z = field; }
    private ref int RefReturnMethod() => ref field;
    private ref readonly int RefReadOnlyReturnMethod() => ref field;

    // static
    static int staticField;
    static readonly int staticReadOnlyField;
    static int StaticProperty { get; set; }
    static int StaticGetOnlyProperty { get; }
    public static int StaticGetOnlyPrivateProperty { private get; set; }
    public static int StaticSetOnlyPrivateProperty { get; private set; }
    private static int StaticSetOnlyProperty { set => staticField = value; }
    private static ref int StaticRefGetOnlyProperty => ref staticField;
    private static ref readonly int StaticRefReadOnlyGetOnlyProperty => ref staticField;
    private static void StaticVoidMethod() { }
    static int StaticReturnMethod() => staticField;
    static int StaticParameterMethod(int x, int y) => x + y;
    static void StaticRefOutInMethod(in int x, ref int y, out int z, ref readonly int xyz) { z = staticField; }
    static ref int StaticRefReturnMethod() => ref staticField;
    static ref readonly int StaticRefReadOnlyReturnMethod() => ref staticField;
    static ref int StaticRefReturnMethodParameter() => ref staticField;
}

Proxy type can be class => class or struct, struct => ref struct.

using PrivateProxy;

public class Sample;

// class proxy type both supports class and struct(recommend is struct)
[GeneratePrivateProxy(typeof(Sample))]
public partial class SampleProxy1;

[GeneratePrivateProxy(typeof(Sample))]
public partial struct SampleProxy2;

public struct SamplleStruct;

// struct only supports ref struct(when use standard struct, analyzer shows error)
[GeneratePrivateProxy(typeof(SamplleStruct))]
public ref partial struct SamplleStructProxy;

GeneratePrivateProxyAttribute has two constructor, when use PrivateProxyGenerateKinds parameter, can configure generate member kind.

public GeneratePrivateProxyAttribute(Type target) // use PrivateProxyGenerateKinds.All
public GeneratePrivateProxyAttribute(Type target, PrivateProxyGenerateKinds generateKinds)

[Flags]
internal enum PrivateProxyGenerateKinds
{
    All = 0, // Field | Method | Property | Instance | Static
    Field = 1,
    Method = 2,
    Property = 4,
    Instance = 8,
    Static = 16,
}

Limitation

Currently, the following features are not supported

  • Generics type
  • ref struct
    • ref field can not pass to ref method parameter
  • Types from external dll(for example String)
    • Probably, by enabling MetadataImportOptions.All in the Source Generator, it should be possible to read it. However, I haven't been able to find a way to do that. I need help.
  • Non public return/parameter type(ignore generate)
    • UnsafeAccessor requires public type

License

This library is licensed under the MIT License.