RepletoryLib.FileStorage.Abstractions 1.0.0

dotnet add package RepletoryLib.FileStorage.Abstractions --version 1.0.0
                    
NuGet\Install-Package RepletoryLib.FileStorage.Abstractions -Version 1.0.0
                    
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="RepletoryLib.FileStorage.Abstractions" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RepletoryLib.FileStorage.Abstractions" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="RepletoryLib.FileStorage.Abstractions" />
                    
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 RepletoryLib.FileStorage.Abstractions --version 1.0.0
                    
#r "nuget: RepletoryLib.FileStorage.Abstractions, 1.0.0"
                    
#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 RepletoryLib.FileStorage.Abstractions@1.0.0
                    
#: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=RepletoryLib.FileStorage.Abstractions&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=RepletoryLib.FileStorage.Abstractions&version=1.0.0
                    
Install as a Cake Tool

RepletoryLib.FileStorage.Abstractions

Provider-agnostic file storage interfaces, models, and options for upload, download, delete, copy, move, listing, and presigned URL generation.

Part of the RepletoryLib ecosystem -- standalone, reusable .NET 10 libraries with zero business logic.

NuGet .NET 10 License: MIT


Overview

RepletoryLib.FileStorage.Abstractions defines the core contracts and models for file storage operations. It provides IFileStorageService for full CRUD operations on files (upload, download, delete, copy, move, metadata, listing, presigned URLs) and IFilePathStrategy for generating organized storage paths with multi-tenant and date-based partitioning.

This package contains no implementations -- it is the abstraction layer that concrete providers (AWS S3, local filesystem, Azure Blob, etc.) implement. Reference this package in your application code to program against interfaces, then swap providers without changing business logic.

Key Features

  • IFileStorageService -- Full file storage contract: upload, bulk upload, download (stream and bytes), delete, bulk delete, copy, move, metadata, listing, presigned URLs, and public URLs
  • IFilePathStrategy -- Pluggable strategy for generating organized storage paths from contextual information
  • FileUploadRequest / FileUploadResult -- Request/result models with content type inference, visibility control, and custom metadata
  • BulkUploadResult -- Partial-success model for bulk uploads with per-file outcomes
  • FileMetadata -- Rich metadata model including size, timestamps, content type, visibility, and custom key-value pairs
  • FileListRequest / FileListResult -- Paginated file listing with prefix filtering, delimiter support, and continuation tokens
  • PresignedUrlRequest -- Configurable presigned URL generation with expiry and HTTP method control
  • FileVisibility enum -- Public vs. Private access control
  • FileStorageOptions -- Shared configuration for max file size, allowed extensions, default visibility, and path templates

Installation

dotnet add package RepletoryLib.FileStorage.Abstractions

Or add to your .csproj:

<PackageReference Include="RepletoryLib.FileStorage.Abstractions" Version="1.0.0" />

Note: RepletoryLib packages are published to a local BaGet feed. See the main repository README for feed configuration.

Dependencies

Package Type
RepletoryLib.Common RepletoryLib

Quick Start

1. Reference the abstractions in your service layer

using RepletoryLib.FileStorage.Abstractions.Interfaces;
using RepletoryLib.FileStorage.Abstractions.Models;

public class DocumentService
{
    private readonly IFileStorageService _storage;

    public DocumentService(IFileStorageService storage) => _storage = storage;

    public async Task<FileUploadResult> UploadDocumentAsync(Stream content, string fileName)
    {
        var request = new FileUploadRequest
        {
            Content = content,
            FileName = fileName,
            Folder = "documents"
        };

        return await _storage.UploadAsync(request);
    }
}

2. Register a concrete provider in Program.cs

// AWS S3 provider
using RepletoryLib.FileStorage.Aws;
builder.Services.AddRepletoryAwsFileStorage(builder.Configuration);

// -- OR -- Local filesystem provider
using RepletoryLib.FileStorage.Local;
builder.Services.AddRepletoryLocalFileStorage(builder.Configuration);

Configuration

FileStorageOptions

