Skip to main content

Efficient File Management on Azure Blob Storage: CRUD Operations and Upload Strategies

Efficient File Management on Azure Blob Storage: CRUD Operations and Upload Strategies
Table of Contents

The short version
#

Four ways to get a file into Azure Blob Storage from a .NET and Angular app, and when each one earns its keep:

  1. Standard upload (IFormFile): simple form posts, fine for small files.
  2. Chunk-based upload: splits the file into blocks for better memory use and retry.
  3. Stream upload: pipes the request body straight through, lowest memory footprint.
  4. SAS upload: the browser uploads directly to Azure, skipping your server entirely.

They differ mainly in memory use, scalability, and how much client code you write. My default for anything large in production is SAS; the other three earn their place in narrower cases below.

Introduction
#

The upload code is where a file feature usually goes wrong. It works on the developer’s 2 MB test image, then someone uploads a 4 GB video and the API process runs out of memory, because the whole file was buffered before it ever reached Azure. Which upload strategy you choose is really a decision about where the bytes live in transit: browser memory, server memory, or neither.

This post walks through four strategies against Azure Blob Storage from a .NET API and an Angular client, with the CRUD operations around them, and it stays honest about the trade-offs of each.


1. Understanding the Concept
#

What It Is
#

Azure Blob Storage is a Microsoft-managed service for storing large amounts of unstructured data in the cloud. It offers three types of blobs:

  1. Block blobs: For text and binary data (the type you use for file uploads).

  2. Append blobs: Optimized for append operations, such as logging.

  3. Page blobs: For random read/write access, often used for virtual disks.

Why It Matters
#

For apps that need high availability, durability, and scale, how you move files in and out of Blob Storage directly affects both cost and reliability. Common scenarios:

  • Serving media files through a content delivery network (CDN).

  • Backing up and archiving data.

  • Storing and processing large datasets for analytics.

A concrete scenario
#

The case that pushed me through all four strategies: a document portal where users upload anything from a 50 KB signed PDF to a multi-gigabyte scan batch. Small files went through a plain form post; the large ones needed direct-to-Azure uploads with progress bars, or the server buckled. One app, two upload paths, which is exactly why it pays to know all four.


2. Step-by-Step Implementation
#

2.1. Prerequisites
#

Tools and Frameworks
#

  • Azure account with a Blob Storage resource.

  • Azure Storage SDK for your programming language (e.g., .NET, Python).

  • Development environment (e.g., Visual Studio, VS Code).

  • Basic knowledge of REST APIs and cloud storage.

Setup Instructions
#

  1. Create an Azure Blob Storage account via the Azure Portal.

  2. Install the Azure Storage SDK for your language: dotnet add package Azure.Storage.Blobs

  3. Retrieve your storage account connection string from the Azure Portal.


2.2. Code Walkthrough
#

Establishing a Connection
#

    private readonly BlobServiceClient _blobServiceClient;

    public BlobStorageService(IConfiguration configuration)
    {
        var connectionString = configuration.GetConnectionString("AzureStorage");
        _blobServiceClient = new BlobServiceClient(connectionString);
    }

Creating a Container
#

      var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
        await containerClient.CreateIfNotExistsAsync();

2.2.1 Uploading a File: Four Strategic Approaches
#

Azure Blob Storage gives you several ways to upload, each suited to a different file size and memory budget. Here are four approaches and where each one fits.

2.2.1.1 Standard File Upload (IFormFile)
#

flowchart LR Client([Client Browser]) API[.NET API Server] Azure[(Azure Blob Storage)] Client -- "HTTP POST with FormData" --> API API -- "Upload via SDK" --> Azure API -- "Success Response" --> Client classDef client fill:#f9f,stroke:#333,stroke-width:1px; classDef api fill:#bbf,stroke:#333,stroke-width:1px; classDef azure fill:#bfb,stroke:#333,stroke-width:1px; class Client client; class API api; class Azure azure;

Description:
#

  • IFormFile is a high-level abstraction in ASP.NET Core used for handling file uploads via the multipart/form-data encoding.

  • It provides an in-memory or temporary storage solution for files uploaded in a form.

How It Works:
#

  • The file is uploaded from the client browser to your server using standard form submission
  • The entire file is either buffered in memory or saved to a temporary location (e.g., disk) before processing
  • Your server then uploads the complete file to Azure Blob Storage
  • The framework manages file processing through model binding

Key Features:
#

  • File is accessed via properties like FileName, Length, and OpenReadStream().
  • Simple implementation with built-in ASP.NET Core support
  • Good for small to medium files with straightforward requirements

