For intermediate releases, see the GitHub Repository[^]
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.
To understand how the API works, the following is a summation function written using Flexpressions:
Func<int, int> sumFunc = Flexpression<Func<int, int>>
.If<int>(input => input == null)
.Throw(() => new ArgumentNullException("input"))
.If<int>(input => input.Length == 0)
.Throw(() => new ArgumentException("The array must contain elements.", "input"))
.Set<int>("sum", () => 0)
.Foreach<int, int, int>("x", input => input)
.Set<int, int, int>("sum", (sum, x) => sum + x)
.Return<int, int>(sum => sum)
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.
The Flexpression API currently provides the following functionality:
- Can construct any
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
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
- 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
- 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>).
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).
- You may still call the methods with the
EditorBrowsableAttribute, though this approach is not advisable, as it may produce unintented behaviors.
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
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
ParameterExpressions (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
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 class is the starting point of producing an expression tree.
Parameters - The collection of input parameters based on the signature
Compile() - Creates the expression tree, compiles it, and returns the
delegate of type
Create() - Creates a
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
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.
- Inserts an
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
GetLabelTargets() - Returns the current collection of labels.
GetVariablesInScope() - Iterates over all of the variables defined in this
Block, any parent
Blocks, and eventually any parameters within the
Goto() - Jumps to the provided label supplied as either a name or
If() - Returns an
If block with the provided condition.
InsertLabel() - Inserts a new label into the current position of the
Return() - Inserts a return statement into the
Block (with or without a value).
- 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
Throw() - Inserts a throw into the
Block with the provided exception.
Try() - Returns the
Block of a
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 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 class encapsulates a switch construct with a switch value of type
Case() - Returns a
SwitchCase with case value of type
Default() - Returns a default
SwitchCase with no case value.
EndSwitch() - Ends the current switch statement.
SwitchCase class encapsulates a switch case with a case value of type
Begin() - Returns the
Block of the
Case() - Adds another case value to the current
Default() - Adds a default case to the current
EndCase() - Ends the current
Try class encapsulates a try statement from C#.
Catch() - Returns the
Block of the catch statement with the optional variable name and
EndTry() - Ends the current
Finally() - Returns the
Block of the finally statement.
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.
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.
September 8th, 2012 - 184.108.40.206 - Initial Release