Canducci.FileUpload.FluentValidation 1.0.0.3

There is a newer version of this package available.
See the version list below for details.
dotnet add package Canducci.FileUpload.FluentValidation --version 1.0.0.3
                    
NuGet\Install-Package Canducci.FileUpload.FluentValidation -Version 1.0.0.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Canducci.FileUpload.FluentValidation" Version="1.0.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Canducci.FileUpload.FluentValidation" Version="1.0.0.3" />
                    
Directory.Packages.props
<PackageReference Include="Canducci.FileUpload.FluentValidation" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Canducci.FileUpload.FluentValidation --version 1.0.0.3
                    
#r "nuget: Canducci.FileUpload.FluentValidation, 1.0.0.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Canducci.FileUpload.FluentValidation@1.0.0.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Canducci.FileUpload.FluentValidation&version=1.0.0.3
                    
Install as a Cake Addin
#tool nuget:?package=Canducci.FileUpload.FluentValidation&version=1.0.0.3
                    
Install as a Cake Tool

Canducci.FileUpload

A file upload library for ASP.NET Core with validation, flexible configuration, and support for synchronous and asynchronous operations.

NuGet NuGet Downloads .NET

Portuguese: README_PTBR.md

Version 1.0.0.3 (stable version)

What's New

  • FileUploadStatus enum: Replaces the ambiguous Success boolean property with a clear status indicator
    • AllSucceeded - All files were uploaded successfully
    • PartialSuccess - Some files succeeded, some failed
    • AllFailed - All files failed to upload
    • Empty - No files were provided
  • SuccessCount property: Returns the count of successfully uploaded files
  • FailureCount property: Returns the count of failed uploads
  • Complete XML documentation in English for all public APIs
  • FileUploadValidationAttribute: Integration with ASP.NET Core Data Annotations
  • Multiple FileUploadOptions constructors: Different ways to initialize with various constraints
  • Case-insensitive extension validation: Safe extension comparison
  • .NET 8, 9, and 10 support: Compatibility with multiple .NET versions
  • 91 comprehensive unit tests: Complete test coverage for reliability

Installation

Package Manager

Install-Package Canducci.FileUpload

.NET CLI

dotnet add package Canducci.FileUpload

PackageReference

<PackageReference Include="Canducci.FileUpload" Version="1.0.0.3" />

Configuration

Program.cs

Add the service in Program.cs:

using Canducci.FileUpload.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Add FileUpload with default configuration (random filename)
builder.Services.AddFileUpload();

// OR with custom configuration (GUID filename)
builder.Services.AddFileUpload(config =>
{
    config.FileUploadConfigurationName = FileUploadConfigurationName.Guid;
});

var app = builder.Build();

Configuration Options

Option Description
FileUploadConfigurationName.Random Generates random filename (default)
FileUploadConfigurationName.Guid Generates GUID-based filename

Controller Usage

Basic Example

using Canducci.FileUpload;
using Canducci.FileUpload.Enuns;
using Canducci.FileUpload.Interfaces;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class UploadController : ControllerBase
{
    [HttpPost]
    public IActionResult Upload(IFileUpload fileUpload)
    {
        var result = fileUpload.Save("uploads", options =>
        {
            options.MaxFileSizeBytes = 5 * 1024 * 1024; // 5 MB
            options.RejectEmptyFiles = true;
            options.AllowedExtensions.Add(".jpg");
            options.AllowedExtensions.Add(".png");
            options.AllowedExtensions.Add(".pdf");
        });

        switch (result.Status)
        {
            case FileUploadStatus.AllSucceeded:
                return Ok(new
                {
                    Message = "Upload successful!",
                    SuccessCount = result.SuccessCount,
                    Files = result.Files.Select(f => new
                    {
                        f.FileNameSent,
                        f.FileNameSave,
                        f.Extension,
                        f.Length,
                        f.Path
                    })
                });

            case FileUploadStatus.PartialSuccess:
                return Ok(new
                {
                    Message = "Some files uploaded successfully",
                    SuccessCount = result.SuccessCount,
                    FailureCount = result.FailureCount,
                    Files = result.Files.Where(f => f.Success).Select(f => f.FileNameSave),
                    Errors = result.Files.Where(f => !f.Success).Select(f => new
                    {
                        f.FileNameSent,
                        Error = f.Exception?.Message
                    })
                });

            case FileUploadStatus.AllFailed:
                return BadRequest(new
                {
                    Message = "All files failed to upload",
                    FailureCount = result.FailureCount,
                    Errors = result.Files.Select(f => new
                    {
                        f.FileNameSent,
                        Error = f.Exception?.Message
                    })
                });

            case FileUploadStatus.Empty:
            default:
                return BadRequest(new { Message = "No files provided" });
        }
    }
}

