Learn · Migration

Moving Web Forms uploads to ASP.NET Core

Upload code is often the last thing ported, because it touches configuration, handlers, page lifecycle and storage all at once. The good news: every Web Forms concept has a direct Core equivalent, and the browser half barely changes.

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

  1. Stand up the Core endpoint and prove a small upload end to end.
  2. Port validation rules exactly, then test the rejections, not just the successes.
  3. Port the storage step, keeping the same naming and location as the Web Forms app.
  4. Move one low-risk page over and watch it in production before moving the rest.
  5. 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.
  6. 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.