Chd.Min.IO
8.5.9
dotnet add package Chd.Min.IO --version 8.5.9
NuGet\Install-Package Chd.Min.IO -Version 8.5.9
<PackageReference Include="Chd.Min.IO" Version="8.5.9" />
<PackageVersion Include="Chd.Min.IO" Version="8.5.9" />
<PackageReference Include="Chd.Min.IO" />
paket add Chd.Min.IO --version 8.5.9
#r "nuget: Chd.Min.IO, 8.5.9"
#:package Chd.Min.IO@8.5.9
#addin nuget:?package=Chd.Min.IO&version=8.5.9
#tool nuget:?package=Chd.Min.IO&version=8.5.9
MinIO Helper Library For .NET Core
Chd (Cleverly Handle Difficulty) library helps you cleverly handle difficulty, write code quickly, and keep your application stable.
π Table of Contents
- About
- Why MinIO & Object Storage?
- Features
- Installation & Setup
- Configuration
- Usage Overview
- Deep Dive: Image Processing Pipeline
- Real-World Examples
- Testing & Sample Code
- Best Practices
- FAQ
- Authors
About
Chd.Library.MinIO is a modern, robust .NET 8+ integration for MinIO (or any S3-compatible) storage.
It offers developer-friendly, highly efficient, and production-tested tools for secure file, document, and image storage.
Thanks to advanced optimization features, it's perfect for user-generated content, SaaS, and media-rich web/mobile/cloud apps.
π― Why MinIO & Object Storage? (vs Traditional File Systems)
The Problem with Traditional File Storage
Many developers still rely on traditional file systems (NTFS, ext4, network shares) or basic cloud file services for storing user uploads, media, and documents. While simple to start with, these approaches quickly become problematic at scale:
| Storage Type | Key Limitations |
|---|---|
| Local File System (NTFS, ext4) | β No built-in replication or redundancy<br>β Hard to scale horizontally<br>β Server-bound (no multi-region)<br>β Complex backup/disaster recovery<br>β No native CDN integration |
| Network File Shares (SMB, NFS) | β Poor performance over WAN<br>β Single point of failure<br>β Limited concurrent access<br>β Complex permission management |
| Azure Blob Storage | β οΈ Vendor lock-in (Azure-only)<br>β οΈ Expensive at scale (egress fees)<br>β οΈ Complex pricing model<br>β Good SDK support |
| AWS S3 | β οΈ Vendor lock-in (AWS-only)<br>β οΈ Expensive for large downloads (egress costs)<br>β οΈ Unpredictable bills at scale<br>β Industry standard, reliable |
| Google Cloud Storage | β οΈ Vendor lock-in (GCP-only)<br>β οΈ Similar pricing concerns<br>β οΈ Less mature ecosystem than AWS |
| HP Cloud Object Storage | β οΈ Enterprise-focused, complex setup<br>β οΈ Limited community support<br>β οΈ Proprietary APIs (less portable) |
Why MinIO is the Superior Choice
MinIO is a high-performance, open-source, S3-compatible object storage solution that solves all these problems:
β 1. 100% Free & Open Source
- No licensing fees, no hidden costs
- Deploy on your own hardware or cloud
- Full control over your data sovereignty
β 2. S3-Compatible API (Industry Standard)
- Works with AWS SDK, tools, and existing S3 code
- Easy migration from/to AWS S3, Azure, or others
- No vendor lock-inβswitch providers anytime
β 3. Self-Hosted = Cost Savings
Cost Comparison Example (1TB storage, 500GB/month transfer):
| Provider | Monthly Cost (USD) |
|---|---|
| AWS S3 | ~$60-120 |
| Azure Blob (Hot) | ~$50-100 |
| Google Cloud Storage | ~$50-110 |
| MinIO (Self-hosted) | ~$5-20 (server only) |
π° Save 70-90% on storage costs by hosting MinIO on your own infrastructure!
β 4. Superior Performance
- 10x faster than traditional NFS/SMB shares
- Built for parallel, concurrent access (handles 1000s of requests/sec)
- Automatic erasure coding for data protection without RAID overhead
- Native support for NVMe, SSD, and modern hardware
β 5. Enterprise-Grade Features (Free!)
- Built-in versioning and lifecycle management
- Server-side encryption (AES-256)
- Multi-tenancy (bucket-level isolation)
- Distributed mode for high availability and scalability
- Event notifications (Webhook, Kafka, NATS, etc.)
β 6. Cloud-Native & Container-Ready
- Deploy with Docker, Kubernetes, or bare metal
- Scales horizontally (just add nodes)
- Perfect for microservices and modern cloud architectures
β 7. No Egress Fees or Bandwidth Surprises
- Unlike AWS/Azure, no charges for downloads (you control bandwidth)
- Predictable, transparent costs
Real-World Use Case Comparison
| Scenario | Traditional FS | Azure Blob | AWS S3 | MinIO |
|---|---|---|---|---|
| Store 10TB of user photos | β Slow, no redundancy | β οΈ $200+/mo | β οΈ $230+/mo | β $50-80/mo |
| Serve 1M requests/day | β Bottleneck | β οΈ Egress fees | β οΈ Egress fees | β Free (self-hosted) |
| Multi-region replication | β Manual setup | β Yes | β Yes | β Yes (easy) |
| No vendor lock-in | β Yes | β Azure-only | β AWS-only | β Yes |
| Self-hosted (on-prem/hybrid) | β Yes | β Cloud-only | β Cloud-only | β Yes |
| Full data control | β Yes | β Limited | β Limited | β Yes |
When to Choose MinIO Over Alternatives
β Use MinIO if you:
- Want cost-effective, scalable object storage
- Need S3 compatibility without AWS vendor lock-in
- Require self-hosted or hybrid cloud deployments
- Have compliance/regulatory requirements (GDPR, HIPAA, etc.)
- Want predictable costs (no surprise bills)
- Need high performance for AI/ML workloads, media streaming, or backups
β οΈ Use AWS S3/Azure if you:
- Already have deep integration with AWS/Azure services
- Don't want to manage infrastructure (fully managed)
- Need global edge CDN (AWS CloudFront integration)
Why This Library Matters
This library (Chd.Min.IO) makes MinIO even better by:
- Abstracting away low-level S3 API complexity
- Adding automatic image optimization (resize, compress, format conversion)
- Providing production-ready error handling and retry logic
- Offering developer-friendly APIs for .NET projects
- Supporting both cloud and self-hosted MinIO deployments
TL;DR: Get the power of AWS S3 without the cost, vendor lock-in, or complexity!
Features
- π Plug-and-play .NET 8+ support with DI and builder extension
- β‘ Asynchronous upload/download for single files or batches
- πΌοΈ Auto format detection & conversion (JPEG/PNG/WebP) for images
- π Automatic resize, crop, compress, and strict memory/size limiting
- π§ Preserves transparency, disallows "fake" image uploads, rejects corrupt files
- π¦ Automatic bucket management, multi-tenant/resource support
- π File versioning (update/delete operations preserve old versions)
- π₯οΈ Support for both MinIO/S3 and local file servers (SMB/NFS)
- π Production-tested for large enterprises and high-scale web/SaaS products
Installation & Setup
1. Add NuGet package
dotnet add package Chd.Min.IO
2. Register on Startup
Add to your Program.cs or startup routine:
using Microsoft.AspNetCore.Builder;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Register MinIO (reads from appsettings.json)
app.UseMinIO();
app.Run();
Or manually:
using Chd.Min.IO.Containers;
var config = new MinIOConfig
{
Host = "localhost:9000",
AccessKey = "admin",
SecretKey = "secret123",
BucketName = "my-bucket"
};
app.UseMinIO(config);
Configuration
Add the following to your appsettings.json:
{
"Minio": {
"Host": "localhost:9000",
"AccessKey": "admin",
"SecretKey": "secret_key_for_min.io",
"BucketName": "software",
"Url": "http://localhost:9000"
}
}
Configuration Options:
- Host: MinIO or S3 endpoint (e.g.,
s3.amazonaws.comfor AWS S3) - AccessKey: MinIO/S3 access key
- SecretKey: MinIO/S3 secret key
- BucketName: Default storage bucket (auto-created if not exists)
- Url: Base URL for generating public links (optional)
Usage Overview
All primary APIs are accessible via FileUtils and ImageUtils classes.
Both async and batch workflows are supported.
π File Operations (FileUtils)
1. Upload Single File
using Chd.Min.IO.Utils;
// Simple upload to default bucket
var result = await FileUtils.Upload(formFile);
// Upload to specific bucket and path
var result = await FileUtils.Upload(
file: formFile,
bucketName: "my-bucket",
objectName: "documents/report.pdf"
);
// Upload with custom content type
var result = await FileUtils.Upload(
file: formFile,
objectName: "files/data.json",
contentType: "application/json"
);
// Check result
if (result.IsFaulted)
Console.WriteLine($"Error: {result.ErrorMessage}");
else
Console.WriteLine($"Uploaded to: {result.Path}");
2. Upload Multiple Files
// Batch upload with prefix
List<IFormFile> files = GetFilesFromRequest();
var results = await FileUtils.Upload(
files: files,
prefix: "user-123",
bucketName: "uploads",
path: "documents"
);
// Check results
foreach (var result in results)
{
if (result.IsFaulted)
Console.WriteLine($"Error: {result.ErrorMessage}");
else
Console.WriteLine($"Uploaded: {result.Path}");
}
3. Download Files
// Download file from default bucket
var fileResult = await FileUtils.Get("documents/report.pdf");
if (!fileResult.IsFaulted)
{
byte[] fileData = fileResult.Data.ToArray();
string contentType = fileResult.ContentType;
// Return as file download
return File(fileData, contentType, "report.pdf");
}
// Download from specific bucket
var fileResult = await FileUtils.Get(
bucketName: "my-bucket",
objectName: "images/photo.jpg"
);
4. Update Existing File (with Versioning)
// Update file - old version is automatically saved
var result = await FileUtils.Update(
file: newFile,
bucketName: "my-bucket",
oldobjectName: "documents/report.pdf"
);
// Old version is saved to: updated/20241215103045_report.pdf
// New file replaces the original path
5. Delete File (with Soft Delete)
// Delete file (moves to deleted/ folder with timestamp)
var result = await FileUtils.Delete("documents/report.pdf");
// Delete from specific bucket
var result = await FileUtils.Delete(
bucketName: "my-bucket",
fileFullPath: "documents/report.pdf"
);
// Old version is saved to: deleted/20241215103045_report.pdf
// Original file is removed
6. Upload to Network File Server (SMB/NFS)
// Upload to Windows network share or Linux NFS mount
var result = await FileUtils.UploadToFileServer(
file: formFile,
fileServerPath: @"\\server\share\uploads",
prefix: "user-456"
);
// Or Linux NFS path
var result = await FileUtils.UploadToFileServer(
file: formFile,
fileServerPath: "/mnt/nfs/uploads",
prefix: "backup"
);
Console.WriteLine($"Saved to: {result.Path}");
πΌοΈ Image Operations (ImageUtils)
1. Simple Image Upload with Auto-Optimization
using Chd.Min.IO.Utils;
// Auto-resize and compress (transparent PNGs preserved)
var result = await ImageUtils.SimpleUpload(
file: imageFile,
bucketName: "images",
folderName: "products",
maxkb: 500, // Max 500KB
width: 1200, // Max width 1200px
height: 800 // Max height 800px
);
// Result: Image is resized, compressed, and saved
// PNG with transparency β stays PNG
// PNG without transparency β converted to JPEG
2. Upload with Format Conversion
using ImageMagick;
// Force convert to JPEG (great for photos)
var result = await ImageUtils.SimpleUpload(
file: imageFile,
fileName: "product-001.jpg",
format: MagickFormat.Jpg,
maxkb: 300,
density: 96
);
// Convert to WebP (modern, smallest size)
var result = await ImageUtils.SimpleUpload(
file: imageFile,
format: MagickFormat.WebP,
percentage: 85 // 85% quality
);
// Auto format (MagickFormat.A)
// - PNG with alpha β PNG
// - PNG without alpha β JPEG
var result = await ImageUtils.SimpleUpload(
file: imageFile,
format: MagickFormat.A // Auto-detect
);
3. Batch Image Upload
List<IFormFile> productImages = GetProductImages();
var results = await ImageUtils.Upload(
files: productImages,
prefix: "product-batch-2024",
bucketName: "catalog-images",
folderName: "products",
format: MagickFormat.Jpg,
maxkb: 512,
width: 1024,
height: 768
);
foreach (var result in results)
{
Console.WriteLine($"{result.FileName} β {result.Path}");
}
4. Dynamic Image Resize on Download
// Get original image
var image = await ImageUtils.Get("products/chair.jpg");
// Get thumbnail (300x300, preserve aspect ratio)
var thumbnail = await ImageUtils.Get(
objectName: "products/chair.jpg",
width: 300,
height: 300,
ignoreAspectRatio: false // Preserve aspect ratio
);
// Get square crop (force exact dimensions)
var square = await ImageUtils.Get(
objectName: "products/chair.jpg",
width: 500,
height: 500,
ignoreAspectRatio: true // Force 500x500 square
);
// Get with aspect ratio (e.g., 16:9 for video thumbnails)
var videoThumb = await ImageUtils.Get(
objectName: "videos/thumbnail.jpg",
aspectRatio: "16/9",
width: 1920
);
// Result: 1920x1080 image
// Reduce by percentage
var smallImage = await ImageUtils.Get(
objectName: "products/chair.jpg",
percentage: 50 // 50% of original size
);
5. Upload Image to Network File Server
// Upload optimized image to file server
var result = await ImageUtils.UploadImageToFileServer(
file: imageFile,
fileServerPath: @"\\server\images\products",
prefix: "prod",
maxkb: 800,
width: 1600,
height: 1200,
format: MagickFormat.Jpg
);
Console.WriteLine($"Saved locally: {result.Path}");
π Result Object Structure
All upload/update/delete operations return a Result object:
public class Result
{
public string FileName { get; set; } // "report.pdf"
public string Path { get; set; } // "documents/report.pdf"
public string ContentType { get; set; } // "application/pdf"
public string Message { get; set; } // Success/error message
public bool IsFaulted { get; set; } // true if error occurred
public string ErrorMessage { get; set; } // Detailed error (if any)
}
// Example usage:
var result = await FileUtils.Upload(file);
if (result.IsFaulted)
{
Log.Error($"Upload failed: {result.ErrorMessage}");
return BadRequest(result.Message);
}
else
{
Log.Info($"File saved to: {result.Path}");
return Ok(new { path = result.Path, fileName = result.FileName });
}
Download operations return a RequestResult object:
public class RequestResult
{
public MemoryStream Data { get; set; } // File contents
public string ContentType { get; set; } // MIME type
public bool IsFaulted { get; set; }
public string ErrorMessage { get; set; }
}
// Example usage:
var result = await FileUtils.Get("documents/file.pdf");
if (!result.IsFaulted)
{
return File(result.Data, result.ContentType, "download.pdf");
}
π¨ Advanced Image Processing Scenarios
Scenario 1: User Avatar Upload
// Accept user avatar, force square, compress, save as JPEG
var result = await ImageUtils.SimpleUpload(
file: avatarFile,
bucketName: "user-avatars",
fileName: $"{userId}-avatar.jpg",
format: MagickFormat.Jpg,
width: 512,
height: 512,
ignoreAspectRetio: true, // Force square
maxkb: 200, // Max 200KB
density: 96
);
// Result: 512x512 JPEG, under 200KB
Scenario 2: Product Catalog with Transparency
// Upload product image (preserve PNG if transparent, else JPEG)
var result = await ImageUtils.SimpleUpload(
file: productImage,
bucketName: "products",
folderName: "catalog",
format: MagickFormat.A, // Auto-detect (PNG if alpha, else JPEG)
maxkb: 800,
width: 1200
);
// Result:
// - Logo with transparency β PNG (transparency preserved)
// - Product photo β JPEG (smaller file size)
Scenario 3: Generate Multiple Thumbnail Sizes
var originalPath = "products/chair-original.jpg";
// Upload original (high quality)
var original = await ImageUtils.SimpleUpload(
file: imageFile,
folderName: "originals",
fileName: originalPath,
maxkb: 2048 // 2MB max
);
// Generate and save thumbnail
var thumbFile = imageFile; // Re-use same IFormFile
var thumb = await ImageUtils.SimpleUpload(
thumbFile,
folderName: "thumbnails",
fileName: "chair-thumb.jpg",
width: 150,
height: 150,
ignoreAspectRetio: true
);
// Generate medium size
var medium = await ImageUtils.SimpleUpload(
thumbFile,
folderName: "medium",
fileName: "chair-medium.jpg",
width: 600,
height: 400
);
// Or dynamically resize on download:
var thumbOnDemand = await ImageUtils.Get(
originalPath,
width: 150,
height: 150
);
Scenario 4: High-Quality Print Images
// Upload image for print (high density, minimal compression)
var result = await ImageUtils.SimpleUpload(
file: printImage,
bucketName: "print-ready",
density: 300, // 300 DPI for print
depth: 16, // 16-bit color depth
percentage: 100, // No quality loss
format: MagickFormat.Tiff
);
ποΈ Bucket Management
- Buckets are auto-created on first use (lazy-initialization)
- You can change buckets per environment or logical tenant
- Compatible with all MinIO and S3-compatible providers
// Each tenant can have their own bucket
await FileUtils.Upload(
file: file,
bucketName: $"tenant-{tenantId}",
objectName: "documents/file.pdf"
);
// Environment-based buckets
var bucketName = env.IsDevelopment()
? "dev-uploads"
: "prod-uploads";
await FileUtils.Upload(file, bucketName: bucketName);
β οΈ Error Handling & Robustness
try
{
var result = await FileUtils.Upload(file);
if (result.IsFaulted)
{
// Handle known errors (corrupt file, size limit, etc.)
_logger.LogWarning($"Upload warning: {result.Message}");
return BadRequest(new { error = result.ErrorMessage });
}
return Ok(new {
path = result.Path,
fileName = result.FileName,
contentType = result.ContentType
});
}
catch (Exception ex)
{
// Handle unexpected errors (network, permissions, etc.)
_logger.LogError(ex, "Upload failed");
return StatusCode(500, "Internal server error");
}
Built-in Safety Features:
- β Corrupt files are rejected with clear error messages
- β Over-sized images are auto-compressed or rejected
- β File extension spoofing is detected (magic byte validation)
- β Illegal filename characters are auto-removed
- β Automatic versioning on update/delete (no data loss)
- β Zero data loss in high-load environments
- β Transparent PNG preservation (no "white box" bugs)
- β Configurable memory limits prevent DoS attacks
π· Deep Dive: Advanced Image Processing, Storage, and Optimization
This library is not just a bridge to S3 or MinIO β it provides a full, robust, and production-tested media optimization pipeline
tailored for real-world .NET applications. Here's how it outclasses basic "file save" logic and why it makes operational, UX, and cost sense.
π What Makes Image Handling in This Library Special?
Bandwidth and Storage Efficiency:
Large camera/photo uploads are automatically resized, compressed, andβwhere possibleβreformatted to cut storage costs and page loads by 60β80%+.Product-Grade Quality:
Transparency (alpha channel) is never lost by accidental format downscaling: Avatars, logos, overlays always look crisp, no "white box" bugs!Failsafe Security:
Extension spoofing (e.g. JPG that's really a .doc) and broken/corrupt files are detectedβyour system is never DOS-ed by file upload attacks.Image Trustworthiness:
What the user sees is always what is deliveredβpixel shape, background, sharpness preserved based on business rules.Automation Simplicity:
Devs don't have to handwrite format conversion, downsize, or edge-case filters; all this logic is handled automatically, tested in prod at scale!
π§ͺ Supported Pipeline Features
1. Auto Format Selection & Conversion
- Detects if uploaded image has an alpha channel (transparency)
- Preserves PNG for transparent images (e.g. logos, overlays)
- Converts PNGβJPEG for non-transparent images (huge space savings)
- Supports WebP for next-gen web clients
- GIFs (animated/static) preserved unless explicit conversion requested
2. Gradual Memory & Dimension Downscaling
maxWidth,maxHeightrestrict user uploads to maintain UX bounds- After resizing, applies compression so final file fits
maxkblimit - If image can't be shrunk below limit, fails gracefully with error
3. Content-based File Validation
- Validates magic bytes and decodes within bounds
- Fakes (e.g. .jpg with PDF contents) are blocked with error
- Rejects zero-length or broken/corrupted images
4. Bulk, Batch & Parallel Image Workflow
- Async, parallel-safe; entire catalogs can be processed with high throughput
π Algorithm: "What Format Will My Image Be Saved As?"
| Input Format | Has Transparency? | Output Format (auto) | Notes |
|---|---|---|---|
| PNG | Yes | PNG | Crisp overlays, logos preserved |
| PNG | No | JPEG | Huge savings for photos |
| JPEG | N/A | JPEG | Recompressed if too large |
| GIF | N/A | GIF | Animation/static kept |
| WebP | N/A | WebP or JPEG | Browser support dependent |
Always checks actual binary, not just extension!
You can override format (format: MagickFormat.Jpg) or allow auto selection (format: MagickFormat.A).
π‘ Real-World Scenario Table
| Case | What Happens | Result for User/UI |
|---|---|---|
| User uploads selfie from phone (7MB) | Resized to maxWidth/maxHeight, down to 1MB as JPEG |
Fast page load, fits quota |
| Company logo (transparent PNG, 300KB) | Saved as PNG, transparency preserved | Overlay crisp, no white box |
| Fake .jpg (PDF file!) | Blocked at validation step | No File Spoof/Exploit Risk |
| Animated GIF meme | Saved as GIF, animation preserved | Animation works, no loss |
β οΈ Complex Edge Cases & Warnings
Uploading "maxed-out" PNGs: If a user attempts to upload a 13MB lossless PNG with a 2MB limit, system downscales (pixels + quality) until threshold is met or returns "file too large" error
Batch uploads: All pipeline logic applies per-image, never "short-circuits"
Image caching: Downloaded images are cached for 20 seconds by default (configurable)
π Engineering and Business Value
- Reduce bill shock: Prevents runaway storage costs via auto-compression
- Improve time-to-first-paint: Users see media faster, CDNs cache efficiently
- Bulletproof corrupt upload defense: No more bug tickets of blank images
- Explicitly control user content: Out-of-range uploads rejected before save
- Great for web, mobile, API and desktop clients
π Real-World Production Examples
Example 1: E-Commerce Product Upload (Razor Pages)
// Pages/Products/Create.cshtml.cs
public class CreateModel : PageModel
{
[BindProperty]
public IFormFile ProductImage { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (ProductImage == null)
return Page();
// Upload optimized product image
var result = await ImageUtils.SimpleUpload(
file: ProductImage,
bucketName: "product-images",
folderName: $"products/{DateTime.UtcNow:yyyy/MM}",
fileName: $"{Guid.NewGuid()}.jpg",
format: MagickFormat.Jpg,
maxkb: 800,
width: 1200,
height: 900
);
if (result.IsFaulted)
{
ModelState.AddModelError("", result.ErrorMessage);
return Page();
}
// Save product with image path
var product = new Product
{
Name = ProductName,
ImagePath = result.Path
};
await _db.Products.AddAsync(product);
await _db.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
Example 2: User Avatar Upload (Web API)
// Controllers/UsersController.cs
[HttpPost("avatar")]
public async Task<IActionResult> UploadAvatar(IFormFile avatar)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
// Delete old avatar if exists
var user = await _db.Users.FindAsync(userId);
if (!string.IsNullOrEmpty(user.AvatarPath))
{
await FileUtils.Delete(user.AvatarPath);
}
// Upload new avatar (square, compressed)
var result = await ImageUtils.SimpleUpload(
file: avatar,
bucketName: "user-avatars",
fileName: $"{userId}.jpg",
format: MagickFormat.Jpg,
width: 512,
height: 512,
ignoreAspectRetio: true,
maxkb: 200
);
if (result.IsFaulted)
return BadRequest(result.ErrorMessage);
// Update user record
---
## π Changelog
### Version 8.5.9 (2025-01-XX)
π **Security Update - Critical**
#### π‘οΈ Security Fixes
- **CRITICAL**: Updated `Magick.NET-Q8-AnyCPU` from 14.2.0 to 14.10.3
- Fixed 8+ HIGH severity vulnerabilities
- Fixed 20+ MODERATE severity vulnerabilities
- Fixed 12+ LOW severity vulnerabilities
- **Affected advisories**: GHSA-6hjr-v6g4-3fm8, GHSA-72hf-fj62-w6j4, GHSA-543g-8grm-9cw6, and 40+ more
- Updated `Magick.NET.Core` from 14.2.0 to 14.10.3
#### π Documentation
- Enhanced README with detailed security notes
- Added comprehensive image processing pipeline documentation
#### β οΈ Breaking Changes
None - Safe to upgrade!
#### π Migration Guide
Simply update your package reference:
```bash
dotnet add package Chd.Library.MinIO --version 8.5.9
Recommendation: This is a security-critical update. Please upgrade immediately.
Version 8.5.8 (Previous)
Standard image processing features
MinIO integration improvements user.AvatarPath = result.Path; await _db.SaveChangesAsync();
return Ok(new { avatarUrl = $"/api/users/{userId}/avatar" }); }
[HttpGet("{userId}/avatar")] public async Task<IActionResult> GetAvatar(string userId, [FromQuery] int? size) { var user = await _db.Users.FindAsync(userId); if (user == null || string.IsNullOrEmpty(user.AvatarPath)) return NotFound();
// Get avatar (optionally resized)
var result = size.HasValue
? await ImageUtils.Get(user.AvatarPath, width: (uint)size.Value, height: (uint)size.Value)
: await ImageUtils.Get(user.AvatarPath);
if (result.IsFaulted)
return NotFound();
return File(result.Data, result.ContentType);
}
### Example 3: Document Management with Versioning
```csharp
// Services/DocumentService.cs
public class DocumentService
{
public async Task<Result> UploadDocument(IFormFile file, string category)
{
var fileName = $"{category}/{DateTime.UtcNow:yyyy/MM/dd}/{Guid.NewGuid()}_{file.FileName}";
return await FileUtils.Upload(
file: file,
bucketName: "company-documents",
objectName: fileName
);
}
public async Task<Result> UpdateDocument(IFormFile newFile, string existingPath)
{
// Old version is automatically saved to updated/ folder
return await FileUtils.Update(
file: newFile,
oldobjectName: existingPath
);
}
public async Task<Result> DeleteDocument(string path)
{
// File is moved to deleted/ folder, not permanently removed
return await FileUtils.Delete(path);
}
public async Task<RequestResult> DownloadDocument(string path)
{
return await FileUtils.Get(path);
}
}
Example 4: Multi-Tenant SaaS File Storage
// Services/TenantStorageService.cs
public class TenantStorageService
{
private string GetTenantBucket(string tenantId)
{
return $"tenant-{tenantId}-files";
}
public async Task<Result> UploadTenantFile(string tenantId, IFormFile file, string folder)
{
var bucketName = GetTenantBucket(tenantId);
var objectName = $"{folder}/{Guid.NewGuid()}_{file.FileName}";
return await FileUtils.Upload(
file: file,
bucketName: bucketName,
objectName: objectName
);
}
public async Task<RequestResult> GetTenantFile(string tenantId, string path)
{
var bucketName = GetTenantBucket(tenantId);
return await FileUtils.Get(bucketName, path);
}
}
Testing & Sample Code
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Chd.Min.IO.Utils;
using Chd.Min.IO.Containers;
using Microsoft.AspNetCore.Http;
[TestClass]
public class MinIOTests
{
static MinIOTests()
{
// Initialize MinIO
var config = new MinIOConfig
{
Host = "localhost:9000",
AccessKey = "minioadmin",
SecretKey = "minioadmin",
BucketName = "test-bucket"
};
MinIODependenyInjectionExtensions.UseMinIO(null, config);
}
[TestMethod]
public async Task TestFileUpload()
{
using var fs = File.OpenRead("testfiles/sample.pdf");
var formFile = new FormFile(fs, 0, fs.Length, "sample", "sample.pdf");
var result = await FileUtils.Upload(formFile);
Assert.IsFalse(result.IsFaulted);
Assert.IsNotNull(result.Path);
Console.WriteLine($"Uploaded to: {result.Path}");
}
[TestMethod]
public async Task TestImageUploadWithOptimization()
{
using var fs = File.OpenRead("testfiles/photo.jpg");
var formFile = new FormFile(fs, 0, fs.Length, "photo", "photo.jpg");
var result = await ImageUtils.SimpleUpload(
formFile,
maxkb: 500,
width: 1200,
height: 800
);
Assert.IsFalse(result.IsFaulted);
Assert.IsTrue(result.Path.EndsWith(".jpg"));
}
[TestMethod]
public async Task TestImageDownloadWithResize()
{
var result = await ImageUtils.Get(
"products/chair.jpg",
width: 300,
height: 300
);
Assert.IsFalse(result.IsFaulted);
Assert.IsNotNull(result.Data);
Assert.IsTrue(result.Data.Length < 100 * 1024); // Under 100KB
}
}
Best Practices & Production Benefits
β Do's
- Always set size limits: Use
maxkb,maxWidth,maxHeightfor user uploads - Handle errors gracefully: Check
result.IsFaultedand log errors - Use meaningful paths: Organize files by date, category, user ID
- Enable format auto-detection: Use
MagickFormat.Afor images - Implement versioning: Use
Update()andDelete()methods for audit trails - Cache aggressively: ImageUtils has built-in 20-second cache
- Use bucket-per-tenant: For SaaS apps, isolate tenant data
β Don'ts
- Don't skip validation: Always check
IsFaultedbefore using results - Don't ignore transparency: Use auto-format to preserve PNG alpha
- Don't store originals forever: Delete or archive old versions periodically
- Don't hardcode bucket names: Use configuration or environment variables
- Don't expose raw paths: Generate secure, time-limited URLs instead
π° Cost Savings
- Automatic image optimization can reduce storage by 60-80%
- Self-hosted MinIO saves 70-90% vs AWS S3
- No egress fees = predictable monthly costs
FAQ
Q: Can I use with Amazon S3, Wasabi, or other S3-compatible providers?
A: Yes! Just update Host, AccessKey, and SecretKey in config. Works with any S3 API.
Q: Does it support animated GIFs?
A: Yes, animations are preserved unless you explicitly convert to another format.
Q: What happens if a user uploads a 12MB photo with maxkb: 2?
A: The image is progressively scaled down and compressed until under 2MB; if impossible, upload fails with error.
Q: Can I upload files in parallel?
A: Yes! All methods are async/await-friendly and thread-safe.
Q: How does file versioning work?
A: Update() saves old file to updated/{timestamp}_{filename}. Delete() saves to deleted/{timestamp}_{filename}. Original file is replaced/removed.
Q: Is there a file size limit?
A: MinIO supports files up to 5TB. This library doesn't impose hard limits, but you can set maxkb for images.
Q: Can I use this with local file servers (not MinIO)?
A: Yes! Use FileUtils.UploadToFileServer() and ImageUtils.UploadImageToFileServer() for SMB/NFS shares.
Q: Does it work with .NET 8 and .NET 9?
A: Yes, fully compatible with .NET 8 and .NET 9.
Authors
- Mehmet YoldaΕ (LinkedIn)
See also contributors on NuGet
Acknowledgements
- Inspired by MinIO, AWS S3, and the .NET open-source community
- Built with Minio SDK and Magick.NET
- Kudos to real-world users for testing and feedback
For issues, feature requests, or contributions, please visit mehmet-yoldas/library-core
| Product | Versions 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 was computed. 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 was computed. 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. |
-
net8.0
- Chd.Common (>= 8.6.1)
- Magick.NET-Q8-AnyCPU (>= 14.10.3)
- Microsoft.AspNetCore.Http.Abstractions (>= 2.3.9)
- Microsoft.AspNetCore.Http.Features (>= 5.0.17)
- Microsoft.Extensions.Configuration.Abstractions (>= 9.0.1)
- Microsoft.Extensions.Configuration.Binder (>= 9.0.1)
- Microsoft.Extensions.Configuration.FileExtensions (>= 9.0.1)
- Microsoft.Extensions.Configuration.Json (>= 9.0.1)
- Minio (>= 5.0.0)
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 |
|---|