On this page

Skip to content

An Exploration of Best Practices for File Uploads in ASP.NET Core Web API

This article documents my thoughts and experiences regarding file upload handling in Web API development. It does not cover specific implementation code. Due to the scope of this article, advanced topics such as chunked uploads are not included.

Development Patterns and Data Reception

  • Full-stack Development: Without specific AJAX handling, most scenarios involve submitting form data directly, with the backend using [FromForm] to receive the data.
  • Frontend-Backend Separation: Most scenarios use AJAX to transmit JSON, with the backend using [FromBody] to receive the data.

Two Main Processing Methods

Method 1: Using Form Submit or FormData

Pros:

  • Does not increase data transmission volume.

Cons:

  • In ASP.NET Core Web API, [FromBody] and [FromForm] use different ModelBinder implementations when binding complex types. If you need to handle custom types, you must write a new ModelBinder for [FromForm] and a new JsonConverter for [FromBody].
  • Having two different processing methods in the system can confuse developers during maintenance, leading to inconsistent development standards.
  • Significant impact on the frontend; the transmission method must be completely changed, especially if file upload functionality is added to an existing API.

Method 2: Using JSON to Transmit Base64 Strings

Pros:

  • Convenient for both frontend and backend; no extra processing required.
  • High API format consistency.

Cons:

  • Data size increases by approximately 1.33x, which may cause performance issues.

Conflict Case Between Frontend and Backend Engineers

While transmitting Base64 via JSON is convenient for both sides, a backend engineer at my previous company refused to use this method and switched to [FromForm], expecting the frontend to use FormData. This led to a dispute.

I cannot say that the engineer's approach was wrong—they might have been aiming for better performance—but when a team already has established practices, communication should precede implementation. I did not delve into their specific approach at the time, so I am unsure if they intended to split file uploads into a separate API or convert all file-related APIs to [FromForm]. If it were the latter, the dispute would be more understandable.

For the backend, this change is minor, requiring only the addition of the [FromForm] attribute to complex types. However, for the frontend, it means the entire transmission method must be adjusted.

This is an approach shared by a colleague a few months ago, which I find to be relatively superior:

  1. Create a separate file upload API (using [FromForm]).
  2. Record file information in a file data table and return a file ID.
  3. Pass the file ID, rather than the file content, in the main business data creation/update API.

Pros:

  • Does not increase data transmission volume.
  • Simpler for the frontend to handle.
  • High API format consistency.

Note: This approach also solves a problem I encountered during implementation. Originally, when uploading files, the business data creation API lacked a file ID, but updates might require changing a specific file. This forced the frontend to pass an ID additionally, resulting in inconsistent formats between creation and update APIs. With this method, both only need to pass the file ID, maintaining consistency.

Implementation Details

Based on my colleague's sharing and my own practical experience, I have summarized the following implementation details:

  1. File Data Structure Design.

    • Create a dedicated file attachment table to record filename, storage path, file size, download count, creation time, and enabled status.
    • The main business table records the file ID, or a mapping table is created to handle one-to-many relationships.
  2. File Upload Workflow.

    • The frontend calls the file upload API first to send the file directly to the backend.
    • The backend stores the file on the file server and writes the file information into the file table.
    • The backend returns the file ID to the frontend.
    • When the frontend creates/updates business data, it only needs to pass the file ID.
  3. Data Management Mechanism.

    • When business data is created, mark the file status as "Enabled".
    • Set up a scheduled task to periodically clean up files that have not been enabled for more than a day, along with their database records, to avoid occupying excessive space.
    • When deleting business data, you can choose to delete the associated files or keep them but mark them as "Disabled".
  4. File Data Validation.

    • Create a custom ValidationAttribute to verify the validity of the file data.
    • Since file information is stored in the database, you can obtain it within the validation attribute using Dependency Injection (DI):
    csharp
    protected override ValidationResult IsValid(
        object value, ValidationContext validationContext
    ) {
        using IServiceScope scope = validationContext.CreateScope();
        // Get the corresponding Service, Repository, or EfContext
        IFileService service = scope.ServiceProvider.GetRequiredService<IFileService>();
    }
    • Note the limitations of file type validation: it cannot detect file extension spoofing. Even with file signature identification, except for .exe files, most file types are either difficult to identify or prone to false positives.
    • IValidatableObject's Validate method can also use this injection approach, but I personally dislike fetching database data within Request Input. For logical validation involving database data, I prefer handling it in the Action or Service layer.

Extended Discussion: Considerations for File Download Implementation

After decoupling file uploads into a separate API, file download implementation also faces two design choices:

1. Generic File Download API (FileController):

  • Pros: Simple implementation, one API fits all scenarios, high code reusability.
  • Cons: Difficult permission control; cannot set granular permission checks for different business scenarios.

2. Implementing Download API in Business Logic Controllers (OrderController.DownloadInvoice()):

  • Pros: Can implement fine-grained permission control, high integration with business logic.
  • Cons: Each business module needs to write similar download logic, higher implementation complexity.

Personal Preference

I prefer integrating file download functionality into the business logic Controller. If the final requirement is purely downloading without any additional permission control logic, it might be considered over-engineering. Conversely, if a generic download method is used, the cost of modification will be higher when you eventually encounter files requiring permission control, or you will need to build additional handling mechanisms for specific files.

To reduce code duplication, you can create a base file download service class to encapsulate common logic, which can then be used by various business modules via Dependency Injection.

Change Log

  • 2025-03-10 Initial version created.