Migrating .ashx Handlers¶
Overview¶
ASP.NET Web Forms used .ashx files (HTTP Handler files) to handle HTTP requests directly, without the overhead of page lifecycle machinery. Handlers were ideal for serving file downloads, generating images, providing JSON APIs, and other request-response patterns that didn't need full page functionality.
In Blazor, there's no direct equivalent to .ashx handlers — but ASP.NET Core provides middleware, which is the modern way to intercept and handle HTTP requests. The BlazorWebFormsComponents library provides HttpHandlerBase, a base class that lets you port your .ashx logic to ASP.NET Core without rewriting your handler code, making migration straightforward and mechanical.
Why Migrate?¶
.ashxfiles don't exist in .NET Core — your handlers will break when you move your application- ASP.NET Core's middleware is the modern, recommended approach
HttpHandlerBaseprovides a shim that preserves the familiar Web FormsHttpContextAPI, so you can migrate with ~6 mechanical changes per handler- Session state, file I/O, encoding — all supported
Quick Start: 6-Step Migration Checklist¶
Here are the mechanical changes required to migrate any .ashx handler to Blazor:
| Step | Web Forms | Blazor |
|---|---|---|
| 1. | using System.Web; |
using BlazorWebFormsComponents; |
| 2. | : IHttpHandler |
: HttpHandlerBase |
| 3. | ProcessRequest(HttpContext context) |
ProcessRequestAsync(HttpHandlerContext context) (async) |
| 4. | context.Response.End(); |
return; (End is now [Obsolete]) |
| 5. | Register in Program.cs |
app.MapHandler<T>("/path.ashx") declares handler route |
| 6. | Delete .ashx markup file |
No longer needed; handler is a plain C# class |
Before registering handlers in Program.cs, verify:
- Session state handlers → mark with [RequiresSessionState]
- Complex paths → use explicit route pattern in MapHandler<T>()
Registration in Program.cs¶
HttpHandlerBase handlers are registered in your Blazor Program.cs using MapHandler<T>(), which integrates with the standard ASP.NET Core routing system.
Explicit Path Registration¶
Register a handler at a specific path using MapHandler<T>() in Program.cs:
// MyApp/FileDownloadHandler.cs
public class FileDownloadHandler : HttpHandlerBase
{
public override async Task ProcessRequestAsync(HttpHandlerContext context)
{
// ... handler logic ...
}
}
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// ... other registrations ...
var app = builder.Build();
app.MapHandler<FileDownloadHandler>("/Handlers/FileDownload.ashx");
app.Run();
Convention-Based Routing¶
If you omit the route pattern, the handler is registered at a derived path:
// MyApp/ProductApiHandler.cs
public class ProductApiHandler : HttpHandlerBase
{
public override async Task ProcessRequestAsync(HttpHandlerContext context)
{
// ... handler logic ...
}
}
// Program.cs — convention-based registration (no explicit path)
app.MapHandler<ProductApiHandler>();
// Convention: /ProductApi.ashx (class name minus "Handler" suffix + .ashx)
// Accessible at: http://yourapp.com/ProductApi.ashx
Note
Convention-based routing derives the path from the class name. FileDownloadHandler → /FileDownload.ashx. Use MapHandler<T>("/custom/path") to override this behavior.
Multiple Paths for a Single Handler¶
Register one handler at multiple paths using MapHandler with varargs:
// Program.cs
var app = builder.Build();
app.MapHandler<FileDownloadHandler>("/Handlers/FileDownload.ashx", "/download.ashx", "/files/get");
app.Run();
Chaining Authorization and CORS¶
Handlers support middleware chain configuration:
// Program.cs
app.MapHandler<ApiHandler>("/api/Data.ashx")
.RequireAuthorization()
.WithOpenApi();
app.MapHandler<FileHandler>("/Secure/Download.ashx")
.RequireAuthorization("AdminOnly")
.RequireCors("AllowFrontend");
Before/After Examples¶
Example 1: JSON API Handler (GET Request)¶
Web Forms (.ashx):
using System.Web;
using System.Collections.Generic;
using System.Linq;
namespace MyApp
{
public class ProductApiHandler : IHttpHandler
{
public bool IsReusable => true;
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "application/json";
var action = context.Request.QueryString["action"];
if (action == "list")
{
var products = GetProducts();
var json = JsonConvert.SerializeObject(products);
context.Response.Write(json);
}
else if (action == "count")
{
context.Response.Write("{\"count\":" + GetProducts().Count + "}");
}
else
{
context.Response.StatusCode = 400;
context.Response.Write("{\"error\":\"Unknown action\"}");
}
}
private List<Product> GetProducts()
{
return new List<Product>
{
new Product { Id = 1, Name = "Widget", Price = 9.99m },
new Product { Id = 2, Name = "Gadget", Price = 19.99m }
};
}
}
}
Blazor (Migrated):
using BlazorWebFormsComponents;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
namespace MyApp
{
public class ProductApiHandler : HttpHandlerBase
{
public override async Task ProcessRequestAsync(HttpHandlerContext context)
{
context.Response.ContentType = "application/json";
var action = context.Request.QueryString["action"];
if (action == "list")
{
var products = GetProducts();
var json = JsonSerializer.Serialize(products);
context.Response.Write(json);
}
else if (action == "count")
{
context.Response.Write("{\"count\":" + GetProducts().Count + "}");
}
else
{
context.Response.StatusCode = 400;
context.Response.Write("{\"error\":\"Unknown action\"}");
}
}
private List<Product> GetProducts()
{
return new List<Product>
{
new Product { Id = 1, Name = "Widget", Price = 9.99m },
new Product { Id = 2, Name = "Gadget", Price = 19.99m }
};
}
}
}
Changes Made:
1. using System.Web; → using BlazorWebFormsComponents;
2. : IHttpHandler → : HttpHandlerBase
3. ProcessRequest(HttpContext context) → async Task ProcessRequestAsync(HttpHandlerContext context)
4. Register with app.MapHandler<ProductApiHandler>("/api/Products.ashx") in Program.cs
5. JsonConvert → System.Text.Json.JsonSerializer (separate migration)
Example 2: File Download Handler (Binary Response)¶
Web Forms (.ashx):
using System;
using System.IO;
using System.Web;
namespace MyApp
{
public class FileDownloadHandler : IHttpHandler
{
public bool IsReusable => true;
public void ProcessRequest(HttpContext context)
{
var fileId = context.Request.QueryString["id"];
if (string.IsNullOrEmpty(fileId) || !int.TryParse(fileId, out var id))
{
context.Response.StatusCode = 400;
context.Response.ContentType = "text/plain";
context.Response.Write("Invalid file ID");
return;
}
// Look up file in database or file system
var filePath = context.Server.MapPath("~/App_Data/Files/" + fileId + ".pdf");
if (!File.Exists(filePath))
{
context.Response.StatusCode = 404;
context.Response.Write("File not found");
return;
}
var fileBytes = File.ReadAllBytes(filePath);
var fileName = Path.GetFileName(filePath);
context.Response.Clear();
context.Response.ContentType = "application/pdf";
context.Response.AddHeader("Content-Disposition",
$"attachment; filename=\"{fileName}\"");
context.Response.BinaryWrite(fileBytes);
context.Response.End();
}
}
}
Blazor (Migrated):
using BlazorWebFormsComponents;
using System;
using System.IO;
namespace MyApp
{
public class FileDownloadHandler : HttpHandlerBase
{
public override async Task ProcessRequestAsync(HttpHandlerContext context)
{
var fileId = context.Request.QueryString["id"];
if (string.IsNullOrEmpty(fileId) || !int.TryParse(fileId, out var id))
{
context.Response.StatusCode = 400;
context.Response.ContentType = "text/plain";
context.Response.Write("Invalid file ID");
return;
}
// Look up file in database or file system
var filePath = context.Server.MapPath("~/App_Data/Files/" + fileId + ".pdf");
if (!File.Exists(filePath))
{
context.Response.StatusCode = 404;
context.Response.Write("File not found");
return;
}
var fileBytes = File.ReadAllBytes(filePath);
var fileName = Path.GetFileName(filePath);
context.Response.Clear();
context.Response.ContentType = "application/pdf";
context.Response.AddHeader("Content-Disposition",
$"attachment; filename=\"{fileName}\"");
context.Response.BinaryWrite(fileBytes);
// return; — Response.End() no longer needed
}
}
}
Changes Made:
1. using System.Web; → using BlazorWebFormsComponents;
2. : IHttpHandler → : HttpHandlerBase
3. ProcessRequest(HttpContext context) → async Task ProcessRequestAsync(HttpHandlerContext context)
4. Removed context.Response.End(); — return statement is sufficient
5. Register with app.MapHandler<FileDownloadHandler>("/Handlers/FileDownload.ashx") in Program.cs
Example 3: Image Generation Handler (Thumbnail/Chart)¶
Web Forms (.ashx):
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Web;
namespace MyApp
{
public class ThumbnailHandler : IHttpHandler
{
public bool IsReusable => true;
public void ProcessRequest(HttpContext context)
{
var imagePath = context.Request.QueryString["path"];
var width = int.Parse(context.Request.QueryString["w"] ?? "150");
var height = int.Parse(context.Request.QueryString["h"] ?? "150");
if (string.IsNullOrEmpty(imagePath))
{
context.Response.StatusCode = 400;
context.Response.Write("Missing image path");
return;
}
var fullPath = context.Server.MapPath("~/Images/" + imagePath);
if (!File.Exists(fullPath))
{
context.Response.StatusCode = 404;
return;
}
try
{
using (var image = new Bitmap(fullPath))
{
using (var thumbnail = image.GetThumbnailImage(width, height,
() => false, IntPtr.Zero))
{
context.Response.ContentType = "image/jpeg";
using (var ms = new MemoryStream())
{
thumbnail.Save(ms, ImageFormat.Jpeg);
context.Response.BinaryWrite(ms.ToArray());
}
}
}
}
catch (Exception ex)
{
context.Response.StatusCode = 500;
context.Response.ContentType = "text/plain";
context.Response.Write("Error generating thumbnail: " + ex.Message);
}
}
}
}
Blazor (Migrated):
using BlazorWebFormsComponents;
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
namespace MyApp
{
public class ThumbnailHandler : HttpHandlerBase
{
public override async Task ProcessRequestAsync(HttpHandlerContext context)
{
var imagePath = context.Request.QueryString["path"];
var width = int.Parse(context.Request.QueryString["w"] ?? "150");
var height = int.Parse(context.Request.QueryString["h"] ?? "150");
if (string.IsNullOrEmpty(imagePath))
{
context.Response.StatusCode = 400;
context.Response.Write("Missing image path");
return;
}
var fullPath = context.Server.MapPath("~/Images/" + imagePath);
if (!File.Exists(fullPath))
{
context.Response.StatusCode = 404;
return;
}
try
{
using (var image = new Bitmap(fullPath))
{
using (var thumbnail = image.GetThumbnailImage(width, height,
() => false, IntPtr.Zero))
{
context.Response.ContentType = "image/jpeg";
using (var ms = new MemoryStream())
{
thumbnail.Save(ms, ImageFormat.Jpeg);
context.Response.BinaryWrite(ms.ToArray());
}
}
}
}
catch (Exception ex)
{
context.Response.StatusCode = 500;
context.Response.ContentType = "text/plain";
context.Response.Write("Error generating thumbnail: " + ex.Message);
}
}
}
}
Changes Made:
1. using System.Web; → using BlazorWebFormsComponents;
2. : IHttpHandler → : HttpHandlerBase
3. ProcessRequest(HttpContext context) → async Task ProcessRequestAsync(HttpHandlerContext context)
4. Register with app.MapHandler<ThumbnailHandler>("/Handlers/Thumbnail.ashx") in Program.cs
Note
Image generation logic transfers directly. No rewrite needed for System.Drawing code — the API is identical between Web Forms and Core.
API Reference¶
HttpHandlerContext¶
The context object passed to ProcessRequestAsync. It wraps the ASP.NET Core HttpContext and provides familiar Web Forms properties.
public class HttpHandlerContext
{
public HttpHandlerRequest Request { get; } // Request data
public HttpHandlerResponse Response { get; } // Response output
public HttpHandlerServer Server { get; } // Server utilities
public ISession? Session { get; } // Session state (if [RequiresSessionState])
public ClaimsPrincipal User { get; } // Current user principal
public IDictionary<object, object> Items { get; } // Request-scoped items dictionary
}
HttpHandlerRequest¶
Encapsulates HTTP request data: query strings, form data, headers, files.
public class HttpHandlerRequest
{
// Query strings: context.Request.QueryString["id"]
public string? this[string key] { get; }
public NameValueCollection QueryString { get; }
// Form data (POST): context.Request.Form["field"]
public NameValueCollection Form { get; }
// File uploads: context.Request.Files["upload"]
public IFormFileCollection Files { get; }
// HTTP method: "GET", "POST", etc.
public string HttpMethod { get; }
// Request headers
public IHeaderDictionary Headers { get; }
// Request content type
public string? ContentType { get; }
// Raw request stream
public Stream InputStream { get; }
// Request URL information
public string Path { get; }
public string RawUrl { get; }
public string Url { get; }
// Is the request authenticated?
public bool IsAuthenticated { get; }
// Client IP address
public string RemoteAddr { get; }
}
Common Usage:
var userId = context.Request.QueryString["id"]; // GET parameter
var userName = context.Request.Form["username"]; // POST field
var file = context.Request.Files["upload"]; // Uploaded file
if (context.Request.IsAuthenticated)
{
// User is logged in
}
HttpHandlerResponse¶
Controls HTTP response output: content type, status code, headers, body content.
public class HttpHandlerResponse
{
// Content type (e.g., "application/json", "image/png")
public string ContentType { get; set; }
// HTTP status code (200, 404, 500, etc.)
public int StatusCode { get; set; }
// Response stream for binary writes
public Stream OutputStream { get; }
// Write text to response (sync)
public void Write(string text);
// Write text to response (async) — preferred
public Task WriteAsync(string text);
// Write binary data to response (sync)
public void BinaryWrite(byte[] data);
// Write binary data to response (async) — preferred
public Task BinaryWriteAsync(byte[] data);
// Add a response header
public void AddHeader(string name, string value);
// Clear response content and headers
public void Clear();
// [Obsolete] In Web Forms, End() threw ThreadAbortException.
// In Core, it sets a flag. Use 'return' instead.
[Obsolete("Use 'return' to exit handler.")]
public void End();
// Check if Response.End() was called
public bool IsEnded { get; }
}
Common Usage:
// Set response type and status
context.Response.ContentType = "application/json";
context.Response.StatusCode = 200;
// Write content
context.Response.Write("{\"message\":\"success\"}");
// Add header for download
context.Response.AddHeader("Content-Disposition",
"attachment; filename=\"report.pdf\"");
// For file downloads, clear and write binary
context.Response.Clear();
context.Response.ContentType = "application/pdf";
context.Response.BinaryWrite(fileBytes);
HttpHandlerServer¶
Server-side utilities: path mapping, encoding/decoding.
public class HttpHandlerServer
{
// Map virtual path to physical file path
// ~/path → webroot, /path → content root
public string MapPath(string virtualPath);
// HTML-encode a string (prevent XSS)
public string HtmlEncode(string text);
// HTML-decode a string
public string HtmlDecode(string text);
// URL-encode a string
public string UrlEncode(string text);
// URL-decode a string
public string UrlDecode(string text);
}
Common Usage:
// Get physical file path from virtual path
var file = context.Server.MapPath("~/App_Data/users.xml");
// Safe HTML output (prevent XSS)
var safe = context.Server.HtmlEncode("<script>alert(1)</script>");
// Result: "<script>alert(1)</script>"
// URL encoding for query strings
var encoded = context.Server.UrlEncode("hello world");
// Result: "hello+world"
Session State¶
If your handler uses session state (e.g., storing user preferences, shopping cart), mark it with the [RequiresSessionState] attribute. The framework will automatically load session before invoking your handler.
Enabling Session in Program.cs¶
First, configure session in your Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Add session services
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(20);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
var app = builder.Build();
// Enable session middleware
app.UseSession();
// Register handlers (after UseSession)
app.MapHandler<ShoppingCartHandler>("/Handlers/ShoppingCart.ashx");
app.Run();
Using Session in a Handler¶
Mark the handler with [RequiresSessionState]:
[RequiresSessionState]
public class ShoppingCartHandler : HttpHandlerBase
{
public override async Task ProcessRequestAsync(HttpHandlerContext context)
{
var action = context.Request.QueryString["action"];
if (action == "add")
{
var productId = context.Request.Form["productId"];
var quantity = int.Parse(context.Request.Form["quantity"] ?? "1");
// Get cart from session (returns null if not set)
var cart = context.Session?.GetObject<List<CartItem>>("Cart")
?? new List<CartItem>();
cart.Add(new CartItem { ProductId = productId, Quantity = quantity });
// Save cart back to session
context.Session?.SetObject("Cart", cart);
context.Response.ContentType = "application/json";
context.Response.Write("{\"status\":\"added\"}");
}
else if (action == "view")
{
var cart = context.Session?.GetObject<List<CartItem>>("Cart")
?? new List<CartItem>();
context.Response.ContentType = "application/json";
var json = JsonSerializer.Serialize(new { items = cart, count = cart.Count });
context.Response.Write(json);
}
}
}
public class CartItem
{
public string ProductId { get; set; }
public int Quantity { get; set; }
}
Warning
Session is cookie-based in ASP.NET Core, not in-process. If you relied on Web Forms session state server or SQL session store, you must reconfigure that separately. For now, session is volatile and lost on app restart.
Note
GetObject<T>() and SetObject<T>() are extension methods on ISession provided by BWFC. They use System.Text.Json to serialize/deserialize objects.
What's Not Supported¶
Some Web Forms patterns cannot be shimmed in ASP.NET Core. Here's a clear list and recommended workarounds:
Response.End() — ThreadAbort Pattern¶
Web Forms:
context.Response.Write("Data");
context.Response.End(); // Threw ThreadAbortException, halted execution
Blazor:
Warning
In Web Forms, Response.End() threw an exception to stop the handler mid-execution. ASP.NET Core has no equivalent. The HttpHandlerResponse.End() method is marked [Obsolete] and only sets a flag. You must change Response.End() to return.
Server.Transfer() — Server-Side Redirect¶
Not Supported. Server.Transfer() re-executes another handler in the same request without a round-trip. ASP.NET Core has no equivalent.
Workaround: Use Response.Redirect() or restructure as a service method:
// Web Forms (not supported)
// context.Server.Transfer("/Handlers/Other.ashx?id=123");
// Blazor alternative 1: Redirect (browser round-trip)
context.Response.Redirect("/Handlers/Other.ashx?id=123");
// Blazor alternative 2: Refactor as shared service
var handler = new OtherHandler();
await handler.ProcessRequestAsync(context);
Application["key"] — Global State¶
Not Supported. Application was a global dictionary shared across requests. ASP.NET Core discourages this pattern.
Workaround: Use dependency injection or IMemoryCache:
// Web Forms (not supported)
// var count = (int)context.Application["RequestCount"];
// Blazor: Use DI + singleton service
public class StatsHandler : HttpHandlerBase
{
private readonly AppStatisticsService _stats;
public StatsHandler(AppStatisticsService stats)
{
_stats = stats;
}
public override async Task ProcessRequestAsync(HttpHandlerContext context)
{
var count = _stats.GetRequestCount();
context.Response.Write(count.ToString());
}
}
// Register in Program.cs
builder.Services.AddSingleton<AppStatisticsService>();
// ...
app.MapHandler<StatsHandler>("/api/Stats.ashx");
context.Cache — Web Forms Caching¶
Not Supported. Web Forms System.Web.Caching.Cache doesn't exist in Core.
Workaround: Use IMemoryCache:
// Web Forms (not supported)
// var cached = context.Cache["key"];
// Blazor: Inject IMemoryCache
public class CachedDataHandler : HttpHandlerBase
{
private readonly IMemoryCache _cache;
public CachedDataHandler(IMemoryCache cache)
{
_cache = cache;
}
public override async Task ProcessRequestAsync(HttpHandlerContext context)
{
if (!_cache.TryGetValue("data", out List<Data> data))
{
data = await LoadDataFromDatabase();
_cache.Set("data", data, TimeSpan.FromMinutes(5));
}
context.Response.ContentType = "application/json";
context.Response.Write(JsonSerializer.Serialize(data));
}
}
// Register in Program.cs
builder.Services.AddMemoryCache();
// ...
app.MapHandler<CachedDataHandler>("/api/Data.ashx");
Server.Execute() — Execute Without Transfer¶
Not Supported. Similar to Server.Transfer() — there's no way to execute another handler and return to the caller.
Workaround: Refactor as a service or async method.
Complex Request.Files Scenarios¶
Partially Supported. The IFormFile API in Core differs from Web Forms HttpPostedFile.
Web Forms:
var file = context.Request.Files["upload"];
file.SaveAs(context.Server.MapPath("~/uploads/" + file.FileName));
Blazor:
var file = context.Request.Files["upload"];
var path = context.Server.MapPath("~/uploads/" + file.FileName);
using (var stream = System.IO.File.Create(path))
{
await file.CopyToAsync(stream);
}
The logic is the same; the API is slightly different.
Interaction with AshxHandlerMiddleware¶
If your application uses the AshxHandlerMiddleware (which returns 410 Gone for old .ashx files), migrated handlers will bypass the middleware and be handled by the routing system instead.
Setup:
// Program.cs
var app = builder.Build();
// Ashx middleware (returns 410 for unmigrated handlers)
app.UseMiddleware<AshxHandlerMiddleware>();
// Handler routing (takes precedence over middleware for migrated handlers)
app.MapHandler<ProductApiHandler>("/api/Products.ashx");
// Other middleware...
app.Run();
How it works:
1. Request arrives for /api/Products.ashx
2. AshxHandlerMiddleware runs first, checks: "Is there a handler registered for this path?"
3. Yes → middleware passes through (short-circuits)
4. ASP.NET Core routing finds the MapHandler<T>() endpoint and executes it
5. If no handler found, middleware returns 410 Gone (old handlers are gone)
This setup lets you migrate handlers incrementally: old ones return 410; new ones work normally.
Dependency Injection in Handlers¶
Handlers support constructor injection. Register your services in Program.cs and they'll be available:
public class UserApiHandler : HttpHandlerBase
{
private readonly UserRepository _repo;
public UserApiHandler(UserRepository repo)
{
_repo = repo;
}
public override async Task ProcessRequestAsync(HttpHandlerContext context)
{
var users = await _repo.GetAllAsync();
context.Response.ContentType = "application/json";
context.Response.Write(JsonSerializer.Serialize(users));
}
}
// Program.cs
builder.Services.AddScoped<UserRepository>();
// ...
app.MapHandler<UserApiHandler>("/api/users.ashx");
Testing Handlers¶
Use TestServer to test handlers in isolation:
// xUnit test
[Fact]
public async Task GetProducts_ReturnsJson()
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddBlazorWebFormsComponents();
})
.Configure(app =>
{
app.MapHandler<ProductApiHandler>("/api/Products.ashx");
});
using (var server = new TestServer(builder))
using (var client = server.CreateClient())
{
var response = await client.GetAsync("/api/Products.ashx?action=list");
Assert.True(response.IsSuccessStatusCode);
Assert.Contains("application/json", response.Content.Headers.ContentType.ToString());
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("Widget", content);
}
}
Common Patterns and Examples¶
Example: JSON POST Handler with Form Data¶
public class FormSubmitHandler : HttpHandlerBase
{
private readonly FormRepository _repo;
public FormSubmitHandler(FormRepository repo)
{
_repo = repo;
}
public override async Task ProcessRequestAsync(HttpHandlerContext context)
{
if (context.Request.HttpMethod != "POST")
{
context.Response.StatusCode = 405;
context.Response.Write("{\"error\":\"Method not allowed\"}");
return;
}
var name = context.Request.Form["name"];
var email = context.Request.Form["email"];
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(email))
{
context.Response.StatusCode = 400;
context.Response.ContentType = "application/json";
context.Response.Write("{\"error\":\"Missing fields\"}");
return;
}
await _repo.SaveFormAsync(new { name, email });
context.Response.ContentType = "application/json";
context.Response.Write("{\"status\":\"submitted\"}");
}
}
// Program.cs
app.MapHandler<FormSubmitHandler>("/api/Submit.ashx");
Example: Authenticated Handler (GET only)¶
public class ExportHandler : HttpHandlerBase
{
public override async Task ProcessRequestAsync(HttpHandlerContext context)
{
if (!context.User.Identity?.IsAuthenticated ?? true)
{
context.Response.StatusCode = 403;
context.Response.Write("Unauthorized");
return;
}
var data = await GetExportDataAsync();
var csv = ConvertToCSV(data);
context.Response.ContentType = "text/csv";
context.Response.AddHeader("Content-Disposition",
"attachment; filename=\"export.csv\"");
context.Response.Write(csv);
}
}
// Program.cs
app.MapHandler<ExportHandler>("/secure/export.ashx")
.RequireAuthorization();
Troubleshooting¶
| Problem | Solution |
|---|---|
| Handler returns 404 | Verify the route pattern in MapHandler<T>() matches the request URL. Check that the MapHandler call exists in Program.cs. |
| Session state is null | Add [RequiresSessionState] attribute. Ensure app.UseSession() is called in Program.cs. |
| "Response.End() was called" warning | Remove Response.End() and use return instead. |
| Dependency injection fails | Register services in Program.cs before builder.Build(). |
| File upload not working | Check content type is multipart/form-data. Use context.Request.Files["fieldName"]. |
| CORS blocked | Use .RequireCors() on the handler route. Ensure CORS policy is registered. |
| Old handler returns 410 | This is correct — old .ashx files should return 410 until migrated. Use AshxHandlerMiddleware for this. |
Summary¶
Migrating .ashx handlers to Blazor with HttpHandlerBase is straightforward:
- 6 mechanical changes per handler (shown in Quick Start)
- Familiar API surface —
context.Request,context.Response,context.Serverwork as in Web Forms - Modern routing — Use
MapHandler<T>()inProgram.csfor explicit route registration - DI support — Inject services via constructor
- Session state — Mark with
[RequiresSessionState], configure inProgram.cs - What's unsupported —
Response.End(),Server.Transfer(), global state — listed with workarounds
Your handler logic stays nearly identical. You're just updating the registration and removing obsolete patterns.
For questions or edge cases, refer to the API Reference section or consult the BlazorWebFormsComponents repository.