Benday.AzureStorage
1.4.0
dotnet add package Benday.AzureStorage --version 1.4.0
NuGet\Install-Package Benday.AzureStorage -Version 1.4.0
<PackageReference Include="Benday.AzureStorage" Version="1.4.0" />
<PackageVersion Include="Benday.AzureStorage" Version="1.4.0" />
<PackageReference Include="Benday.AzureStorage" />
paket add Benday.AzureStorage --version 1.4.0
#r "nuget: Benday.AzureStorage, 1.4.0"
#:package Benday.AzureStorage@1.4.0
#addin nuget:?package=Benday.AzureStorage&version=1.4.0
#tool nuget:?package=Benday.AzureStorage&version=1.4.0
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 | Versions 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. |
-
net10.0
- Azure.Data.Tables (>= 12.12.0)
- Azure.Identity (>= 1.21.0)
- Azure.Storage.Blobs (>= 12.29.2)
- Azure.Storage.Queues (>= 12.27.1)
- Benday.Common.Interfaces (>= 1.0.3)
- Microsoft.AspNetCore.StaticFiles (>= 2.3.12)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.11)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.11)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
-
net8.0
- Azure.Data.Tables (>= 12.12.0)
- Azure.Identity (>= 1.21.0)
- Azure.Storage.Blobs (>= 12.29.2)
- Azure.Storage.Queues (>= 12.27.1)
- Benday.Common.Interfaces (>= 1.0.3)
- Microsoft.AspNetCore.StaticFiles (>= 2.3.12)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.11)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.11)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
-
net9.0
- Azure.Data.Tables (>= 12.12.0)
- Azure.Identity (>= 1.21.0)
- Azure.Storage.Blobs (>= 12.29.2)
- Azure.Storage.Queues (>= 12.27.1)
- Benday.Common.Interfaces (>= 1.0.3)
- Microsoft.AspNetCore.StaticFiles (>= 2.3.12)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.11)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.11)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
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.