Inside Razor - Part 2 - Expressions

This is part 2 of my Inside Razor series. Read Part 1 here.

In my previous post, I glossed over one line in my sample:

<li>@p.Name ($@p.Price)</li>

Well, it's finally time to get in to how this is parsed! So, to recap from last time, when we see the "<li>" here, we know that we are parsing a block of markup which ends at the "</li>". The markup parser scans forward until it finds the end tag, but before it reaches it, it sees an "@". So, just as with "@foreach", it switches to the code parser.

This is where things get a bit different. The C# code parser looks at that first identifier: "p" and checks its internal list of C# keywords. Of course, "p" is not a C# keyword, so the C# code parser enters "Implicit Expression" mode. The algorithm for parsing implicit expressions is something like the following:

  1. First, Read an identifier
  2. Is the next character a "(" or "["?
    • Yes - Read to the matching ")" or "]", then Go To 2
    • No - Continue to 3
  3. Is the next character a "."?
    • Yes - Continue to 4
    • No - End of Expression
  4. Is the character AFTER the "." a valid start character for a C# identifier?
    • Yes - Read the "." and Go To 1
    • No - DO NOT Read the ".", and End the Expression

The high-level overview of this algorithm is that an implicit expression is an identifier, followed by any number of method calls ("()"), indexing expressions ("[]") and member access expressions ("."). And, whitespace is not allowed (except for within "()" or "[]"). So for example, these are all valid implicit expressions in Razor:

@p.Name.ToString()[6 - 2]
@p.Name.Replace("ASPX", "Razor")[i++]

However, the following are not valid, and the second section (after the arrow, "==>") is the only part that would be considered part of the expression by Razor:

@1 + 1 ==> @
@p++ ==> @p
@p . Name ==> @p
@p.Name.Length - 1 ==> @p.Name.Length

This is why we have another syntax for expressions: "@(...)". This syntax allows anything you want within the "()". So, you can write all of the previous examples using that syntax as an escape-hatch:

@(1 + 1) 
@(p . Name)
@(p.Name.Length - 1)

Once we've identified the expression, we pass it along to our code generator. When generating the code for "@foreach () { ... }", we just dump that code into the generated C# class as-is, but when we identify an expression (either implicit or explicit) we do something a little different. You probably noticed that unlike ASPX, there is only one control construct: "@", there is no "@=" to distinguish code that we run vs. expressions that we render the value of. This is where some of the magic of Razor comes in. If we see "@foreach" for example, we know that "foreach" is a C# keyword, so that block is written as a statement to be executed. When we see "@p.Name" or "@(1 + 1)", we know that they are expressions, so after executing them, we render the result. So basically:

  • @if, @switch, @try, @foreach, @for, etc. are equivalent to "<% %>"
  • @p.Name, @(p++), @(1 + 1), etc. are equivalent to "<%: %>"

Another side note is that expressions are equivalent to "<%:" and NOT "<%=". We made a decision in Razor that HTML encoding should be the default, and that if you want to write unencoded strings, you can use the IHtmlString interface that has been blogged about before.

So, with all that background, we can quickly jump back to our initial sample:

<li>@p.Name ($@p.Price)</li>

When we see "@p.Name" we identify that as an expression, but the space before the "(" stops us from interpreting it as a method call. Then " ($" are all markup and when we see the "@", we interpret "@p.Price" as an expression and stop at the ")".

So there's a quick overview of how Razor identifies and parses expressions. In my next post I'm going to discuss hosting the Razor parser outside of ASP.Net. As before, please feel free to leave comments if you have questions, or send me a tweet (@anurse) or an email (andrew AT andrewnurse DOT net).