Example:
#

    [HttpPost("upload")] // POST api/blob/upload
    [Consumes("multipart/form-data")]
    public async Task<IActionResult> UploadBlobAsync([FromForm] BlobUploadRequest request)
    {
        using var stream = request.File.OpenReadStream();
        await _blobStorageService.UploadBlobAsync(request.ContainerName, request.BlobName, stream);
        return Ok();
    }
public class BlobUploadRequest
{
    public string ContainerName { get; set; }
    public string BlobName { get; set; }
    public IFormFile File { get; set; }
}
    public async Task UploadBlobAsync(string containerName, string blobName, Stream content)
    {
        if (!IsValidContainerName(containerName))
        {
            throw new ArgumentException("Invalid container name.");
        }
        string blobNameWithTimestamp = GenerateBlobNameWithTimestamp(blobName);

        var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
        await containerClient.CreateIfNotExistsAsync();

        var blobClient = containerClient.GetBlobClient(blobNameWithTimestamp);
        await blobClient.UploadAsync(content, overwrite: true);
    }

Pros:
#

  • Ease of Use: Very easy to implement and integrates well with form submissions.

  • Validation: Supports model validation (e.g., checking file size or type).

  • Convenience: Provides metadata like the file name and content type.

Cons:
#

  • Client Memory Usage: High - entire file loaded into browser memory
  • Server Memory Usage: High - file is fully loaded into server memory
  • Not Suitable for Large Files: Uploading very large files can cause performance issues or out-of-memory exceptions.

Best For:
#

  • Small to medium-sized files (typically under 100MB)
  • Simple implementations where ease of use is prioritized
  • Applications where server-side processing of the file is needed before storage

2.2.1.2 Chunk-Based Upload
#

flowchart LR Client([Client Browser]) API[.NET API Server] Azure[(Azure Blob Storage)] Client -- "Split file into chunks" --> Client Client -- "HTTP POST chunk #1" --> API API -- "Upload chunk" --> Azure Client -- "HTTP POST chunk #2" --> API API -- "Upload chunk" --> Azure Client -- "HTTP POST final chunk" --> API API -- "Upload chunk" --> Azure API -- "Combine chunks" --> Azure API -- "Success Response" --> Client classDef client fill:#f9f,stroke:#333,stroke-width:1px; classDef api fill:#bbf,stroke:#333,stroke-width:1px; classDef azure fill:#bfb,stroke:#333,stroke-width:1px; class Client client; class API api; class Azure azure;

Description:
#

  • Chunked uploads break a large file into smaller parts (chunks) and upload them sequentially.

  • Each chunk is processed independently and combined later at the destination.

How It Works:
#

  • A client splits the file into chunks of a specified size (typically 5MB)
  • Each chunk is sent in a separate HTTP request to your server
  • Your server uploads each chunk to Azure Blob Storage
  • After all chunks are uploaded, the server instructs Azure to combine them
  • The result is a single complete file in Azure Blob Storage

Key Features:
#

  • Better memory management on both client and server
  • Improved reliability with ability to retry individual chunks
  • Detailed progress tracking
  • Possibility for resumable uploads

Example:
#

[HttpPost("upload-chunk")]
 public async Task<IActionResult> UploadChunk(
        [FromForm] IFormFile chunk,
        [FromForm] string containerName,
        [FromForm] string blobName,
        [FromForm] int chunkIndex,
        [FromForm] int totalChunks)
    {
        if (chunk == null || chunk.Length == 0)
        {
            return BadRequest(new { Message = "Chunk is missing or empty." });
        }

        try
        {
            // Stream the chunk to the service
            using var stream = chunk.OpenReadStream();
            await _blobStorageService.UploadChunkAsync(containerName, blobName, stream, chunkIndex, totalChunks);

            return Ok(new { Message = $"Chunk {chunkIndex + 1}/{totalChunks} uploaded successfully." });
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { Message = "Error uploading chunk.", Error = ex.Message });
        }
}
    public async Task UploadChunkAsync(string containerName, string blobName, Stream chunkData, int chunkIndex, int totalChunks)
    {
        if (!IsValidContainerName(containerName))
        {
            throw new ArgumentException("Invalid container name.");
        }

        // Get the container client
        var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
        await containerClient.CreateIfNotExistsAsync();

        // Get the block blob client
        var blockBlobClient = containerClient.GetBlockBlobClient(blobName);

        // Generate a unique block ID for each chunk
        var blockId = Convert.ToBase64String(Encoding.UTF8.GetBytes(chunkIndex.ToString("d6")));

        // Stage the chunk as a block
        await blockBlobClient.StageBlockAsync(blockId, chunkData);

        // If this is the last chunk, commit the block list
        if (chunkIndex + 1 == totalChunks)
        {
            // Create a list of block IDs
            var blockList = Enumerable.Range(0, totalChunks)
                .Select(index => Convert.ToBase64String(Encoding.UTF8.GetBytes(index.ToString("d6"))))
                .ToList();

            // Commit the block list to assemble the final blob
            await blockBlobClient.CommitBlockListAsync(blockList);
        }
    }