Property Type Default Description
MaxFileSizeBytes long 104857600 (100 MB) Maximum allowed file size in bytes
AllowedExtensions List<string> [] (all allowed) Allowed file extensions (e.g., [".jpg", ".png", ".pdf"]). Empty list allows all
DefaultVisibility FileVisibility Private Default access visibility for uploaded files
PathTemplate string "{tenant}/{year}/{month}/{filename}" Token-based template for organizing storage paths
PreserveOriginalFileName bool true When false, a GUID-based file name is generated

Section name: "FileStorage"

{
  "FileStorage": {
    "MaxFileSizeBytes": 52428800,
    "AllowedExtensions": [".jpg", ".jpeg", ".png", ".gif", ".pdf", ".docx"],
    "DefaultVisibility": "Private",
    "PathTemplate": "{tenant}/{year}/{month}/{day}/{filename}",
    "PreserveOriginalFileName": true
  }
}

Path Template Tokens

Token Description Example Value
{tenant} Tenant ID from FilePathContext.TenantId, or "default" a1b2c3d4-...
{year} Upload year 2026
{month} Upload month (zero-padded) 03
{day} Upload day (zero-padded) 15
{filename} Original or GUID-based file name report.pdf

Usage Examples

Upload a File

using RepletoryLib.FileStorage.Abstractions.Enums;
using RepletoryLib.FileStorage.Abstractions.Interfaces;
using RepletoryLib.FileStorage.Abstractions.Models;

public class ImageService
{
    private readonly IFileStorageService _storage;

    public ImageService(IFileStorageService storage) => _storage = storage;

    public async Task<FileUploadResult> UploadImageAsync(Stream imageStream, string fileName, Guid tenantId)
    {
        var request = new FileUploadRequest
        {
            Content = imageStream,
            FileName = fileName,
            ContentType = "image/jpeg",
            Folder = "avatars",
            Visibility = FileVisibility.Public,
            Metadata = new Dictionary<string, string>
            {
                ["uploaded-by"] = "image-service",
                ["tenant-id"] = tenantId.ToString()
            }
        };

        return await _storage.UploadAsync(request);
    }
}

Bulk Upload

public async Task<BulkUploadResult> UploadBatchAsync(IEnumerable<(Stream Content, string FileName)> files)
{
    var requests = files.Select(f => new FileUploadRequest
    {
        Content = f.Content,
        FileName = f.FileName,
        Folder = "batch-imports"
    });

    var result = await _storage.UploadBulkAsync(requests);

    // Partial success -- check individual results
    foreach (var r in result.Results.Where(r => !r.Success))
    {
        _logger.LogWarning("Failed to upload: {Error}", r.Error);
    }

    _logger.LogInformation("Bulk upload: {Success}/{Total} succeeded",
        result.SuccessCount, result.Results.Count);

    return result;
}

Download Files

// Download as stream (caller must dispose)
await using var stream = await _storage.DownloadAsync("default/2026/03/report.pdf");
await stream.CopyToAsync(httpResponse.Body);

// Download as byte array
byte[] bytes = await _storage.DownloadBytesAsync("default/2026/03/report.pdf");

Copy, Move, and Delete

// Copy a file
await _storage.CopyAsync("originals/photo.jpg", "backups/photo.jpg");

// Move a file (copy + delete source)
await _storage.MoveAsync("temp/upload.pdf", "documents/final.pdf");

// Delete a single file
await _storage.DeleteAsync("temp/upload.pdf");

// Bulk delete
await _storage.DeleteBulkAsync(new[] { "temp/file1.txt", "temp/file2.txt", "temp/file3.txt" });

Check Existence and Get Metadata

// Check if file exists
bool exists = await _storage.ExistsAsync("default/2026/03/report.pdf");

// Get file metadata
FileMetadata metadata = await _storage.GetMetadataAsync("default/2026/03/report.pdf");
Console.WriteLine($"File: {metadata.FileName}");
Console.WriteLine($"Size: {metadata.SizeBytes} bytes");
Console.WriteLine($"Type: {metadata.ContentType}");
Console.WriteLine($"Created: {metadata.CreatedAt}");
Console.WriteLine($"Visibility: {metadata.Visibility}");

List Files with Pagination

var request = new FileListRequest
{
    Prefix = "default/2026/03/",
    Delimiter = "/",
    MaxResults = 50
};

