ASPX Markup: The Original Prompt Engineering Language
This post is part of a series
- Dim Intelligence As Artificial: VB.NET's Verbose Syntax as Natural Language Bridge
- The UpdatePanel Prophet: AJAX Before It Was Cool, AI Before It Was Mainstream
- ASPX Markup: The Original Prompt Engineering Language
The epiphany came while I was writing a complex prompt template with Jinja2. As I typed {% for item in context %}
, my fingers froze. I'd written this before. Not in Python. Not in 2024. But in 2003, with <asp:Repeater>
tags. My screen started to blur as the truth crystallized: ASPX wasn't a markup language. It was the first prompt engineering syntax, and we've been trying to recreate it ever since.
The Declarative Prophecy
<%@ Page Language="VB" AutoEventWireup="true" %>
<%@ Import Namespace="System.AI.Models" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<ai:PromptTemplate ID="PromptTemplate1" runat="server"
Model="gpt-4"
Temperature="0.7"
MaxTokens="<%= ConfigurationManager.AppSettings("MaxTokens") %>">
<SystemPrompt>
You are a helpful assistant trained on <asp:Literal ID="litTrainingData" runat="server" />.
</SystemPrompt>
<UserPrompt>
<asp:PlaceHolder ID="phContext" runat="server" />
<%= Request.QueryString("question") %>
</UserPrompt>
</ai:PromptTemplate>
</asp:Content>
This isn't web development. This is prompt composition with compile-time validation. Every ASPX page was a prompt template waiting to be recognized.
Server Controls: The Original Prompt Components
<asp:Repeater ID="rptContext" runat="server">
<HeaderTemplate>
Previous conversation:
</HeaderTemplate>
<ItemTemplate>
User: <%# Eval("UserMessage") %>
Assistant: <%# Eval("AssistantResponse") %>
</ItemTemplate>
<SeparatorTemplate>
---
</SeparatorTemplate>
</asp:Repeater>
That's not a Repeater. That's a few-shot prompting component with built-in formatting. The HeaderTemplate is the instruction, ItemTemplate is the example format, and SeparatorTemplate defines token boundaries.
Data Binding: The First Context Injection
Protected Sub Page_Load(sender As Object, e As EventArgs)
' This was RAG before RAG
rptContext.DataSource = GetRelevantConversationHistory()
rptContext.DataBind()
' Dynamic prompt assembly
litTrainingData.Text = LoadTrainingDataDescription()
phContext.Controls.Add(New LiteralControl(BuildContextString()))
End Sub
<%# Eval() %>
wasn't data binding—it was variable substitution in prompt templates. Every DataBind() call was injecting context into a prompt.
The <%= %> Enlightenment
<div class="prompt-construction">
Temperature: <%= Model.Temperature %>
Max Tokens: <%= Model.MaxTokens %>
System: You are an expert in <%= Domain %>.
Task: <%= String.Format("Analyze the following {0} and provide {1}",
InputType, OutputFormat) %>
Context:
<% For Each doc In RelevantDocuments %>
- <%= doc.Summary %>
<% Next %>
</div>
These aren't server tags. They're template variables, control flow, and dynamic prompt construction. Langchain is just trying to recreate what ASPX had in 2002.
Custom Server Controls: Reusable Prompt Components
Public Class ChainOfThoughtPrompt
Inherits WebControl
Public Property Question As String
Public Property Steps As Integer = 3
Protected Overrides Sub RenderContents(writer As HtmlTextWriter)
writer.WriteLine($"Question: {Question}")
writer.WriteLine("Let's think step by step:")
For i As Integer = 1 To Steps
writer.WriteLine($"{i}. [Step {i} reasoning here]")
Next
writer.WriteLine("Therefore, the answer is:")
End Sub
End Class
<cc:ChainOfThoughtPrompt ID="cot1" runat="server"
Question="<%# CurrentQuestion %>"
Steps="5" />
We were building reusable prompt components as server controls. Every custom control was a prompt pattern waiting to be discovered.
Control Directives: Prompt Configuration
<%@ Control Language="VB"
ClassName="FewShotPrompt"
CodeFile="FewShotPrompt.ascx.vb"
CompilationMode="Always"
EnableViewState="false" %>
<%@ OutputCache Duration="3600" VaryByParam="model;temperature" %>
<script runat="server">
Public Property Examples As List(Of Example)
Public Property TaskDescription As String
</script>
<div class="few-shot-prompt">
Task: <%= TaskDescription %>
Examples:
<asp:ListView ID="lvExamples" runat="server">
<ItemTemplate>
Input: <%# Eval("Input") %>
Output: <%# Eval("Output") %>
</ItemTemplate>
</asp:ListView>
Now complete:
Input: <asp:Literal ID="litCurrentInput" runat="server" />
Output:
</div>
OutputCache wasn't caching HTML—it was caching prompt templates for reuse. VaryByParam was implementing prompt versioning.
The PlaceHolder Revelation
<asp:PlaceHolder ID="phDynamicPrompt" runat="server" Visible="false" />
<script runat="server">
Sub BuildPrompt()
If UserIsExpert Then
phDynamicPrompt.Controls.Add(LoadControl("~/Prompts/ExpertPrompt.ascx"))
Else
phDynamicPrompt.Controls.Add(LoadControl("~/Prompts/BeginnerPrompt.ascx"))
End If
phDynamicPrompt.Visible = True
End Sub
</script>
PlaceHolder controls were implementing conditional prompt sections before prompt engineering even had a name.
Validation Controls as Prompt Constraints
<asp:TextBox ID="txtUserInput" runat="server" TextMode="MultiLine" />
<asp:RequiredFieldValidator runat="server"
ControlToValidate="txtUserInput"
ErrorMessage="Prompt cannot be empty" />
<asp:RegularExpressionValidator runat="server"
ControlToValidate="txtUserInput"
ValidationExpression="^[^<>]{1,1000}$"
ErrorMessage="Prompt must be 1-1000 chars, no HTML" />
<asp:CustomValidator runat="server"
ControlToValidate="txtUserInput"
OnServerValidate="ValidateNoPromptInjection"
ErrorMessage="Potential prompt injection detected" />
Validation controls weren't validating user input—they were implementing prompt safety and injection prevention.
Master Pages: System Prompt Inheritance
<%@ Master Language="VB" %>
<asp:ContentPlaceHolder ID="SystemPromptBase" runat="server">
You are a helpful, harmless, and honest assistant.
Always refuse to generate harmful content.
<asp:ContentPlaceHolder ID="AdditionalSystemPrompt" runat="server" />
</asp:ContentPlaceHolder>
<asp:ContentPlaceHolder ID="UserInteraction" runat="server" />
Master pages were implementing system prompt inheritance. Base safety instructions in the master, specific behaviors in content pages.
The Inline Expression Evolution
<!-- Data Binding Expression -->
<%# Container.DataItem.ToString() %>
<!-- Evaluated Expression -->
<%= DateTime.Now %>
<!-- HTML Encoded -->
<%: Model.UserInput %>
<!-- Expression Builder -->
<%$ AppSettings:OpenAIKey %>
<!-- Two-way Binding -->
<%# Bind("Temperature") %>
Each expression type was a different prompt interpolation method:
<%# %>
: Context injection<%= %>
: Dynamic values<%: %>
: Sanitized user input<%$ %>
: Configuration variablesBind()
: Two-way prompt parameter binding
The Revelation in the Runtime
Public Class PromptPage
Inherits Page
Protected Overrides Sub Render(writer As HtmlTextWriter)
' Capture the rendered output
Dim stringWriter As New StringWriter()
Dim htmlWriter As New HtmlTextWriter(stringWriter)
MyBase.Render(htmlWriter)
' The rendered page IS the prompt
Dim prompt As String = stringWriter.ToString()
' Send to AI model instead of browser
Dim response = AIModel.Complete(prompt)
' Write AI response
writer.Write(response)
End Sub
End Class
Every ASPX page render was actually assembling a prompt. We were one override away from feeding pages to AI instead of browsers.
The Truth Hidden in web.config
<configuration>
<system.web>
<pages>
<controls>
<add tagPrefix="ai" namespace="AI.WebControls" />
</controls>
</pages>
<httpHandlers>
<add verb="*" path="*.prompt" type="AI.PromptHandler" />
</httpHandlers>
</system.web>
</configuration>
We could have been serving .prompt files instead of .aspx files. The infrastructure was there. We just didn't see it.
The Prophecy Fulfilled
Today, everyone uses Jinja2, Handlebars, or Liquid templates for prompt engineering. But ASPX did it first, and did it better:
- Compile-time checking: Your prompts were validated before deployment
- Strong typing: No runtime type errors in your templates
- IntelliSense: Full IDE support for prompt construction
- Reusable components: Server controls as prompt components
- State management: ViewState for conversation context
- Caching: OutputCache for prompt memoization
- Master pages: System prompt inheritance
- User controls: Composable prompt sections
Next time you're writing {{ context }}
in your prompt template, remember: ASPX was doing this in 2001 with better tooling, stronger typing, and a visual designer.
The future of prompt engineering isn't in text templates. It's in recognizing that we already solved this problem with server controls, data binding, and declarative markup. We just called it web development instead of AI.