Pros:
#

  • Scalability: Allows uploading very large files without overwhelming server resources.

  • Fault Tolerance: If a chunk fails to upload, only that chunk needs to be retried.

  • Memory Efficiency: Much better memory management since only one chunk is in memory at a time

  • Detailed Progress: Enables accurate progress tracking as each chunk completes

Cons:
#

  • Complexity: Requires additional logic to manage chunk creation, upload, and reassembly

  • Metadata Overhead: Involves sending metadata (e.g., file ID, chunk index) with each chunk

Best For:
#

  • Medium to large files (100MB to several GB)
  • Applications requiring detailed progress tracking
  • Environments with potentially unstable network connections
  • Scenarios where memory usage must be carefully managed

2.2.1.3 Stream Upload
#

flowchart LR Client([Client Browser]) API[.NET API Server] Azure[(Azure Blob Storage)] Client -- "Initialize file stream" --> Client Client -- "HTTP POST stream data" --> API API -- "Stream to Azure" --> Azure API -- "Success Response" --> Client classDef client fill:#f9f,stroke:#333,stroke-width:1px; classDef api fill:#bbf,stroke:#333,stroke-width:1px; classDef azure fill:#bfb,stroke:#333,stroke-width:1px; class Client client; class API api; class Azure azure;

Description:
#

  • Stream Upload is an efficient way to upload files by streaming the data directly to Azure Blob Storage.

  • It is ideal for large files or when you want to minimize memory usage on the client and server.

How It Works:
#

  • The client browser initializes a file stream
  • Data is sent to your server in small chunks (handled automatically)
  • Your server immediately forwards each piece of data to Azure as it arrives
  • Neither client nor server ever loads the entire file into memory
  • The file is essentially “piped” from browser to Azure through your server

Key Features:
#

  • Direct Streaming: Access file content directly from the request body as a stream.

  • Memory Efficiency: Exceptionally memory-efficient for both client and server

  • Modern Browser Support: Uses modern browser APIs for streaming

Example:
#

Controller:
#

    [HttpPost("stream-upload")]
    public async Task<IActionResult> StreamUploadAsync()
    {
        try
        {
            var containerName = Request.Headers["Container-Name"].ToString();
            var blobName = Request.Headers["Blob-Name"].ToString();

            if (string.IsNullOrEmpty(containerName) || string.IsNullOrEmpty(blobName))
            {
                return BadRequest(new { Message = "Container-Name and Blob-Name headers are required." });
            }

            // Stream data from the client directly to Azure Blob Storage
            using var stream = Request.Body;

            // Infer the Content-Type

            await _blobStorageService.UploadBlobAsync(containerName, blobName, stream);

            return Ok(new { Message = "File uploaded successfully." });
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { Message = "Error uploading file.", Error = ex.Message });
        }
    }

Service:
#

    public async Task UploadBlobAsync(string containerName, string blobName, Stream content)
    {
        if (!IsValidContainerName(containerName))
        {
            throw new ArgumentException("Invalid container name.");
        }
        string blobNameWithTimestamp = GenerateBlobNameWithTimestamp(blobName);

        var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
        await containerClient.CreateIfNotExistsAsync();

        var blobClient = containerClient.GetBlobClient(blobNameWithTimestamp);
        await blobClient.UploadAsync(content, overwrite: true);
    }

Explanation
#

  1. Headers for metadata:

    • The Container-Name and Blob-Name headers carry the storage location and file name. They must be passed explicitly, since a raw stream includes no metadata by default.
  2. Streaming upload:

    • The file streams directly from Request.Body to Blob Storage, avoiding intermediate buffering and keeping memory use low.
  3. Custom blob names:

    • GenerateBlobNameWithTimestamp appends a timestamp to the blob name so uploads stay unique and don’t overwrite each other.

Pros:
#

  • Memory Efficiency: Extremely memory-efficient since the file is never fully loaded into memory.

  • Direct Processing: Allows streaming directly to storage or a processing pipeline.

  • Scalability: Handles very large files without performance degradation.

Cons:
#

  • No Metadata by Default: File metadata (e.g., name, type) must be passed separately (e.g., via headers or query parameters).

  • Limited Progress Tracking: More difficult to provide detailed progress updates

  • Browser Support: Requires modern browsers with good streaming support

Best For:
#

  • Uploading very large files of any size
  • Applications where memory efficiency is critical
  • Modern web applications with up-to-date browser support

2.2.1.4 Large File Upload with SAS (Direct Upload)
#

