The concept map
| Web Forms | ASP.NET Core |
IHttpHandler registered in Web.config | A mapped endpoint registered in Program.cs |
Server control with OnFileUploaded | A Tag Helper (or plain markup) plus a server-side event handler in options |
httpRuntime maxRequestLength | Options and Kestrel/IIS body-size limits, set in code |
Server.MapPath("~/App_Data") | IWebHostEnvironment.ContentRootPath |
| Storage logic inline in code-behind | A storage provider resolved from DI |
ViewStateUserKey for CSRF | Antiforgery services and token validation |
What does not change
The client-side engine is the same across the family. Chunking, retry, resume, direct-to-cloud transports, the image editor, validation options and locales behave identically — so the JavaScript configuration you already tuned carries over. What you are really porting is the server half.
Registration: from Web.config to Program.cs
Before — Web Forms
<handlers>
<remove name="AjaxUploader" />
<add name="AjaxUploader" verb="*" path="ajaxupload.axd"
type="AjaxUploader.AjaxUploaderHandler, AjaxUploader"
preCondition="integratedMode" />
</handlers>
After — ASP.NET Core
builder.Services.AddCoreUpload(options =>
{
options.MaxFileSizeBytes = 5L * 1024 * 1024 * 1024;
options.AllowedExtensions = [".jpg", ".png", ".pdf"];
options.ChunkSizeBytes = 5 * 1024 * 1024;
});
var app = builder.Build();
app.MapCoreUploadEndpoints();
Configuration that used to live in XML now lives in typed options, which means it is checked at compile time and can vary by environment without editing config transforms.
Server events
The Web Forms FileUploaded event becomes an IUploadEventHandler registered in DI — which means it can take dependencies (a database context, a logger) by constructor injection instead of reaching for statics. The shape of what you write — take the uploaded file, give it a name you generated, move it somewhere permanent — is unchanged.
public sealed class StoreUploadedFile : IUploadEventHandler
{
private readonly IUploaderProvider _provider;
public StoreUploadedFile(IUploaderProvider provider) => _provider = provider;
public async Task OnFileUploadedAsync(FileUploadedEvent e, CancellationToken ct = default)
{
var safeName = Guid.NewGuid().ToString("N") +
Path.GetExtension(e.FileName ?? string.Empty);
await _provider.MoveToAsync(e.FileGuid,
Path.Combine(uploadRoot, safeName), ct);
}
}
// Program.cs
builder.Services.AddSingleton<IUploadEventHandler, StoreUploadedFile>();
Size limits move into code
There is no maxRequestLength. Kestrel has its own maximum request body size, IIS keeps maxAllowedContentLength when hosted behind it, and the component enforces its own MaxFileSizeBytes. Set all three consistently — and, as in Web Forms, the enforcement that matters is the one applied while writing bytes, not the one that trusts a declared length. With chunking on, none of these limits is under pressure anyway, because no single request is large.
Running both stacks during the migration
Most migrations are gradual, with the old and new applications live at once. Three things make that period painless:
- Share the storage layout, not the code. If both write the same naming scheme into the same location, a page can move between stacks without a data migration.
- Keep upload URLs application-relative on both sides, so routing through a proxy does not silently point one app at the other's endpoint.
- Port the strictest validation first. While two servers accept uploads, your effective security is the weaker of the two.
A migration order that works
- Stand up the Core endpoint and prove a small upload end to end.
- Port validation rules exactly, then test the rejections, not just the successes.
- Port the storage step, keeping the same naming and location as the Web Forms app.
- Move one low-risk page over and watch it in production before moving the rest.
- Turn on chunking and confirm with a file genuinely larger than the chunk size — a smaller file takes the single-upload path and proves nothing.
- Retire the Web Forms handler last, once no page posts to it.
Which product for which stack
AjaxUploader is the Web Forms product and stays supported. CoreUpload is the ASP.NET Core product. MultipleUpload is the standalone JavaScript component for everything else. They share the same client engine, so migrating between them is a server-side change.