Introduction
Not every .NET application is completely stateless.
Many modern services need to retain data beyond the lifetime of a container or pod. Background workers may generate files, applications may maintain local working data, and stateful services may need persistent storage for information that must survive restarts.
Running these workloads on Kubernetes introduces an important question: how should persistent storage be represented and tested when the application is developed and orchestrated with .NET Aspire?
.NET Aspire provides a code-first approach for describing distributed applications and their resources. Kubernetes, on the other hand, provides the infrastructure required to run those applications at scale. When persistent storage is involved, developers need to understand where the application definition ends and where Kubernetes storage management begins.
Aspire 13.5 adds capabilities that make Kubernetes-oriented application modeling more useful, but persistent workloads still require careful validation.
This article walks through a practical approach to building a .NET service that uses persistent storage, modeling the application with Aspire, deploying it to Kubernetes, and testing whether data survives pod restarts.
Why Persistent Storage Matters
Containers are designed to be replaceable.
A typical Kubernetes deployment may look like:
Deployment
|
+-- Pod 1
+-- Pod 2
+-- Pod 3If Pod 1 is deleted, Kubernetes can create a replacement:
Old Pod
|
X
|
v
New PodData written only to the container's local filesystem should not be treated as durable application storage.
For persistent workloads, the architecture needs a storage layer:
.NET Application
|
v
Container
|
v
Persistent Volume Claim
|
v
Persistent Volume
|
v
Storage BackendThis distinction is critical when developing stateful .NET services.
Stateless vs Stateful Services
Before adding persistent storage, determine whether the application actually needs it.
| Characteristic | Stateless Service | Stateful Service |
|---|---|---|
| Local application data | Temporary | Persistent |
| Pod replacement | Usually simple | Requires storage consideration |
| Horizontal scaling | Generally easier | More complex |
| Persistent volume | Usually unnecessary | Often required |
| Example workload | REST API | File-processing service |
A normal ASP.NET Core API that stores all business data in a database may not need a persistent volume.
A service that creates files that must survive pod replacement may need one.
Do not add persistent storage simply because Kubernetes supports it.
Understand Aspire's Role
.NET Aspire helps developers define and orchestrate distributed applications.
A simplified application can contain:
AppHost
|
+-- Orders API
|
+-- Worker
|
+-- DatabaseThe AppHost describes relationships between application resources.
For example:
var builder = DistributedApplication.CreateBuilder(args);
var ordersApi = builder.AddProject<Projects.OrdersApi>("orders-api");
builder.AddProject<Projects.OrderWorker>("order-worker")
.WithReference(ordersApi);
builder.Build().Run();The exact resources in a production application will depend on its architecture.
When Kubernetes becomes the deployment target, the important question is how those application resources map to Kubernetes infrastructure.
Add a Persistent Storage Requirement
Consider a document-processing service.
Its responsibility is:
Upload
|
v
Process Document
|
v
Generate Output
|
v
Store ResultSuppose generated files must remain available after a pod restart.
The application now needs durable storage.
Conceptually:
Document Worker
|
v
/data/output
|
v
Persistent VolumeThe application should not need to know which physical storage system Kubernetes uses.
It should simply work with its mounted filesystem path.
Design the .NET Service Around a Storage Path
A useful pattern is to make the storage location configurable.
var builder = WebApplication.CreateBuilder(args);
var storagePath =
builder.Configuration["Storage:Path"]
?? "/data";
builder.Services.AddSingleton(
new StorageOptions(storagePath));
var app = builder.Build();
app.MapPost("/documents", async (
IFormFile file,
StorageOptions options) =>
{
var filePath = Path.Combine(
options.Path,
Path.GetFileName(file.FileName));
await using var stream =
File.Create(filePath);
await file.CopyToAsync(stream);
return Results.Ok(new
{
Path = filePath
});
});
app.Run();
public sealed record StorageOptions(string Path);The important design decision is that the application does not hard-code a host-specific storage location.
The storage path is configuration.
That makes the service easier to run in:
Development
Container
Kuberneteswithout changing application code.
Configure the Storage Path
For local development, you might use:
{
"Storage": {
"Path": "./data"
}
}Inside a container, the application can use:
/dataKubernetes can then mount persistent storage at that location.
This creates a clean separation:
Application
|
v
Configured Path
|
v
Container Mount
|
v
Persistent StorageDefine a Kubernetes PersistentVolumeClaim
A Kubernetes workload normally requests persistent storage through a PersistentVolumeClaim.
For example:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: document-storage
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10GiThe exact storage class and capacity should be selected according to the Kubernetes environment and workload.
The application should not assume that 10Gi is appropriate for every deployment.
Mount the Volume Into the Pod
The Deployment can mount the claim:
apiVersion: apps/v1
kind: Deployment
metadata:
name: document-worker
spec:
replicas: 1
selector:
matchLabels:
app: document-worker
template:
metadata:
labels:
app: document-worker
spec:
containers:
- name: document-worker
image: document-worker:latest
volumeMounts:
- name: document-data
mountPath: /data
volumes:
- name: document-data
persistentVolumeClaim:
claimName: document-storageThe important relationship is:
PVC
|
v
Pod Volume
|
v
/data
|
v
.NET ApplicationThe application simply reads and writes /data.
Connect Application Configuration to Kubernetes
The application's configuration should match the mounted location.
For example:
env:
- name: Storage__Path
value: "/data"ASP.NET Core configuration maps the double underscore to a nested configuration key.
The application can therefore read:
builder.Configuration["Storage:Path"]without knowing that Kubernetes is providing the directory.
This is a useful configuration boundary.
Model the Application With Aspire
Aspire can define the application resources and their relationships.
For example:
var builder = DistributedApplication.CreateBuilder(args);
var worker =
builder.AddProject<Projects.DocumentWorker>(
"document-worker");
builder.Build().Run();For local development, the application can use a local directory.
The Kubernetes deployment then provides the persistent volume.
This distinction is important because local orchestration and production infrastructure do not necessarily have identical storage semantics.
Local Storage Is Not the Same as Kubernetes Persistence
During local development, you might mount:
./data:/dataThe directory survives because it exists on the developer's machine.
Kubernetes storage works differently.
The pod may disappear:
Pod A
|
Xbut the persistent volume remains:
Persistent Storage
|
v
Pod B
|
v
/dataTherefore, the actual persistence test must happen in the Kubernetes environment.
Test Data Persistence
A simple persistence test should follow these steps.
Step 1: Deploy the Application
Start the application with its persistent volume.
Step 2: Write Data
Create a test file:
/data/persistence-test.txtThe file might contain:
Persistence test successfulStep 3: Verify the File
Confirm that the application can read the file.
Step 4: Delete the Pod
For example:
kubectl delete pod <pod-name>Kubernetes should create a replacement pod.
Step 5: Read the File Again
The new pod should be able to access the same persistent volume.
The expected flow is:
Write File
|
v
Pod A
|
v
Persistent Volume
|
X
Pod A Deleted
|
v
Pod B Created
|
v
Persistent Volume
|
v
Read FileIf the file disappears, the application is not using persistent storage correctly.
Test Application Restart Separately
Pod deletion is not the only failure scenario.
Also test a normal application restart.
For example:
Application
|
v
Restart
|
v
Same Pod
|
v
Read Persistent Data
Join the conversation! Your thoughts help the community grow.