Benday.AzureStorage 1.4.0

dotnet add package Benday.AzureStorage --version 1.4.0
                    
NuGet\Install-Package Benday.AzureStorage -Version 1.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Benday.AzureStorage" Version="1.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Benday.AzureStorage" Version="1.4.0" />
                    
Directory.Packages.props
<PackageReference Include="Benday.AzureStorage" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Benday.AzureStorage --version 1.4.0
                    
#r "nuget: Benday.AzureStorage, 1.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Benday.AzureStorage@1.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Benday.AzureStorage&version=1.4.0
                    
Install as a Cake Addin
#tool nuget:?package=Benday.AzureStorage&version=1.4.0
                    
Install as a Cake Tool

Benday.AzureStorage

Simplified Azure Storage access for .NET -- blob repositories, Table Storage repositories, typed queue clients, queue health checks, and DI helpers.

Targets .NET 8.0, 9.0, and 10.0.

Written by Benjamin Day Pluralsight Author | Microsoft MVP https://www.benday.com https://www.honestcheetah.com info@benday.com YouTube: https://www.youtube.com/@_benday

Installation

dotnet add package Benday.AzureStorage --version 1.0.0-alpha

Configuration

Add an AzureStorage section to your appsettings.json:

{
  "AzureStorage": {
    "ConnectionString": "your-connection-string",
    "CreateStructures": true
  }
}

For local development with Azurite:

{
  "AzureStorage": {
    "UseDevelopmentStorage": true,
    "CreateStructures": true
  }
}

For Managed Identity (DefaultAzureCredential):

{
  "AzureStorage": {
    "AccountName": "mystorageaccount",
    "UseDefaultAzureCredential": true,
    "CreateStructures": true
  }
}

Setting CreateStructures to true automatically creates containers, tables, and queues if they don't exist.

Service Registration

Register core Azure Storage services in Program.cs:

builder.Services.AddBendayAzureStorage(builder.Configuration);

This registers AzureStorageConfig, BlobServiceClient, TableServiceClient, QueueClientFactory, and IMimeTypeUtil as singletons.

Then register the specific resources you need:

// Blob container
builder.Services.AddBlobRepository("my-container");

// Table Storage repository
builder.Services.AddTableRepository<CustomerRepository, CustomerEntity>();

// Typed queue
builder.Services.AddTypedQueue<OrderMessage>("order-queue", opts =>
{
    opts.MaxDequeueCount = 3;
});

Table Storage

Define an Entity

Table Storage entities must implement both ITableEntity (from Azure.Data.Tables) and IEntityIdentity<string> (from Benday.Common.Interfaces):

using Azure;
using Azure.Data.Tables;
using Benday.Common.Interfaces;

public class CustomerEntity : ITableEntity, IEntityIdentity<string>
{
    public string Id { get; set; } = string.Empty;
    public string PartitionKey { get; set; } = "default";
    public string RowKey { get; set; } = string.Empty;
    public DateTimeOffset? Timestamp { get; set; }
    public ETag ETag { get; set; }

    // Your custom properties
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public string Region { get; set; } = string.Empty;
}

Create a Repository

Extend TableRepository<T> and provide the table name:

using Benday.AzureStorage.Tables;

public class CustomerRepository : TableRepository<CustomerEntity>
{
    public CustomerRepository(
        TableServiceClient serviceClient,
        AzureStorageConfig config,
        ILogger<CustomerRepository> logger)
        : base(serviceClient, config, logger)
    {
    }

    protected override string TableName => "customers";

    // Optional: override the default partition key
    protected override string DefaultPartitionKey => "default";
}

Use the Repository

public class CustomerService
{
    private readonly ITableRepository<CustomerEntity> _repo;

    public CustomerService(ITableRepository<CustomerEntity> repo)
    {
        _repo = repo;
    }

    public async Task CreateCustomerAsync(string name, string email, string region)
    {
        var customer = new CustomerEntity
        {
            Id = Guid.NewGuid().ToString(),
            RowKey = Guid.NewGuid().ToString(),
            PartitionKey = region,
            Name = name,
            Email = email,
            Region = region
        };

        await _repo.SaveAsync(customer);
    }