flowchart LR subgraph Client["Client Browser"] File[File] XMLReq[XMLHttpRequest] end subgraph Server["NET API Server"] BlobAPI[Blob Storage API] end Azure[(Azure Blob Storage)] File -- "Selected for upload" --> XMLReq Client -- "Request SAS URL" --> Server Server -- "Generate SAS URL" --> Server Server -- "Return SAS URL" --> Client XMLReq -- "Direct upload with progress tracking" --> Azure Azure -- "Upload confirmation" --> Client classDef client fill:#f9f,stroke:#333,stroke-width:1px; classDef server fill:#bbf,stroke:#333,stroke-width:1px; classDef azure fill:#bfb,stroke:#333,stroke-width:1px; class Client client; class Server server; class Azure azure;
Detailed SAS Upload Process (Large Files)
#
sequenceDiagram participant Browser as Client Browser participant API as .NET API Server participant Azure as Azure Blob Storage Browser->>API: POST /blob/get-upload-url with file metadata Note over API: Generate SAS token with
limited permissions API->>Browser: Return SAS URL Note over Browser: Prepare XMLHttpRequest
with progress event tracking Browser->>Azure: PUT request directly to SAS URL Note over Browser,Azure: Upload progress events
trigger UI updates Azure->>Browser: Upload confirmation Note over Browser: Update UI to show
completed upload Note right of Azure: No server resources
used during actual
file transfer

How It Works:
#

  • Client requests a SAS URL from your server
  • Server generates a time-limited SAS token with specific permissions
  • Server returns the SAS URL to the client
  • Client uploads directly to Azure Blob Storage using the SAS URL
  • Your server is not involved in the file transfer, only in generating the SAS token

Key Features:
#

  • Server resources are not used during file transfer
  • Most scalable solution for very large files
  • Detailed progress tracking possible on client-side
  • Best security approach for cloud storage

Example:
#

Controller:

[HttpPost("get-upload-url")]
public async Task<IActionResult> GetLargeFileUploadUrl([FromBody] LargeFileUploadRequest request)
{
    try
    {
        // Generate a SAS URL for the client to upload directly to Azure
        var sasDetails = await _blobStorageService.GetBlobSasUriAsync(
            request.ContainerName,
            request.BlobName,
            DateTimeOffset.UtcNow.AddHours(1));

        return Ok(new LargeFileUploadResponse 
        { 
            SasUri = sasDetails.SasUri,
            Expiry = sasDetails.Expiry
        });
    }
    catch (Exception ex)
    {
        return StatusCode(500, new { Message = "Error generating upload URL.", Error = ex.Message });
    }
}

Service:

public async Task<BlobSasDetails> GetBlobSasUriAsync(string containerName, string blobName, DateTimeOffset expiryTime)
{
    var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
    await containerClient.CreateIfNotExistsAsync();
    
    var blobClient = containerClient.GetBlobClient(blobName);
    
    // Create SAS token with write permission
    var sasBuilder = new BlobSasBuilder
    {
        BlobContainerName = containerName,
        BlobName = blobName,
        Resource = "b",
        ExpiresOn = expiryTime
    };
    
    sasBuilder.SetPermissions(BlobSasPermissions.Write | BlobSasPermissions.Create);
    
    var sasToken = sasBuilder.ToSasQueryParameters(
        new StorageSharedKeyCredential(_blobServiceClient.AccountName, 
        GetAccountKeyFromConnectionString())).ToString();
    
    return new BlobSasDetails
    {
        SasUri = blobClient.Uri + "?" + sasToken,
        Expiry = expiryTime
    };
}

Client-Side Implementation:
#

async uploadWithSAS(file: File): Promise<void> {
  // Step 1: Request SAS URL from your server
  const response = await this.http.post<SasResponse>('/api/blob/get-upload-url', {
    containerName: 'uploads',
    blobName: file.name
  }).toPromise();
  
  // Step 2: Upload directly to Azure using the SAS URL
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('PUT', response.sasUri, true);
    xhr.setRequestHeader('x-ms-blob-type', 'BlockBlob');
    xhr.setRequestHeader('Content-Type', file.type);
    
    // Track progress
    xhr.upload.onprogress = (e) => {
      const percentComplete = Math.round((e.loaded / e.total) * 100);
      this.uploadProgress.next(percentComplete);
    };
    
    xhr.onload = () => {
      if (xhr.status === 201) {
        resolve();
      } else {
        reject(`Upload failed: ${xhr.status}`);
      }
    };
    
    xhr.onerror = () => reject('Upload failed due to network error');
    xhr.send(file);
  });
}

Pros:
#

  • Server Efficiency: Minimal server resource usage since your server doesn’t handle the file data
  • Scalability: Extremely scalable - upload large files without affecting your server
  • Direct Communication: Creates direct line between client and Azure Storage
  • Progress Tracking: Detailed client-side progress tracking

