Debugging ASP.NET generated code


Update 2/4/2010: changed Technique #2 to use Debugger.Break()

 

This post applies to any ASP.NET app that uses .aspx files, whether WebForms or MVC.

When you write an aspx/ascx/master file (I’ll just say aspx for here on, but it applies to all), it gets compiled dynamically by the ASP.NET runtime.  Note that this is true whether you use a Web Site or a Web Application Project (WAP).  While in a WAP, most of the code is built by Visual Studio, the aspx pages themselves are always built dynamically.

Normally, when you work with aspx files, you only need to worry about what you write in there, and the specifics of what ASP.NET generates under the cover are somewhat of an implementation details.  However, in some cases it’s pretty useful to look at the generated code, either to learn exactly what it does, or to make sense of tricky issues.

Note: one case where it’s particularly useful is if you’re implementing ProcessGeneratedCode in a ControlBuilder to generate custom code.  In that case, you really need to debug into the generated code if anything goes wrong.

For illustration, let’s take a really simple example page, and go through both how we can view the generated code, and then actually debug it.

Here is our lame little test page:

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
    protected void Button1_Click(object sender, EventArgs e) {
        Label1.Text = Server.HtmlEncode(TextBox1.Text);
    }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:TextBox ID="TextBox1" runat="server" />
        <asp:Button ID="Button1" runat="server" Text="Button" onclick="Button1_Click" />
        <asp:Label ID="Label1" runat="server" Text="Label" />
    </div>
    </form>
</body>
</html>

So you click on the button, and it puts the textbox’s text into the label.  I know, exciting stuff!  Now run the page and enjoy its rich feature set, then read on…

 

Looking at the generated code

Before we get into how to debug the generated code, let’s first discuss how we can simply look at it.  First, it’s a good idea to turn on the debug flag and turn off the batch flag when debugging issue.  This is done in web.config:

<compilation debug="true" batch="false">

Once you have that, there is a simple well known trick to see the generated code: deliberately add a compile error to the code.  e.g. change void to void2, or something like that.  Then when you run the page, you’ll get the familiar ASP.NET yellow error screen.  Near the bottom, you should see:

image

The first link can be useful on occasions to see what was passed to the compiler, but the real gold is in the second link, which displays the full generated code for the page.

e.g. here is an extract from it:

