<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: SeniorDev</title>
    <description>The latest articles on DEV Community by SeniorDev (@vsprogrammerdev).</description>
    <link>https://dev.to/vsprogrammerdev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4234153%2F900bc7af-485f-4be9-bd6c-e07238b71afe.png</url>
      <title>DEV Community: SeniorDev</title>
      <link>https://dev.to/vsprogrammerdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vsprogrammerdev"/>
    <language>en</language>
    <item>
      <title>Mapping every JavaScript call to its ASP.NET endpoint - even in a VB.NET WebForms app</title>
      <dc:creator>SeniorDev</dc:creator>
      <pubDate>Tue, 10 Nov 2026 15:52:36 +0000</pubDate>
      <link>https://dev.to/vsprogrammerdev/mapping-every-javascript-call-to-its-aspnet-endpoint-even-in-a-vbnet-webforms-app-2bld</link>
      <guid>https://dev.to/vsprogrammerdev/mapping-every-javascript-call-to-its-aspnet-endpoint-even-in-a-vbnet-webforms-app-2bld</guid>
      <description>&lt;p&gt;Find All References stops where C# ends and JavaScript begins. Here's how I built a Visual Studio extension that links the two - and what legacy code taught me about matching URLs to routes.&lt;/p&gt;

&lt;p&gt;"If I change this endpoint, what breaks?"&lt;/p&gt;

&lt;p&gt;On a small ASP.NET Core app you can answer that from memory. On a big one you can't, and Visual Studio won't help you: &lt;strong&gt;Find All References stops at the language boundary.&lt;/strong&gt; It knows every C# call to a controller method, but it has no idea that &lt;code&gt;fetch('/api/orders/checkout')&lt;/code&gt; in a JavaScript file, an &lt;code&gt;$.ajax&lt;/code&gt; call inside an &lt;code&gt;.aspx&lt;/code&gt; page, or an &lt;code&gt;asp-action&lt;/code&gt; in a Razor view reaches that same method.&lt;/p&gt;

&lt;p&gt;I hit this hardest on a legacy application: VB.NET WebForms plus Web API 2, around 780 &lt;code&gt;.vb&lt;/code&gt; files, with JavaScript split between a &lt;code&gt;Scripts&lt;/code&gt; folder and &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; blocks inside &lt;code&gt;.aspx&lt;/code&gt; and &lt;code&gt;.ascx&lt;/code&gt; pages. Renaming a route there meant searching for URL fragments and hoping.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;FullStackCodeLens&lt;/strong&gt;, a Visual Studio extension that puts a line above your code showing who calls it and where it calls out to - across C#, VB.NET, JavaScript/TypeScript, Razor, Blazor and WebForms. This post is about how it works and the surprisingly tricky parts of matching a URL in JavaScript to the route that serves it.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/wWjM8H6ghSU" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;p&gt;Take a plain API controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;ApiController&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"api/products"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductsController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ControllerBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"{id}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;IActionResult&lt;/span&gt; &lt;span class="nc"&gt;GetById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;IActionResult&lt;/span&gt; &lt;span class="nc"&gt;Search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;HttpPut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"{id}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;IActionResult&lt;/span&gt; &lt;span class="nc"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ProductDto&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;NoContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the JavaScript that calls it, written the way real code bases mix styles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; $&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/products/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;searchProducts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`/api/products/search?term=${encodeURIComponent(term)}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;saveProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; $&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ajax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'/api/products/'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'PUT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Above &lt;code&gt;GetById&lt;/code&gt; you now see which JavaScript calls reach it, and above &lt;code&gt;loadProduct&lt;/code&gt; you see the action it calls - each name clickable:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgb45x4bkg4zzmm5ytdzm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgb45x4bkg4zzmm5ytdzm.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm470xmpr5rktrgtdlf6o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm470xmpr5rktrgtdlf6o.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When there are many callers, the line expands into a list with file and line number, and a "+N more" link instead of running off the screen:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft61xbfz2zytabieftgrh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft61xbfz2zytabieftgrh.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why text scanning and not Roslyn
&lt;/h2&gt;

&lt;p&gt;The obvious tool for code analysis in .NET is Roslyn. I chose regular-expression scanning of the source text instead, for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Half of the problem isn't C#.&lt;/strong&gt; JavaScript, TypeScript, &lt;code&gt;.cshtml&lt;/code&gt;, &lt;code&gt;.razor&lt;/code&gt;, &lt;code&gt;.aspx&lt;/code&gt; and &lt;code&gt;.ascx&lt;/code&gt; files all need reading, and Roslyn doesn't understand any of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legacy projects often don't build cleanly&lt;/strong&gt; on a developer's machine, or are VB.NET projects with old project formats. A scanner that only needs the files works anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed.&lt;/strong&gt; A full scan of that 780-file application takes about 1-1.5 seconds, in parallel on a background thread.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trade-off is that a scanner only sees what code &lt;em&gt;looks like&lt;/em&gt;. Most of the work went into the cases where looks are misleading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hard part 1: building the URL from JavaScript
&lt;/h2&gt;