Cons:
#

  • Security Considerations: Requires careful management of SAS tokens and permissions
  • Complexity: More complex client-side implementation
  • Less Control: Server has less control over the upload process

Best For:
#

  • Very large files in production environments
  • Applications with high scalability requirements
  • Scenarios where server resource conservation is critical

Comparing the four strategies
#

Upload Methods Comparison
#

FeatureStandard UploadChunked UploadStream UploadSAS Upload
Client MemoryHighMediumVery LowMedium
Server MemoryHighMinimalLowMinimal
Implementation ComplexityLowMediumMediumHigh
Direct Upload to AzureNoNoNoYes
Progress TrackingSimpleDetailedLimitedDetailed
Resumable UploadsNoPossibleNoNo
File Size SuitabilitySmall (<100MB)Medium to LargeAny SizeVery Large
ScalabilityLowMediumHighVery High
Browser CompatibilityAll BrowsersAll BrowsersModern BrowsersAll Browsers
Server ProcessingYesYesYesNo (token generation only)

Implementation Details
#

MethodClient-Side TechnologyServer-Side TechnologyWhen to Choose
Standard UploadForm dataIFormFile + BlobClientSimple requirements, small files
Chunked UploadJavaScript to split fileBlockBlobClient with stagesLarge files with reliable progress tracking
Stream UploadFetch API with streamingRequest.Body direct streamingMemory efficiency is critical
SAS UploadXMLHttpRequest/fetch with PUTSAS token generation onlyMaximum scalability for large files

Memory Usage Patterns
#

Upload TypeClient Memory PatternServer Memory PatternAzure SDK Usage
StandardEntire file loaded at onceEntire file in memorySingle UploadAsync call
ChunkedOne chunk at a timeOne chunk at a timeStageBlockAsync + CommitBlockListAsync
StreamSmall read bufferSmall forward bufferStreaming UploadAsync
SASDepends on implementationToken generation onlyNo SDK usage for upload

When to Use Each Strategy
#

  • Standard Upload (IFormFile): Use for simple uploads where file size is small to medium (up to 100MB), and you want convenience and ease of use.

  • Chunked Uploads: Use when uploading large files (100MB to several GB) that need detailed progress tracking, resumability, or fault tolerance.

  • Stream Upload: Use for memory-efficient handling of files of any size when using modern browsers. Best when memory usage must be minimized.

  • SAS Upload: Use for maximum scalability with very large files in production environments. Ideal when you want to minimize server resource usage during uploads.


2.2.2 Listing All Files and Retrieving File Content with SAS
#

2.2.2.1 List All Files from a Container (File URL with SAS Token )
#

This method retrieves all blobs (files) in a specified Azure Blob Storage container. Optionally, you can filter by a folder path and include a Shared Access Signature (SAS) URI for direct access to the files.

Controller Example:

    [HttpGet("list")] // GET api/blob/list
    public async Task<IActionResult> GetAllBlobsAsync(string containerName, string path = null)
    {
        var blobs = await _blobStorageService.GetAllBlobsAsync(containerName, path, true, DateTimeOffset.UtcNow.AddHours(1));
        return Ok(blobs);
    }

Service Example:

    public async Task<List<BlobDetails>> GetAllBlobsAsync(string containerName, string path = null, bool includeSasUri = false, DateTimeOffset? sasExpiryTime = null)
    {
        var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
        var blobs = containerClient.GetBlobsAsync(prefix: path);
        var blobDetailsList = new List<BlobDetails>();

        await foreach (var blobItem in blobs)
        {
            var blobDetails = new BlobDetails
            {
                Name = blobItem.Name,
                CreatedOn = blobItem.Properties.CreatedOn,
                Metadata = blobItem.Metadata,
            };

            if (includeSasUri && sasExpiryTime.HasValue)
            {
                blobDetails.SasUri = await GetBlobSasUriAsync(containerName, blobItem.Name, sasExpiryTime.Value);
            }

            blobDetailsList.Add(blobDetails);
        }

        return blobDetailsList;
    }

Explanation:

Parameters:

  • containerName: The name of the Azure Blob Storage container from which blobs are listed.

  • path: Optional folder path for filtering blobs.

  • includeSasUri: If true, includes a SAS token URI for each blob.

  • sasExpiryTime: Specifies how long the SAS token will remain valid.

Behavior:

  • The method fetches blobs asynchronously using GetBlobsAsync, which supports efficient streaming of large lists of blobs.

  • Each blob’s details, including its name, creation time, and metadata, are stored in the BlobDetails object.

  • If SAS URIs are requested, the method generates them for secure, time-limited access to the files.

Response Object: The BlobDetails object contains:

  • Name: The name of the blob.

  • CreatedOn: The creation timestamp of the blob.

  • Metadata: Custom metadata associated with the blob.

  • SasUri (optional): A time-bound URI for direct file access.

