Skip to content

Getting Started with Pragmatic.Storage

This guide walks through adding file upload and deletion to a Pragmatic.Design application.

  • Pragmatic.Storage NuGet package referenced
  • ASP.NET Core host with UseStaticFiles() configured (for local disk)

In your Program.cs:

using Pragmatic.Storage;
await PragmaticApp.RunAsync(args, app =>
{
app.UseStorage(sp => new LocalDiskFileStorage(
Path.Combine(builder.Environment.ContentRootPath, "wwwroot"),
sp.GetRequiredService<ILogger<LocalDiskFileStorage>>()));
});

Or using the IServiceCollection extension directly:

builder.Services.AddLocalDiskStorage(builder.Environment.WebRootPath);

Swap to your cloud provider without changing any domain code:

app.UseStorage<AzureBlobFileStorage>();
public class UploadDocument : DomainAction<DocumentDto>
{
private IFileStorage _storage = null!;
private IRepository<Document, Guid> _repository = null!;
public required IFormFile File { get; init; }
public required string Category { get; init; }
public override async Task<Result<DocumentDto, IError>> Execute(CancellationToken ct = default)
{
// Validate file
if (File.Length == 0)
return new ValidationError("File is empty");
if (File.Length > 10 * 1024 * 1024)
return new ValidationError("File exceeds 10 MB limit");
// Save to storage
await using var stream = File.OpenReadStream();
var uri = await _storage.SaveAsync(stream, File.FileName, Category, ct);
// Create domain entity
var document = Document.Create(File.FileName, uri, File.Length, Category);
await _repository.AddAsync(document, ct);
await _repository.SaveAsync(ct);
return document.ToDto();
}
}
app.MapPost("/api/files", async (IFormFile file, string container, IFileStorage storage) =>
{
await using var stream = file.OpenReadStream();
var uri = await storage.SaveAsync(stream, file.FileName, container);
return Results.Created(uri.ToString(), new { uri });
});
public class DeleteDocument : VoidDomainAction
{
private IFileStorage _storage = null!;
private IRepository<Document, Guid> _repository = null!;
public required Guid DocumentId { get; init; }
public override async Task<VoidResult> Execute(CancellationToken ct = default)
{
var document = await _repository.GetByIdAsync(DocumentId, ct);
if (document is null)
return new NotFoundError("Document", DocumentId);
// Delete from storage
await _storage.DeleteAsync(document.FileUri, ct);
// Delete from database
await _repository.RemoveAsync(document, ct);
await _repository.SaveAsync(ct);
return VoidResult.Success();
}
}

For local disk storage, files are stored under wwwroot/files/. Enable static file serving:

app.UseStaticFiles();

URIs returned by LocalDiskFileStorage are relative (e.g., /files/photos/abc123.jpg) and map directly to the static files path.

For cloud storage, SaveAsync returns absolute URIs (e.g., https://account.blob.core.windows.net/photos/abc123.jpg) that clients can access directly.

The Uri stored on your entity works as a direct download URL:

<!-- For local disk: relative URI -->
<img src="/files/photos/abc123.jpg" />
<!-- For cloud: absolute URI -->
<img src="https://account.blob.core.windows.net/photos/abc123.jpg" />

Use descriptive container names that match your domain:

ContainerContent
"photos"User-uploaded images
"documents"PDF/Word documents
"imports"CSV/Excel import files
"invoices"Generated invoice PDFs
"avatars"User profile pictures

Containers map to folders (local disk) or blob containers (Azure) or prefixes (S3).

In tests, you can use a simple in-memory implementation:

public class InMemoryFileStorage : IFileStorage
{
private readonly Dictionary<Uri, byte[]> _files = new();
public async Task<Uri> SaveAsync(
Stream content, string fileName, string container, CancellationToken ct = default)
{
using var ms = new MemoryStream();
await content.CopyToAsync(ms, ct);
var uri = new Uri($"/files/{container}/{Guid.NewGuid():N}{Path.GetExtension(fileName)}",
UriKind.Relative);
_files[uri] = ms.ToArray();
return uri;
}
public Task DeleteAsync(Uri fileUri, CancellationToken ct = default)
{
_files.Remove(fileUri);
return Task.CompletedTask;
}
public byte[]? GetContent(Uri uri) => _files.GetValueOrDefault(uri);
}

Register in test setup:

services.AddSingleton<IFileStorage>(new InMemoryFileStorage());

A common pattern is to switch provider based on the environment:

await PragmaticApp.RunAsync(args, app =>
{
if (app.Environment.IsDevelopment())
{
app.UseStorage(sp => new LocalDiskFileStorage(
webRootPath,
sp.GetRequiredService<ILogger<LocalDiskFileStorage>>()));
}
else
{
app.UseStorage<AzureBlobFileStorage>();
}
});