var result = await _storage.ListAsync(request);

foreach (var file in result.Files)
{
    Console.WriteLine($"{file.FileKey} ({file.SizeBytes} bytes)");
}

// Paginate through results
while (result.IsTruncated)
{
    request.ContinuationToken = result.ContinuationToken;
    result = await _storage.ListAsync(request);

    foreach (var file in result.Files)
    {
        Console.WriteLine($"{file.FileKey} ({file.SizeBytes} bytes)");
    }
}

Generate Presigned URLs

// Generate a download URL valid for 2 hours
var url = await _storage.GetPresignedUrlAsync(new PresignedUrlRequest
{
    FileKey = "default/2026/03/report.pdf",
    Expiry = TimeSpan.FromHours(2),
    Method = HttpMethod.Get
});

// Generate an upload URL valid for 15 minutes
var uploadUrl = await _storage.GetPresignedUrlAsync(new PresignedUrlRequest
{
    FileKey = "uploads/new-document.pdf",
    Expiry = TimeSpan.FromMinutes(15),
    Method = HttpMethod.Put
});

Custom File Path Strategy

using RepletoryLib.FileStorage.Abstractions.Interfaces;
using RepletoryLib.FileStorage.Abstractions.Models;

public class CustomFilePathStrategy : IFilePathStrategy
{
    public string GeneratePath(FilePathContext context)
    {
        var tenant = context.TenantId?.ToString() ?? "shared";
        var date = context.UploadDate;
        var uniqueName = $"{Guid.NewGuid()}{Path.GetExtension(context.FileName)}";

        var path = $"{tenant}/{date:yyyy}/{date:MM}/{date:dd}/{uniqueName}";

        if (!string.IsNullOrWhiteSpace(context.Folder))
        {
            path = $"{context.Folder.TrimEnd('/')}/{path}";
        }

        return path;
    }
}

// Register in DI (overrides the provider's default strategy)
builder.Services.AddSingleton<IFilePathStrategy, CustomFilePathStrategy>();

API Reference

IFileStorageService

Method Returns Description
UploadAsync(request, ct) Task<FileUploadResult> Upload a single file
UploadBulkAsync(requests, ct) Task<BulkUploadResult> Upload multiple files with partial-success semantics
DownloadAsync(fileKey, ct) Task<Stream> Download a file as a stream (caller disposes)
DownloadBytesAsync(fileKey, ct) Task<byte[]> Download a file as a byte array
DeleteAsync(fileKey, ct) Task Delete a single file
DeleteBulkAsync(fileKeys, ct) Task Delete multiple files
ExistsAsync(fileKey, ct) Task<bool> Check if a file exists
CopyAsync(sourceKey, destinationKey, ct) Task Copy a file to a new location
MoveAsync(sourceKey, destinationKey, ct) Task Move a file (copy + delete source)
GetMetadataAsync(fileKey, ct) Task<FileMetadata> Get file metadata
ListAsync(request, ct) Task<FileListResult> List files with prefix filtering and pagination
GetPresignedUrlAsync(request, ct) Task<string> Generate a temporary presigned URL
GetPublicUrlAsync(fileKey, ct) Task<string> Get the public URL for a file

IFilePathStrategy

Method Returns Description
GeneratePath(context) string Generate a storage path from contextual information

FileUploadRequest

Property Type Required Description
Content Stream Yes File content stream
FileName string Yes Original file name
ContentType string? No MIME type (inferred from extension if null)
Folder string? No Optional folder prefix for organizing the file
Visibility FileVisibility? No Access visibility (defaults to FileStorageOptions.DefaultVisibility)
Metadata Dictionary<string, string>? No Custom metadata key-value pairs

FileUploadResult

Property Type Description
Success bool Whether the upload succeeded
FileKey string Unique storage key for the uploaded file
Url string Public or presigned URL to access the file
Error string? Error message if the upload failed
Metadata FileMetadata? Metadata of the uploaded file

BulkUploadResult

Property Type Description
Results List<FileUploadResult> Per-file upload results
SuccessCount int Number of successful uploads (computed)
FailureCount int Number of failed uploads (computed)

FileMetadata

