Eventuous.Azure.Storage.Blobs 0.17.0

dotnet add package Eventuous.Azure.Storage.Blobs --version 0.17.0
                    
NuGet\Install-Package Eventuous.Azure.Storage.Blobs -Version 0.17.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="Eventuous.Azure.Storage.Blobs" Version="0.17.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Eventuous.Azure.Storage.Blobs" Version="0.17.0" />
                    
Directory.Packages.props
<PackageReference Include="Eventuous.Azure.Storage.Blobs" />
                    
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 Eventuous.Azure.Storage.Blobs --version 0.17.0
                    
#r "nuget: Eventuous.Azure.Storage.Blobs, 0.17.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 Eventuous.Azure.Storage.Blobs@0.17.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=Eventuous.Azure.Storage.Blobs&version=0.17.0
                    
Install as a Cake Addin
#tool nuget:?package=Eventuous.Azure.Storage.Blobs&version=0.17.0
                    
Install as a Cake Tool

Eventuous Azure Blob Storage Projections

This package adds Azure Blob Storage projections to applications built with Eventuous. It allows you to project event store events to Azure Blob Storage as state objects, maintaining a separate state document for each event stream.

Using projections

Create your own projection class that inherits from BlobStorageProjector<T> where T is your state type. The state type must be a class with a parameterless constructor.

Register event handlers using the On<TEvent> methods. When an event is received, the projector retrieves the current state blob (or creates a new state instance if the blob doesn't exist), applies the event to the state using the registered event handler, and uploads the updated state back to Blob Storage.

The class provides two constructors:

  • BlobStorageProjector(BlobContainerClient container, ... where the container client is passed directly
  • BlobStorageProjector(BlobServiceClient serviceClient, string containerName, ... where the service client is set up by Azure DI and the container name is set by the projection

The blob container must exist before the projector handles events; the projector doesn't create it.

JSON serialization is configured via BlobStorageProjectorOptions.JsonOptions. When you keep serializer options in ASP.NET Core DI, pass them on: new BlobStorageProjectorOptions { JsonOptions = options.Value }.

By default, the blob ID is extracted from the stream using context.Stream.GetId(). You can override this by providing a custom getBlobId function in the event registration:

public class BookingProjection : BlobStorageProjector<BookingState> {
    public BookingProjection(BlobServiceClient client)
        : base(client, "bookings-container") {

        // Uses default blob ID from stream
        On<BookingImported>((state, evt) => {
            state.RoomId = evt.RoomId;
            state.CheckInDate = evt.CheckIn;
            return state;
        });

        // Custom blob ID using event data
        On<BookingPaymentRegistered>(
            (state, evt) => {
                state.PaidAmount += evt.AmountPaid;
                return state;
            },
            context => new ValueTask<string>($"custom-{context.Message.BookingId}")
        );
    }
}

Projector options

The BlobStorageProjectorOptions class provides several configuration options for fine-tuning the projector behavior.

Option Type Default Description
JsonOptions JsonSerializerOptions? null (uses JsonSerializerOptions.Web) JSON serializer options for state serialization/deserialization. Controls formatting, naming policies, etc.
RaceRetries int 0 Number of retry attempts for optimistic concurrency conflicts. Increase when concurrent updates are likely.
IdempotencyMode IdempotencyMode IdempotencyMode.None Controls duplicate message detection behavior.

Idempotency modes

The IdempotencyMode enum controls how the projector handles duplicate messages:

  • None - No idempotency checks. Will process messages and updates blob, without checking for duplicates.
  • ByGlobalPosition - Skips processing if the existing blob has a global position set in its metadata that indicates it has already been processed. The event global position must be greater than that stored in the blob. This mode requires a subscription that provides real global positions, such as an all-stream subscription. Do not use it with message broker subscriptions where the global position is always zero — the first event would store position 0 and every subsequent event would be treated as a duplicate and silently ignored. Use ByMessageId instead.
  • ByMessageId - Use this when building projections directly from integration events. Skips processing if the message ID in the blob metadata matches that in the event. Note, this means the idempotency is weaker as only the last message ID is checked. Older messages that are replayed will be processed as normal.

Custom blob naming

By default, blob names are generated using GetBlobName(string id) which creates names in the format {id}/{StateType}.json, where id defaults to the stream ID from context.Stream.GetId().

You can customize blob naming in two ways:

1. Override the virtual methods globally for all events:

public class BookingProjection : BlobStorageProjector<BookingState> {
    // ...

    protected override string GetBlobName(string id) => $"bookings/{id}.json";
}

When the blob name depends on the event, override the overload that takes the consume context instead:

protected override string GetBlobName(string id, IMessageConsumeContext context)
    => $"projections/{context.Stream}/{id}.json";

The default implementation of the two-argument overload calls the one-argument overload, so overriding the two-argument version replaces the naming completely — a one-argument override is then never called.

2. Override blob ID per event handler using getBlobId:

On<BookingPaymentRegistered>(
    (state, evt) => {
        state.PaidAmount += evt.AmountPaid;
        return state;
    },
    // Custom blob ID for this specific event only
    context => new ValueTask<string>($"payments-{context.Message.BookingId}")
);

Note that getBlobId returns a blob ID, not a full blob name: the result is still passed to GetBlobName, so with the default naming the example above produces payments-{id}/BookingState.json. To change the full blob path, override GetBlobName as well.

Use per-event blob ID overrides when you need different events to target different blobs within the same projector, such as when the business identifier differs from the stream identifier.

Features

  • Automatic state management - Creates new state instances when blobs don't exist
  • Optimistic concurrency control - Uses ETags for safe concurrent updates
  • Idempotency - Prevents duplicate processing with configurable modes
  • Retry handling - Automatic retries for race conditions
  • Flexible blob naming - Customizable blob ID and naming conventions
  • Metadata storage - Automatically stores stream info, positions, and message IDs

Background

The projector stores each state as a separate blob in Azure Blob Storage. Each blob contains:

  • The serialized state object (JSON by default)
  • Metadata including stream name, message ID, stream position, and global position; because Azure requires metadata values to be ASCII, the stream name and message ID are stored percent-encoded
  • Content type set to application/json

This approach provides natural partitioning by stream and enables efficient state retrieval for individual streams.

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
0.17.0 70 9/30/2026
0.16.5-alpha.0.33 58 9/21/2026
0.16.5-alpha.0.32 65 9/20/2026
0.16.5-alpha.0.31 59 9/20/2026
0.16.5-alpha.0.30 77 8/27/2026
0.16.5-alpha.0.29 71 8/27/2026
0.16.5-alpha.0.28 81 8/26/2026
0.16.5-alpha.0.27 73 8/21/2026
0.16.5-alpha.0.26 66 8/21/2026
0.16.5-alpha.0.24 66 8/21/2026
0.16.5-alpha.0.22 72 8/21/2026
0.16.5-alpha.0.19 71 8/20/2026
0.16.5-alpha.0.18 71 8/20/2026
0.16.5-alpha.0.17 75 8/19/2026