# Azure Storage Services Standard

> **Scope:** blazor-azure
> **Layer:** 2 (on keyword)
> **Keywords:** azure storage, blob, queue, table, file storage
> **Load When:** azure storage keywords detected

Azure Storage services patterns for Blob, Queue, Table, and File storage.

---

## Overview

Azure Storage provides:
- **Blob Storage**: Object storage for unstructured data (files, images, videos)
- **Queue Storage**: Message queuing for async processing
- **Table Storage**: NoSQL key-value store
- **File Storage**: SMB file shares

**Stack:** .NET 10 + Blazor Server

---

## Core Principles

1. **Managed Identity**: Use Managed Identity over connection strings
2. **SAS Tokens**: Use SAS for temporary access delegation
3. **Retry Policies**: Configure exponential backoff for transient failures
4. **Lifecycle Management**: Auto-delete old blobs with lifecycle policies
5. **Cost Optimization**: Use appropriate storage tiers (Hot/Cool/Archive)

---

## Installation

```bash
dotnet add package Azure.Storage.Blobs
dotnet add package Azure.Storage.Queues
dotnet add package Azure.Identity
```

---

## Blob Storage

### DI Configuration

```csharp
// Program.cs
using Azure.Identity;
using Azure.Storage.Blobs;

builder.Services.AddSingleton(sp =>
{
    var blobServiceUri = new Uri(builder.Configuration["AzureStorage:BlobServiceUri"]!);
    return new BlobServiceClient(blobServiceUri, new DefaultAzureCredential());
});
```

### Upload Blob

```csharp
public class FileUploadService
{
    private readonly BlobServiceClient _blobServiceClient;

    public FileUploadService(BlobServiceClient blobServiceClient)
    {
        _blobServiceClient = blobServiceClient;
    }

    public async Task<string> UploadFileAsync(Stream fileStream, string fileName, string containerName)
    {
        var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
        await containerClient.CreateIfNotExistsAsync();

        var blobClient = containerClient.GetBlobClient(fileName);

        await blobClient.UploadAsync(fileStream, overwrite: true);

        return blobClient.Uri.ToString();
    }
}
```

### Download Blob

```csharp
public async Task<Stream> DownloadFileAsync(string fileName, string containerName)
{
    var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
    var blobClient = containerClient.GetBlobClient(fileName);

    var response = await blobClient.DownloadAsync();
    return response.Value.Content;
}
```

### Generate SAS Token

```csharp
using Azure.Storage.Sas;

public string GenerateSasUri(string containerName, string blobName, TimeSpan validity)
{
    var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
    var blobClient = containerClient.GetBlobClient(blobName);

    var sasBuilder = new BlobSasBuilder
    {
        BlobContainerName = containerName,
        BlobName = blobName,
        Resource = "b", // Blob
        StartsOn = DateTimeOffset.UtcNow,
        ExpiresOn = DateTimeOffset.UtcNow.Add(validity)
    };

    sasBuilder.SetPermissions(BlobSasPermissions.Read);

    var sasUri = blobClient.GenerateSasUri(sasBuilder);
    return sasUri.ToString();
}
```

---

## Queue Storage

### DI Configuration

```csharp
// Program.cs
builder.Services.AddSingleton(sp =>
{
    var queueServiceUri = new Uri(builder.Configuration["AzureStorage:QueueServiceUri"]!);
    return new QueueServiceClient(queueServiceUri, new DefaultAzureCredential());
});
```

### Send Message

```csharp
public class QueueService
{
    private readonly QueueServiceClient _queueServiceClient;

    public QueueService(QueueServiceClient queueServiceClient)
    {
        _queueServiceClient = queueServiceClient;
    }

    public async Task SendMessageAsync(string queueName, string message)
    {
        var queueClient = _queueServiceClient.GetQueueClient(queueName);
        await queueClient.CreateIfNotExistsAsync();

        await queueClient.SendMessageAsync(message);
    }

    public async Task SendMessageWithDelayAsync(string queueName, string message, TimeSpan delay)
    {
        var queueClient = _queueServiceClient.GetQueueClient(queueName);
        await queueClient.CreateIfNotExistsAsync();

        await queueClient.SendMessageAsync(message, visibilityTimeout: delay);
    }
}
```

### Process Messages

