Getting Started with Pragmatic.Storage
This guide walks through adding file upload and deletion to a Pragmatic.Design application.
Prerequisites
Section titled “Prerequisites”Pragmatic.StorageNuGet package referenced- ASP.NET Core host with
UseStaticFiles()configured (for local disk)
Step 1: Register the Storage Provider
Section titled “Step 1: Register the Storage Provider”Development (local disk)
Section titled “Development (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);Production (custom provider)
Section titled “Production (custom provider)”Swap to your cloud provider without changing any domain code:
app.UseStorage<AzureBlobFileStorage>();Step 2: Create an Upload Action
Section titled “Step 2: Create an Upload Action”With Pragmatic.Actions
Section titled “With Pragmatic.Actions”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(); }}With Minimal API
Section titled “With Minimal API”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 });});Step 3: Create a Delete Action
Section titled “Step 3: Create a Delete Action”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(); }}Step 4: Serve Files
Section titled “Step 4: Serve Files”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.
Step 5: Display in Frontend
Section titled “Step 5: Display in Frontend”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" />Container Conventions
Section titled “Container Conventions”Use descriptive container names that match your domain:
| Container | Content |
|---|---|
"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).
Testing
Section titled “Testing”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());Environment-Based Configuration
Section titled “Environment-Based Configuration”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>(); }});