&lt;p&gt;A URL in JavaScript is rarely a single string literal. The scanner has to turn all of these into a pattern it can match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`/api/orders/${orderId}/cancel`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'POST'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// template literal&lt;/span&gt;
$&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/api/invoices/"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                       &lt;span class="c1"&gt;// concatenation&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Host&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"/api/customers/"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                      &lt;span class="c1"&gt;// via a variable&lt;/span&gt;
$&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ajax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"DELETE"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Template literals and concatenated expressions become wildcards, so &lt;code&gt;/api/invoices/" + id&lt;/code&gt; becomes &lt;code&gt;/api/invoices/{*}&lt;/code&gt;. Plain variables are resolved from their assignments in the same file, and prefixes like &lt;code&gt;location.protocol + "//" + location.host&lt;/code&gt; are dropped. Kendo data sources (&lt;code&gt;read:&lt;/code&gt; / &lt;code&gt;update:&lt;/code&gt; / &lt;code&gt;destroy:&lt;/code&gt;) and &lt;code&gt;url: function () { return ... }&lt;/code&gt; are read too.&lt;/p&gt;

&lt;p&gt;The HTTP verb matters as much as the URL, and it isn't always on the same line. One bug I found while writing the demo for this article:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cartId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/orders/checkout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'POST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;cartId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cartId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;giftWrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scanner read only to the end of the &lt;code&gt;fetch(&lt;/code&gt; line, found no method, and assumed GET - so this POST matched no route. It now reads the whole argument list up to the matching parenthesis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hard part 2: one URL, several matching routes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/api/products/search?term=lamp&lt;/code&gt; textually matches &lt;strong&gt;both&lt;/strong&gt; &lt;code&gt;api/products/search&lt;/code&gt; and &lt;code&gt;api/products/{id}&lt;/code&gt; - &lt;code&gt;search&lt;/code&gt; is a perfectly good value for &lt;code&gt;{id}&lt;/code&gt;. Counting the call for both actions is wrong: ASP.NET prefers the literal segment, so &lt;code&gt;GetById&lt;/code&gt; would show a caller it never gets.&lt;/p&gt;

&lt;p&gt;So each call is owned only by its best route(s), using the same rules ASP.NET does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an &lt;strong&gt;attribute route&lt;/strong&gt; wins over a conventional route;&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;literal segment beats a parameter&lt;/strong&gt;: a known word in the URL facing &lt;code&gt;{id}&lt;/code&gt; costs a little, while a computed value (&lt;code&gt;${x}&lt;/code&gt;) facing a literal &lt;code&gt;search&lt;/code&gt; costs a lot, because it's only a guess;&lt;/li&gt;
&lt;li&gt;Web API 2 overloads that differ only by query parameters - &lt;code&gt;Get(id)&lt;/code&gt; versus &lt;code&gt;Get(id, bankId)&lt;/code&gt; - are told apart by which query keys the call actually sends.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Checking this against the legacy application, 37 calls lost a wrong second owner (&lt;code&gt;/companies/users&lt;/code&gt; no longer counted against &lt;code&gt;GetCompanyInfo({id})&lt;/code&gt;), and nothing else changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hard part 3: routes that aren't written on the action
&lt;/h2&gt;

&lt;p&gt;Web API 2 apps often have no route attributes at all. The routes live in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vb"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MapHttpRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="s2"&gt;"DefaultApi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;routeTemplate&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="s2"&gt;"api/{controller}/{id}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;defaults&lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="k"&gt;New&lt;/span&gt; &lt;span class="k"&gt;With&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RouteParameter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To know that &lt;code&gt;InvoicesController.GetInvoice(id)&lt;/code&gt; serves &lt;code&gt;GET /api/invoices/5&lt;/code&gt;, the extension reads every &lt;code&gt;MapHttpRoute&lt;/code&gt; in the solution, fills in &lt;code&gt;{controller}&lt;/code&gt;, keeps the optional &lt;code&gt;{id}&lt;/code&gt; only when the action actually has that parameter, and takes the verb from the action's name prefix (&lt;code&gt;Get...&lt;/code&gt;, &lt;code&gt;Post...&lt;/code&gt;, &lt;code&gt;Delete...&lt;/code&gt;), as Web API does.&lt;/p&gt;

&lt;p&gt;VB.NET itself had a surprise: a method containing an XML literal with SQL inside, like &lt;code&gt;(&amp;lt;string&amp;gt;SELECT ... &amp;lt;/string&amp;gt;)&lt;/code&gt;, left a parenthesis "open" for the line-joining logic and swallowed the methods after it. The reader now joins continued lines only for declarations and attributes, never for statement code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ei5g788kae228jeck8x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ei5g788kae228jeck8x.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On that application, the first C#-only version found &lt;strong&gt;0&lt;/strong&gt; routes. The current one finds &lt;strong&gt;387&lt;/strong&gt;, and matches 143 of the 145 AJAX calls in the main script. The two left over target URLs no server route defines - real dead code, which is useful to know too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hard part 4: JavaScript calling JavaScript
&lt;/h2&gt;

&lt;p&gt;Linking &lt;code&gt;showAlert(...)&lt;/code&gt; in one script to &lt;code&gt;function showAlert&lt;/code&gt; in another sounds easy until you run it on a real solution. Rules I had to add after seeing the false positives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A file's own parameters and variables are never cross-file calls.&lt;/strong&gt; &lt;code&gt;callback(response)&lt;/code&gt; matched a &lt;code&gt;function callback&lt;/code&gt; in a bootstrap file 60 times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Names shorter than 3 characters are ignored&lt;/strong&gt; - minified code is full of &lt;code&gt;e(&lt;/code&gt; and &lt;code&gt;t(&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundles aren't targets.&lt;/strong&gt; A &lt;code&gt;bundle.js&lt;/code&gt; that contains 80% of the functions of several smaller scripts would otherwise "own" every call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Libraries link only to themselves.&lt;/strong&gt; jQuery's internal helpers matched function names in Modernizr.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Object members (&lt;code&gt;var Invoices_Ajax = { load: function ...&lt;/code&gt; called as &lt;code&gt;Invoices_Ajax.load(...)&lt;/code&gt;), jQuery plugins (&lt;code&gt;this.refresh = function&lt;/code&gt; inside &lt;code&gt;$.fn.invoicePanel&lt;/code&gt;), &lt;code&gt;onclick="..."&lt;/code&gt; attributes in pages, and script sent from code-behind (&lt;code&gt;ScriptManager.RegisterStartupScript(...)&lt;/code&gt;) are all covered.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4qguuxey7aej35hv8icz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4qguuxey7aej35hv8icz.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Visual Studio's CodeLens can't go
&lt;/h2&gt;

&lt;p&gt;The first versions used Visual Studio's own CodeLens API. It doesn't reach far enough: CodeLens only decorates &lt;em&gt;declarations&lt;/em&gt;, so it can't sit above &lt;code&gt;app.MapPost(...)&lt;/code&gt; in &lt;code&gt;Program.cs&lt;/code&gt; or above an anonymous callback. And on some project types - classic WebForms web projects in particular - Visual Studio shows no CodeLens in JavaScript files at all.&lt;/p&gt;

&lt;p&gt;So the labels are drawn by the extension itself, using the same editor mechanism CodeLens uses (inter-line adornments). That also made it possible to put them in &lt;code&gt;.cshtml&lt;/code&gt;, &lt;code&gt;.razor&lt;/code&gt;, &lt;code&gt;.aspx&lt;/code&gt; and &lt;code&gt;.ascx&lt;/code&gt; files:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyhm570xvz2civzovienc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyhm570xvz2civzovienc.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Futirn04meiyynzulsu7i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Futirn04meiyynzulsu7i.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool windows
&lt;/h2&gt;

&lt;p&gt;Besides the in-editor lines there are four tool windows. The one I use most is &lt;strong&gt;Route Explorer&lt;/strong&gt;: every route in the solution with its callers, flagging conflicts (two actions that answer the same URL), routes nobody calls, routes without &lt;code&gt;[Authorize]&lt;/code&gt;, and &lt;strong&gt;DTO drift&lt;/strong&gt; - like the &lt;code&gt;giftWrap&lt;/code&gt; property in the &lt;code&gt;checkout()&lt;/code&gt; example above, which the server's &lt;code&gt;CheckoutRequest&lt;/code&gt; doesn't have. It can also export the routes as a &lt;code&gt;.http&lt;/code&gt; file or an OpenAPI document.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzw4lpzdpnxxebdi9rtgi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzw4lpzdpnxxebdi9rtgi.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;FullStackCodeLens is on the Visual Studio Marketplace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=ExtendedPlugins.ext01" rel="noopener noreferrer"&gt;Visual Studio 2022 / 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=ExtendedPlugins.FullStackCodeLensVS2019" rel="noopener noreferrer"&gt;Visual Studio 2019 edition&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full disclosure: it's my own extension and it's paid - a 30-day trial with everything enabled, then a $12 one-time license, no subscription.&lt;/p&gt;

&lt;p&gt;If you try it on a large or old code base, I'd really like to hear which call patterns it misses. Every rule in this post came from a real project that broke an earlier version.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>javascript</category>
      <category>visualstudio</category>
      <category>aspnet</category>
    </item>
  </channel>
</rss>