Async Example

[HttpPost("async")]
public async Task<IActionResult> UploadAsync(IFileUpload fileUpload, CancellationToken cancellationToken)
{
    var result = await fileUpload.SaveAsync("uploads", options =>
    {
        options.MaxFileSizeBytes = 10 * 1024 * 1024; // 10 MB
        options.RejectEmptyFiles = true;
    }, cancellationToken);

    if (result.Status == FileUploadStatus.AllSucceeded)
    {
        return Ok(new { Message = "Async upload completed!", result.SuccessCount });
    }

    return BadRequest(new { Message = "Upload failed", result.FailureCount });
}

Multiple Files Example

[HttpPost("multiple")]
public IActionResult UploadMultiple(IFileUpload fileUpload)
{
    if (fileUpload.Count == 0)
    {
        return BadRequest("No files uploaded");
    }

    var result = fileUpload.Save("documents", options =>
    {
        options.MaxFileSizeBytes = 20 * 1024 * 1024; // 20 MB
        options.AllowedExtensions.Add(".docx");
        options.AllowedExtensions.Add(".xlsx");
        options.AllowedExtensions.Add(".pdf");
    });

    return Ok(new
    {
        Status = result.Status.ToString(),
        TotalUploaded = fileUpload.Count,
        Succeeded = result.SuccessCount,
        Failed = result.FailureCount,
        Files = result.Files.Where(f => f.Success).Select(f => f.FileNameSave),
        Errors = result.Files.Where(f => !f.Success).Select(f => new { f.FileNameSent, Error = f.Exception?.Message })
    });
}

Predefined Options Example

[HttpPost("images")]
public IActionResult UploadImages(IFileUpload fileUpload)
{
    // Parameterized constructor with multiple extensions
    var options = new FileUploadOptions(2 * 1024 * 1024, true, ".jpg", ".jpeg", ".png", ".gif", ".webp");

    var result = fileUpload.Save("images", options);

    return result.Status == FileUploadStatus.AllSucceeded
        ? Ok(result.Files.Select(f => f.Path)) 
        : BadRequest($"Failed: {result.FailureCount} of {fileUpload.Count} images");
}

Fluent API Example

[HttpPost("fluent")]
public IActionResult UploadFluent(IFileUpload fileUpload)
{
    var options = new FileUploadOptions();
    options.AllowedExtensions
        .AddImages()      // All 10 image extensions
        .AddDocuments()   // All 10 document extensions
        .AddArchives();   // All 5 archive extensions

    var result = fileUpload.Save("uploads", options);
    return Ok(result);
}

Data Annotations Example

using System.ComponentModel.DataAnnotations;
using Canducci.FileUpload.Attributes;

public class FileUploadModel
{
    [FileUploadValidation(
        MaxFileSizeBytes = 5242880, 
        RejectEmptyFiles = true,
        AllowedExtensions = new[] { ".pdf", ".docx", ".xlsx" }
    )]
    public IFileUpload Files { get; set; }
}

[HttpPost("validated")]
public IActionResult ValidatedUpload(FileUploadModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var result = model.Files.Save("documents");
    return Ok(result);
}

Validation Options

Option Type Default Description
MaxFileSizeBytes long 10485760 (10 MB) Maximum file size in bytes
RejectEmptyFiles bool true Reject empty files (0 bytes)
AllowedExtensions HashSet<string> Empty Allowed extensions (empty = all)

FileUploadOptions Constructors

// Default constructor with default values
var options1 = new FileUploadOptions();

// Constructor with max size and empty file rejection
var options2 = new FileUploadOptions(5_000_000, false);

// Constructor with all constraints
var options3 = new FileUploadOptions(2_000_000, true, ".pdf", ".docx", ".xlsx");

Result Properties

IFileUploadResult

Property Type Description
Status FileUploadStatus Overall upload operation status
SuccessCount int Number of successfully uploaded files
FailureCount int Number of failed uploads
Files IReadOnlyList<IFileItemResult> List of per-file results

FileUploadStatus Enum

Value Description
AllSucceeded All files were uploaded successfully
PartialSuccess Some files succeeded, some failed
AllFailed All files failed to upload
Empty No files were provided for upload

IFileItemResult

