Benday.BlobStorage 1.0.1-alpha

This is a prerelease version of Benday.BlobStorage.
dotnet add package Benday.BlobStorage --version 1.0.1-alpha
                    
NuGet\Install-Package Benday.BlobStorage -Version 1.0.1-alpha
                    
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.BlobStorage" Version="1.0.1-alpha" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Benday.BlobStorage" Version="1.0.1-alpha" />
                    
Directory.Packages.props
<PackageReference Include="Benday.BlobStorage" />
                    
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.BlobStorage --version 1.0.1-alpha
                    
#r "nuget: Benday.BlobStorage, 1.0.1-alpha"
                    
#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.BlobStorage@1.0.1-alpha
                    
#: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.BlobStorage&version=1.0.1-alpha&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Benday.BlobStorage&version=1.0.1-alpha&prerelease
                    
Install as a Cake Tool

Benday.BlobStorage

Connects entities implementing IBlobOwner to blob attachments in Azure Storage. Works with any storage backend -- Cosmos DB, Table Storage, EF Core, or anything else -- as long as the entity implements IBlobOwner.

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.BlobStorage --version 1.0.0-alpha

This package depends on Benday.AzureStorage which will be installed automatically.

How It Works

BlobBridge<T> wraps an IBlobRepository and prefixes all blob paths with the entity's GetBlobPrefix() value. This means each entity instance gets its own isolated folder of blob attachments.

Getting Started

1. Implement IBlobOwner on Your Entity

IBlobOwner requires a single method that returns the blob path prefix for that entity:

using Benday.Common.Interfaces;

// This entity can be stored anywhere -- Cosmos DB, EF Core, Table Storage, etc.
public class Invoice : IBlobOwner
{
    public string TenantId { get; set; } = string.Empty;
    public string InvoiceId { get; set; } = string.Empty;

    // All blobs for this entity will be stored under this prefix.
    // For example: "tenant-42/invoices/inv-123/"
    public string GetBlobPrefix() => $"{TenantId}/invoices/{InvoiceId}/";
}

2. Register Azure Storage Services

In Program.cs, register the core services and a blob container:

builder.Services.AddBendayAzureStorage(builder.Configuration);
builder.Services.AddBlobRepository("my-container");

3. Use BlobBridge to Manage Attachments

using Benday.BlobStorage;

public class InvoiceAttachmentService
{
    private readonly BlobBridge<Invoice> _bridge;

    public InvoiceAttachmentService(IBlobRepository blobRepository)
    {
        _bridge = new BlobBridge<Invoice>(blobRepository);
    }

    // Upload from a stream
    public async Task<string> AttachFileAsync(Invoice invoice, string filename, Stream content)
    {
        // Stores at "tenant-42/invoices/inv-123/receipt.pdf"
        return await _bridge.AttachAsync(invoice, filename, content,
            metadata: new Dictionary<string, string>
            {
                ["uploaded-by"] = "system"
            });
    }

    // Upload from a local file path
    public async Task<string> AttachLocalFileAsync(Invoice invoice, string localPath)
    {
        var filename = Path.GetFileName(localPath);
        return await _bridge.AttachFileAsync(invoice, filename, localPath);
    }

    // Upload from a byte array
    public async Task<string> AttachBytesAsync(Invoice invoice, string filename, byte[] data)
    {
        return await _bridge.AttachBytesAsync(invoice, filename, data);
    }

    // List all attachments for an entity
    public async IAsyncEnumerable<string> ListAttachmentNamesAsync(Invoice invoice)
    {
        await foreach (var blob in _bridge.ListAttachmentsAsync(invoice))
        {
            yield return blob.Name;
        }
    }

    // Download an attachment as bytes
    public async Task<byte[]> DownloadAttachmentAsync(Invoice invoice, string filename)
    {
        return await _bridge.DownloadAttachmentBytesAsync(invoice, filename);
    }

    // Download an attachment to a local file
    public async Task DownloadToFileAsync(Invoice invoice, string filename, string localPath)
    {
        await _bridge.DownloadAttachmentToFileAsync(invoice, filename, localPath);
    }

    // Get a time-limited download URL
    public Uri GetDownloadUrl(Invoice invoice, string filename)
    {
        return _bridge.GetAttachmentSasUri(
            invoice,
            filename,
            expiry: TimeSpan.FromMinutes(15),
            downloadFilename: filename); // Sets Content-Disposition for browser downloads
    }

    // Check if an attachment exists
    public async Task<bool> HasAttachmentAsync(Invoice invoice, string filename)
    {
        return await _bridge.AttachmentExistsAsync(invoice, filename);
    }

    // Delete a specific attachment
    public async Task DeleteAttachmentAsync(Invoice invoice, string filename)
    {
        await _bridge.DeleteAttachmentAsync(invoice, filename);
    }

    // Delete all attachments for an entity
    public async Task DeleteAllAttachmentsAsync(Invoice invoice)
    {
        await _bridge.DeleteAttachmentsAsync(invoice, maxConcurrency: 25);
    }
}

Combining with Table Storage

An entity can implement both IBlobOwner and ITableEntity to store its data in Table Storage while keeping file attachments in Blob Storage:

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

public class ProjectDocument : ITableEntity, IEntityIdentity<string>, IBlobOwner
{
    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; }

    public string ProjectId { get; set; } = string.Empty;
    public string Title { get; set; } = string.Empty;

    public string GetBlobPrefix() => $"projects/{ProjectId}/docs/{RowKey}/";
}
public class ProjectDocumentService
{
    private readonly ITableRepository<ProjectDocument> _tableRepo;
    private readonly BlobBridge<ProjectDocument> _blobBridge;

    public ProjectDocumentService(
        ITableRepository<ProjectDocument> tableRepo,
        IBlobRepository blobRepo)
    {
        _tableRepo = tableRepo;
        _blobBridge = new BlobBridge<ProjectDocument>(blobRepo);
    }

    public async Task<ProjectDocument> CreateDocumentAsync(
        string projectId, string title, string filename, Stream fileContent)
    {
        var doc = new ProjectDocument
        {
            Id = Guid.NewGuid().ToString(),
            RowKey = Guid.NewGuid().ToString(),
            PartitionKey = projectId,
            ProjectId = projectId,
            Title = title
        };

        // Save metadata to Table Storage
        await _tableRepo.SaveAsync(doc);

        // Attach the file to Blob Storage
        await _blobBridge.AttachAsync(doc, filename, fileContent);

        return doc;
    }

    public async Task DeleteDocumentAsync(string projectId, string documentId)
    {
        var doc = await _tableRepo.GetByIdAsync(projectId, documentId);

        if (doc != null)
        {
            // Delete blobs first, then the table row
            await _blobBridge.DeleteAttachmentsAsync(doc);
            await _tableRepo.DeleteAsync(projectId, documentId);
        }
    }
}

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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1-alpha 50 4/4/2026
1.0.0-alpha 50 4/3/2026

Initial alpha release. BlobBridge for connecting IBlobOwner entities to blob attachments in Azure Storage.