Azure Blob Storage in .NET: Permissions, Persistence, and Retrieving Data
Reading Time: 8 minutes
The Problem
At first glance, Azure Blob Storage looks simple: upload files, store them cheaply, and download them later. But the real engineering questions show up immediately after that first successful upload:
- Who should be allowed to read or write blobs?
- Should an application use RBAC, SAS tokens, or anonymous access?
- How durable is the data if an Azure region fails?
- Which redundancy option should you choose?
- How should a .NET application upload and retrieve blobs securely without hardcoded keys?
A lot of teams solve the storage problem but leave the security and durability model vague. That usually leads to one of two bad outcomes:
- storage keys copied into configuration files and shared everywhere
- a storage account configured correctly enough to work, but not clearly enough to trust in production
Blob storage is not just about saving bytes. It is about controlling access, choosing the right persistence guarantees, and retrieving data efficiently from application code.
The Solution
Azure Blob Storage gives you durable object storage with several strong building blocks:
- Data plane authorization through Microsoft Entra ID and Azure RBAC
- Scoped temporary access using SAS tokens when direct RBAC is not practical
- Persistence options through redundancy models such as LRS, ZRS, GRS, and GZRS
- Access tiers such as Hot, Cool, Cold, and Archive for cost management
- First-class .NET SDK support through
Azure.Storage.Blobs
For most .NET applications, the practical default is:
- authenticate with
DefaultAzureCredential - use Azure RBAC instead of storage account keys
- give the app’s managed identity only the minimum required role
- use
BlobServiceClientas the entry point for upload and download operations - choose redundancy based on business recovery requirements, not guesswork
Permissions
Blob Storage has two broad permission models:
1. Azure RBAC with Microsoft Entra ID
This is the preferred model for application code.
Typical built-in roles include:
- Storage Blob Data Reader: read blobs only
- Storage Blob Data Contributor: read, write, delete blobs
- Storage Blob Data Owner: full blob data access including ACL-related operations
This model is ideal when:
- your app runs in Azure App Service, Container Apps, Functions, AKS, or VMs
- you can assign a managed identity to the workload
- you want least privilege without distributing account keys
2. Shared Access Signatures (SAS)
A SAS token gives time-limited, permission-limited access to a specific resource.
This is useful when:
- you need to let a browser or external client upload directly
- you need short-lived delegated access
- the caller cannot authenticate with Entra ID directly
A SAS should be treated as a temporary capability, not a default application authentication model.
3. Anonymous public access
This should be enabled only for explicitly public content such as open documentation assets or static files intended for public distribution.
If the blob is not meant to be public, do not rely on obscurity. Disable public access and require authenticated access.
Persistence and Durability
Blob data is persistent by default, but the redundancy option determines how resilient it is.
Common options:
- LRS: Locally Redundant Storage. Copies data within a single datacenter. Cheapest, but weakest disaster recovery story.
- ZRS: Zone-Redundant Storage. Copies data across availability zones in the same region.
- GRS: Geo-Redundant Storage. Replicates data to a paired secondary region.
- GZRS: Geo-Zone-Redundant Storage. Zone redundancy in the primary region plus geo-replication to a secondary region.
A practical rule of thumb:
- use LRS for dev/test or easily reproducible data
- use ZRS when you need strong regional availability
- use GRS or GZRS when disaster recovery across regions matters
Blob Storage also supports access tiers:
- Hot: frequently accessed data
- Cool: infrequently accessed data kept for at least 30 days
- Cold: rarely accessed data kept longer
- Archive: cheapest storage, but retrieval requires rehydration and takes time
Durability and cost are separate decisions. A blob can be highly durable and still belong in a cooler access tier if reads are infrequent.
.NET Setup
Install the SDK packages:
dotnet add package Azure.Storage.Blobs
dotnet add package Azure.Identity
Store only the storage account URI in configuration:
{
"BlobStorage": {
"AccountName": "codingluggagestorage",
"ContainerName": "documents"
}
}
Create an options class:
public sealed class BlobStorageOptions
{
public const string SectionName = "BlobStorage";
public string AccountName { get; set; } = string.Empty;
public string ContainerName { get; set; } = string.Empty;
public string AccountUri => $"https://{AccountName}.blob.core.windows.net";
}
Register the client using DefaultAzureCredential:
using Azure.Identity;
using Azure.Storage.Blobs;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddOptions<BlobStorageOptions>()
.Bind(builder.Configuration.GetSection(BlobStorageOptions.SectionName));
builder.Services.AddSingleton(provider =>
{
var options = provider
.GetRequiredService<IOptions<BlobStorageOptions>>()
.Value;
return new BlobServiceClient(
new Uri(options.AccountUri),
new DefaultAzureCredential());
});
This is the best default because the same code works:
- locally through Azure CLI or Visual Studio sign-in
- in Azure through a managed identity
No account key is needed in code or configuration.
Uploading Data
A simple application service can wrap container and blob operations:
using Azure;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Microsoft.Extensions.Options;
public sealed class BlobDocumentStore
{
private readonly BlobContainerClient _container;
private readonly ILogger<BlobDocumentStore> _logger;
public BlobDocumentStore(
BlobServiceClient blobServiceClient,
IOptions<BlobStorageOptions> options,
ILogger<BlobDocumentStore> logger)
{
var storageOptions = options.Value;
_container = blobServiceClient.GetBlobContainerClient(storageOptions.ContainerName);
_logger = logger;
}
public async Task UploadAsync(
string blobName,
Stream content,
string contentType,
CancellationToken cancellationToken = default)
{
await _container.CreateIfNotExistsAsync(
publicAccessType: PublicAccessType.None,
cancellationToken: cancellationToken);
var blobClient = _container.GetBlobClient(blobName);
await blobClient.UploadAsync(
content,
new BlobUploadOptions
{
HttpHeaders = new BlobHttpHeaders
{
ContentType = contentType
}
},
cancellationToken);
_logger.LogInformation("Uploaded blob {BlobName}", blobName);
}
}
Important points:
CreateIfNotExistsAsyncis convenient for demos and bootstrap flows, but many teams create containers through IaC insteadPublicAccessType.Nonekeeps the container private- content type should be set explicitly so downstream consumers get the right MIME type
Retrieving Data
The most common retrieval patterns are:
- download the whole blob
- stream the blob to the caller
- check whether the blob exists before attempting a read
Download into a stream
public async Task<Stream?> OpenReadAsync(
string blobName,
CancellationToken cancellationToken = default)
{
var blobClient = _container.GetBlobClient(blobName);
if (!await blobClient.ExistsAsync(cancellationToken))
{
return null;
}
var response = await blobClient.DownloadStreamingAsync(cancellationToken: cancellationToken);
return response.Value.Content;
}
This is useful when you want to return the stream directly from an API endpoint without buffering the full file in memory.
Download as bytes
public async Task<byte[]?> DownloadBytesAsync(
string blobName,
CancellationToken cancellationToken = default)
{
var blobClient = _container.GetBlobClient(blobName);
if (!await blobClient.ExistsAsync(cancellationToken))
{
return null;
}
var response = await blobClient.DownloadContentAsync(cancellationToken);
return response.Value.Content.ToArray();
}
This is simpler, but it buffers the entire blob in memory. Fine for smaller files, but not ideal for large content.
Read blob metadata and properties
public async Task<BlobProperties?> GetPropertiesAsync(
string blobName,
CancellationToken cancellationToken = default)
{
var blobClient = _container.GetBlobClient(blobName);
if (!await blobClient.ExistsAsync(cancellationToken))
{
return null;
}
var response = await blobClient.GetPropertiesAsync(cancellationToken: cancellationToken);
return response.Value;
}
This is useful when you need:
- content length
- content type
- last modified timestamp
- ETag
- custom metadata
Returning a Blob from an ASP.NET Core Endpoint
A common pattern is reading from Blob Storage and streaming the content from an API:
app.MapGet("/files/{blobName}", async (
string blobName,
BlobDocumentStore store,
CancellationToken cancellationToken) =>
{
var stream = await store.OpenReadAsync(blobName, cancellationToken);
if (stream is null)
{
return Results.NotFound();
}
return Results.File(stream, contentType: "application/octet-stream");
});
For a production system, you would usually also retrieve the blob properties and return the actual content type instead of a generic fallback.
Using SAS for Retrieval
Sometimes you do not want your API to proxy blob content. In that case, your server can generate a short-lived SAS URL and return it to the client.
That pattern is useful when:
- the client should download directly from Blob Storage
- you want to reduce API bandwidth and memory usage
- access should expire automatically after a short time
Example using a user delegation SAS:
using Azure.Storage.Sas;
public async Task<Uri> CreateReadSasAsync(
string blobName,
CancellationToken cancellationToken = default)
{
var blobClient = _container.GetBlobClient(blobName);
var startsOn = DateTimeOffset.UtcNow.AddMinutes(-5);
var expiresOn = DateTimeOffset.UtcNow.AddMinutes(15);
var delegationKey = await _container.GetParentBlobServiceClient()
.GetUserDelegationKeyAsync(startsOn, expiresOn, cancellationToken);
var sasBuilder = new BlobSasBuilder
{
BlobContainerName = _container.Name,
BlobName = blobName,
Resource = "b",
StartsOn = startsOn,
ExpiresOn = expiresOn
};
sasBuilder.SetPermissions(BlobSasPermissions.Read);
return blobClient.GenerateUserDelegationSasUri(sasBuilder, delegationKey.Value);
}
This is better than handing out account keys because:
- access is temporary
- permissions are scoped
- the token can be limited to one blob and one operation
Security Recommendations
For most systems, these are the right defaults:
- use managed identity and
DefaultAzureCredential - assign Storage Blob Data Reader or Storage Blob Data Contributor only as needed
- do not use storage account keys in application code
- keep containers private unless content is intentionally public
- use SAS only for short-lived delegated client access
- log failures and access patterns so blob usage is observable
Summary
Azure Blob Storage is more than a place to dump files. The real design work is choosing the right permission model, the right persistence guarantees, and the right retrieval pattern for your application.
In practice, the strongest .NET default looks like this:
- authenticate with
DefaultAzureCredential - use Azure RBAC with a managed identity
- keep containers private by default
- select redundancy based on recovery requirements
- stream blob content when files are large
- issue short-lived SAS URLs only when clients need direct access
That gives you secure access, durable storage, and a clean retrieval story without pushing secrets into configuration or overcomplicating the code.
References
- Azure Blob Storage overview: https://learn.microsoft.com/azure/storage/blobs/storage-blobs-overview
- Azure Blob Storage .NET SDK: https://learn.microsoft.com/dotnet/api/overview/azure/storage.blobs-readme
- Authorize access to blobs with Microsoft Entra ID: https://learn.microsoft.com/azure/storage/blobs/authorize-access-azure-active-directory
- User delegation SAS for Blob Storage: https://learn.microsoft.com/azure/storage/blobs/storage-blob-user-delegation-sas-create-dotnet