Click here to Skip to main content
Click here to Skip to main content
Articles » Languages » C# » General » Revisions
 
Go to top

Flexpressions

, 8 Sep 2012
Rate this:
Please Sign up or sign in to vote.
An intuitive-fluent API for generating Linq Expressions.
This is an old version of the currently published article.

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

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()); // 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 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 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

    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 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).
    • 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 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)

    Share

    About the Author

    Andrew Rissing
    Software Developer (Senior)
    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


    Discussions posted for the Published version of this article. Posting a message here will take you to the publicly available article in order to continue your conversation in public.
     
    GeneralMy vote of 5 PinmemberPaulo Zemek10-Jun-13 11:25 
    GeneralRe: My vote of 5 PinmemberAndrew Rissing10-Jun-13 11:46 
    QuestionWhat problem does that solve? PinmemberAndreas Gieriet9-Sep-12 9:46 
    AnswerRe: What problem does that solve? PinmemberAndrew Rissing9-Sep-12 10:52 
    GeneralRe: What problem does that solve? PinmemberAndreas Gieriet9-Sep-12 14:15 
    GeneralRe: What problem does that solve? PinmemberAndrew Rissing9-Sep-12 17:15 
    GeneralRe: What problem does that solve? [modified] PinmemberAndreas Gieriet10-Sep-12 4:01 
    GeneralRe: What problem does that solve? PinmemberAndrew Rissing10-Sep-12 5:15 
    GeneralRe: What problem does that solve? [modified] PinmemberAndre_Prellwitz10-Sep-12 10:28 
    GeneralRe: What problem does that solve? PinmemberAndrew Rissing10-Sep-12 11:25 
    GeneralRe: What problem does that solve? PinmemberAndre_Prellwitz10-Sep-12 15:00 
    GeneralRe: What problem does that solve? [modified] PinmemberAndrew Rissing10-Sep-12 15:52 
    GeneralRe: What problem does that solve? PinmemberAndrew Rissing10-Sep-12 3:01 
    GeneralMy vote of 5 PinprotectorMarc Clifton9-Sep-12 9:43 
    QuestionNext time I write expressions PinprotectorPete O'Hanlon8-Sep-12 21:38 
    AnswerRe: Next time I write expressions PinmemberAndrew Rissing9-Sep-12 6:15 

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

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

    | Advertise | Privacy | Mobile
    Web02 | 2.8.140922.1 | Last Updated 9 Sep 2012
    Article Copyright 2012 by Andrew Rissing
    Everything else Copyright © CodeProject, 1999-2014
    Terms of Service
    Layout: fixed | fluid