Common Mistakes
These are the most common issues developers encounter when using Pragmatic.Storage. Each section shows the wrong approach, the correct approach, and explains why.
1. Reading the Stream Before Passing It to SaveAsync
Section titled “1. Reading the Stream Before Passing It to SaveAsync”Wrong:
public override async Task<Result<Uri, IError>> Execute(CancellationToken ct){ await using var stream = File.OpenReadStream();
// Read the entire stream to validate content using var reader = new StreamReader(stream); var content = await reader.ReadToEndAsync(ct); if (content.Contains("malicious")) return new ValidationError("File contains prohibited content");
// SaveAsync receives an exhausted stream -- position is at the end var uri = await _storage.SaveAsync(stream, File.FileName, "imports", ct); return uri;}Runtime result: SaveAsync writes zero bytes because the stream position is at the end. The file is “saved” but empty. No exception is thrown.
Right:
public override async Task<Result<Uri, IError>> Execute(CancellationToken ct){ await using var stream = File.OpenReadStream();
// Copy to a seekable MemoryStream for inspection using var ms = new MemoryStream(); await stream.CopyToAsync(ms, ct);
ms.Position = 0; using var reader = new StreamReader(ms, leaveOpen: true); var content = await reader.ReadToEndAsync(ct); if (content.Contains("malicious")) return new ValidationError("File contains prohibited content");
ms.Position = 0; // Reset before saving var uri = await _storage.SaveAsync(ms, File.FileName, "imports", ct); return uri;}Why: IFormFile.OpenReadStream() returns a forward-only stream. Once read, the position is at the end and subsequent reads return nothing. If you need to inspect the content before saving, copy to a MemoryStream, inspect, reset the position, then pass to SaveAsync.
2. Not Disposing the File Stream
Section titled “2. Not Disposing the File Stream”Wrong:
public override async Task<Result<Uri, IError>> Execute(CancellationToken ct){ var stream = File.OpenReadStream(); // No await using! var uri = await _storage.SaveAsync(stream, File.FileName, "photos", ct); return uri; // Stream is never disposed -- resource leak}Runtime result: Works but leaks the stream handle. Under load, this can exhaust file handles or memory, especially with large uploads.
Right:
public override async Task<Result<Uri, IError>> Execute(CancellationToken ct){ await using var stream = File.OpenReadStream(); var uri = await _storage.SaveAsync(stream, File.FileName, "photos", ct); return uri;}Why: IFormFile.OpenReadStream() returns a stream that holds resources (file handles, buffers). The caller owns the stream lifetime and must dispose it. Use await using to ensure cleanup even if SaveAsync throws.
3. Hardcoding the Base Path for LocalDiskFileStorage
Section titled “3. Hardcoding the Base Path for LocalDiskFileStorage”Wrong:
app.UseStorage(sp => new LocalDiskFileStorage( @"C:\inetpub\wwwroot\myapp", // Hardcoded path! sp.GetRequiredService<ILogger<LocalDiskFileStorage>>()));Runtime result: Works on the developer’s machine. Fails on CI, Docker, Linux deployments, or any machine without that exact path.
Right:
app.UseStorage(sp => new LocalDiskFileStorage( builder.Environment.WebRootPath, // Uses ASP.NET Core's environment sp.GetRequiredService<ILogger<LocalDiskFileStorage>>()));Or with AddLocalDiskStorage:
builder.Services.AddLocalDiskStorage(builder.Environment.WebRootPath);Why: IHostEnvironment.WebRootPath resolves to the correct wwwroot directory regardless of the deployment environment. Hardcoded paths break portability and fail silently when the directory does not exist (LocalDiskFileStorage creates subdirectories but not the base path itself).
4. Using the Same IFileStorage Registration Twice
Section titled “4. Using the Same IFileStorage Registration Twice”Wrong:
builder.Services.AddLocalDiskStorage(builder.Environment.WebRootPath);
// Later in the same file or in a startup stepapp.UseStorage<AzureBlobFileStorage>();Runtime result: Both register IFileStorage as a singleton. The last registration wins — AzureBlobFileStorage is used. AddLocalDiskStorage has no effect. No warning is emitted.
Right:
Choose one registration based on the environment:
if (app.Environment.IsDevelopment()){ app.UseStorage(sp => new LocalDiskFileStorage( webRootPath, sp.GetRequiredService<ILogger<LocalDiskFileStorage>>()));}else{ app.UseStorage<AzureBlobFileStorage>();}Why: IFileStorage is a singleton with a single implementation. DI “last registration wins” means the second call silently replaces the first. This is by design (it enables overriding), but accidental double registration leads to confusion about which provider is active.
5. Storing the File URI as a String Instead of a Uri
Section titled “5. Storing the File URI as a String Instead of a Uri”Wrong:
public class Document{ public string FileUrl { get; set; } = ""; // String, not Uri}
// In the actionvar uri = await _storage.SaveAsync(stream, File.FileName, "documents", ct);document.FileUrl = uri.ToString();
// Later, when deletingawait _storage.DeleteAsync(new Uri(document.FileUrl)); // May fail for relative URIs!Runtime result: new Uri("/files/documents/abc.pdf") throws UriFormatException because the Uri constructor treats a relative path as invalid without specifying UriKind.Relative. The entity was saved with a relative string, but DeleteAsync cannot reconstruct the Uri.
Right:
public class Document{ public Uri FileUri { get; set; } = null!; // Store as Uri}
// In the actiondocument.FileUri = await _storage.SaveAsync(stream, File.FileName, "documents", ct);
// Later, when deletingawait _storage.DeleteAsync(document.FileUri); // Same Uri object, always worksWhy: LocalDiskFileStorage returns relative URIs (new Uri("/files/...", UriKind.Relative)). Storing as string and reconstructing with new Uri(string) fails because the default constructor requires absolute URIs. Store the Uri directly and pass it back to DeleteAsync without conversion.
6. Not Calling UseStaticFiles for LocalDiskFileStorage
Section titled “6. Not Calling UseStaticFiles for LocalDiskFileStorage”Wrong:
builder.Services.AddLocalDiskStorage(builder.Environment.WebRootPath);
var app = builder.Build();// Missing: app.UseStaticFiles();app.MapPragmaticEndpoints();Runtime result: Files are saved successfully to wwwroot/files/photos/abc.jpg, but requesting /files/photos/abc.jpg returns 404. The file exists on disk but ASP.NET Core does not serve it.
Right:
builder.Services.AddLocalDiskStorage(builder.Environment.WebRootPath);
var app = builder.Build();app.UseStaticFiles(); // Required to serve files from wwwroot/app.MapPragmaticEndpoints();Why: LocalDiskFileStorage writes files under wwwroot/files/ and returns relative URIs that map to static file paths. But ASP.NET Core does not serve static files unless UseStaticFiles() is in the middleware pipeline. Without it, the files are saved but inaccessible via HTTP.
7. Using LocalDiskFileStorage in Production
Section titled “7. Using LocalDiskFileStorage in Production”Wrong:
// appsettings.Production.json has no storage config override// Program.cs always registers local diskbuilder.Services.AddLocalDiskStorage(builder.Environment.WebRootPath);Production issues:
- Files are lost on container restart (ephemeral storage)
- No CDN, no geographic distribution
- No backup or redundancy
- Disk space limits on cloud VMs
- Horizontal scaling fails (each instance has its own local disk)
Right:
if (app.Environment.IsDevelopment()){ app.UseStorage(sp => new LocalDiskFileStorage( webRootPath, sp.GetRequiredService<ILogger<LocalDiskFileStorage>>()));}else{ app.UseStorage<AzureBlobFileStorage>(); // Or S3FileStorage}Why: LocalDiskFileStorage is designed for development and demos. It has no persistence guarantees, no replication, and no CDN integration. In production, use a cloud storage provider that offers durability, availability, and scalability. The IFileStorage abstraction makes switching trivial — zero changes to domain code.
8. Forgetting to Register the Cloud Provider’s Dependencies
Section titled “8. Forgetting to Register the Cloud Provider’s Dependencies”Wrong:
app.UseStorage<AzureBlobFileStorage>();// But BlobServiceClient is not registered!Runtime result: InvalidOperationException at first file upload: “Unable to resolve service for type ‘Azure.Storage.Blobs.BlobServiceClient’.” The storage service is registered but its dependency is not.
Right:
// Register the Azure SDK clientbuilder.Services.AddSingleton(new BlobServiceClient(connectionString));
// Then register the storage providerapp.UseStorage<AzureBlobFileStorage>();Why: UseStorage<T>() registers T as IFileStorage via AddSingleton<IFileStorage, T>(). The DI container constructs T by resolving its constructor parameters. If AzureBlobFileStorage takes BlobServiceClient in its constructor, that type must also be registered. The storage registration does not magically register the provider’s dependencies.
9. Using Container Names with Special Characters
Section titled “9. Using Container Names with Special Characters”Wrong:
var uri = await _storage.SaveAsync(stream, file.FileName, "user uploads/2024", ct);Runtime result: LocalDiskFileStorage creates a directory named user uploads/2024 under files/, which contains a path separator. On Windows, this creates nested directories user uploads\2024\. On Azure Blob, container names cannot contain spaces or slashes and the operation throws.
Right:
var uri = await _storage.SaveAsync(stream, file.FileName, "user-uploads", ct);Why: Container names should be lowercase, hyphen-separated identifiers without spaces or path separators. This ensures compatibility across all providers: Azure Blob requires lowercase alphanumeric + hyphens (3-63 chars), S3 has similar rules, and local disk avoids filesystem edge cases.
10. Deleting Files Without Cleaning Up Entity References
Section titled “10. Deleting Files Without Cleaning Up Entity References”Wrong:
public override async Task<VoidResult> Execute(CancellationToken ct){ // Delete the file await _storage.DeleteAsync(document.FileUri, ct);
// Forgot to remove or update the entity! // document.FileUri now points to a deleted file}Runtime result: The file is deleted from storage, but the entity still holds the old URI. Any subsequent request to serve that URI returns 404 (local disk) or an error (cloud). The entity appears to have a valid attachment but the file is gone.
Right:
public override async Task<VoidResult> Execute(CancellationToken ct){ // Delete the file await _storage.DeleteAsync(document.FileUri, ct);
// Remove the entity (or clear the URI) await _repository.RemoveAsync(document, ct); await _repository.SaveAsync(ct);
return VoidResult.Success();}Why: IFileStorage and your persistence layer are separate systems with no transactional guarantee. Always update or delete the entity reference when deleting the physical file, and vice versa. For maximum safety, delete the entity first (preventing further access) and then delete the physical file.
Quick Reference
Section titled “Quick Reference”| Mistake | Symptom |
|---|---|
| Reading stream before SaveAsync | Empty file saved, zero bytes |
| Not disposing stream | Resource leak under load |
| Hardcoded base path | Fails on any machine without that exact path |
| Double IFileStorage registration | Wrong provider silently active |
| Storing URI as string | UriFormatException on relative URIs when reconstructing |
| Missing UseStaticFiles | Files saved but 404 on HTTP requests |
| LocalDisk in production | Files lost on restart, no CDN, no scaling |
| Missing cloud provider dependencies | InvalidOperationException at first upload |
| Special characters in container names | Provider-specific errors, inconsistent behavior |
| Deleting files without entity cleanup | Dangling URI references, 404 for users |