Security:

  • SAS URIs are generated only if explicitly requested via the includeSasUri parameter.

  • The sasExpiryTime limits how long the SAS token stays valid, which narrows the window for misuse.

Use Case
#

  • File Browsing: Ideal for listing all files in a container or a specific folder, with the option to generate secure links for download or sharing.

  • Dynamic Access: SAS tokens allow users to access files securely without exposing storage credentials.

  • Metadata Retrieval: Useful for displaying metadata or auditing file properties.

2.2.2.2 Retrieve File Content (Stream)
#

To retrieve a blob’s content, you can stream the file directly from Azure Blob Storage to the client:

Controller Example:

    [HttpGet("download")] // GET api/blob/download
    public async Task<IActionResult> DownloadBlobAsync(string containerName, string blobName)
    {
        var stream = await _blobStorageService.DownloadBlobAsync(containerName, blobName);
        return File(stream, "application/octet-stream", blobName);
    }

Service Example

   public async Task<Stream> DownloadBlobAsync(string containerName, string blobName)
    {
        var containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
        var blobClient = containerClient.GetBlobClient(blobName);
        var downloadInfo = await blobClient.DownloadAsync();
        return downloadInfo.Value.Content;
    }

Explanation:

  • Stream-Based Retrieval: This approach allows you to stream the file directly to the client without first saving it to the server’s local storage.

  • Secure Access: Authentication runs through the Azure SDK’s built-in mechanisms (Managed Identity or a connection string), so storage credentials never appear in client code.

  • Content-Type Handling: application/octet-stream is used to indicate a binary file. This can be adjusted based on the file type (e.g., text/plain for text files, image/jpeg for images).

Use Case:

  • Ideal for File Sharing: Suitable for scenarios where files need to be shared or downloaded securely by authorized clients without exposing storage credentials.

  • Efficient for Large Files: Streaming avoids loading the entire file into memory, making it efficient for large file downloads.

2.2.3 UI Implementation
#

Let’s build a modern, user-friendly file upload interface in Angular to interact with our backend services for blob uploads. Our interface will support:

  • Drag-and-drop file selection
  • Multiple upload methods selection
  • Detailed progress tracking
  • File listing and management

File Upload Interface Example

Angular Service for File Uploads
#

First, let’s create a service that handles the different upload methods:

// storage.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpEvent, HttpEventType, HttpHeaders, HttpRequest } from '@angular/common/http';
import { Observable, Subject } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class StorageService {
  private apiUrl = '/api/blob';
  private uploadProgress = new Subject<number>();
  
  uploadProgress$ = this.uploadProgress.asObservable();

  constructor(private http: HttpClient) { }

  // 1. Standard File Upload
  uploadMultipartFormData(file: File, containerName: string): Observable<any> {
    const formData = new FormData();
    formData.append('file', file);
    formData.append('containerName', containerName);
    formData.append('blobName', file.name);

    const req = new HttpRequest('POST', `${this.apiUrl}/upload`, formData, {
      reportProgress: true
    });

    return this.http.request(req).pipe(
      map(event => this.getEventMessage(event, file))
    );
  }

  // 2. Chunked Upload
  uploadFileChunked(file: File, containerName: string, chunkSize: number = 5 * 1024 * 1024): Observable<any> {
    const totalChunks = Math.ceil(file.size / chunkSize);
    let currentChunk = 0;
    const uploadChunk$ = new Subject<any>();

    const processNextChunk = () => {
      if (currentChunk >= totalChunks) {
        uploadChunk$.complete();
        return;
      }

      const start = currentChunk * chunkSize;
      const end = Math.min(file.size, start + chunkSize);
      const chunk = file.slice(start, end);

      const formData = new FormData();
      formData.append('chunk', chunk, 'chunk');
      formData.append('containerName', containerName);
      formData.append('blobName', file.name);
      formData.append('chunkIndex', currentChunk.toString());
      formData.append('totalChunks', totalChunks.toString());

      this.http.post(`${this.apiUrl}/upload-chunk`, formData).subscribe(
        (response) => {
          currentChunk++;
          this.uploadProgress.next(Math.round((currentChunk / totalChunks) * 100));
          uploadChunk$.next({ progress: (currentChunk / totalChunks) * 100 });
          processNextChunk();
        },
        (error) => {
          uploadChunk$.error(error);
        }
      );
    };

    // Start the chunking process
    processNextChunk();
    return uploadChunk$.asObservable();
  }

  // 3. Stream Upload
  uploadFileStream(file: File, containerName: string): Observable<any> {
    const headers = new HttpHeaders({
      'Container-Name': containerName,
      'Blob-Name': file.name
    });

    const req = new HttpRequest('POST', `${this.apiUrl}/stream-upload`, file, {
      headers,
      reportProgress: true
    });

    return this.http.request(req).pipe(
      map(event => this.getEventMessage(event, file))
    );
  }

  // 4. SAS Direct Upload
  uploadLargeFile(file: File, containerName: string): Observable<any> {
    const sasUpload$ = new Subject<any>();
    
    // Step 1: Get SAS URL from server
    this.http.post<{sasUri: string, expiry: string}>(`${this.apiUrl}/get-upload-url`, {
      containerName,
      blobName: file.name
    }).subscribe(
      (response) => {
        // Step 2: Upload directly to Azure using the SAS URL
        const xhr = new XMLHttpRequest();
        xhr.open('PUT', response.sasUri, true);
        xhr.setRequestHeader('x-ms-blob-type', 'BlockBlob');
        xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream');
        
        // Track progress
        xhr.upload.onprogress = (e) => {
          if (e.lengthComputable) {
            const percentComplete = Math.round((e.loaded / e.total) * 100);
            this.uploadProgress.next(percentComplete);
            sasUpload$.next({ progress: percentComplete });
          }
        };
        
        xhr.onload = () => {
          if (xhr.status === 201) {
            sasUpload$.next({ complete: true });
            sasUpload$.complete();
          } else {
            sasUpload$.error(`Upload failed: ${xhr.status}`);
          }
        };
        
        xhr.onerror = () => {
          sasUpload$.error('Upload failed due to network error');
        };
        
        xhr.send(file);
      },
      (error) => {
        sasUpload$.error(error);
      }
    );
    
    return sasUpload$.asObservable();
  }

  // List files in a container
  listFiles(containerName: string, path: string = null): Observable<any> {
    let url = `${this.apiUrl}/list?containerName=${containerName}`;
    if (path) {
      url += `&path=${path}`;
    }
    return this.http.get(url);
  }

  // Helper method to format upload progress events
  private getEventMessage(event: HttpEvent<any>, file: File) {
    switch (event.type) {
      case HttpEventType.UploadProgress:
        const progress = Math.round(100 * event.loaded / event.total);
        this.uploadProgress.next(progress);
        return { progress };
      
      case HttpEventType.Response:
        return { complete: true };
      
      default:
        return { progress: 0 };
    }
  }
}