```csharp
using Azure.Storage.Queues.Models;

public async Task ProcessMessagesAsync(string queueName, CancellationToken cancellationToken)
{
    var queueClient = _queueServiceClient.GetQueueClient(queueName);

    while (!cancellationToken.IsCancellationRequested)
    {
        QueueMessage[] messages = await queueClient.ReceiveMessagesAsync(maxMessages: 32);

        foreach (var message in messages)
        {
            try
            {
                // Process message
                await ProcessMessageAsync(message.MessageText);

                // Delete message after successful processing
                await queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt);
            }
            catch (Exception ex)
            {
                // Log error, message will become visible again after timeout
                _logger.LogError(ex, "Failed to process message {MessageId}", message.MessageId);
            }
        }

        if (messages.Length == 0)
        {
            await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
        }
    }
}
```

---

## Table Storage

### DI Configuration

```csharp
dotnet add package Azure.Data.Tables

// Program.cs
builder.Services.AddSingleton(sp =>
{
    var tableServiceUri = new Uri(builder.Configuration["AzureStorage:TableServiceUri"]!);
    return new TableServiceClient(tableServiceUri, new DefaultAzureCredential());
});
```

### Entity Model

```csharp
using Azure;
using Azure.Data.Tables;

public class ProductEntity : ITableEntity
{
    public string PartitionKey { get; set; } = default!; // Category
    public string RowKey { get; set; } = default!;       // ProductId
    public DateTimeOffset? Timestamp { get; set; }
    public ETag ETag { get; set; }

    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int Stock { get; set; }
}
```

### CRUD Operations

```csharp
public class ProductTableService
{
    private readonly TableClient _tableClient;

    public ProductTableService(TableServiceClient tableServiceClient)
    {
        _tableClient = tableServiceClient.GetTableClient("products");
        _tableClient.CreateIfNotExists();
    }

    public async Task AddProductAsync(ProductEntity product)
    {
        await _tableClient.AddEntityAsync(product);
    }

    public async Task<ProductEntity?> GetProductAsync(string category, string productId)
    {
        try
        {
            return await _tableClient.GetEntityAsync<ProductEntity>(category, productId);
        }
        catch (RequestFailedException ex) when (ex.Status == 404)
        {
            return null;
        }
    }

    public async Task<List<ProductEntity>> QueryProductsAsync(string category)
    {
        var query = _tableClient.QueryAsync<ProductEntity>(p => p.PartitionKey == category);

        var results = new List<ProductEntity>();
        await foreach (var product in query)
        {
            results.Add(product);
        }

        return results;
    }

    public async Task UpdateProductAsync(ProductEntity product)
    {
        await _tableClient.UpdateEntityAsync(product, ETag.All, TableUpdateMode.Replace);
    }

    public async Task DeleteProductAsync(string category, string productId)
    {
        await _tableClient.DeleteEntityAsync(category, productId);
    }
}
```

---

## Best Practices

### Retry Policy

```csharp
using Azure.Core;

var options = new BlobClientOptions
{
    Retry =
    {
        Mode = RetryMode.Exponential,
        MaxRetries = 3,
        Delay = TimeSpan.FromSeconds(2),
        MaxDelay = TimeSpan.FromSeconds(10)
    }
};

var blobServiceClient = new BlobServiceClient(blobServiceUri, credential, options);
```

### Blob Metadata

```csharp
var metadata = new Dictionary<string, string>
{
    { "uploadedBy", userId },
    { "fileType", "invoice" },
    { "department", "finance" }
};

await blobClient.UploadAsync(stream, metadata: metadata);

// Retrieve metadata
var properties = await blobClient.GetPropertiesAsync();
var uploadedBy = properties.Value.Metadata["uploadedBy"];
```

### Lifecycle Management

```json
// Configure in Azure Portal or via Bicep
{
  "rules": [
    {
      "name": "deleteOldBackups",
      "enabled": true,
      "type": "Lifecycle",
      "definition": {
        "filters": {
          "blobTypes": ["blockBlob"],
          "prefixMatch": ["backups/"]
        },
        "actions": {
          "baseBlob": {
            "delete": {
              "daysAfterModificationGreaterThan": 30
            }
          }
        }
      }
    }
  ]
}
```

---

## Bicep Configuration

```bicep
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  properties: {
    allowBlobPublicAccess: false
    minimumTlsVersion: 'TLS1_2'
    supportsHttpsTrafficOnly: true
  }
}

// Grant Container App Blob access
resource blobRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  scope: storageAccount
  name: guid(storageAccount.id, principalId, 'Storage Blob Data Contributor')
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
    principalId: principalId
  }
}
```

---

## References

- [Azure Blob Storage](https://learn.microsoft.com/azure/storage/blobs/)
- [Azure Queue Storage](https://learn.microsoft.com/azure/storage/queues/)
- [Azure Table Storage](https://learn.microsoft.com/azure/storage/tables/)

---

*MORPH-SPEC by Polymorphism Tech*
