Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C# 4.0

Flexpressions

Rate me:
Please Sign up or sign in to vote.
5.00/5 (19 votes)
10 Oct 2012CPOL10 min read 39.7K   507   47   18
An intuitive-fluent API for generating Linq Expressions.

Flexpressions - By Andrew Rissing

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:

C#
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()); // 45
var result2 = Enumerable.Range(0, 10).Sum(); // 45

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:

C#
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");

        // The Flexpression objects don't need to be called fluently.
        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();
    }

    // Using extension methods, the fluent nature of the API can be extended seamlessly.
    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);

                // sb.Append(prefix);
                block.Act
                (
                    Expression.Call
                    (
                        paramSb,
                        typeof(StringBuilder).GetMethod("Append", new[] { typeof(string) }),
                        Expression.Constant(prefix, typeof(string))
                    )
                );

                memberExpression = Expression.MakeMemberAccess(paramObj, mi);

                // No error checking here for nulls, but you get the idea...
                // sb.AppendLine(obj.Member.ToString());
                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:

C#
<#@ 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
      • Break
      • Continue
    • 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 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 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

Image 2

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

Image 3

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.
  • <a id="ActMethod" class="anchor" title="ActMethod" name="ActMethod"><code>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 Blocks, 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).
  • <a id="SetMethod" class="anchor" title="SetMethod" name="SetMethod"><code>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

Image 4

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

Image 5

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

Image 6

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

Image 7

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
United States United States
Since I've begun my profession as a software developer, I've learned one important fact - change is inevitable. Requirements change, code changes, and life changes.

So..If you're not moving forward, you're moving backwards.

Comments and Discussions

 
GeneralMe Like Pin
Brisingr Aerowing28-Jan-15 13:20
professionalBrisingr Aerowing28-Jan-15 13:20 
GeneralRe: Me Like Pin
Andrew Rissing29-Jan-15 4:39
Andrew Rissing29-Jan-15 4:39 
GeneralMy vote of 5 Pin
Paulo Zemek10-Jun-13 11:25
mvaPaulo Zemek10-Jun-13 11:25 
GeneralRe: My vote of 5 Pin
Andrew Rissing10-Jun-13 11:46
Andrew Rissing10-Jun-13 11:46 
QuestionWhat problem does that solve? Pin
Andreas Gieriet9-Sep-12 9:46
professionalAndreas Gieriet9-Sep-12 9:46 
AnswerRe: What problem does that solve? Pin
Andrew Rissing9-Sep-12 10:52
Andrew Rissing9-Sep-12 10:52 
GeneralRe: What problem does that solve? Pin
Andreas Gieriet9-Sep-12 14:15
professionalAndreas Gieriet9-Sep-12 14:15 
GeneralRe: What problem does that solve? Pin
Andrew Rissing9-Sep-12 17:15
Andrew Rissing9-Sep-12 17:15 
GeneralRe: What problem does that solve? Pin
Andreas Gieriet10-Sep-12 4:01
professionalAndreas Gieriet10-Sep-12 4:01 
GeneralRe: What problem does that solve? Pin
Andrew Rissing10-Sep-12 5:15
Andrew Rissing10-Sep-12 5:15 
GeneralRe: What problem does that solve? Pin
Andre_Prellwitz10-Sep-12 10:28
Andre_Prellwitz10-Sep-12 10:28 
GeneralRe: What problem does that solve? Pin
Andrew Rissing10-Sep-12 11:25
Andrew Rissing10-Sep-12 11:25 
GeneralRe: What problem does that solve? Pin
Andre_Prellwitz10-Sep-12 15:00
Andre_Prellwitz10-Sep-12 15:00 
GeneralRe: What problem does that solve? Pin
Andrew Rissing10-Sep-12 15:52
Andrew Rissing10-Sep-12 15:52 
GeneralRe: What problem does that solve? Pin
Andrew Rissing10-Sep-12 3:01
Andrew Rissing10-Sep-12 3:01 
GeneralMy vote of 5 Pin
Marc Clifton9-Sep-12 9:43
mvaMarc Clifton9-Sep-12 9:43 
Impressive. This makes expressions a lot more accessible!
QuestionNext time I write expressions Pin
Pete O'Hanlon8-Sep-12 21:38
subeditorPete O'Hanlon8-Sep-12 21:38 
AnswerRe: Next time I write expressions Pin
Andrew Rissing9-Sep-12 6:15
Andrew Rissing9-Sep-12 6:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.