diff --git a/.github/plugins/azure-sdk-dotnet/README.md b/.github/plugins/azure-sdk-dotnet/README.md index 934d5134..ddb2c712 100644 --- a/.github/plugins/azure-sdk-dotnet/README.md +++ b/.github/plugins/azure-sdk-dotnet/README.md @@ -1,6 +1,6 @@ # azure-sdk-dotnet -Azure SDK patterns and best practices for .NET developers. Covers 29 skills spanning AI, resource management, identity, messaging, and Key Vault libraries. +Azure SDK patterns and best practices for .NET developers. Covers 30 skills spanning AI, resource management, identity, messaging, storage, and Key Vault libraries. ## Install @@ -43,5 +43,6 @@ npx skills add microsoft/skills --skill azure-sdk-dotnet | `azure-search-documents-dotnet` | AI Search | | `azure-security-keyvault-keys-dotnet` | Key Vault Keys | | `azure-servicebus-dotnet` | Service Bus | +| `azure-storage-blob-dotnet` | Blob Storage | | `m365-agents-dotnet` | M365 Agents SDK | | `microsoft-azure-webjobs-extensions-authentication-events-dotnet` | Auth Events Extension | diff --git a/.github/plugins/azure-sdk-dotnet/skills/azure-storage-blob-dotnet/SKILL.md b/.github/plugins/azure-sdk-dotnet/skills/azure-storage-blob-dotnet/SKILL.md new file mode 100644 index 00000000..097e5e54 --- /dev/null +++ b/.github/plugins/azure-sdk-dotnet/skills/azure-storage-blob-dotnet/SKILL.md @@ -0,0 +1,383 @@ +--- +name: azure-storage-blob-dotnet +description: | + Azure Storage Blob SDK for .NET. Client library for storing and managing unstructured data (files, images, backups, logs) in Azure Blob Storage. Use for uploading, downloading, streaming, listing, copying, and deleting blobs, managing containers, generating SAS tokens, and setting blob properties/metadata. Triggers: "Azure Blob Storage .NET", "BlobServiceClient", "BlobContainerClient", "BlobClient", "upload download blob C#", "Azure.Storage.Blobs", "SAS token blob", "blob streaming", "block blob". +license: MIT +metadata: + author: Microsoft + version: "1.0.0" + package: Azure.Storage.Blobs +--- + +# Azure.Storage.Blobs (.NET) + +Client library for storing and managing unstructured data in Azure Blob Storage. + +## Installation + +```bash +dotnet add package Azure.Storage.Blobs +dotnet add package Azure.Identity +``` + +**Current Version**: 12.29.0 (stable) + +## Environment Variables + +```bash +AZURE_STORAGE_ACCOUNT_URL=https://.blob.core.windows.net # Required for Entra ID auth +AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=... # Alternative to Entra ID auth +``` + +## Client Creation + +### BlobServiceClient with DefaultAzureCredential + +Prefer Microsoft Entra ID (`DefaultAzureCredential`) over account keys or connection strings. + +```csharp +using Azure.Identity; +using Azure.Storage.Blobs; + +string accountUrl = Environment.GetEnvironmentVariable("AZURE_STORAGE_ACCOUNT_URL") + ?? throw new InvalidOperationException("AZURE_STORAGE_ACCOUNT_URL not set"); + +var serviceClient = new BlobServiceClient(new Uri(accountUrl), new DefaultAzureCredential()); +``` + +### BlobContainerClient + +```csharp +// From the service client +BlobContainerClient containerClient = serviceClient.GetBlobContainerClient("mycontainer"); + +// Direct construction with DefaultAzureCredential +var containerClient2 = new BlobContainerClient( + new Uri($"{accountUrl}/mycontainer"), + new DefaultAzureCredential()); +``` + +### BlobClient + +```csharp +// From the container client +BlobClient blobClient = containerClient.GetBlobClient("myblob.txt"); + +// With virtual directory structure +BlobClient nested = containerClient.GetBlobClient("folder/subfolder/myblob.txt"); +``` + +### With Connection String (non-Entra fallback) + +```csharp +string connectionString = Environment.GetEnvironmentVariable("AZURE_STORAGE_CONNECTION_STRING"); +var serviceClient = new BlobServiceClient(connectionString); +``` + +## Core Patterns + +### Create Container + +```csharp +// Throws RequestFailedException (409) if the container already exists +BlobContainerClient container = await serviceClient.CreateBlobContainerAsync("mycontainer"); + +// Or from an existing container client +await containerClient.CreateIfNotExistsAsync(); +``` + +### Upload Data + +```csharp +using Azure.Storage.Blobs.Models; + +// Upload a string via BinaryData (overwrite enabled) +await blobClient.UploadAsync(BinaryData.FromString("Hello, Azure Blob Storage!"), overwrite: true); + +// Upload a file from a local path +await blobClient.UploadAsync("local-file.txt", overwrite: true); + +// Upload from a stream +await using (FileStream stream = File.OpenRead("large-file.bin")) +{ + await blobClient.UploadAsync(stream, overwrite: true); +} +``` + +### Upload with Headers and Metadata + +```csharp +using Azure.Storage.Blobs.Models; + +var options = new BlobUploadOptions +{ + HttpHeaders = new BlobHttpHeaders + { + ContentType = "application/json", + CacheControl = "max-age=3600" + }, + Metadata = new Dictionary + { + ["author"] = "john", + ["version"] = "1.0" + } +}; + +await using FileStream stream = File.OpenRead("data.json"); +await blobClient.UploadAsync(stream, options); +``` + +### Upload Only If Not Exists + +```csharp +using Azure; +using Azure.Storage.Blobs.Models; + +var options = new BlobUploadOptions +{ + Conditions = new BlobRequestConditions { IfNoneMatch = ETag.All } +}; + +try +{ + await using FileStream stream = File.OpenRead("data.json"); + await blobClient.UploadAsync(stream, options); +} +catch (RequestFailedException ex) when (ex.Status == 409) +{ + Console.WriteLine("Blob already exists"); +} +``` + +### Download Data + +```csharp +using Azure.Storage.Blobs.Models; + +// Download content into memory (small blobs) +BlobDownloadResult result = await blobClient.DownloadContentAsync(); +string text = result.Content.ToString(); + +// Download to a local file +await blobClient.DownloadToAsync("downloaded-file.txt"); +``` + +### Download to Stream (memory-efficient streaming) + +```csharp +BlobDownloadStreamingResult download = await blobClient.DownloadStreamingAsync(); +await using (Stream source = download.Content) +await using (FileStream destination = File.Create("large-download.bin")) +{ + await source.CopyToAsync(destination); +} + +// Or open a readable stream directly +await using Stream blobStream = await blobClient.OpenReadAsync(); +``` + +### Upload via Writable Stream + +```csharp +await using Stream writeStream = await blobClient.OpenWriteAsync(overwrite: true); +await writeStream.WriteAsync(Encoding.UTF8.GetBytes("Streaming data...")); +``` + +### List Blobs + +```csharp +using Azure.Storage.Blobs.Models; + +// List all blobs +await foreach (BlobItem blob in containerClient.GetBlobsAsync()) +{ + Console.WriteLine($"Blob: {blob.Name}, Size: {blob.Properties.ContentLength}"); +} + +// List with a prefix (virtual directory) +await foreach (BlobItem blob in containerClient.GetBlobsAsync(prefix: "folder/subfolder/")) +{ + Console.WriteLine(blob.Name); +} +``` + +### List Blobs by Hierarchy (directories) + +```csharp +using Azure.Storage.Blobs.Models; + +await foreach (BlobHierarchyItem item in + containerClient.GetBlobsByHierarchyAsync(prefix: "data/", delimiter: "/")) +{ + if (item.IsPrefix) + Console.WriteLine($"Directory: {item.Prefix}"); + else + Console.WriteLine($"Blob: {item.Blob.Name}"); +} +``` + +### Delete Blob + +```csharp +using Azure.Storage.Blobs.Models; + +// Delete if exists (idempotent) +await blobClient.DeleteIfExistsAsync(); + +// Delete including snapshots +await blobClient.DeleteIfExistsAsync(DeleteSnapshotsOption.IncludeSnapshots); +``` + +### Copy Blob + +```csharp +using Azure.Storage.Blobs.Models; + +// Async copy (large blobs or cross-account) +CopyFromUriOperation operation = await destBlobClient.StartCopyFromUriAsync(sourceBlobUri); +await operation.WaitForCompletionAsync(); + +// Synchronous copy from URL (same account, smaller blobs) +await destBlobClient.SyncCopyFromUriAsync(sourceBlobUri); +``` + +### Generate SAS Token + +`GenerateSasUri` requires the client to be authorized with a shared key. When using +`DefaultAzureCredential`, create a **user delegation SAS** instead (see below). + +```csharp +using Azure.Storage.Sas; + +// Shared-key authorized client (blob-level, read-only, 1-day expiry) +if (blobClient.CanGenerateSasUri) +{ + var sasBuilder = new BlobSasBuilder(BlobSasPermissions.Read, DateTimeOffset.UtcNow.AddDays(1)) + { + BlobContainerName = blobClient.BlobContainerName, + BlobName = blobClient.Name, + Resource = "b" + }; + + Uri sasUri = blobClient.GenerateSasUri(sasBuilder); +} +``` + +### Generate User Delegation SAS (with Entra ID) + +```csharp +using Azure.Storage.Blobs.Models; +using Azure.Storage.Sas; + +UserDelegationKey delegationKey = await serviceClient.GetUserDelegationKeyAsync( + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddDays(1)); + +var sasBuilder = new BlobSasBuilder(BlobSasPermissions.Read, DateTimeOffset.UtcNow.AddDays(1)) +{ + BlobContainerName = containerClient.Name, + BlobName = "myblob.txt", + Resource = "b" +}; + +var uriBuilder = new BlobUriBuilder(blobClient.Uri) +{ + Sas = sasBuilder.ToSasQueryParameters(delegationKey, serviceClient.AccountName) +}; + +Uri sasUri = uriBuilder.ToUri(); +``` + +### Blob Properties and Metadata + +```csharp +using Azure.Storage.Blobs.Models; + +// Get properties +BlobProperties properties = await blobClient.GetPropertiesAsync(); +Console.WriteLine($"Size: {properties.ContentLength}"); +Console.WriteLine($"Content-Type: {properties.ContentType}"); +Console.WriteLine($"Last Modified: {properties.LastModified}"); + +// Set metadata +var metadata = new Dictionary { ["processed"] = "true", ["version"] = "2.0" }; +await blobClient.SetMetadataAsync(metadata); + +// Set HTTP headers +await blobClient.SetHttpHeadersAsync(new BlobHttpHeaders +{ + ContentType = "application/octet-stream", + CacheControl = "max-age=86400" +}); +``` + +### Lease Blob + +```csharp +using Azure.Storage.Blobs.Models; +using Azure.Storage.Blobs.Specialized; + +BlobLeaseClient leaseClient = blobClient.GetBlobLeaseClient(); + +// Acquire a 60-second lease (use TimeSpan.FromSeconds(-1) for infinite) +BlobLease lease = await leaseClient.AcquireAsync(TimeSpan.FromSeconds(60)); +try +{ + await blobClient.UploadAsync(BinaryData.FromString("Updated content"), overwrite: true); + await leaseClient.RenewAsync(); +} +finally +{ + await leaseClient.ReleaseAsync(); +} +``` + +## Error Handling + +All service errors surface as `RequestFailedException`. Inspect `Status` and `ErrorCode`. + +```csharp +using Azure; + +try +{ + BlobDownloadResult result = await blobClient.DownloadContentAsync(); +} +catch (RequestFailedException ex) +{ + switch (ex.Status) + { + case 404: // BlobNotFound + Console.Error.WriteLine($"Blob not found. ErrorCode: {ex.ErrorCode}"); + break; + case 409: // Conflict (lease, already exists) + Console.Error.WriteLine($"Conflict. ErrorCode: {ex.ErrorCode}"); + throw; + case 403: // Authorization failure + Console.Error.WriteLine($"Access denied. ErrorCode: {ex.ErrorCode}"); + throw; + default: + Console.Error.WriteLine($"Error {ex.Status}: {ex.Message}"); + throw; + } +} +``` + +## Best Practices + +- Use `DefaultAzureCredential` over account keys or connection strings for authentication. +- For SAS with Entra ID, use a **user delegation SAS** — `GenerateSasUri` requires shared-key auth. +- Pass `overwrite: true` explicitly; uploads fail if the blob exists and overwrite is not set. +- Use `CreateIfNotExistsAsync` / `DeleteIfExistsAsync` for idempotent operations. +- Stream large blobs with `OpenReadAsync` / `OpenWriteAsync` / `DownloadStreamingAsync` instead of buffering in memory. +- Catch `RequestFailedException` and branch on `Status` / `ErrorCode` — never swallow generic exceptions. +- Reuse a single `BlobServiceClient` instance; it is thread-safe and manages connection pooling. + +## Trigger Phrases + +- "Azure Blob Storage .NET" +- "upload download blob C#" +- "BlobServiceClient BlobContainerClient" +- "blob streaming" +- "SAS token blob" +- "blob metadata properties" diff --git a/.github/plugins/azure-skills/skills/azure-storage/SKILL.md b/.github/plugins/azure-skills/skills/azure-storage/SKILL.md index 21bdc643..fc24b22e 100644 --- a/.github/plugins/azure-skills/skills/azure-storage/SKILL.md +++ b/.github/plugins/azure-skills/skills/azure-storage/SKILL.md @@ -87,7 +87,7 @@ For deep documentation on specific services: For building applications with Azure Storage SDKs, see the condensed guides: -- **Blob Storage**: [Python](references/sdk/azure-storage-blob-py.md) | [TypeScript](references/sdk/azure-storage-blob-ts.md) | [Java](references/sdk/azure-storage-blob-java.md) | [Rust](references/sdk/azure-storage-blob-rust.md) +- **Blob Storage**: [Python](references/sdk/azure-storage-blob-py.md) | [TypeScript](references/sdk/azure-storage-blob-ts.md) | [Java](references/sdk/azure-storage-blob-java.md) | [.NET](references/sdk/azure-storage-blob-dotnet.md) | [Rust](references/sdk/azure-storage-blob-rust.md) - **Queue Storage**: [Python](references/sdk/azure-storage-queue-py.md) | [TypeScript](references/sdk/azure-storage-queue-ts.md) - **File Shares**: [Python](references/sdk/azure-storage-file-share-py.md) | [TypeScript](references/sdk/azure-storage-file-share-ts.md) - **Data Lake**: [Python](references/sdk/azure-storage-file-datalake-py.md) diff --git a/.github/plugins/azure-skills/skills/azure-storage/references/sdk/azure-storage-blob-dotnet.md b/.github/plugins/azure-skills/skills/azure-storage/references/sdk/azure-storage-blob-dotnet.md new file mode 100644 index 00000000..58b22549 --- /dev/null +++ b/.github/plugins/azure-skills/skills/azure-storage/references/sdk/azure-storage-blob-dotnet.md @@ -0,0 +1,35 @@ +# Blob Storage — .NET SDK Quick Reference + +> Condensed from **azure-storage-blob-dotnet**. Full patterns (SAS tokens, +> user delegation SAS, streaming, lease management, copy, properties/metadata) +> in the **azure-storage-blob-dotnet** plugin skill if installed. + +## Install +```bash +dotnet add package Azure.Storage.Blobs +dotnet add package Azure.Identity +``` + +## Quick Start +```csharp +using Azure.Identity; +using Azure.Storage.Blobs; + +var serviceClient = new BlobServiceClient( + new Uri(Environment.GetEnvironmentVariable("AZURE_STORAGE_ACCOUNT_URL")), + new DefaultAzureCredential()); + +BlobContainerClient containerClient = serviceClient.GetBlobContainerClient("mycontainer"); +BlobClient blobClient = containerClient.GetBlobClient("myblob.txt"); + +await blobClient.UploadAsync(BinaryData.FromString("Hello, Azure Blob Storage!"), overwrite: true); +``` + +## Best Practices +- Use DefaultAzureCredential over account keys / connection strings. See [auth-best-practices.md](../auth-best-practices.md) +- Pass `overwrite: true` explicitly on `UploadAsync` — uploads fail if the blob exists otherwise +- Use `CreateIfNotExistsAsync()` / `DeleteIfExistsAsync()` for idempotent operations +- Use `BlobUploadOptions` to set `HttpHeaders` and `Metadata` on upload +- Stream large blobs with `OpenReadAsync` / `OpenWriteAsync` / `DownloadStreamingAsync` +- For SAS with Entra ID, use a user delegation SAS (`GetUserDelegationKeyAsync` + `BlobSasBuilder`) +- Handle `RequestFailedException` — branch on `Status` (404/409/403) and `ErrorCode` diff --git a/Agents.md b/Agents.md index f9754ab3..76c91fb0 100644 --- a/Agents.md +++ b/Agents.md @@ -172,7 +172,7 @@ npx skills add microsoft/skills |----------|--------|--------|----------| | **Core** | 6 | — | `mcp-builder`, `skill-creator`, `copilot-sdk` | | **Python** | 41 | `-py` | `azure-ai-projects-py`, `azure-cosmos-py`, `azure-ai-ml-py` | -| **.NET** | 29 | `-dotnet` | `azure-ai-projects-dotnet`, `azure-resource-manager-cosmosdb-dotnet`, `azure-security-keyvault-keys-dotnet` | +| **.NET** | 30 | `-dotnet` | `azure-ai-projects-dotnet`, `azure-storage-blob-dotnet`, `azure-security-keyvault-keys-dotnet` | | **TypeScript** | 25 | `-ts` | `azure-ai-projects-ts`, `azure-storage-blob-ts`, `aspire-ts` | | **Java** | 26 | `-java` | `azure-ai-projects-java`, `azure-cosmos-java`, `azure-eventhub-java` | diff --git a/README.md b/README.md index 73b716f8..90521106 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Coding agents like [Copilot CLI](https://github.com/features/copilot/cli) and [G | Resource | Description | |----------|-------------| -| **[174 Skills](#skill-catalog)** | Domain-specific knowledge for Azure SDK and Foundry development | +| **[175 Skills](#skill-catalog)** | Domain-specific knowledge for Azure SDK and Foundry development | | **[Plugins](#plugins)** | Installable plugin packages (deep-wiki, azure-skills and more) | | **[Custom Agents](#agents)** | Role-specific agents (backend, frontend, infrastructure, planner) | | **[AGENTS.md](AGENTS.md)** | Template for configuring agent behavior in your projects | @@ -70,14 +70,14 @@ Coding agents like [Copilot CLI](https://github.com/features/copilot/cli) and [G ## Skill Catalog -> 174 skills across language plugins — see [skill catalog](#skill-catalog) below for the full breakdown +> 175 skills across language plugins — see [skill catalog](#skill-catalog) below for the full breakdown | Language | Count | Suffix | |----------|-------|--------| | [Core](#core) | 10 | — | | [Foundry (Language-Agnostic)](#foundry-language-agnostic) | 11 | — | | [Python](#python) | 39 | `-py` | -| [.NET](#net) | 28 | `-dotnet` | +| [.NET](#net) | 29 | `-dotnet` | | [TypeScript](#typescript) | 25 | `-ts` | | [Java](#java) | 25 | `-java` | | [Rust](#rust) | 7 | `-rust` | @@ -241,7 +241,7 @@ Coding agents like [Copilot CLI](https://github.com/features/copilot/cli) and [G ### .NET -> 29 skills • suffix: `-dotnet` +> 30 skills • suffix: `-dotnet`
Foundry & AI (6 skills) @@ -267,10 +267,11 @@ Coding agents like [Copilot CLI](https://github.com/features/copilot/cli) and [G
-Data & Storage (6 skills) +Data & Storage (7 skills) | Skill | Description | |-------|-------------| +| [azure-storage-blob-dotnet](.github/plugins/azure-sdk-dotnet/skills/azure-storage-blob-dotnet/) | Blob Storage — upload, download, streaming, SAS tokens, containers. | | [azure-mgmt-fabric-dotnet](.github/plugins/azure-sdk-dotnet/skills/azure-mgmt-fabric-dotnet/) | Fabric ARM — provision, scale, suspend/resume Fabric capacities. | | [azure-resource-manager-cosmosdb-dotnet](.github/plugins/azure-sdk-dotnet/skills/azure-resource-manager-cosmosdb-dotnet/) | Cosmos DB ARM — create accounts, databases, containers, RBAC. | | [azure-resource-manager-mysql-dotnet](.github/plugins/azure-sdk-dotnet/skills/azure-resource-manager-mysql-dotnet/) | MySQL Flexible Server — servers, databases, firewall, HA. | @@ -653,13 +654,13 @@ pnpm test ### Test Coverage Summary -**128 skills with 1158 test scenarios** — all skills have acceptance criteria and test scenarios. +**129 skills with 1170 test scenarios** — all skills have acceptance criteria and test scenarios. | Language | Skills | Scenarios | Top Skills by Scenarios | |----------|--------|-----------|-------------------------| | Core | 7 | 72 | `copilot-sdk` (11), `podcast-generation` (8), `skill-creator` (8) | | Python | 41 | 331 | `azure-ai-projects-py` (12), `pydantic-models-py` (12), `azure-ai-translation-text-py` (11) | -| .NET | 29 | 290 | `azure-resource-manager-sql-dotnet` (14), `azure-resource-manager-redis-dotnet` (14), `azure-servicebus-dotnet` (13) | +| .NET | 30 | 302 | `azure-resource-manager-sql-dotnet` (14), `azure-resource-manager-redis-dotnet` (14), `azure-servicebus-dotnet` (13) | | TypeScript | 25 | 270 | `azure-storage-blob-ts` (17), `azure-servicebus-ts` (14), `aspire-ts` (13) | | Java | 26 | 195 | `azure-storage-blob-java` (12), `azure-identity-java` (12), `azure-data-tables-java` (11) | diff --git a/docs-site/src/data/skills.json b/docs-site/src/data/skills.json index 15ee3a4e..9b2df899 100644 --- a/docs-site/src/data/skills.json +++ b/docs-site/src/data/skills.json @@ -902,6 +902,13 @@ "category": "data", "path": ".github/plugins/azure-skills/skills/azure-storage" }, + { + "name": "azure-storage-blob-dotnet", + "description": "Azure Storage Blob SDK for .NET. Client library for storing and managing unstructured data (files, images, backups, logs) in Azure Blob Storage. Use for uploading, downloading, streaming, listing, copying, and deleting blobs, managing containers, generating SAS tokens, and setting blob properties/metadata. Triggers: \"Azure Blob Storage .NET\", \"BlobServiceClient\", \"BlobContainerClient\", \"BlobClient\", \"upload download blob C#\", \"Azure.Storage.Blobs\", \"SAS token blob\", \"blob streaming\", \"block blob\".\n", + "lang": "dotnet", + "category": "data", + "path": ".github/plugins/azure-sdk-dotnet/skills/azure-storage-blob-dotnet" + }, { "name": "azure-storage-blob-java", "description": "Build blob storage applications with Azure Storage Blob SDK for Java. Use when uploading, downloading, or managing files in Azure Blob Storage, working with containers, or implementing streaming data operations.", diff --git a/tests/README.md b/tests/README.md index e84bcdd9..1a7b3a68 100644 --- a/tests/README.md +++ b/tests/README.md @@ -187,13 +187,13 @@ A result **passes** if it has no error-severity findings. ## Test Coverage -**123 skills with 1114 test scenarios** +**124 skills with 1126 test scenarios** | Language | Skills | Scenarios | |----------|--------|-----------| | Core | 5 | 51 | | Python | 41 | 333 | -| .NET | 28 | 286 | +| .NET | 29 | 298 | | TypeScript | 23 | 249 | | Java | 26 | 195 | diff --git a/tests/scenarios/azure-storage-blob-dotnet/acceptance-criteria.md b/tests/scenarios/azure-storage-blob-dotnet/acceptance-criteria.md new file mode 100644 index 00000000..1f3bb1bf --- /dev/null +++ b/tests/scenarios/azure-storage-blob-dotnet/acceptance-criteria.md @@ -0,0 +1,442 @@ +# Azure Storage Blob .NET SDK Acceptance Criteria + +**SDK**: `Azure.Storage.Blobs` +**Repository**: https://github.com/Azure/azure-sdk-for-net +**Purpose**: Skill testing acceptance criteria for validating generated code correctness + +--- + +## 1. Client Builder Patterns + +### ✅ CORRECT: BlobServiceClient with DefaultAzureCredential + +```csharp +using Azure.Identity; +using Azure.Storage.Blobs; + +var serviceClient = new BlobServiceClient( + new Uri(Environment.GetEnvironmentVariable("AZURE_STORAGE_ACCOUNT_URL")), + new DefaultAzureCredential()); +``` + +### ✅ CORRECT: BlobContainerClient Direct Construction + +```csharp +using Azure.Identity; +using Azure.Storage.Blobs; + +var containerClient = new BlobContainerClient( + new Uri("https://myaccount.blob.core.windows.net/mycontainer"), + new DefaultAzureCredential()); +``` + +### ✅ CORRECT: BlobClient for a Specific Blob + +```csharp +BlobContainerClient containerClient = serviceClient.GetBlobContainerClient("mycontainer"); +BlobClient blobClient = containerClient.GetBlobClient("folder/myfile.txt"); +``` + +### ❌ INCORRECT: Hardcoded Connection String / Account Key + +```csharp +// WRONG - hardcoded secret in source +var client = new BlobServiceClient( + "DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=base64key==;"); +``` + +### ❌ INCORRECT: Legacy Package + +```csharp +// WRONG - deprecated v11 package +using Microsoft.Azure.Storage; +using Microsoft.Azure.Storage.Blob; +// Use Azure.Storage.Blobs (v12) instead +``` + +--- + +## 2. Container Operations + +### ✅ CORRECT: Create Container If Not Exists + +```csharp +await containerClient.CreateIfNotExistsAsync(); +``` + +### ✅ CORRECT: List Containers + +```csharp +using Azure.Storage.Blobs.Models; + +await foreach (BlobContainerItem container in serviceClient.GetBlobContainersAsync()) +{ + Console.WriteLine(container.Name); +} +``` + +### ❌ INCORRECT: Not Handling Already Exists + +```csharp +// WRONG - throws RequestFailedException (409) if the container exists +await serviceClient.CreateBlobContainerAsync("mycontainer"); +// Use CreateIfNotExistsAsync instead for idempotency +``` + +--- + +## 3. Upload Operations + +### ✅ CORRECT: Upload with BinaryData + +```csharp +await blobClient.UploadAsync(BinaryData.FromString("Hello, Azure Blob Storage!"), overwrite: true); +``` + +### ✅ CORRECT: Upload from File + +```csharp +await blobClient.UploadAsync("path/to/local/file.txt", overwrite: true); +``` + +### ✅ CORRECT: Upload from Stream + +```csharp +await using FileStream stream = File.OpenRead("largefile.bin"); +await blobClient.UploadAsync(stream, overwrite: true); +``` + +### ✅ CORRECT: Upload with Options (Headers, Metadata) + +```csharp +using Azure.Storage.Blobs.Models; + +var options = new BlobUploadOptions +{ + HttpHeaders = new BlobHttpHeaders + { + ContentType = "application/json", + CacheControl = "max-age=3600" + }, + Metadata = new Dictionary + { + ["author"] = "john", + ["version"] = "1.0" + } +}; + +await using FileStream stream = File.OpenRead("data.json"); +await blobClient.UploadAsync(stream, options); +``` + +### ✅ CORRECT: Upload Only If Not Exists + +```csharp +using Azure; +using Azure.Storage.Blobs.Models; + +var options = new BlobUploadOptions +{ + Conditions = new BlobRequestConditions { IfNoneMatch = ETag.All } +}; + +try +{ + await using FileStream stream = File.OpenRead("data.json"); + await blobClient.UploadAsync(stream, options); +} +catch (RequestFailedException ex) when (ex.Status == 409) +{ + Console.WriteLine("Blob already exists"); +} +``` + +### ❌ INCORRECT: Missing Overwrite Flag + +```csharp +// WRONG - throws if the blob already exists +await blobClient.UploadAsync(BinaryData.FromString(content)); +``` + +--- + +## 4. Download Operations + +### ✅ CORRECT: Download to Memory + +```csharp +using Azure.Storage.Blobs.Models; + +BlobDownloadResult result = await blobClient.DownloadContentAsync(); +string text = result.Content.ToString(); +``` + +### ✅ CORRECT: Download to File + +```csharp +await blobClient.DownloadToAsync("path/to/downloaded/file.txt"); +``` + +### ✅ CORRECT: Download with Streaming + +```csharp +using Azure.Storage.Blobs.Models; + +BlobDownloadStreamingResult download = await blobClient.DownloadStreamingAsync(); +await using Stream source = download.Content; +await using FileStream destination = File.Create("large-download.bin"); +await source.CopyToAsync(destination); +``` + +### ❌ INCORRECT: Not Handling Missing Blob + +```csharp +// WRONG - no handling for a missing blob +BlobDownloadResult result = await blobClient.DownloadContentAsync(); +// Should catch RequestFailedException with Status 404 +``` + +--- + +## 5. List Blobs + +### ✅ CORRECT: List All Blobs + +```csharp +using Azure.Storage.Blobs.Models; + +await foreach (BlobItem blob in containerClient.GetBlobsAsync()) +{ + Console.WriteLine($"Blob: {blob.Name}, Size: {blob.Properties.ContentLength}"); +} +``` + +### ✅ CORRECT: List with Prefix (Virtual Directory) + +```csharp +using Azure.Storage.Blobs.Models; + +await foreach (BlobItem blob in containerClient.GetBlobsAsync(prefix: "folder/subfolder/")) +{ + Console.WriteLine(blob.Name); +} +``` + +### ✅ CORRECT: List by Hierarchy (Directories) + +```csharp +using Azure.Storage.Blobs.Models; + +await foreach (BlobHierarchyItem item in + containerClient.GetBlobsByHierarchyAsync(prefix: "data/", delimiter: "/")) +{ + if (item.IsPrefix) + Console.WriteLine($"Directory: {item.Prefix}"); + else + Console.WriteLine($"Blob: {item.Blob.Name}"); +} +``` + +--- + +## 6. Delete Operations + +### ✅ CORRECT: Delete If Exists + +```csharp +await blobClient.DeleteIfExistsAsync(); +``` + +### ✅ CORRECT: Delete with Snapshots + +```csharp +using Azure.Storage.Blobs.Models; + +await blobClient.DeleteIfExistsAsync(DeleteSnapshotsOption.IncludeSnapshots); +``` + +### ❌ INCORRECT: Not Handling Missing Blob + +```csharp +// WRONG - throws RequestFailedException (404) if the blob does not exist +await blobClient.DeleteAsync(); +// Use DeleteIfExistsAsync instead +``` + +--- + +## 7. SAS Token Generation + +### ✅ CORRECT: User Delegation SAS (Entra ID) + +```csharp +using Azure.Storage.Blobs.Models; +using Azure.Storage.Sas; + +UserDelegationKey delegationKey = await serviceClient.GetUserDelegationKeyAsync( + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddDays(1)); + +var sasBuilder = new BlobSasBuilder(BlobSasPermissions.Read, DateTimeOffset.UtcNow.AddDays(1)) +{ + BlobContainerName = containerClient.Name, + BlobName = "myblob.txt", + Resource = "b" +}; + +var uriBuilder = new BlobUriBuilder(blobClient.Uri) +{ + Sas = sasBuilder.ToSasQueryParameters(delegationKey, serviceClient.AccountName) +}; + +Uri sasUri = uriBuilder.ToUri(); +``` + +### ✅ CORRECT: Service SAS via Shared Key + +```csharp +using Azure.Storage.Sas; + +if (blobClient.CanGenerateSasUri) +{ + var sasBuilder = new BlobSasBuilder(BlobSasPermissions.Read, DateTimeOffset.UtcNow.AddDays(1)) + { + BlobContainerName = blobClient.BlobContainerName, + BlobName = blobClient.Name, + Resource = "b" + }; + + Uri sasUri = blobClient.GenerateSasUri(sasBuilder); +} +``` + +### ❌ INCORRECT: Overly Permissive, Long-Lived SAS + +```csharp +// WRONG - all permissions and a one-year expiry +var sasBuilder = new BlobSasBuilder(BlobSasPermissions.All, DateTimeOffset.UtcNow.AddYears(1)); +``` + +--- + +## 8. Blob Properties and Metadata + +### ✅ CORRECT: Get and Set Properties + +```csharp +using Azure.Storage.Blobs.Models; + +BlobProperties properties = await blobClient.GetPropertiesAsync(); +Console.WriteLine($"Size: {properties.ContentLength}, Content-Type: {properties.ContentType}"); + +await blobClient.SetMetadataAsync(new Dictionary +{ + ["processed"] = "true", + ["version"] = "2.0" +}); + +await blobClient.SetHttpHeadersAsync(new BlobHttpHeaders +{ + ContentType = "application/octet-stream", + CacheControl = "max-age=86400" +}); +``` + +--- + +## 9. Blob Leasing + +### ✅ CORRECT: Acquire and Release Lease + +```csharp +using Azure.Storage.Blobs.Models; +using Azure.Storage.Blobs.Specialized; + +BlobLeaseClient leaseClient = blobClient.GetBlobLeaseClient(); + +BlobLease lease = await leaseClient.AcquireAsync(TimeSpan.FromSeconds(60)); +try +{ + await blobClient.UploadAsync(BinaryData.FromString("Updated content"), overwrite: true); +} +finally +{ + await leaseClient.ReleaseAsync(); +} +``` + +--- + +## 10. Copy Operations + +### ✅ CORRECT: Copy from URI + +```csharp +using Azure.Storage.Blobs.Models; + +// Async copy (large blobs or cross-account) +CopyFromUriOperation operation = await destBlobClient.StartCopyFromUriAsync(sourceBlobUri); +await operation.WaitForCompletionAsync(); + +// Synchronous copy from URL (same account, smaller blobs) +await destBlobClient.SyncCopyFromUriAsync(sourceBlobUri); +``` + +--- + +## 11. Error Handling + +### ✅ CORRECT: Handle RequestFailedException + +```csharp +using Azure; +using Azure.Storage.Blobs.Models; + +try +{ + BlobDownloadResult result = await blobClient.DownloadContentAsync(); +} +catch (RequestFailedException ex) +{ + switch (ex.Status) + { + case 404: + Console.Error.WriteLine($"Blob not found. ErrorCode: {ex.ErrorCode}"); + break; + case 409: + Console.Error.WriteLine($"Conflict. ErrorCode: {ex.ErrorCode}"); + break; + case 403: + Console.Error.WriteLine($"Access denied. ErrorCode: {ex.ErrorCode}"); + break; + default: + Console.Error.WriteLine($"Error {ex.Status}: {ex.Message}"); + break; + } +} +``` + +### ❌ INCORRECT: Generic Exception Handling + +```csharp +// WRONG - loses status code and error detail +try +{ + await blobClient.UploadAsync(data, overwrite: true); +} +catch (Exception ex) +{ + Console.WriteLine($"Error: {ex.Message}"); +} +``` + +--- + +## 12. Streaming Uploads + +### ✅ CORRECT: OpenWriteAsync for Streaming + +```csharp +await using Stream writeStream = await blobClient.OpenWriteAsync(overwrite: true); +await writeStream.WriteAsync(Encoding.UTF8.GetBytes("Streaming data...")); +``` diff --git a/tests/scenarios/azure-storage-blob-dotnet/scenarios.yaml b/tests/scenarios/azure-storage-blob-dotnet/scenarios.yaml new file mode 100644 index 00000000..09bc2b5b --- /dev/null +++ b/tests/scenarios/azure-storage-blob-dotnet/scenarios.yaml @@ -0,0 +1,380 @@ +# yaml-language-server: $schema=https://json.schemastore.org/any.json +# Test scenarios for azure-storage-blob-dotnet skill evaluation +# Each scenario tests a specific usage pattern against acceptance criteria + +config: + model: gpt-4 + max_tokens: 2000 + temperature: 0.3 + +scenarios: + # BlobServiceClient with DefaultAzureCredential + - name: blob_service_client_default_credential + prompt: | + Create a BlobServiceClient using DefaultAzureCredential with the + account URL read from an environment variable. + expected_patterns: + - "BlobServiceClient" + - "DefaultAzureCredential" + - 'new Uri\(' + - "Environment.GetEnvironmentVariable" + - "AZURE_STORAGE_ACCOUNT_URL" + forbidden_patterns: + - "AccountKey=" + - 'Microsoft\.Azure\.Storage' + tags: + - authentication + - client + mock_response: | + using Azure.Identity; + using Azure.Storage.Blobs; + + string accountUrl = Environment.GetEnvironmentVariable("AZURE_STORAGE_ACCOUNT_URL") + ?? throw new InvalidOperationException("AZURE_STORAGE_ACCOUNT_URL not set"); + + var serviceClient = new BlobServiceClient(new Uri(accountUrl), new DefaultAzureCredential()); + + # BlobContainerClient and BlobClient + - name: container_and_blob_clients + prompt: | + From a BlobServiceClient, get a BlobContainerClient and a BlobClient + for a blob inside a virtual directory. + expected_patterns: + - "BlobContainerClient" + - "BlobClient" + - "GetBlobContainerClient" + - "GetBlobClient" + forbidden_patterns: + - "AccountKey=" + tags: + - client + - hierarchy + mock_response: | + using Azure.Identity; + using Azure.Storage.Blobs; + + var serviceClient = new BlobServiceClient( + new Uri(Environment.GetEnvironmentVariable("AZURE_STORAGE_ACCOUNT_URL")), + new DefaultAzureCredential()); + + BlobContainerClient containerClient = serviceClient.GetBlobContainerClient("mycontainer"); + BlobClient blobClient = containerClient.GetBlobClient("folder/myfile.txt"); + + # Upload operations + - name: upload_operations + prompt: | + Show different upload methods: BinaryData string, local file path, + and a stream, all with the overwrite flag. + expected_patterns: + - "UploadAsync" + - "BinaryData.FromString" + - "overwrite: true" + - "File.OpenRead" + forbidden_patterns: + - 'UploadAsync\(stream\);' + tags: + - upload + - crud + mock_response: | + using Azure.Storage.Blobs; + + // Upload string content + await blobClient.UploadAsync(BinaryData.FromString("Hello, Azure Blob Storage!"), overwrite: true); + + // Upload from a local file + await blobClient.UploadAsync("path/to/local/file.txt", overwrite: true); + + // Upload from a stream + await using FileStream stream = File.OpenRead("largefile.bin"); + await blobClient.UploadAsync(stream, overwrite: true); + + # Upload with headers and metadata + - name: upload_with_options + prompt: | + Upload a blob with custom HTTP headers (Content-Type, Cache-Control) + and metadata using BlobUploadOptions. + expected_patterns: + - "BlobUploadOptions" + - "BlobHttpHeaders" + - "ContentType" + - "Metadata" + - "UploadAsync" + forbidden_patterns: [] + tags: + - upload + - metadata + mock_response: | + using Azure.Storage.Blobs; + using Azure.Storage.Blobs.Models; + + var options = new BlobUploadOptions + { + HttpHeaders = new BlobHttpHeaders + { + ContentType = "application/json", + CacheControl = "max-age=3600" + }, + Metadata = new Dictionary + { + ["author"] = "john", + ["version"] = "1.0" + } + }; + + await using FileStream stream = File.OpenRead("data.json"); + await blobClient.UploadAsync(stream, options); + + # Download operations + - name: download_operations + prompt: | + Show downloading a blob into memory with DownloadContentAsync and to a + local file with DownloadToAsync. + expected_patterns: + - "DownloadContentAsync" + - "DownloadToAsync" + - "BlobDownloadResult" + - "Content.ToString" + forbidden_patterns: [] + tags: + - download + - crud + mock_response: | + using Azure.Storage.Blobs.Models; + + // Download content into memory (small blobs) + BlobDownloadResult result = await blobClient.DownloadContentAsync(); + string text = result.Content.ToString(); + + // Download to a local file + await blobClient.DownloadToAsync("path/to/downloaded/file.txt"); + + # Download with streaming + - name: download_streaming + prompt: | + Download a large blob using a stream for memory-efficient processing. + expected_patterns: + - "DownloadStreamingAsync" + - "BlobDownloadStreamingResult" + - "CopyToAsync" + forbidden_patterns: + - "DownloadContentAsync" + tags: + - download + - streaming + mock_response: | + using Azure.Storage.Blobs.Models; + + BlobDownloadStreamingResult download = await blobClient.DownloadStreamingAsync(); + await using Stream source = download.Content; + await using FileStream destination = File.Create("large-download.bin"); + await source.CopyToAsync(destination); + + # List blobs + - name: list_blobs + prompt: | + List blobs in a container, including prefix filtering and hierarchical + listing of virtual directories. + expected_patterns: + - "GetBlobsAsync" + - "GetBlobsByHierarchyAsync" + - "prefix:" + - "delimiter:" + - "IsPrefix" + forbidden_patterns: [] + tags: + - list + - query + mock_response: | + using Azure.Storage.Blobs.Models; + + // List all blobs + await foreach (BlobItem blob in containerClient.GetBlobsAsync()) + { + Console.WriteLine($"Blob: {blob.Name}, Size: {blob.Properties.ContentLength}"); + } + + // List by hierarchy (directories) + await foreach (BlobHierarchyItem item in + containerClient.GetBlobsByHierarchyAsync(prefix: "data/", delimiter: "/")) + { + if (item.IsPrefix) + Console.WriteLine($"Directory: {item.Prefix}"); + else + Console.WriteLine($"Blob: {item.Blob.Name}"); + } + + # User delegation SAS (Entra ID) + - name: generate_user_delegation_sas + prompt: | + Generate a user delegation SAS for a blob with read permission and a + one-day expiry, using Entra ID (no account key). + expected_patterns: + - "GetUserDelegationKeyAsync" + - "BlobSasBuilder" + - "BlobSasPermissions.Read" + - "ToSasQueryParameters" + - "BlobUriBuilder" + forbidden_patterns: + - "AccountKey=" + - "StorageSharedKeyCredential" + tags: + - sas + - security + mock_response: | + using Azure.Storage.Blobs.Models; + using Azure.Storage.Sas; + + UserDelegationKey delegationKey = await serviceClient.GetUserDelegationKeyAsync( + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddDays(1)); + + var sasBuilder = new BlobSasBuilder(BlobSasPermissions.Read, DateTimeOffset.UtcNow.AddDays(1)) + { + BlobContainerName = containerClient.Name, + BlobName = "myblob.txt", + Resource = "b" + }; + + var uriBuilder = new BlobUriBuilder(blobClient.Uri) + { + Sas = sasBuilder.ToSasQueryParameters(delegationKey, serviceClient.AccountName) + }; + + Uri sasUri = uriBuilder.ToUri(); + + # Error handling + - name: blob_error_handling + prompt: | + Handle RequestFailedException for a download, branching on status codes + for not found, conflict, and access denied. + expected_patterns: + - "RequestFailedException" + - "ex.Status" + - "ErrorCode" + - "404" + - "409" + - "403" + forbidden_patterns: + - 'catch \(Exception' + tags: + - error-handling + mock_response: | + using Azure; + using Azure.Storage.Blobs.Models; + + try + { + BlobDownloadResult result = await blobClient.DownloadContentAsync(); + } + catch (RequestFailedException ex) + { + switch (ex.Status) + { + case 404: + Console.Error.WriteLine($"Blob not found. ErrorCode: {ex.ErrorCode}"); + break; + case 409: + Console.Error.WriteLine($"Conflict. ErrorCode: {ex.ErrorCode}"); + throw; + case 403: + Console.Error.WriteLine($"Access denied. ErrorCode: {ex.ErrorCode}"); + throw; + default: + Console.Error.WriteLine($"Error {ex.Status}: {ex.Message}"); + throw; + } + } + + # Blob properties and metadata + - name: blob_properties_metadata + prompt: | + Get blob properties and set custom metadata and HTTP headers on a blob. + expected_patterns: + - "GetPropertiesAsync" + - "SetMetadataAsync" + - "SetHttpHeadersAsync" + - "BlobProperties" + - "ContentLength" + forbidden_patterns: [] + tags: + - metadata + - properties + mock_response: | + using Azure.Storage.Blobs.Models; + + BlobProperties properties = await blobClient.GetPropertiesAsync(); + Console.WriteLine($"Size: {properties.ContentLength}"); + Console.WriteLine($"Content-Type: {properties.ContentType}"); + Console.WriteLine($"Last Modified: {properties.LastModified}"); + + var metadata = new Dictionary + { + ["processed"] = "true", + ["version"] = "2.0" + }; + await blobClient.SetMetadataAsync(metadata); + + await blobClient.SetHttpHeadersAsync(new BlobHttpHeaders + { + ContentType = "application/octet-stream", + CacheControl = "max-age=86400" + }); + + # Blob leasing + - name: blob_leasing + prompt: | + Acquire a lease on a blob to prevent concurrent modification, perform an + operation, then release the lease. + expected_patterns: + - "GetBlobLeaseClient" + - "BlobLeaseClient" + - "AcquireAsync" + - "ReleaseAsync" + forbidden_patterns: [] + tags: + - leasing + - concurrency + mock_response: | + using Azure.Storage.Blobs.Models; + using Azure.Storage.Blobs.Specialized; + + BlobLeaseClient leaseClient = blobClient.GetBlobLeaseClient(); + + BlobLease lease = await leaseClient.AcquireAsync(TimeSpan.FromSeconds(60)); + try + { + await blobClient.UploadAsync(BinaryData.FromString("Updated content"), overwrite: true); + await leaseClient.RenewAsync(); + } + finally + { + await leaseClient.ReleaseAsync(); + } + + # Delete and copy + - name: delete_and_copy + prompt: | + Delete a blob idempotently, and copy a blob from a source URI using both + async and synchronous copy. + expected_patterns: + - "DeleteIfExistsAsync" + - "StartCopyFromUriAsync" + - "WaitForCompletionAsync" + - "SyncCopyFromUriAsync" + forbidden_patterns: + - 'blobClient\.DeleteAsync\(\);' + tags: + - delete + - copy + mock_response: | + using Azure.Storage.Blobs.Models; + + // Idempotent delete + await blobClient.DeleteIfExistsAsync(DeleteSnapshotsOption.IncludeSnapshots); + + // Async copy (large blobs or cross-account) + CopyFromUriOperation operation = await destBlobClient.StartCopyFromUriAsync(sourceBlobUri); + await operation.WaitForCompletionAsync(); + + // Synchronous copy from URL (same account, smaller blobs) + await destBlobClient.SyncCopyFromUriAsync(sourceBlobUri);