Skip to content

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.


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:

Program.cs
builder.Services.AddLocalDiskStorage(builder.Environment.WebRootPath);
// Later in the same file or in a startup step
app.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 action
var uri = await _storage.SaveAsync(stream, File.FileName, "documents", ct);
document.FileUrl = uri.ToString();
// Later, when deleting
await _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 action
document.FileUri = await _storage.SaveAsync(stream, File.FileName, "documents", ct);
// Later, when deleting
await _storage.DeleteAsync(document.FileUri); // Same Uri object, always works

Why: 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 disk
builder.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 client
builder.Services.AddSingleton(new BlobServiceClient(connectionString));
// Then register the storage provider
app.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.


MistakeSymptom
Reading stream before SaveAsyncEmpty file saved, zero bytes
Not disposing streamResource leak under load
Hardcoded base pathFails on any machine without that exact path
Double IFileStorage registrationWrong provider silently active
Storing URI as stringUriFormatException on relative URIs when reconstructing
Missing UseStaticFilesFiles saved but 404 on HTTP requests
LocalDisk in productionFiles lost on restart, no CDN, no scaling
Missing cloud provider dependenciesInvalidOperationException at first upload
Special characters in container namesProvider-specific errors, inconsistent behavior
Deleting files without entity cleanupDangling URI references, 404 for users