The purpose of this library is to compile the most common and simplest operations in expression trees during the build to avoid their slow compilation at runtime or invocation overhead after interpretation.
Add NuGet package to your .NET Standard 2.0 - compatible project
PM> Install-Package ExpressionDelegates.Generation
If your project doesn't have linq expressions to generate delegates from and you just want to use the generated delegates from other assemblies, please install ExpressionDelegates.Base
NuGet package to avoid any code generation attempts.
PM> Install-Package ExpressionDelegates.Base
API is very simple:
ExpressionDelegates.Accessors.Find(string signature).Getter(object targetObject)
ExpressionDelegates.Accessors.Find(MemberInfo member).Setter(object targetObject, object value)
ExpressionDelegates.Methods.Find(string signature).Invoke(object targetObject, params object[] args)
ExpressionDelegates.Methods.Find(MethodInfo method).Invoke(object targetObject, params object[] args)
ExpressionDelegates.Constructors.Find(string signature ).Invoke(params object[] args)
ExpressionDelegates.Constructors.Find(Constructors constructor).Invoke(params object[] args)
Expression<Func<string, int>> expression = s => s.Length;
MemberInfo accessorInfo = ((MemberExpression)expression.Body).Member;
Accessor lengthAccessor = ExpressionDelegates.Accessors.Find(accessorInfo);
// or
lengthAccessor = ExpressionDelegates.Accessors.Find("System.String.Length");
var value = lengthAccessor.Get("17 letters string");
// value == 17
If property/field is read-only, Accessor.Set
will be null
.
If property is write-only, Accessor.Get
will be null
.
Expression<Func<string, char, bool>> expression = (s, c) => s.Contains(c);
MethodInfo methodInfo = ((MethodCallExpression)expression.Body).Method;
Method containsMethod = ExpressionDelegates.Methods.Find(methodInfo);
// or
containsMethod = ExpressionDelegates.Methods.Find("System.String.Contains(System.Char)");
var value = containsMethod.Invoke("Hello", 'e');
// value == true
Expression<Func<char, int, string>> expression = (c, i) => new string(c, i);
ConstructorInfo ctorInfo = ((NewExpression)expression.Body).Constructor;
Constructor stringCtor = ExpressionDelegates.Constructors.Find(ctorInfo);
// or
stringCtor = ExpressionDelegates.Constructors.Find("System.String.String(System.Char, System.Int32)");
var value = stringCtor.Invoke('c', 5);
// value == "ccccc"
ExpressionDelegates.Generation
package has source code generators which run through the code of a target project right before the compilation, search for System.Linq.Expressions.Expression<T>
lambdas, extract expressions for properties, fields, method and constructor invocations and generate additional .cs
files with registration of their delegates.
Generated files are located in \obj\{configuration}\{platform}\g\
and look like this:
using static ExpressionDelegates.Accessors;
namespace ExpressionDelegates.AccessorRegistration
{
public static class ModuleInitializer
{
public static void Initialize()
{
Add("NameSpace.TestClass.Field", o => ((NameSpace.TestClass)o).Field, (t, m) => ((NameSpace.TestClass)t).Field = (System.Int32)m);
Add("NameSpace.TestClass.InternalProperty", o => ((NameSpace.TestClass)o).InternalProperty, (t, m) => ((NameSpace.TestClass)t).InternalProperty = (System.Int32)m);
Add("NameSpace.TestClass.NestedGenericProperty", o => ((NameSpace.TestClass)o).NestedGenericProperty, (t, m) => ((NameSpace.TestClass)t).NestedGenericProperty = (System.Collections.Generic.IDictionary<System.Int32, System.Collections.Generic.ICollection<System.String>>)m);
Add("NameSpace.TestClass.NestingClass.NestedProperty", o => ((NameSpace.TestClass.NestingClass)o).NestedProperty, (t, m) => ((NameSpace.TestClass.NestingClass)t).NestedProperty = (System.String)m);
Add("NameSpace.TestClass.NestingProperty", o => ((NameSpace.TestClass)o).NestingProperty, (t, m) => ((NameSpace.TestClass)t).NestingProperty = (NameSpace.TestClass.NestingClass)m);
Add("NameSpace.TestClass.Property", o => ((NameSpace.TestClass)o).Property, (t, m) => ((NameSpace.TestClass)t).Property = (System.Int32)m);
Add("NameSpace.TestClass.StaticProperty", o => NameSpace.TestClass.StaticProperty, (t, m) => NameSpace.TestClass.StaticProperty = (System.Int32)m);
Add("System.Collections.Generic.IDictionary<System.Int32, System.Collections.Generic.ICollection<System.String>>.Values", o => ((System.Collections.Generic.IDictionary<System.Int32, System.Collections.Generic.ICollection<System.String>>)o).Values, null);
Add("System.String.Length", o => ((System.String)o).Length, null);
}
}
}
Code generation is implemented using Uno.SourceGeneration which provides the broadest framework support while maintaining C# Source Generators compatibility which target C# 9 and .NET 5. The current code generation solution is likely to be replaced with С# Source Generators in the future as .NET 5 becomes more widespread.
After the code has been compiled, ModuleInit.Fody injects ModuleInitializer.Initialize()
calls into the assembly module initializer (assembly constructor) so that generated delegates plug in ExpressionDelegates
classes once the target assembly is loaded. This solution will be replaced with C# 9 Module Initializers when C# 9 becomes more popular.
Parameters for the delegates, their count and type checking are up to you. InvalidCastException
is thrown in case of using wrong parameters for a delegate signature. IndexOutOfRangeException
is thrown for an unexpected parameter count.
- No delegates for
private
orprotected
classes and members, only forinternal
andpublic
- No delegates for methods or constructors with
ref
parameters - No delegates for anonymous types (not sure if needed)
- Static members are supported
- Generics are supported
- Nested classes are supported
- Dynamic properties/fields are supported
Results
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.572 (2004/?/20H1)
AMD Ryzen 5 1600, 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.100
[Host] : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT
DefaultJob : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT
Type | Method | Mean | Error | StdDev |
---|---|---|---|---|
ConstructorPerformance | 'Cached CompileFast Invoke' | 4.4837 ns | 0.1249 ns | 0.1908 ns |
ConstructorPerformance | 'Direct Delegate Invoke' | 4.6937 ns | 0.0443 ns | 0.0370 ns |
ConstructorPerformance | 'Cached Compile Invoke' | 5.1806 ns | 0.3148 ns | 0.9281 ns |
ConstructorPerformance | 'Cached ExpressionDelegates.Constructor Invoke' | 5.8940 ns | 0.0459 ns | 0.0430 ns |
ConstructorPerformance | ConstructorInfo.Invoke | 121.3839 ns | 0.4510 ns | 0.4219 ns |
ConstructorPerformance | 'ExpressionDelegates.Constructor Find and Invoke' | 191.1785 ns | 2.0766 ns | 1.7340 ns |
ConstructorPerformance | 'Cached Interpretation Invoke' | 199.8162 ns | 1.7841 ns | 1.6689 ns |
ConstructorPerformance | 'Interpret and Invoke' | 2,211.3971 ns | 13.7784 ns | 12.2142 ns |
ConstructorPerformance | 'CompileFast and Invoke' | 79,809.5180 ns | 473.7356 ns | 419.9543 ns |
ConstructorPerformance | 'Compile and Invoke' | 88,701.7674 ns | 962.4325 ns | 853.1713 ns |
GetterPerformance | 'Cached Compile Invoke' | 1.4551 ns | 0.0085 ns | 0.0080 ns |
GetterPerformance | 'Direct Delegate Invoke' | 1.7740 ns | 0.0291 ns | 0.0273 ns |
GetterPerformance | 'Cached CompileFast Invoke' | 2.0495 ns | 0.0089 ns | 0.0084 ns |
GetterPerformance | 'Cached CreateDelegate Invoke' | 2.3477 ns | 0.0526 ns | 0.0492 ns |
GetterPerformance | 'Cached ExpressionDelegates.Accessor Invoke' | 5.8792 ns | 0.1525 ns | 0.3009 ns |
GetterPerformance | 'Cached Interpretation Invoke' | 100.4755 ns | 1.8990 ns | 3.3754 ns |
GetterPerformance | PropertyInfo.GetValue | 155.8979 ns | 1.7932 ns | 1.6774 ns |
GetterPerformance | 'ExpressionDelegates.Accessor Find and Invoke' | 163.2990 ns | 1.4388 ns | 1.3458 ns |
GetterPerformance | 'CreateDelegate and Invoke' | 560.0708 ns | 5.8383 ns | 5.1755 ns |
GetterPerformance | 'Interpret and Invoke' | 2,715.0402 ns | 19.6335 ns | 18.3652 ns |
GetterPerformance | 'CompileFast and Invoke' | 74,540.2230 ns | 213.9485 ns | 200.1275 ns |
GetterPerformance | 'Compile and Invoke' | 88,103.7519 ns | 235.3721 ns | 208.6513 ns |
MethodPerformance | 'Cached Compile Invoke' | 0.5895 ns | 0.0132 ns | 0.0123 ns |
MethodPerformance | 'Cached CompileFast Invoke' | 1.1551 ns | 0.0309 ns | 0.0258 ns |
MethodPerformance | 'Direct Delegate Invoke' | 1.1767 ns | 0.0289 ns | 0.0270 ns |
MethodPerformance | 'Cached CreateDelegate Invoke' | 2.0462 ns | 0.0115 ns | 0.0102 ns |
MethodPerformance | 'Cached ExpressionDelegates.Method Invoke' | 4.1000 ns | 0.0185 ns | 0.0173 ns |
MethodPerformance | 'Cached Interpretation Invoke' | 90.7167 ns | 0.8028 ns | 0.6704 ns |
MethodPerformance | MethodInfo.Invoke | 101.6796 ns | 0.5752 ns | 0.5381 ns |
MethodPerformance | 'ExpressionDelegates.Method Find and Invoke' | 186.4856 ns | 2.5224 ns | 2.3595 ns |
MethodPerformance | 'CreateDelegate and Invoke' | 530.2247 ns | 3.9376 ns | 3.6832 ns |
MethodPerformance | 'Interpret and Invoke' | 2,599.1128 ns | 30.2115 ns | 28.2599 ns |
MethodPerformance | 'CompileFast and Invoke' | 58,216.1233 ns | 168.4641 ns | 149.3391 ns |
MethodPerformance | 'Compile and Invoke' | 83,292.3139 ns | 922.4315 ns | 817.7115 ns |
If you've found an error, please file an issue.
Patches are encouraged, and may be submitted by forking this project and submitting a pull request. If your change is substantial, please raise an issue first to discuss it.
This project is licensed under the MIT License. See the LICENSE file for details.