Property Type Description
Success bool Whether the file was saved successfully
FileNameSent string Original filename sent by client
FileNameSave string Filename saved on server
Extension string File extension
Length long File size in bytes
Directory string Directory where file was saved
Path string Full file path
Exception Exception? Exception if error occurred

Error Messages

Message Description
File is Empty File is empty and RejectEmptyFiles = true
File is TooLarge File exceeds MaxFileSizeBytes
ExtensionNotAllowed Extension not in AllowedExtensions

Complete Controller Example

using Canducci.FileUpload;
using Canducci.FileUpload.Enuns;
using Canducci.FileUpload.Interfaces;
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Controllers;

[ApiController]
[Route("api/[controller]")]
public class FilesController : ControllerBase
{
    [HttpPost("upload")]
    [RequestSizeLimit(50 * 1024 * 1024)] // 50 MB request limit
    public async Task<IActionResult> Upload(
        IFileUpload fileUpload,
        CancellationToken cancellationToken)
    {
        if (fileUpload.Count == 0)
        {
            return BadRequest(new { Error = "No files uploaded" });
        }

        // Using parameterized constructor
        var options = new FileUploadOptions(
            maxFileSizeBytes: 10 * 1024 * 1024,
            rejectEmptyFiles: true,
            allowedExtensions: ".jpg", ".jpeg", ".png", ".pdf", ".docx");

        var result = await fileUpload.SaveAsync("uploads", options, cancellationToken);

        return result.Status switch
        {
            FileUploadStatus.AllSucceeded => Ok(new
            {
                Message = "All files saved successfully",
                Count = result.SuccessCount,
                Files = result.Files.Select(f => new { f.FileNameSave, f.Path })
            }),
            FileUploadStatus.PartialSuccess => Ok(new
            {
                Message = "Some files were saved",
                Succeeded = result.SuccessCount,
                Failed = result.FailureCount
            }),
            FileUploadStatus.AllFailed => BadRequest(new
            {
                Message = "Failed to save all files",
                Errors = result.Files.Select(f => f.Exception?.Message)
            }),
            _ => BadRequest(new { Error = "No files provided" })
        };
    }

    [HttpPost("images")]
    public IActionResult UploadImages(IFileUpload fileUpload)
    {
        var options = new FileUploadOptions();
        options.AllowedExtensions.AddImages(); // All image extensions

        var result = fileUpload.Save("images", options);

        return result.Status == FileUploadStatus.AllSucceeded
            ? Ok(result.Files)
            : BadRequest(result.Files.Where(f => !f.Success));
    }

    [HttpPost("documents")]
    public IActionResult UploadDocuments(IFileUpload fileUpload)
    {
        var options = new FileUploadOptions(5 * 1024 * 1024, true);
        options.AllowedExtensions.AddDocuments(); // All document extensions

        var result = fileUpload.Save("documents", options);
        return Ok(result);
    }
}

Security Features

  • Filename sanitization: Removes dangerous characters and handles path traversal
  • Case-insensitive extension validation: Protects against case-based bypasses
  • Overwrite prevention: Generates unique names using UUID or random
  • Atomic save: Saves to temporary file first, then renames atomically
  • CancellationToken support: Safely cancels long-running operations
  • Granular error handling: Returns detailed status per file

Supported File Extensions

The library provides constants and fluent methods for common extensions:

Images (10 extensions)

.jpg, .jpeg, .png, .gif, .webp, .svg, .bmp, .tiff, .tif, .heic

Documents (10 extensions)

.pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .txt, .csv, .odt

Archives (5 extensions)

.zip, .rar, .7z, .tar, .gz

Audio/Video (5 extensions)

.mp3, .wav, .mp4, .mkv, .avi

Library Highlights

Developed with focus on:

  • ✅ Security: Rigorous validation and protection against common attacks
  • ✅ Performance: Async operations and streaming for large files
  • ✅ Usability: Simple and intuitive API with Fluent API support
  • ✅ Documentation: Complete XML comments in English
  • ✅ Testing: Comprehensive 91 unit test suite
  • ✅ Compatibility: Supports .NET 8, 9, and 10

What's New

  • FileUploadStatus enum: Replaces the ambiguous Success boolean property with a clear status indicator
    • AllSucceeded - All files were uploaded successfully
    • PartialSuccess - Some files succeeded, some failed
    • AllFailed - All files failed to upload
    • Empty - No files were provided
  • SuccessCount property: Returns the count of successfully uploaded files
  • FailureCount property: Returns the count of failed uploads
  • Complete XML documentation in English for all public APIs

Installation

Package Manager

Install-Package Canducci.FileUpload

.NET CLI