    public async Task<CustomerEntity?> GetCustomerAsync(string region, string id)
    {
        return await _repo.GetByIdAsync(partitionKey: region, rowKey: id);
    }

    public async Task<IList<CustomerEntity>> GetCustomersByRegionAsync(string region)
    {
        return await _repo.GetAllAsync(partitionKey: region);
    }

    public async Task<IList<CustomerEntity>> SearchByNameAsync(string namePrefix)
    {
        return await _repo.QueryAsync(c => c.Name.CompareTo(namePrefix) >= 0);
    }

    public async Task DeleteCustomerAsync(string region, string id)
    {
        await _repo.DeleteAsync(partitionKey: region, rowKey: id);
    }
}

The overloads that take only a string id (no partition key) use the DefaultPartitionKey value automatically.

Blob Repository

public class ReportService
{
    private readonly IBlobRepository _blobs;

    public ReportService(IBlobRepository blobs)
    {
        _blobs = blobs;
    }

    public async Task<string> UploadReportAsync(string path, Stream content)
    {
        return await _blobs.UploadAsync(path, content);
    }

    public async Task<byte[]> DownloadReportAsync(string path)
    {
        return await _blobs.DownloadBytesAsync(path);
    }

    public async Task<bool> ReportExistsAsync(string path)
    {
        return await _blobs.ExistsAsync(path);
    }

    public Uri GetReportUrl(string path)
    {
        return _blobs.GetSasUri(path, expiry: TimeSpan.FromHours(1));
    }
}

Typed Queues

public class OrderMessage
{
    public string OrderId { get; set; } = string.Empty;
    public string CustomerId { get; set; } = string.Empty;
    public decimal Total { get; set; }
}
public class OrderQueueService
{
    private readonly TypedQueueClient<OrderMessage> _queue;

    public OrderQueueService(TypedQueueClient<OrderMessage> queue)
    {
        _queue = queue;
    }

    public async Task EnqueueOrderAsync(string orderId, string customerId, decimal total)
    {
        await _queue.SendAsync(new OrderMessage
        {
            OrderId = orderId,
            CustomerId = customerId,
            Total = total
        });
    }

    public async Task ProcessNextAsync()
    {
        var message = await _queue.ReceiveAsync(visibilityTimeout: TimeSpan.FromMinutes(5));

        if (message == null)
            return; // Queue is empty

        // Check for poison messages first
        if (await _queue.MoveToPoisonIfNeededAsync(message))
            return; // Moved to poison queue

        try
        {
            Console.WriteLine($"Processing order {message.Body!.OrderId}");
            await _queue.CompleteAsync(message);
        }
        catch
        {
            // Release back to queue for retry
            await _queue.ReleaseAsync(message, visibilityDelay: TimeSpan.FromSeconds(30));
            throw;
        }
    }
}

Messages are JSON-serialized and Base64-encoded automatically. Messages that exceed MaxDequeueCount are moved to a poison queue (default name: {queueName}-poison).

Queue Listeners

TypedQueueListener<T> continuously polls a queue and hands each message to an IQueueMessageHandler<T>. It builds on TypedQueueClient<T> and owns the reusable plumbing -- the polling loop with backoff, per-message elapsed-time logging, releasing failed messages for retry, and moving poison messages aside -- so your handler contains only the domain logic.

Implement a handler:

using Benday.AzureStorage.Queues;

public class OrderMessageHandler : IQueueMessageHandler<OrderMessage>
{
    private readonly ILogger<OrderMessageHandler> _logger;

    public OrderMessageHandler(ILogger<OrderMessageHandler> logger)
    {
        _logger = logger;
    }

    public async Task HandleAsync(
        TypedQueueMessage<OrderMessage> message, CancellationToken cancellationToken)
    {
        var order = message.Body!;

        _logger.LogInformation("Processing order {OrderId}", order.OrderId);

        await FulfillOrderAsync(order, cancellationToken);

        // Return normally to complete (delete) the message.
        // Throw to release it for retry; once it reaches MaxDequeueCount the
        // listener moves it to the poison queue automatically.
    }
}

