For intermediate releases, see the GitHub Repository[^]
Introduction
With the introduction of .NET 3.5, developers were given a powerful tool for creating code at runtime, namely System.Linq.Expressions. It combined the efficiency of compiled code and the flexibility of the reflection namespace, amongst other things.
Unfortunately, expressions have considerable constraints on their usage when expressed as lambda expressions. The following is an excerpt from the C# Language Specification 5.0 (i.e. .NET 4.5):
Certain lambda expressions cannot be converted to expression tree types: Even though the conversion exists, it fails at compile-time. This is the case if the lambda expression: - *Has a block body
- *Contains simple or compound assignment operators
- Contains a dynamically bound expression
- Is async
|
Flexpressions (Fluent-expressions) is my solution to the first two bullets (*). In addition, I added a few high level abstractions that simplify the construction of expressions. Furthermore, I have included some utility classes that might be useful for those working with expressions.
Example #1
To understand how the API works, the following is a summation function written using Flexpressions:
Func<int[], int> sumFunc = Flexpression<Func<int[], int>>
.Create(false, "input")
.If<int[]>(input => input == null)
.Throw(() => new ArgumentNullException("input"))
.EndIf()
.If<int[]>(input => input.Length == 0)
.Throw(() => new ArgumentException("The array must contain elements.", "input"))
.EndIf()
.Declare<int>("sum")
.Set<int>("sum", () => 0)
.Foreach<int, int[], int[]>("x", input => input)
.Set<int, int, int>("sum", (sum, x) => sum + x)
.End()
.Return<int, int>(sum => sum)
.CreateLambda()
.Compile();
var result = sumFunc(Enumerable.Range(0, 10).ToArray()); var result2 = Enumerable.Range(0, 10).Sum();
To produce the above code using expressions, one would need to write a fairly complicated expression spanning several pages and interleaved with reflection code, as shown here. Just from a code maintenance perspective, the benefits of Flexpressions are obvious.
Example #2
While the Flexpression classes are fluent, they can be used in a non-fluent way. Thus, the API allows for a variety of solutions that can be both expressive and dynamic. The following is a function that stringifies the provided type's properties and fields:
class Program
{
static void Main(string[] args)
{
var action = Expressions.Stringifier<MyClass>(true, false);
var result = action(new MyClass() { A = 123, B = 23.21, C = 31241, D = "abcdf" });
}
}
public class MyClass
{
public int A { get; set; }
public double B { get; set; }
public long C { get; set; }
public string D { get; set; }
}
public static class Expressions
{
public static Func<T, string> Stringifier<T>(bool writeProperties, bool writeFields)
{
var block = Flexpression<Func<T, string>>.Create(false, "obj");
if (typeof(T).IsClass)
block.If<T>((obj) => obj == null)
.Throw(() => new ArgumentNullException("obj"))
.EndIf();
return block
.Set<StringBuilder>("sb", () => new StringBuilder())
.WriteMembers<Flexpression<Func<T, string>>, T>(writeProperties, writeFields)
.Return<StringBuilder, string>(sb => sb.ToString())
.Compile();
}
private static Block<T> WriteMembers<T, O>(this Block<T> block, bool writeProperties, bool writeFields) where T : IFlexpression
{
MemberExpression memberExpression;
ParameterExpression paramSb = block.GetVariablesInScope().First(x => x.Name == "sb");
ParameterExpression paramObj = block.GetVariablesInScope().First(x => x.Name == "obj");
foreach (MemberInfo mi in typeof(O).GetMembers())
{
if (((mi.MemberType == MemberTypes.Field) && writeFields) || ((mi.MemberType == MemberTypes.Property) && writeProperties))
{
string prefix = string.Format("{0}: ", mi.Name);
block.Act
(
Expression.Call
(
paramSb,
typeof(StringBuilder).GetMethod("Append", new[] { typeof(string) }),
Expression.Constant(prefix, typeof(string))
)
);
memberExpression = Expression.MakeMemberAccess(paramObj, mi);
block.Act
(
Expression.Call
(
paramSb,
typeof(StringBuilder).GetMethod("AppendLine", new[] { typeof(string) }),
Expression.Call(memberExpression, memberExpression.Type.GetMethod("ToString", Type.EmptyTypes))
)
);
}
}
return block;
}
}
Example #3
In case you'd like to use the API, but don't care for the potential performance overhead of using it - T4 (text templates) and ExpressionExtensions.ToCSharpString()
can help.
Modifying the Example #2 Stringifier
method to produce a LambdaExpression
, rather than immediately compiling it, you could create a T4 file like the following:
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".cs" #>
<#@ Assembly Name="System.Core" #>
<#@ Assembly Name="$(ProjectDir)$(OutDir)$(TargetFileName)" #>
<#@ Import Namespace="Test" #>
using System;
namespace Test
{
public class Utility
{
public static Func<MyClass, string> GetMyClassStringifier()
{
<#= Expressions.Stringifier<MyClass>(true, true).ToCSharpString() #>
return expression.Compile();
}
}
}
ExpressionExtensions.ToCSharpString()
will produce the expression in a variable expression
, which needs only to be wrapped in a method and compiled to produce the desired delegate. Using the API this way combines the maintainability of Flexpressions with the performance of a hard-coded expression tree.
Granted, if you were going to go about using Flexpressions this way, you should probably just invest the time into generating code from T4 templates. Though, expressions do not abide by the same rules as compiled code would (e.g. accessing private members), so there may be limited uses for this. Ultimately, the goal with this sample was to highlight the power of ExpressionExtensions.ToCSharpString()
.
Features
The Flexpression API currently provides the following functionality:
- Can construct any
Func
/Action
delegate type. - Can restrict or allow the use of an outer (or captured) variable.
- Can produce either an expression tree or the typed delegate directly.
- Can be supplied the parameters names for inputs to the delegate, if not they will be auto-generated.
- Language constructs:
- General operations (e.g. method calls) (see the
Act
method on Block
) - The
Act
method also provides the ability to supply Expression
objects directly to circumvent the API if it gets in the way.
- Do/While/Foreach loops
- Inserting labels
- Goto statements
- If/ElseIf/Else blocks
- Assignments (see the
Set
method on Block
) - Auto-declaring variables if not already declared.
- Switch statements
- Case/Default blocks
- Case and Default statements can be chained together (this is not switch statement fall through)
- Throw statements
- Try Blocks
- Try/Finally
- Try/Catch
- Try/Catch/Finally
- Catch
- Catch<T>(Exception)
- Catch<T>(Exception e)
- Using Blocks
- 100% code coverage with unit tests
The utility classes packaged within the Flexpression API provides the following functionality:
- Reverse engineering an expression tree back to C# code
- Useful for learning about expression trees, debugging, and generating code with T4 templates.
- Expression rewriter to replace an expression's parameters with the provided list.
- Extracting a type's true name rather than the alias'd version (ex. List`1 => List<int>).
API Overview
Expressions are immutable. Rewriting an expression tree is possible, but doing so is really just constructing a new tree based on the old. Ultimately, the best route to construct an expression tree is to use external scaffolding. This design constrain ultimately lends itself to a fluent interface.
In designing the fluent interface, I employed the use of the EditorBrowsableAttribute
to reduce the intellisense clutter. The attribute streamlines the API to help reduce the mental friction when using Flexpressions (see here for more information about it).
Caveats
- You may still call the methods with the
EditorBrowsableAttribute
, though this approach is not advisable, as it may produce unintented behaviors. - The
EditorBrowsableAttribute
is only effective when the Flexpression project is not contained within the current solution.
Every class within the Flexpression API is generic. Aside from the Flexpression
class (which is the topmost object), the reason for the generic type is to enforce the correct functionality once you end operations on the current object.
For example, once you end an If<Block<Flexpression<Action>>>
, it will return its parent of type Block<Flexpression<Action>>
, which then provides access to the Block
's operations.
At every new level, the prior parent is stored off in the child type's generic arguments. It may make for some very long class names and generate a lot of types, but it definitely brings simplicity to the API and implicitly enforces a proper syntax.
Under The Hood
Flexpressions keeps track of all the ParameterExpression
s (i.e. parameters and variables) used within the query. Then when an expression is provided, the code remaps each parameter to an existing ParameterExpression
based on the names. By acquiring each piece in parts, Flexpressions is able to circumvent the restrictions enforced by .NET.
As mentioned in the API Overview section, expressions are immutable. While most of the operations contained within Flexpressions immediately generate an expression object, some are delayed as all of the components are not yet defined. For instance, the If
class delays construction of the ConditinalExpression
, until the body of the true (and possibly false) case has been filled out. It is not until the Flexpression object is converted to an expression tree that If
constructs a ConditionalExpression
.
Class Overview
The following is an overview of the public classes exposed out through the API.
Note: Many of the methods listed here have overloads, but for simplicity, I am only providing a general overview to not muddy the waters too much.
Flexpression
The Flexpression
class is the starting point of producing an expression tree.
Parameters
- The collection of input parameters based on the signature S
.Compile()
- Creates the expression tree, compiles it, and returns the delegate
of type S
.Create()
- Creates a Flexpression
instance.CreateLambda()
- Creates the expression tree and returns it.GetLabelTargets()
- Returns the current collection of labels.GetVariablesInScope()
- At this level, it is equivalent to just iterating over the Parameters
property.
Block
The Block
class is the main workhorse of the Flexpression API. It is responsible for a majority of the content you will produce with Flexpressions.
Variables
- The collection of variables defined at this Block
, available to this Block,
and all of its children.Act()
- Inserts an Expression
or Expression<T>
into the Block
. The method can be used to circumvent the API if any limitations are discovered.Break()
- Performs a break operation, which is only valid if within a loop construct.Continue()
- Performs a continue operation, which is only valid if within a loop construct.Declare<V>()
- Declares a new variable of type V
with the specified name.Do()
- Returns the Block
of a do loop with the provided conditional checked after the first loop iteration.End()
- Ends the current block and returns to the parent.Foreach<V, R>()
- Returns the Block
of a foreach loop with the provided variable of type V
and collection of type R
.GetLabelTargets()
- Returns the current collection of labels.GetVariablesInScope()
- Iterates over all of the variables defined in this Block
, any parent Block
s, and eventually any parameters within the Flexpression
instance.Goto()
- Jumps to the provided label supplied as either a name or LabelTarget
instance.If()
- Returns an If
block with the provided condition.InsertLabel()
- Inserts a new label into the current position of the Block
.Return()
- Inserts a return statement into the Block
(with or without a value).Set<R>()
- Sets the named variable of type R
with the provided value. If the variable has not been declared, it will be declared prior to the value being set.Switch<R>()
- Returns a Switch
with a switch value of type R
.Throw()
- Inserts a throw into the Block
with the provided exception.Try()
- Returns the Block
of a Try
statement.Using<R>()
- Returns the Block
of a using statement.While()
- Returns the Block
of a while loop with the provided conditional checked prior to the first loop ieration.
If
The If
class encapsulates an if statement from C#.
Else()
- Returns the Block
of the false branch of the if statement.ElseIf()
- Returns the Block
of the true branch of the provided conditional.EndIf()
- Ends the current if statement.
Switch
The Switch
class encapsulates a switch construct with a switch value of type R
.
Case()
- Returns a SwitchCase
with case value of type R
.Default()
- Returns a default SwitchCase
with no case value.EndSwitch()
- Ends the current switch statement.
SwitchCase
The SwitchCase
class encapsulates a switch case with a case value of type R
.
Begin()
- Returns the Block
of the SwitchCase
.Case()
- Adds another case value to the current SwitchCase
.Default()
- Adds a default case to the current SwitchCase
.EndCase()
- Ends the current SwitchCase
.
Try
The Try
class encapsulates a try statement from C#.
Catch()
- Returns the Block
of the catch statement with the optional variable name and Exception
type.EndTry()
- Ends the current Try
.Finally()
- Returns the Block
of the finally statement.
Performance
To put things into perspective, I created a benchmark unit test to compare creating a LambdaExpression
with Flexpressions and with a hardcoded Expression
tree. The performance of the Flexpression API wavered between 3.5 to 4 times slower than the hardcoded Expression tree. Granted, this was a difference of 1.5 seconds for 10,000 iterations (~150 µs slower per operation). When compared to the ease of development and the fact that the results will likely be cached in some way anyways, I believe this is completely acceptable.
Memory is the other critical likely to be impacted due to the number of generic types generated, but again this is likely negligible due to the fact that a typical method is not likely to produce more than 10 types and each type is potentially resuable across other Flexpression operations.
Future Development
Features not currently planned for implementation:
- For loop - The for loop would require three different parameters, of which two have 16 different variations. In the end, it would produce a total of 256 different overloads, which is a little cumbersome. I could break this up, but it defeats the simplicity of the statement. So for now, unless someone has a great idea to resolve this, I'm not planning on implementing it.
My request to those who use the framework is - please let me know if you have any suggestions to improve the API. I'd love to see this framework become more useful and flexible, so if you have a suggestion please pass it along. Thanks.
History
-
September 9th, 2012 - 1.0.1.0
- Removed
EditorBrowsableAttribute
from interfaces to ease extension of the API. - Removed "Debug - No Moles" build configuration from project files (was unused).
- Added two new examples to the article.
- September 8th, 2012 - 1.0.0.0 - Initial Release