Upload Component Implementation
#

Now, let’s create a component that uses our service to provide a user-friendly upload interface:

// file-upload.component.ts
import { Component } from '@angular/core';
import { StorageService } from '../storage.service';

@Component({
  selector: 'app-file-upload',
  templateUrl: './file-upload.component.html',
  styleUrls: ['./file-upload.component.scss']
})
export class FileUploadComponent {
  selectedFile: File = null;
  containerName: string = 'uploads';
  uploadProgress: number = 0;
  uploading: boolean = false;
  uploadMethod: string = 'standard';
  dragOver: boolean = false;
  
  uploadMethods = [
    { value: 'standard', label: 'Standard Upload', description: 'Best for small files' },
    { value: 'chunked', label: 'Chunked Upload', description: 'Better for large files' },
    { value: 'stream', label: 'Stream Upload', description: 'Memory efficient' },
    { value: 'sas', label: 'Direct Upload (SAS)', description: 'Fastest for very large files' }
  ];

  constructor(private storageService: StorageService) {
    this.storageService.uploadProgress$.subscribe(progress => {
      this.uploadProgress = progress;
    });
  }

  onFileSelected(event: any): void {
    if (event.target.files.length > 0) {
      this.selectedFile = event.target.files[0];
    }
  }

  onDrop(event: DragEvent): void {
    event.preventDefault();
    this.dragOver = false;
    
    if (event.dataTransfer.files.length > 0) {
      this.selectedFile = event.dataTransfer.files[0];
    }
  }

  onDragOver(event: DragEvent): void {
    event.preventDefault();
    this.dragOver = true;
  }

  onDragLeave(event: DragEvent): void {
    event.preventDefault();
    this.dragOver = false;
  }

  uploadFile(): void {
    if (!this.selectedFile) {
      alert('Please select a file first');
      return;
    }
    
    this.uploading = true;
    this.uploadProgress = 0;
    let upload$: Observable<any>;
    
    switch (this.uploadMethod) {
      case 'standard':
        upload$ = this.storageService.uploadMultipartFormData(this.selectedFile, this.containerName);
        break;
      case 'chunked':
        upload$ = this.storageService.uploadFileChunked(this.selectedFile, this.containerName);
        break;
      case 'stream':
        upload$ = this.storageService.uploadFileStream(this.selectedFile, this.containerName);
        break;
      case 'sas':
        upload$ = this.storageService.uploadLargeFile(this.selectedFile, this.containerName);
        break;
      default:
        upload$ = this.storageService.uploadMultipartFormData(this.selectedFile, this.containerName);
    }
    
    upload$.subscribe(
      (event) => {
        if (event.complete) {
          this.uploading = false;
          this.selectedFile = null;
          // Refresh file list or show success message
        }
      },
      (error) => {
        console.error('Upload error:', error);
        this.uploading = false;
      }
    );
  }
}