Line 293:          [System.Diagnostics.DebuggerNonUserCodeAttribute()]
Line 294:          private global::System.Web.UI.HtmlControls.HtmlForm @__BuildControlform1() {
Line 295:              global::System.Web.UI.HtmlControls.HtmlForm @__ctrl;
Line 296:              
Line 297:              #line 17 "d:\tmp\WebSiteDebugGeneratedCode\Page.aspx"
Line 298:              @__ctrl = new global::System.Web.UI.HtmlControls.HtmlForm();
Line 299:              
Line 300:              #line default
Line 301:              #line hidden
Line 302:              this.form1 = @__ctrl;
Line 303:              
Line 304:              #line 17 "d:\tmp\WebSiteDebugGeneratedCode\Page.aspx"
Line 305:              @__ctrl.ID = "form1";
Line 306:              
Line 307:              #line default
Line 308:              #line hidden
Line 309:              System.Web.UI.IParserAccessor @__parser = ((System.Web.UI.IParserAccessor)(@__ctrl));
Line 310:              
Line 311:              #line 17 "d:\tmp\WebSiteDebugGeneratedCode\Page.aspx"
Line 312:              @__parser.AddParsedSubObject(new System.Web.UI.LiteralControl("\r\n    <div>\r\n        "));
Line 313:              
Line 314:              #line default
Line 315:              #line hidden
Line 316:              global::System.Web.UI.WebControls.TextBox @__ctrl1;
Line 317:              
Line 318:              #line 17 "d:\tmp\WebSiteDebugGeneratedCode\Page.aspx"
Line 319:              @__ctrl1 = this.@__BuildControlTextBox1();
Line 320:              
Line 321:              #line default
Line 322:              #line hidden
Line 323:              
Line 324:              #line 17 "d:\tmp\WebSiteDebugGeneratedCode\Page.aspx"
Line 325:              @__parser.AddParsedSubObject(@__ctrl1);
Line 326:              
Line 327:              #line default
Line 328:              #line hidden

It looks quite ugly and overly wordy, but it’s fairly easy to see what it does:

  • Creates the HtmlForm
  • Set its ID
  • Add a literal control with the start <div> tag to it (since <div> is not a server control here)
  • Create the TextBox and add it to the HtmlForm

If you try this on more complex pages that use fancier features link Bind() (aka two-way data binding), you can really get a good understanding of how those features work.

 

Debugging the generated code

So looking at the code is nice, but now we really want to actually debug this code.  This requires a few tricks, but is not too difficult.

First, let’s talk a little bit about the concept of line pragmas.  In the generated code above, you see all those lines that start with #line, e.g.

Line 318:              #line 17 "d:\tmp\WebSiteDebugGeneratedCode\Page.aspx"
Line 319:              @__ctrl1 = this.@__BuildControlTextBox1();

What this does is tell the compiler that the code following the #line pragma should be treated as if it were at line 17 of your aspx, rather that in the generated code.  This is very useful, as it allows compiler errors to point to your aspx file, which is what in most cases makes sense.  It also allows you to debug into the aspx page.

But although that’s very useful stuff, in this case it’s exactly what we don’t want, since our goal is to debug straight into the generated code!

The answer is simple: get rid of the line pragmas!  Luckily, there is a little known yet very simple directive that will do just that: LinePragmas="false".  So add that to your page:

<%@ Page Language="C#" LinePragmas="false" %>

Now run your page again.  Note that at this time, we still have the little syntax error that we introduced earlier (e.g. void2).  So you’ll get an error, but it won’t look quite the same.  First, it’ll say that the error is in the generated file, not the aspx file.  e.g. you’ll get something like this:

Source File: c:\Users\davidebb\AppData\Local\Temp\Temporary ASP.NET Files\websitedebuggeneratedcode\165e8896\2804a211\App_Web_page.aspx.cdcab7d2.qmy8q0k7.0.cs    Line: 46 

And then, if you expand the “Show Complete Compilation Source” link, you’ll see that the code no longer has line pragmas.  e.g. the code we looked at above now looks like this:

Line 121:          private global::System.Web.UI.HtmlControls.HtmlForm @__BuildControlform1() {
Line 122:              global::System.Web.UI.HtmlControls.HtmlForm @__ctrl;
Line 123:              @__ctrl = new global::System.Web.UI.HtmlControls.HtmlForm();
Line 124:              this.form1 = @__ctrl;
Line 125:              @__ctrl.ID = "form1";
Line 126:              System.Web.UI.IParserAccessor @__parser = ((System.Web.UI.IParserAccessor)(@__ctrl));
Line 127:              @__parser.AddParsedSubObject(new System.Web.UI.LiteralControl("\r\n    <div>\r\n        "));
Line 128:              global::System.Web.UI.WebControls.TextBox @__ctrl1;
Line 129:              @__ctrl1 = this.@__BuildControlTextBox1();
Line 130:              @__parser.AddParsedSubObject(@__ctrl1);

So same code, minus all the #line pragma.

Now let’s get back to our original goal of debugging our page’s generated code!  I’ll give you two different techniques to do this.

 

Technique #1: find the generated folder and open the file from there

Take the following steps.  It looks a little tricky, but nothing you can’t handle!

  1. Start by making sure you have LinePragmas="false" as discussed above.
  2. Copy the generated file’s folder path from above (in my case c:\Users\davidebb\AppData\Local\Temp\Temporary ASP.NET Files\websitedebuggeneratedcode\165e8896\2804a211), and open it in the windows shell.
  3. Remove the compile error we had put in earlier (e.g. void2)
  4. Run your page again.  This causes it to be recompiled, and it will execute normally.
  5. Now, look back a the generated file’s folder, and sort the files by Date Modified.
  6. The newest file should be named something like App_Web_page.aspx.cdcab7d2.q_hetksf.0.cs.  Drag it into VS to open it.
  7. Set a break point in there.  e.g. in the __BuildControlform1() we looked at.
  8. Hit F5 to debug your app, and it should hit your breakpoint!

You can now step through the code as if you wrote it!  Which you kind of did, indirectly 🙂

 

Technique #2: use a code break point to directly open the file for you.

This is a simpler technique as it doesn’t require you to hunt for the generated source file.  But it does require you to add a line of code to your page.

Start by making sure you have LinePragmas="false" as discussed above.  Now, remove our little compile error above (e.g. void2), and add a Page_Init method in your page that breaks in the debugger.  This can also go in Page_Load, or generally anywhere that executes during the page life cycle.  e.g.

void Page_Init() {
    System.Diagnostics.Debugger.Break();
}

Now hit F5 to debug your page, and boom, it should hit your break point, which has the great benefit of opening the generated file in the debugger!  Now you can set break point is other places, and go you’re all set.  Note that you’ll need to send a 2nd request to the page to debug methods like __BuildControlform1(), since they execute way before Page_Init.

Comments (12)

  1. tretyak says:

    >

    > throw new Exception("I’m a break point!");

    >

    why not Debugger.Break  ?

  2. Richard says:

    "you’ll need to send a 2nd request to the page to debug methods like __BuildControlform1(), since they execute way before Page_Init"

    Why not override the FrameworkInitialize method instead?

    protected override void FrameworkInitialize()

    {

       System.Diagnostics.Debugger.Break();

       base.FrameworkInitialize();

    }

  3. David Ebbo says:

    @Richard: unfortunately, you cannot do this because ASP.NET overrides it in the generated code, so the class would end up with two copies of this method.

  4. Kris says:

    Thanks for sharing this. I have followed the first  technique, but as you mentioned it is a little bit cumbersome. The second one is much more easier and I did not know about LinePragmas.

    I am wondering if you have any such similar tips to understand the page lifecycle. Would appreciate any pointers.

    Thanks.

  5. David Ebbo says:

    @Kris: page life cycle is a complex topic, but it’s fairly well documented on MSDN (and other places).  See http://msdn.microsoft.com/en-us/library/ms178472.aspx

  6. Great post David.

    I am giving a training to the QA team about the page parser, control builders and asp.net codegen, and the page directive to remove line pragmas will come in handy. I didn’t know that existed!

    – Federico

  7. David Ebbo says:

    @federico: yes, the LinePragmas directive is a well kept request. Well, hopefully not anymore! 🙂

  8. Felipe Fujiy says:

    Dont work for me, Technique #1 nor Technique #2.

    At #1, dont stop at breakpoint. At#, dont open the file

  9. David Ebbo says:

    @Felipe: make sure you turn debug=true in the web.config file. Also, verify that standard debugging works before trying the techniques described here.

  10. Vlad says:

    In vs2010+asp.net development server, When I leave line pragmas enabled, vs breaks into aspx file ( (I wrote system.diagnostics.debugger.break() inside aspx file)). When I disable line pragmas (LinePragmas="false") I get "No Source Available / Show Disassembly screen". Pdb for compiled aspx seems to be loaded, the cs, that pdb points to – is open in vs2010, also setting breakpoints in cs gets 'no symbols loaded for this document'.

  11. David Ebbo says:

    @Vlad: strange, I just tried this in VS2010 (with dev web server) and it worked fine.  Not sure what could be different about your test app.  Have you tried from an empty test app to make sure there are no outside factors?

Skip to main content