dotnet add package Canducci.FileUpload

PackageReference

<PackageReference Include="Canducci.FileUpload" Version="1.0.0.1" />

Configuration

Program.cs

Add the service in Program.cs:

using Canducci.FileUpload.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Add FileUpload with default configuration (random filename)
builder.Services.AddFileUpload();

// OR with custom configuration (GUID filename)
builder.Services.AddFileUpload(config =>
{
    config.FileUploadConfigurationName = FileUploadConfigurationName.Guid;
});

var app = builder.Build();

Configuration Options

Option Description
FileUploadConfigurationName.Random Generates random filename (default)
FileUploadConfigurationName.Guid Generates GUID-based filename

Controller Usage

Basic Example

using Canducci.FileUpload;
using Canducci.FileUpload.Enuns;
using Canducci.FileUpload.Interfaces;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class UploadController : ControllerBase
{
    [HttpPost]
    public IActionResult Upload(IFileUpload fileUpload)
    {
        var result = fileUpload.Save("uploads", options =>
        {
            options.MaxFileSizeBytes = 5 * 1024 * 1024; // 5 MB
            options.RejectEmptyFiles = true;
            options.AllowedExtensions.Add(".jpg");
            options.AllowedExtensions.Add(".png");
            options.AllowedExtensions.Add(".pdf");
        });

        switch (result.Status)
        {
            case FileUploadStatus.AllSucceeded:
                return Ok(new
                {
                    Message = "Upload successful!",
                    SuccessCount = result.SuccessCount,
                    Files = result.Files.Select(f => new
                    {
                        f.FileNameSent,
                        f.FileNameSave,
                        f.Extension,
                        f.Length,
                        f.Path
                    })
                });

            case FileUploadStatus.PartialSuccess:
                return Ok(new
                {
                    Message = "Some files uploaded successfully",
                    SuccessCount = result.SuccessCount,
                    FailureCount = result.FailureCount,
                    Files = result.Files.Where(f => f.Success).Select(f => f.FileNameSave),
                    Errors = result.Files.Where(f => !f.Success).Select(f => new
                    {
                        f.FileNameSent,
                        Error = f.Exception?.Message
                    })
                });

            case FileUploadStatus.AllFailed:
                return BadRequest(new
                {
                    Message = "All files failed to upload",
                    FailureCount = result.FailureCount,
                    Errors = result.Files.Select(f => new
                    {
                        f.FileNameSent,
                        Error = f.Exception?.Message
                    })
                });

            case FileUploadStatus.Empty:
            default:
                return BadRequest(new { Message = "No files provided" });
        }
    }
}

Async Example

[HttpPost("async")]
public async Task<IActionResult> UploadAsync(IFileUpload fileUpload, CancellationToken cancellationToken)
{
    var result = await fileUpload.SaveAsync("uploads", options =>
    {
        options.MaxFileSizeBytes = 10 * 1024 * 1024; // 10 MB
        options.RejectEmptyFiles = true;
    }, cancellationToken);

    if (result.Status == FileUploadStatus.AllSucceeded)
    {
        return Ok(new { Message = "Async upload completed!", result.SuccessCount });
    }

    return BadRequest(new { Message = "Upload failed", result.FailureCount });
}

Multiple Files Example

[HttpPost("multiple")]
public IActionResult UploadMultiple(IFileUpload fileUpload)
{
    if (fileUpload.Count == 0)
    {
        return BadRequest("No files uploaded");
    }

    var result = fileUpload.Save("documents", options =>
    {
        options.MaxFileSizeBytes = 20 * 1024 * 1024; // 20 MB
        options.AllowedExtensions.Add(".docx");
        options.AllowedExtensions.Add(".xlsx");
        options.AllowedExtensions.Add(".pdf");
    });

    return Ok(new
    {
        Status = result.Status.ToString(),
        TotalUploaded = fileUpload.Count,
        Succeeded = result.SuccessCount,
        Failed = result.FailureCount,
        Files = result.Files.Where(f => f.Success).Select(f => f.FileNameSave),
        Errors = result.Files.Where(f => !f.Success).Select(f => new { f.FileNameSent, Error = f.Exception?.Message })
    });
}

Predefined Options Example

