Troubleshooting
Practical problem/solution guide for Pragmatic.Storage. Each section covers a common issue, the likely causes, and the fix.
File Saves But Returns Empty Content
Section titled “File Saves But Returns Empty Content”The SaveAsync call succeeds and returns a valid URI, but the stored file has zero bytes.
Checklist
Section titled “Checklist”-
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 likeMemoryStream):ms.Position = 0; // Reset before savingvar uri = await _storage.SaveAsync(ms, fileName, container, ct); -
Is the IFormFile empty? Check
File.Length > 0before saving. A zero-length file produces a valid but empty storage entry. -
Was the stream disposed before SaveAsync completed? If the stream’s scope ends before the async save finishes, the content is lost. Use
awaiton the save call within the stream’susingscope.
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.
Checklist
Section titled “Checklist”-
Is
UseStaticFiles()in the middleware pipeline?app.UseStaticFiles(); // Must be called to serve files from wwwroot/ -
Does the base path match the static files root?
LocalDiskFileStoragesaves to{basePath}/files/. IfbasePathiswwwroot, static files serve fromwwwroot/. These must match. -
Is the request URL correct?
LocalDiskFileStoragereturns 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). -
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.
Checklist
Section titled “Checklist”-
Did you call a registration method? One of these must be called before the host builds:
// Option 1: IPragmaticBuilderapp.UseStorage(sp => new LocalDiskFileStorage(basePath, sp.GetRequiredService<ILogger<LocalDiskFileStorage>>()));// Option 2: Direct DIbuilder.Services.AddLocalDiskStorage(basePath);// Option 3: Custom providerbuilder.Services.AddFileStorage<MyProvider>(); -
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. -
Is the registration happening after
builder.Build()? Service registrations must happen beforeBuild().UseStorageonIPragmaticBuilderruns during the builder phase, so this typically works. ButAddLocalDiskStorageonIServiceCollectionmust be called beforeBuild().
UriFormatException When Deleting Files
Section titled “UriFormatException When Deleting Files”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:
// Entitypublic Uri FileUri { get; set; } = null!;
// Saveentity.FileUri = await _storage.SaveAsync(stream, fileName, container, ct);
// Delete -- same Uri, no conversionawait _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);Cloud Provider Throws at First Upload
Section titled “Cloud Provider Throws at First Upload”SaveAsync throws an exception from the cloud SDK (Azure, S3) on the first call.
Checklist
Section titled “Checklist”-
Are the cloud SDK dependencies registered?
UseStorage<T>()does not register the provider’s dependencies. Register them separately:// Azurebuilder.Services.AddSingleton(new BlobServiceClient(connectionString));app.UseStorage<AzureBlobFileStorage>();// S3builder.Services.AddSingleton<IAmazonS3>(new AmazonS3Client(region)); -
Are the credentials correct? Check the connection string, access key, or managed identity configuration.
-
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. -
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 entirelyawait _storage.DeleteAsync(document.FileUri, ct);await _repository.RemoveAsync(document, ct);await _repository.SaveAsync(ct);
// Option 2: Clear the URIawait _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.
Does IFileStorage support reading files?
Section titled “Does IFileStorage support reading files?”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.
Can I use LocalDiskFileStorage in Docker?
Section titled “Can I use LocalDiskFileStorage in Docker?”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/filesHow do I test file upload endpoints?
Section titled “How do I test file upload endpoints?”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.
Getting Help
Section titled “Getting Help”- GitHub Issues: github.com/nicola-pragmatic/Pragmatic.Design/issues
- Showcase Examples: See the
Showcaseproject for working file upload/download endpoints. - Custom Providers: See custom-providers.md for Azure Blob, S3, and other backend implementations.