Inline C# Expression Migration¶
Web Forms uses several inline expression syntaxes in ASPX and ASCX markup. Each syntax serves a different purpose and has direct Blazor/Razor equivalents. This guide covers the migration of all inline expression types from Web Forms to Blazor.
Expression Overview
Web Forms supported five inline expression syntaxes: <%= %>, <%: %>, <%# %>, <% %>, and <%$ %>. Each has a specific role. Understanding these differences is key to successful migration.
Code Render Blocks (<%= ... %>)¶
What It Was¶
Code render blocks output the result of a C# expression directly to the page as raw, unencoded HTML:
<td><%= Request.QueryString["id"] %></td>
<span><%= DateTime.Now.Year %></span>
<div><%= "<strong>Bold Text</strong>" %></div>
The result is inserted directly into the HTML output without any encoding.
Blazor Equivalent¶
In Blazor/Razor, the @() expression syntax replaces <%= %>. Important: @() performs HTML encoding by default, which is safer than Web Forms' raw output:
XSS Risk and HTML Encoding
<%= %> outputs raw unencoded HTML. This is a security risk if the output comes from user input. Blazor's @() is HTML-encoded by default — safe for user data.
If you intentionally need raw HTML (e.g., from a trusted source like a database), use @((MarkupString)rawHtml):
Request Object Access¶
Web Forms' Request object gives access to query strings, form data, and cookies. In Blazor:
<!-- Query String with NavigationManager -->
<span>@(NavigationManager.Uri.Contains("id=") ? NavigationManager.Uri.Split("id=")[1].Split("&")[0] : "")</span>
<!-- Or use [SupplyParameterFromQuery] in code -->
@code {
[SupplyParameterFromQuery]
public string? Id { get; set; }
}
<!-- Form Data: Use @bind with input elements -->
<input type="text" @bind="username" />
<!-- Cookies with IHttpContextAccessor -->
@inject IHttpContextAccessor HttpContextAccessor
@{
var sessionId = HttpContextAccessor?.HttpContext?.Request.Cookies["sessionid"];
}
<span>@sessionId</span>
Prefer Parameter Binding
Instead of parsing Request.QueryString manually, use [SupplyParameterFromQuery] attributes on component parameters. This is cleaner, type-safe, and more performant.
HTML-Encoded Output (<%: ... %>)¶
What It Was¶
HTML-encoded output blocks automatically HTML-encode the expression result before rendering:
This was a safer alternative to <%= %> because it prevented XSS attacks by encoding special characters.
Blazor Equivalent¶
In Blazor, @() is HTML-encoded by default. This means you can use @() for the same security benefit:
Default Safety
Blazor's default behavior (@value) provides the safety of <%: %> without extra syntax. Always use @value for user-generated content and reserve @((MarkupString)value) only for trusted sources.
Data-Binding Expressions (<%# ... %>)¶
What It Was¶
Data-binding expressions were used in templates (ItemTemplate, EditTemplate, etc.) to output data from the current item in a data-bound control:
<asp:Repeater DataSource="<%# Products %>">
<ItemTemplate>
<tr>
<td><%# Eval("ProductName") %></td>
<td><%# Eval("Price", "{0:C}") %></td>
<td><%# Item.StockLevel %></td>
<td>
<asp:TextBox Text='<%# Bind("ProductName") %>' runat="server" />
</td>
</tr>
</ItemTemplate>
</asp:Repeater>
The Eval() method performed one-way data binding (output only), while Bind() performed two-way binding (output + update).
Blazor Equivalent: Output Only¶
For repeating controls like <Repeater>, use the implicit @context parameter to access the current item:
Context Parameter
By default, the current item in a template is accessed via the @context variable. You can rename it with Context="Item" if you prefer: <Repeater Context="Item"> → @Item.ProductName.
Blazor Equivalent: Two-Way Binding¶
Replace Bind() with the @bind-Value directive:
Complex Formatting¶
For complex formatting beyond a simple format string, use C# methods:
Code Blocks (<% ... %>)¶
What It Was¶
Code blocks executed arbitrary C# code without outputting anything:
<% if (User.IsInRole("Admin")) { %>
<button>Delete</button>
<% } %>
<% foreach (var item in Items) { %>
<div><%# item.Name %></div>
<% } %>
This pattern mixed logic with markup, leading to difficult-to-maintain code.
Blazor Equivalent¶
Blazor provides @if, @foreach, and other control flow directives:
Code Organization
Blazor makes it easy to move complex logic to methods in the @code block instead of embedding it in markup. This improves readability and testability.
Conditional Rendering¶
Web Forms used code blocks for conditional rendering. Blazor uses @if:
Expression Builders (<%$ ... %>)¶
What It Was¶
Expression builders accessed application configuration at compile time:
<!-- Connection Strings -->
<%$ ConnectionStrings:DefaultConnection %>
<!-- App Settings -->
<%$ AppSettings:SiteTitle %>
<!-- Resources (localization) -->
<%$ Resources:Labels, WelcomeMessage %>
Blazor Equivalent¶
Blazor uses dependency injection and the IConfiguration service instead:
@inject IConfiguration Configuration
<!-- Connection String -->
@Configuration.GetConnectionString("DefaultConnection")
<!-- App Setting -->
@Configuration["SiteTitle"]
<!-- In code block: -->
@code {
private string connectionString = "";
protected override void OnInitialized()
{
connectionString = Configuration.GetConnectionString("DefaultConnection");
}
}
Localization (Resources)¶
For localized strings, use IStringLocalizer:
Page Properties and Global Objects¶
Page.Title¶
User Identity¶
@inject AuthenticationStateProvider AuthenticationStateProvider
@if (authState?.User?.Identity?.IsAuthenticated == true)
{
<span>@authState.User.Identity.Name</span>
}
@if (authState?.User?.IsInRole("Admin") == true)
{
<button>Manage</button>
}
@code {
private AuthenticationState authState;
protected override async Task OnInitializedAsync()
{
authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
}
}
Automated Migration with bwfc-migrate.ps1¶
The automated migration script handles many expression conversions automatically:
| Expression | Before | After |
|---|---|---|
| Code render | <%= value %> |
@(value) |
| HTML-encoded | <%: value %> |
@(value) |
| Data binding | <%# Eval("Prop") %> |
@context.Prop |
| Data binding with format | <%# Eval("Price", "{0:C}") %> |
@context.Price.ToString("C") |
| Code blocks | <% if (...) { %> |
@if (...) { |
| Bind method | <%# Bind("Prop") %> |
@bind-Value="@context.Prop" |
| Comments | <%-- text --%> |
@* text *@ |
Script Limitations
The automated script handles simple, direct expression conversions. Complex expressions, method calls, and custom logic may require manual adjustment. Always review migrated code for correctness.
Common Patterns and Gotchas¶
Ternary Expressions¶
String Concatenation¶
Method Calls in Markup¶
Accessing Container Properties¶
What Requires Manual Migration¶
Some patterns cannot be fully automated and require manual attention:
Session State in Expressions¶
<!-- Web Forms -->
<span><%= (string)Session["UserName"] %></span>
<!-- Blazor: Inject a custom service or use distributed caching -->
@inject ISessionService SessionService
<span>@(await SessionService.GetAsync<string>("UserName"))</span>
Complex LINQ Queries in Markup¶
Move complex queries to the code block:
@code {
private List<Product> FilteredProducts =>
Products.Where(p => p.InStock && p.Price < 100).ToList();
}
@foreach (var product in FilteredProducts)
{
<div>@product.Name</div>
}
DataSource Controls¶
Web Forms <asp:SqlDataSource> and similar controls have no Blazor equivalent. Replace with injected services:
@inject ProductService ProductService
@code {
private List<Product> Products = new();
protected override async Task OnInitializedAsync()
{
Products = await ProductService.GetProductsAsync();
}
}
Custom Expression Builders¶
If you created custom expression builders, you'll need to migrate this logic to IConfiguration or custom services.
See Also¶
- AutomatedMigration.md — Script capabilities and conversion table
- Strategies.md — Broader migration patterns
- Databinder.md — Data-binding in detail
- DeprecationGuidance.md — Other deprecated Web Forms patterns