HTML Template
#

<!-- file-upload.component.html -->
<div class="upload-container">
  <h2>Upload Files to Azure Blob Storage</h2>
  
  <div 
    class="dropzone" 
    [class.active]="dragOver"
    [class.disabled]="uploading"
    (dragover)="onDragOver($event)" 
    (dragleave)="onDragLeave($event)" 
    (drop)="onDrop($event)">
    
    <div class="dropzone-content">
      <mat-icon>cloud_upload</mat-icon>
      <h3>Drag & Drop Files Here</h3>
      <p>Or click to browse files</p>
      
      <input 
        type="file" 
        id="fileInput" 
        [disabled]="uploading"
        (change)="onFileSelected($event)" 
        style="display: none;">
      
      <button 
        mat-raised-button 
        color="primary" 
        [disabled]="uploading"
        (click)="fileInput.click()">
        Select File
      </button>
    </div>
  </div>
  
  <div class="file-info" *ngIf="selectedFile">
    <h3>Selected File</h3>
    <p>
      <strong>Name:</strong> {{ selectedFile.name }}<br>
      <strong>Size:</strong> {{ selectedFile.size | fileSize }}<br>
      <strong>Type:</strong> {{ selectedFile.type || 'Unknown' }}
    </p>
  </div>
  
  <div class="upload-options">
    <h3>Upload Method</h3>
    <mat-radio-group [(ngModel)]="uploadMethod" [disabled]="uploading">
      <mat-radio-button 
        *ngFor="let method of uploadMethods" 
        [value]="method.value"
        class="upload-method-option">
        <strong>{{ method.label }}</strong>
        <small>{{ method.description }}</small>
      </mat-radio-button>
    </mat-radio-group>
  </div>
  
  <mat-form-field appearance="outline" class="container-input">
    <mat-label>Container Name</mat-label>
    <input matInput [(ngModel)]="containerName" [disabled]="uploading">
  </mat-form-field>
  
  <div class="upload-progress" *ngIf="uploading">
    <mat-progress-bar 
      mode="determinate" 
      [value]="uploadProgress">
    </mat-progress-bar>
    <p>{{ uploadProgress }}% Uploaded</p>
  </div>
  
  <div class="action-buttons">
    <button 
      mat-raised-button 
      color="primary" 
      [disabled]="!selectedFile || uploading"
      (click)="uploadFile()">
      <mat-icon>cloud_upload</mat-icon>
      Upload File
    </button>
  </div>
</div>

Styling
#

The component styling (dropzone, progress bar, Material overrides) is plain SCSS and not worth reprinting here. The full stylesheet is in the repo.

The Angular side gives you a drag-and-drop upload interface with a method selector, a container name field, live progress, and error handling. Treat it as a working reference, not a hardened product: before production you would add file-type and size validation, auth on the API, and retry logic on the chunked and SAS paths.

The bottom line
#

Compress the whole post to one idea: match the strategy to file size and to where you can afford to spend memory.

  • Small files, simple needs: standard IFormFile. Don’t over-engineer a 2 MB upload.
  • Large files through your server: chunked upload, for retry and progress without buffering the whole file.
  • Memory is the constraint: stream straight from Request.Body so neither side holds the file.
  • Large files in production: SAS direct upload. The browser talks to Azure, your server only mints a short-lived token, and it scales the best of the four.

The rule that outranks the rest: never buffer a whole large file in the API process. Every strategy here except standard upload exists to avoid exactly that.

Further Reading
#

The full .NET and Angular sample is on GitHub: azure-storage-dotnet.

Related

Handling Authorization and Role-Based Access Control (RBAC)

··19 mins
Introduction # Static role checks ([Authorize(Roles = "Admin")]) fall apart the first time someone asks you to add a permission without a redeploy. Once roles and permissions have to change at runtime, hard-coded role attributes become a liability. The Contact Management Application takes a different route: a dynamic policy provider that builds authorization policies from the database at request time, covering both the backend API and the Angular frontend, wired into JWT authentication without breaking the separation of concerns Clean Architecture expects.

Implementing SharePoint File CRUD Operations using Microsoft Graph API

··11 mins
Introduction # Storing files in SharePoint from a .NET service sounds like it should be a solved problem, and it is, as long as you make one decision early: delegated or application permissions. That single choice changes who shows up as “Created by” on every file and whether your service needs a signed-in user at all. Get it wrong and you rebuild the auth layer later.