Azure Blob Storage is the production file storage solution for the BlogApp — scalable, durable (11 nines of durability), and integrates with Azure CDN for global distribution. The IBlobStorageService abstraction allows local disk storage in development (fast, no Azure needed) and Azure Blob Storage in production — swapped via dependency injection configuration based on the environment.
IBlobStorageService Abstraction and Azure Implementation
// ── IBlobStorageService — swappable local vs Azure ─────────────────────────
public interface IBlobStorageService
{
Task<string> UploadAsync(string container, string fileName,
Stream content, string mimeType,
CancellationToken ct = default);
Task DeleteAsync(string container, string fileName, CancellationToken ct = default);
string GetPublicUrl(string container, string fileName);
}
// ── Local storage implementation — development ─────────────────────────────
public class LocalBlobStorageService : IBlobStorageService
{
private readonly string _rootPath;
private readonly string _baseUrl;
public LocalBlobStorageService(IWebHostEnvironment env, IConfiguration config)
{
_rootPath = Path.Combine(env.WebRootPath, "uploads");
_baseUrl = config["App:BaseUrl"] ?? "http://localhost:5000";
Directory.CreateDirectory(_rootPath);
}
public async Task<string> UploadAsync(string container, string fileName,
Stream content, string mimeType, CancellationToken ct)
{
var dir = Path.Combine(_rootPath, container);
Directory.CreateDirectory(dir);
var path = Path.Combine(dir, fileName);
using var fs = File.OpenWrite(path);
await content.CopyToAsync(fs, ct);
return GetPublicUrl(container, fileName);
}
public Task DeleteAsync(string container, string fileName, CancellationToken ct)
{
var path = Path.Combine(_rootPath, container, fileName);
if (File.Exists(path)) File.Delete(path);
return Task.CompletedTask;
}
public string GetPublicUrl(string container, string fileName) =>
$"{_baseUrl}/uploads/{container}/{fileName}";
}
// ── Azure Blob Storage implementation — production ──────────────────────────
// Install: dotnet add package Azure.Storage.Blobs
public class AzureBlobStorageService : IBlobStorageService
{
private readonly BlobServiceClient _client;
private readonly string _cdnBaseUrl;
public AzureBlobStorageService(IConfiguration config)
{
_client = new BlobServiceClient(config["Azure:StorageConnectionString"]);
_cdnBaseUrl = config["Azure:CdnBaseUrl"] ?? config["Azure:BlobBaseUrl"]!;
}
public async Task<string> UploadAsync(string container, string fileName,
Stream content, string mimeType, CancellationToken ct)
{
var containerClient = _client.GetBlobContainerClient(container);
await containerClient.CreateIfNotExistsAsync(PublicAccessType.Blob, ct);
var blob = containerClient.GetBlobClient(fileName);
await blob.UploadAsync(content, new BlobUploadOptions
{
HttpHeaders = new BlobHttpHeaders
{
ContentType = mimeType,
CacheControl = "public, max-age=31536000, immutable", // 1 year cache
}
}, ct);
return GetPublicUrl(container, fileName);
}
public async Task DeleteAsync(string container, string fileName, CancellationToken ct)
{
var blob = _client.GetBlobContainerClient(container).GetBlobClient(fileName);
await blob.DeleteIfExistsAsync(cancellationToken: ct);
}
public string GetPublicUrl(string container, string fileName) =>
$"{_cdnBaseUrl}/{container}/{fileName}";
}
// ── Register in Program.cs ────────────────────────────────────────────────
if (builder.Environment.IsDevelopment())
builder.Services.AddSingleton<IBlobStorageService, LocalBlobStorageService>();
else
builder.Services.AddSingleton<IBlobStorageService, AzureBlobStorageService>();
Cache-Control: public, max-age=31536000, immutable on blob uploads tells browsers and CDNs to cache the image for one year and never revalidate — the immutable directive prevents the browser from checking if the file has changed. This is safe because image filenames are GUIDs — a “changed” image gets a new GUID filename, not an update to the existing file. Immutable caching with content-addressable URLs is the gold standard for static asset caching and dramatically reduces CDN bandwidth costs.new BlobServiceClient(new Uri(blobServiceUri), new DefaultAzureCredential()). This eliminates stored credentials entirely — the App Service’s Managed Identity authenticates to the storage account. Add Storage Blob Data Contributor role to the App Service’s identity on the storage account. This is the most secure Azure storage integration pattern.appsettings.json. Store it in environment variables (Azure__StorageConnectionString), Azure Key Vault, or use Managed Identity (no connection string needed). Rotate storage keys periodically using Azure Portal’s key regeneration feature, which invalidates the old key without downtime if you use SAS tokens or Managed Identity instead of keys directly.Common Mistakes
Mistake 1 — Azure connection string in appsettings.json (committed to source control)
❌ Wrong — "StorageConnectionString": "DefaultEndpointsProtocol=https;AccountName=..." in appsettings; exposed in Git history.
✅ Correct — environment variable or Key Vault reference; never in committed files.
Mistake 2 — No Cache-Control header on blob uploads (every request hits origin)
❌ Wrong — blobs without Cache-Control; CDN fetches from origin on every cache miss; high bandwidth costs.
✅ Correct — Cache-Control: public, max-age=31536000, immutable; CDN caches for 1 year; 99%+ cache hit rate.