.NET proxy generator powered by Roslyn
$ dotnet add package ProxyGen.NET.NET proxy generator powered by Roslyn
This documentation refers the version 10.X of the library
ATTENTION: This version represents a complete revamp of the library. It introduces a brand new (more generic) API which is NOT compatible with the previous versions at all. If you are interested in the legacy -v9- API you can find its documentation here.
ProxyGen.NET is a lightweight, powerful proxy generator for .NET, built on top of Roslyn. It allows you to easily create dynamic proxies that can intercept interfaces, virtual methods and delegates as well as it has support for duck typing � meaning you can proxy objects without requiring them to implement a specific interface. Whether you're building AOP frameworks, logging systems, mock objects, or sophisticated middleware, ProxyGen.NET gives you full control over method interception with a clean and minimal API.
IInterceptor interface:using Solti.Utils.Proxy;
...
public class MyInterceptor: IInterceptor
{
public object Invoke(IInvocationContext context) // Invoking the generated proxy instance will trigger this method
{
if (suppressOriginalMethod)
{
return something;
}
context.Args[0] = someNewVal; // "someNewVal" will be forwarded to the target method
object? result = context.Dispatch(); // call the target
context.Args[1] = outputVal; // modify the "ref" or "out" parameters set by the target method
return result;
}
}
The IInvocationContext provides the following essential properties:
Proxy: The generated proxy instanceMember: The member (property, event or method) being invoked by the callerGenericArguments: Since the Member property always points to the generic definition, the user code should use this value to grab the generic arguments.Args: Arguments passed by the caller. The user code is allowed to modify this array in order to change the input and output (ref or out) parametersusing Solti.Utils.Proxy.Generators;
...
IMyInterface target = new MyClass();
...
IMyInterface proxy;
// create a proxy without target
proxy = InterfaceProxyGenerator<IMyInterface>.Activate(new MyInterceptor()); // or ActivateAsync()
// create a proxy with target
proxy = InterfaceProxyGenerator<IMyInterface>.Activate(new MyInterceptor(), target); // or ActivateAsync()
For further usage examples see this.
Lets suppose we have a virtual/abstract class like this:
public class MyClass(int ctorParameter1, string ctorParameter2)
{
public virtual void DoSomeStuff<T>(T param) {...} // it could be abstract as well
}
using Solti.Utils.Proxy.Generators;
// you can pick the desired constructor by providing the corresponding parameters.
// pass null when you want to call the parameterless constructor
MyClass proxy = ClassProxyGenerator<MyClass>.Activate(Tuple.Create(1986, "abc")); // or ActivateAsync()
For further usage examples see this.
using Solti.Utils.Proxy.Generators;
Func<int, string> proxy = DelegateProxyGenerator<Func<int, string>>.Activate(i => string.Emtpy); // or ActivateAsync()
For further usage examples see this.
public class TargetClass // does not implement IDuck
{
public void Foo() {...}
}
...
public interface IDuck
{
void Foo();
}
using Solti.Utils.Proxy.Generators;
...
TargetClass target = ...;
IDuck duck = DuckGenerator<IDuck, TargetClass>.Activate(target); // or ActivateAsync()
Related tests can be found here.
By setting the ProxyGen.AssemblyCacheDir property in YourApp.runtimeconfig.json you can make the system cache the generated assembly, so next time your app starts and requests the proxy there won't be time consuming emitting operation.
You can do it easily by creating a template file named runtimeconfig.template.json in your project folder:
{
"configProperties": {
"ProxyGen.AssemblyCacheDir": "GeneratedAssemblies"
}
}
This library can be used as a source generator so you can embed the generated proxy type into the assembly that uses it. This is simply done by the Solti.Utils.Proxy.Attributes.EmbedGeneratedTypeAttribute:
[assembly: EmbedGeneratedType(typeof(InterfaceProxyGenerator<IMyInterface>))]
[assembly: EmbedGeneratedType(typeof(DuckGenerator<IMyInterface, MyClass>))]
The xXxGenerator.GetGeneratedType() method returns the embedded type if it is present in the assembly in which the GetGeneratedType() was called. Since all the time consuming operations already happened in compile time, requesting embedded types can significantly improve the performance.
Note that:
-filter:-[*]Proxies.GeneratedClass_* switch)YourProject\Solti.Utils.Proxy\Solti.Utils.Proxy.Internals.ProxyEmbedder\Proxies.GeneratedClass_XxX.cs)<ItemGroup>
<Compile Remove="Solti.Utils.Proxy\**" />
<EmbeddedResource Remove="Solti.Utils.Proxy\**" />
<None Remove="Solti.Utils.Proxy\**" />
</ItemGroup>
ProxyGen is able to dump the generated sources. Due to performance considerations it is disabled by default. To enable
In runtime:
Set the ProxyGen.LogDirectory property (in the same way you could see above) to the desired directory (note that environment variables are supported):
{
"configProperties": {
"ProxyGen.LogDirectory": "%TEMP%"
}
}
In compile time (source generator):
Extend your .csproj with the following:
<PropertyGroup>
<ProxyGen_LogDirectory>$(OutputPath)Logs</ProxyGen_LogDirectory>
</PropertyGroup>
The output should look like this.
[Proxy|Duck]Generator.CacheDirectory is set somewhere)InterfaceInterceptor.Invoke() returns the result of the original method (instead of CALL_TARGET) so in the override you may never need to invoke the method parameter directly.[Proxy|Duck]Generator.GeneratedType[Async] property has been removed. To get the generated proxy type call the [Proxy|Duck]Generator.GetGeneratedType[Async]() method.[Proxy|Duck]Generator.CacheDirectory property has been removed. To set the cache directory tweak the runtimeconfig.json file.InterfaceInterceptor<>.Invoke() has been changed. Invocation parameters can be grabbed from the InvocationContext passed to the Invoke() method.ConcurrentInterfaceInterceptor<> class has been dropped since the InterfaceInterceptor<> class was rewritten in a thread safe manner.Generator.Activate() method.InvocationContext.InvokeTarget property has been removed but you should not be affected by itInterfaceInterceptor<TInterface>.Member|Method has been renamed to InterfaceMember|InterfaceMethodGenerators have been demoted to class. To compare Generator instances use their Id property.This project currently targets netstandard2.0 as well as netstandard2.1 and had been tested against net472, netcoreapp3.1, net5.0, net6.0, net7.0, net8.0 and net9.0.