Property Type Description
FileKey string Unique storage key
FileName string Original file name
ContentType string MIME content type
SizeBytes long File size in bytes
CreatedAt DateTime UTC creation timestamp
LastModifiedAt DateTime? UTC last-modified timestamp
Visibility FileVisibility Access visibility
Metadata Dictionary<string, string> Custom metadata key-value pairs

FileListRequest

Property Type Default Description
Prefix string? null Key prefix to filter files
Delimiter string? null Delimiter for grouping keys (e.g., "/")
MaxResults int 1000 Maximum files to return
ContinuationToken string? null Continuation token for pagination

FileListResult

Property Type Description
Files List<FileMetadata> File metadata entries
ContinuationToken string? Token for next page (null if no more pages)
IsTruncated bool Whether results were truncated

PresignedUrlRequest

Property Type Default Description
FileKey string (required) Storage key of the file
Expiry TimeSpan 1 hour Duration the URL remains valid
Method HttpMethod GET HTTP method the URL is valid for

FilePathContext

Property Type Default Description
FileName string (required) Original file name
TenantId Guid? null Tenant ID for multi-tenant path isolation
UploadDate DateTime DateTime.UtcNow Upload date for date-based path organization
Folder string? null Optional folder prefix

FileVisibility Enum

Value Description
Public File is publicly accessible via URL without authentication
Private File requires authentication or a presigned URL to access

Integration with Other RepletoryLib Packages

Package Relationship
RepletoryLib.Common Direct dependency -- shared base types
RepletoryLib.FileStorage.Aws AWS S3 implementation of IFileStorageService and IFilePathStrategy
RepletoryLib.FileStorage.Local Local filesystem implementation of IFileStorageService and IFilePathStrategy
RepletoryLib.Api Use IFileStorageService in API controllers for file upload endpoints

Testing

using NSubstitute;
using RepletoryLib.FileStorage.Abstractions.Interfaces;
using RepletoryLib.FileStorage.Abstractions.Models;

public class DocumentServiceTests
{
    [Fact]
    public async Task UploadDocumentAsync_returns_success_result()
    {
        // Arrange
        var storage = Substitute.For<IFileStorageService>();
        storage.UploadAsync(Arg.Any<FileUploadRequest>(), Arg.Any<CancellationToken>())
            .Returns(new FileUploadResult
            {
                Success = true,
                FileKey = "documents/default/2026/03/report.pdf",
                Url = "/files/documents/default/2026/03/report.pdf"
            });

        var service = new DocumentService(storage);

        // Act
        using var stream = new MemoryStream("test content"u8.ToArray());
        var result = await service.UploadDocumentAsync(stream, "report.pdf");

        // Assert
        result.Success.Should().BeTrue();
        result.FileKey.Should().Contain("report.pdf");
        await storage.Received(1).UploadAsync(
            Arg.Is<FileUploadRequest>(r => r.FileName == "report.pdf" && r.Folder == "documents"),
            Arg.Any<CancellationToken>());
    }
}

Troubleshooting

Issue Solution
FileUploadResult.Success is false Check the Error property for the failure reason; common causes are file size limits or I/O errors
Content type is application/octet-stream Set ContentType explicitly on FileUploadRequest or ensure the file has a recognized extension
Path template produces unexpected keys Verify PathTemplate tokens in FileStorageOptions match the supported tokens: {tenant}, {year}, {month}, {day}, {filename}
AllowedExtensions not filtering An empty list allows all extensions; populate it with the extensions you want to permit (e.g., [".jpg", ".pdf"])
Files not organized by tenant Set FilePathContext.TenantId when building upload requests; without it, the path defaults to "default"

License

This project is licensed under the MIT License.

Copyright (c) 2024-2026 Repletory.


For complete documentation, infrastructure setup, and configuration reference, see the RepletoryLib main repository.

Product Compatible and additional computed target framework versions.
.NET 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 (2)

Showing the top 2 NuGet packages that depend on RepletoryLib.FileStorage.Abstractions:

Package Downloads
RepletoryLib.FileStorage.Aws

AWS S3 file storage implementation for RepletoryLib

RepletoryLib.FileStorage.Local

Local filesystem file storage implementation for RepletoryLib

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 88 3/2/2026