Register it as a hosted background service:

builder.Services.AddQueueListener<OrderMessage, OrderMessageHandler>(
    "order-queue",
    configureListener: opts =>
    {
        opts.PollingIntervalWhenEmpty = TimeSpan.FromSeconds(10);
        opts.VisibilityTimeout = TimeSpan.FromMinutes(5);
        opts.HeartbeatInterval = TimeSpan.FromMinutes(1);
    },
    configureQueue: queue => queue.MaxDequeueCount = 5);

The generic host starts the listener on startup and stops it on shutdown. AddQueueListener also registers the underlying TypedQueueClient<OrderMessage> (unless you already registered it with AddTypedQueue). Because TypedQueueListener<T> is a BackgroundService, a console or CLI app can register it the same way -- no web host required.

One-Shot / Triggered Hosts (e.g. Azure Container Apps Jobs)

For a host triggered per message rather than left running, register the listener without hosting it and drive a single pass yourself:

builder.Services.AddQueueListener<OrderMessage, OrderMessageHandler>(
    "order-queue",
    hostAsBackgroundService: false);
var listener = app.Services.GetRequiredService<TypedQueueListener<OrderMessage>>();

// Handle a single message, then exit -- keeps each job invocation isolated.
int processed = await listener.RunOnceAsync();

By default RunOnceAsync handles at most one message per call. Set DrainUntilEmpty = true in configureListener to keep processing until the queue is empty.

Health Checks

Monitor queue depth with ASP.NET Core health checks:

builder.Services.AddHealthChecks()
    .AddAzureQueueCheck("order-queue-health", opts =>
    {
        opts.QueueName = "order-queue";
        opts.DegradedThreshold = 50;    // Degraded when >= 50 messages
        opts.UnhealthyThreshold = 200;  // Unhealthy when >= 200 messages
        opts.PoisonIsUnhealthy = true;  // Any poison messages = unhealthy
    });

License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Benday.AzureStorage:

Package Downloads
Benday.BlobStorage

Connects entities with blob attachments in Azure Storage. Works with any storage backend — Cosmos DB, Table Storage, EF Core — as long as the entity implements IBlobOwner.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.4.0 54 9/3/2026
1.2.0 124 7/3/2026
1.1.0-alpha 125 5/15/2026
1.0.1-alpha 129 4/4/2026
1.0.0-alpha 157 4/3/2026

v1.4: User-delegation SAS. GetUserDelegationSasUriAsync and GetSasUriAsync sign with a delegation key from Microsoft Entra ID, so SAS URIs now work under a managed identity or any other token credential rather than requiring a shared account key; the key is cached and refreshed near expiry, and CanGenerateSasUri reports whether a service SAS is possible. Service endpoints are configurable via EndpointSuffix and explicit BlobServiceUri/TableServiceUri/QueueServiceUri, which is what lets a token-credential client target the Azurite emulator. BlobBridge gains SAS-async, GetAttachmentProperties, ranged download, SetAttachmentHttpHeaders, and a content-type argument on the attach methods. New SetHttpHeadersAsync on IBlobRepository stamps content type and disposition on an existing blob without clearing the other headers. Content disposition supports inline rendering and is now signed into the SAS - previously it was appended to the query after signing, which produced a URI the service rejected. CancellationToken added throughout the blob API. Breaking: UploadFileAsync gained a contentType parameter in third position, matching UploadAsync. v1.3: Added TypedQueueListener<T>, a BackgroundService that drives an IQueueMessageHandler<T> per message with a polling loop, per-message elapsed-time logging, release-on-failure retry, and poison handling on top of TypedQueueClient<T>. RunOnceAsync supports single-message or drain-until-empty passes for triggered/one-shot hosts such as Azure Container Apps jobs. Register with AddQueueListener<T, THandler>. v1.2: Merged the former Benday.BlobStorage package into Benday.AzureStorage; BlobBridge now lives in the Benday.AzureStorage.Blobs namespace. Also includes blob repositories, Table Storage repositories, typed queue clients, queue health checks, and DI helpers.