[HttpPost("images")]
public IActionResult UploadImages(IFileUpload fileUpload)
{
    var options = new FileUploadOptions
    {
        MaxFileSizeBytes = 2 * 1024 * 1024, // 2 MB
        RejectEmptyFiles = true
    };
    options.AllowedExtensions.Add(".jpg");
    options.AllowedExtensions.Add(".jpeg");
    options.AllowedExtensions.Add(".png");
    options.AllowedExtensions.Add(".gif");
    options.AllowedExtensions.Add(".webp");

    var result = fileUpload.Save("images", options);

    return result.Status == FileUploadStatus.AllSucceeded
        ? Ok(result.Files.Select(f => f.Path)) 
        : BadRequest($"Failed: {result.FailureCount} of {fileUpload.Count} images");
}

Validation Options

Option Type Default Description
MaxFileSizeBytes long 10485760 (10 MB) Maximum file size in bytes
RejectEmptyFiles bool true Reject empty files (0 bytes)
AllowedExtensions HashSet<string> Empty Allowed extensions (empty = all allowed)

Result Properties

IFileUploadResult

Property Type Description
Status FileUploadStatus Overall status of the upload operation
SuccessCount int Count of files uploaded successfully
FailureCount int Count of files that failed to upload
Files IReadOnlyList<IFileItemResult> List of results per file

FileUploadStatus Enum

Value Description
AllSucceeded All files were uploaded successfully
PartialSuccess Some files succeeded, some failed
AllFailed All files failed to upload
Empty No files were provided for upload

IFileItemResult

Property Type Description
Success bool Whether the file was saved successfully
FileNameSent string Original filename sent by client
FileNameSave string Filename saved on server
Extension string File extension
Length long File size in bytes
Directory string Directory where file was saved
Path string Full file path
Exception Exception? Exception if error occurred

Error Messages

Message Description
File is Empty Empty file and RejectEmptyFiles = true
File is TooLarge File exceeds MaxFileSizeBytes
ExtensionNotAllowed Extension not in AllowedExtensions

Complete Controller Example

using Canducci.FileUpload;
using Canducci.FileUpload.Enuns;
using Canducci.FileUpload.Interfaces;
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Controllers;

[ApiController]
[Route("api/[controller]")]
public class FilesController : ControllerBase
{
    [HttpPost("upload")]
    [RequestSizeLimit(50 * 1024 * 1024)] // 50 MB request limit
    public async Task<IActionResult> Upload(
        IFileUpload fileUpload,
        CancellationToken cancellationToken)
    {
        if (fileUpload.Count == 0)
        {
            return BadRequest(new { Error = "No files uploaded" });
        }

        var result = await fileUpload.SaveAsync("uploads", options =>
        {
            options.MaxFileSizeBytes = 10 * 1024 * 1024;
            options.RejectEmptyFiles = true;
            options.AllowedExtensions.Add(".jpg");
            options.AllowedExtensions.Add(".jpeg");
            options.AllowedExtensions.Add(".png");
            options.AllowedExtensions.Add(".pdf");
            options.AllowedExtensions.Add(".docx");
        }, cancellationToken);

        switch (result.Status)
        {
            case FileUploadStatus.AllSucceeded:
                return Ok(new
                {
                    Message = "Upload completed successfully",
                    SuccessCount = result.SuccessCount,
                    Files = result.Files.Select(f => new
                    {
                        Original = f.FileNameSent,
                        Saved = f.FileNameSave,
                        Size = f.Length,
                        FullPath = f.Path
                    })
                });

            case FileUploadStatus.PartialSuccess:
                return Ok(new
                {
                    Message = "Some files uploaded successfully",
                    SuccessCount = result.SuccessCount,
                    FailureCount = result.FailureCount,
                    SuccessfulFiles = result.Files.Where(f => f.Success).Select(f => f.FileNameSave),
                    Errors = result.Files.Where(f => !f.Success).Select(f => new
                    {
                        File = f.FileNameSent,
                        Error = f.Exception?.Message ?? "Unknown error"
                    })
                });

            case FileUploadStatus.AllFailed:
                return BadRequest(new
                {
                    Message = "All files failed to upload",
                    FailureCount = result.FailureCount,
                    Errors = result.Files.Select(f => new
                    {
                        File = f.FileNameSent,
                        Error = f.Exception?.Message ?? "Unknown error"
                    })
                });

            case FileUploadStatus.Empty:
            default:
                return BadRequest(new { Error = "No files provided" });
        }
    }
}

Example Projects

The Web example applications are located in the Web folder of the repository. Below are the sample projects with direct GitHub links and a short description of what each demonstrates.

Requirements

  • .NET 8.0 or later
  • ASP.NET Core

License

MIT License

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0.7 90 10/20/2026
1.0.0.5 86 10/13/2026
1.0.0.4 93 10/10/2026
1.0.0.3 103 10/5/2026