Skip to content

Troubleshooting

Practical problem/solution guide for Pragmatic.Storage. Each section covers a common issue, the likely causes, and the fix.


The SaveAsync call succeeds and returns a valid URI, but the stored file has zero bytes.

  1. Was the stream read before passing to SaveAsync? If you inspect or copy the stream before saving, the position is at the end. Reset with ms.Position = 0 (only works with seekable streams like MemoryStream):

    ms.Position = 0; // Reset before saving
    var uri = await _storage.SaveAsync(ms, fileName, container, ct);
  2. Is the IFormFile empty? Check File.Length > 0 before saving. A zero-length file produces a valid but empty storage entry.

  3. Was the stream disposed before SaveAsync completed? If the stream’s scope ends before the async save finishes, the content is lost. Use await on the save call within the stream’s using scope.


404 When Accessing Stored Files (LocalDisk)

Section titled “404 When Accessing Stored Files (LocalDisk)”

Files are saved to disk but HTTP requests to the URI return 404.

  1. Is UseStaticFiles() in the middleware pipeline?

    app.UseStaticFiles(); // Must be called to serve files from wwwroot/
  2. Does the base path match the static files root? LocalDiskFileStorage saves to {basePath}/files/. If basePath is wwwroot, static files serve from wwwroot/. These must match.

  3. Is the request URL correct? LocalDiskFileStorage returns relative URIs like /files/photos/abc.jpg. Verify the client is requesting the exact URI (no extra path segments, correct casing on case-sensitive filesystems).

  4. Is the file actually on disk? Navigate to {basePath}/files/{container}/ and verify the file exists. If the file is missing, the save may have failed silently (check logs).


InvalidOperationException: Unable to Resolve IFileStorage

Section titled “InvalidOperationException: Unable to Resolve IFileStorage”

At runtime, resolving IFileStorage throws because no implementation is registered.

  1. Did you call a registration method? One of these must be called before the host builds:

    // Option 1: IPragmaticBuilder
    app.UseStorage(sp => new LocalDiskFileStorage(basePath, sp.GetRequiredService<ILogger<LocalDiskFileStorage>>()));
    // Option 2: Direct DI
    builder.Services.AddLocalDiskStorage(basePath);
    // Option 3: Custom provider
    builder.Services.AddFileStorage<MyProvider>();
  2. Is the registration conditional and the condition not met? If you use if (env.IsDevelopment()) to register local disk but the environment is not Development, no provider is registered. Ensure all code paths register a provider.

  3. Is the registration happening after builder.Build()? Service registrations must happen before Build(). UseStorage on IPragmaticBuilder runs during the builder phase, so this typically works. But AddLocalDiskStorage on IServiceCollection must be called before Build().


Calling DeleteAsync with a URI reconstructed from a string throws UriFormatException.

LocalDiskFileStorage.SaveAsync returns a relative URI (new Uri("/files/photos/abc.jpg", UriKind.Relative)). If you store this as a string and reconstruct it with new Uri(string), the default constructor requires an absolute URI.

Store the Uri directly on your entity, not as a string:

// Entity
public Uri FileUri { get; set; } = null!;
// Save
entity.FileUri = await _storage.SaveAsync(stream, fileName, container, ct);
// Delete -- same Uri, no conversion
await _storage.DeleteAsync(entity.FileUri, ct);

If you must store as string (e.g., database column), reconstruct with UriKind:

var uri = new Uri(entity.FileUrl, UriKind.RelativeOrAbsolute);
await _storage.DeleteAsync(uri, ct);

SaveAsync throws an exception from the cloud SDK (Azure, S3) on the first call.

  1. Are the cloud SDK dependencies registered? UseStorage<T>() does not register the provider’s dependencies. Register them separately:

    // Azure
    builder.Services.AddSingleton(new BlobServiceClient(connectionString));
    app.UseStorage<AzureBlobFileStorage>();
    // S3
    builder.Services.AddSingleton<IAmazonS3>(new AmazonS3Client(region));
  2. Are the credentials correct? Check the connection string, access key, or managed identity configuration.

  3. Does the container/bucket exist? Some providers require pre-created containers. Others (like the Azure example in the docs) call CreateIfNotExistsAsync. Verify your implementation handles container creation.

  4. Is the network accessible? Cloud storage requires network access. In restricted environments (corporate firewalls, air-gapped), HTTP calls to the storage service may be blocked.


File Deleted But Entity Still References It

Section titled “File Deleted But Entity Still References It”

Users see broken images or download links that return 404.

The file was deleted from storage but the entity’s FileUri property was not cleared or the entity was not removed from the database. There is no transactional guarantee between IFileStorage and your persistence layer.

Always update the entity when deleting a file:

// Option 1: Delete the entity entirely
await _storage.DeleteAsync(document.FileUri, ct);
await _repository.RemoveAsync(document, ct);
await _repository.SaveAsync(ct);
// Option 2: Clear the URI
await _storage.DeleteAsync(document.FileUri, ct);
document.SetFileUri(null);
await _repository.SaveAsync(ct);

For maximum safety, delete the entity reference first (preventing further access), then delete the physical file.


Large File Uploads Fail with OutOfMemoryException

Section titled “Large File Uploads Fail with OutOfMemoryException”

Uploading files larger than ~100 MB causes OutOfMemoryException.

If you read the entire file into a byte[] or MemoryStream before passing to SaveAsync, the full content is held in memory. For large files, this exhausts available memory.

Pass the IFormFile stream directly to SaveAsync without buffering:

await using var stream = File.OpenReadStream();
var uri = await _storage.SaveAsync(stream, File.FileName, container, ct);

If you need to buffer (e.g., for content inspection), consider streaming to a temporary file first:

var tempPath = Path.GetTempFileName();
try
{
await using (var tempFs = System.IO.File.Create(tempPath))
await File.CopyToAsync(tempFs, ct);
// Inspect the temp file
// ...
await using var readStream = System.IO.File.OpenRead(tempPath);
var uri = await _storage.SaveAsync(readStream, File.FileName, container, ct);
}
finally
{
System.IO.File.Delete(tempPath);
}

Also check Kestrel’s request body size limit:

builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 200 * 1024 * 1024; // 200 MB
});

Can I use multiple storage providers simultaneously?

Section titled “Can I use multiple storage providers simultaneously?”

Not with the default registration. IFileStorage is a single singleton. If you need different providers for different containers (e.g., local disk for temp files, S3 for permanent storage), implement a routing IFileStorage that delegates based on the container name.

No. The interface intentionally covers only SaveAsync and DeleteAsync. Reading is handled by the URI itself — the client fetches it directly via HTTP (static files, CDN, or presigned URL). This keeps the abstraction minimal and avoids duplicating HTTP serving logic.

Yes, but mount a volume for persistence. Without a volume, files are stored in the container’s ephemeral filesystem and lost on restart:

volumes:
- ./data/files:/app/wwwroot/files

Register an InMemoryFileStorage (see Getting Started) in your test setup. It stores files in a dictionary and supports both SaveAsync and DeleteAsync without touching the filesystem.

What happens if DeleteAsync is called with a non-existent URI?

Section titled “What happens if DeleteAsync is called with a non-existent URI?”

LocalDiskFileStorage checks File.Exists() before deleting and silently skips if the file is not found. Custom providers should follow the same pattern (idempotent delete). The Azure and S3 examples use DeleteIfExistsAsync, which is a no-op for missing files.