NetEvolve.Pulse.SqlServer 0.17.3

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

NetEvolve.Pulse.SqlServer

NuGet Version NuGet Downloads License

SQL Server persistence provider for the Pulse outbox pattern using plain ADO.NET. Provides optimized T-SQL operations with proper transaction support and locking strategies for reliable event delivery in high-throughput scenarios.

Features

  • Plain ADO.NET: No ORM overhead, direct SQL Server access via Microsoft.Data.SqlClient
  • Transaction Support: Enlist outbox operations in existing SqlTransaction instances
  • Optimized Queries: Uses stored procedures with ROWLOCK/READPAST hints for concurrent access
  • Dead Letter Management: Built-in support for inspecting, replaying, and monitoring dead-letter messages via IOutboxManagement
  • Configurable Schema: Customize schema and table names for multi-tenant scenarios
  • Schema Interchangeability: Uses canonical schema compatible with Entity Framework provider

Installation

NuGet Package Manager

Install-Package NetEvolve.Pulse.SqlServer

.NET CLI

dotnet add package NetEvolve.Pulse.SqlServer

PackageReference

<PackageReference Include="NetEvolve.Pulse.SqlServer" Version="x.x.x" />

Database Setup

Before using this provider, execute the schema script to create the required database objects.

The script uses SQLCMD variables (:setvar) and must be run in SQLCMD mode.

Running the Script

sqlcmd utility:

sqlcmd -S your-server -d your-database -i OutboxMessage.sql

SQL Server Management Studio (SSMS):

Enable SQLCMD Mode via Query > SQLCMD Mode (Ctrl+Shift+Q), then execute the script.

Azure Data Studio:

Enable SQLCMD via the query toolbar before executing.

SQLCMD Variables

The script exposes the following configurable variables at the top of OutboxMessage.sql:

Variable Default Description
SchemaName pulse Database schema name
TableName OutboxMessage Table name

To use custom names, change the :setvar values before executing:

:setvar SchemaName "myapp"
:setvar TableName "Events"

Schema Script Contents

The script creates:

  • The configured schema (default: [pulse])
  • The [OutboxMessage] table with two optimized non-clustered indexes

Core stored procedures:

Procedure Purpose
usp_GetPendingOutboxMessages Retrieves and locks pending messages for processing
usp_GetFailedOutboxMessagesForRetry Retrieves failed messages eligible for retry
usp_MarkOutboxMessageCompleted Marks a message as successfully processed
usp_MarkOutboxMessageFailed Marks a message as failed with error details
usp_MarkOutboxMessageDeadLetter Moves a message to dead-letter status
usp_DeleteCompletedOutboxMessages Removes old completed messages older than a given threshold

Management stored procedures:

Procedure Purpose
usp_GetDeadLetterOutboxMessages Returns a paginated list of dead-letter messages
usp_GetDeadLetterOutboxMessage Returns a single dead-letter message by ID
usp_GetDeadLetterOutboxMessageCount Returns the total count of dead-letter messages
usp_ReplayOutboxMessage Resets a single dead-letter message to Pending
usp_ReplayAllDeadLetterOutboxMessages Resets all dead-letter messages to Pending
usp_GetOutboxStatistics Returns message counts grouped by status

Quick Start

using Microsoft.Extensions.DependencyInjection;
using NetEvolve.Pulse;

var services = new ServiceCollection();

services.AddPulse(config => config
    .AddOutbox(
        options => options.Schema = "pulse",
        processorOptions => processorOptions.BatchSize = 100)
    .AddSqlServerOutbox("Server=.;Database=MyDb;Integrated Security=true;TrustServerCertificate=true;")
);

Using Configuration

services.AddPulse(config => config
    .AddOutbox()
    .AddSqlServerOutbox(
        sp => sp.GetRequiredService<IConfiguration>().GetConnectionString("Outbox")!,
        options =>
        {
            options.Schema = "messaging";
            options.TableName = "OutboxMessage";
        })
);

Registered Services

AddSqlServerOutbox(...) registers the following services:

Service Implementation Lifetime
IOutboxRepository SqlServerOutboxRepository Scoped
IOutboxManagement SqlServerOutboxManagement Scoped
TimeProvider TimeProvider.System Singleton (if not already registered)

Dead Letter Management

The IOutboxManagement service is automatically registered when calling AddSqlServerOutbox(...). It provides operations for inspecting and recovering dead-letter messages, as well as monitoring outbox health.

public class OutboxMonitorService
{
    private readonly IOutboxManagement _management;

    public OutboxMonitorService(IOutboxManagement management) =>
        _management = management;

    public async Task PrintStatisticsAsync(CancellationToken ct)
    {
        var stats = await _management.GetStatisticsAsync(ct);
        Console.WriteLine($"Pending: {stats.Pending}");
        Console.WriteLine($"Processing: {stats.Processing}");
        Console.WriteLine($"Completed: {stats.Completed}");
        Console.WriteLine($"Failed: {stats.Failed}");
        Console.WriteLine($"Dead Letter: {stats.DeadLetter}");
        Console.WriteLine($"Total: {stats.Total}");
    }

    public async Task ReplayAllDeadLettersAsync(CancellationToken ct)
    {
        var replayed = await _management.ReplayAllDeadLetterAsync(ct);
        Console.WriteLine($"Replayed {replayed} dead-letter messages.");
    }
}

Available Operations

Method Description
GetStatisticsAsync() Returns message counts grouped by status (OutboxStatistics)
GetDeadLetterMessagesAsync(pageSize, page) Returns a paginated list of dead-letter messages
GetDeadLetterMessageAsync(messageId) Returns a single dead-letter message by ID
GetDeadLetterCountAsync() Returns the total count of dead-letter messages
ReplayMessageAsync(messageId) Resets a single dead-letter message to Pending for reprocessing
ReplayAllDeadLetterAsync() Resets all dead-letter messages to Pending and returns the updated count

Transaction Integration

Manual Transaction Enlistment

public class OrderService
{
    private readonly string _connectionString;
    private readonly IServiceProvider _serviceProvider;

    public async Task CreateOrderAsync(CreateOrderRequest request, CancellationToken ct)
    {
        await using var connection = new SqlConnection(_connectionString);
        await connection.OpenAsync(ct);
        await using var transaction = connection.BeginTransaction();

        try
        {
            // Business operation
            await using var cmd = new SqlCommand("INSERT INTO [Order] ...", connection, transaction);
            await cmd.ExecuteNonQueryAsync(ct);

            // Store event in outbox (same transaction)
            var outbox = new SqlServerEventOutbox(
                connection,
                _serviceProvider.GetRequiredService<IOptions<OutboxOptions>>(),
                TimeProvider.System,
                transaction);

            await outbox.StoreAsync(new OrderCreatedEvent { OrderId = orderId }, ct);

            await transaction.CommitAsync(ct);
        }
        catch
        {
            await transaction.RollbackAsync(ct);
            throw;
        }
    }
}

Using IOutboxTransactionScope

public class UnitOfWork : IOutboxTransactionScope, IAsyncDisposable
{
    private readonly SqlConnection _connection;
    private SqlTransaction? _transaction;

    public async Task BeginTransactionAsync(CancellationToken ct)
    {
        await _connection.OpenAsync(ct);
        _transaction = _connection.BeginTransaction();
    }

    public object? GetCurrentTransaction() => _transaction;

    public async Task CommitAsync(CancellationToken ct)
    {
        if (_transaction is not null)
        {
            await _transaction.CommitAsync(ct);
        }
    }
}

// Register in DI
services.AddScoped<IOutboxTransactionScope, UnitOfWork>();

Schema Customization

To use a custom schema or table name, update the :setvar variables at the top of OutboxMessage.sql before executing, then configure the same names in code:

:setvar SchemaName "myapp"
:setvar TableName "Events"
services.AddPulse(config => config
    .AddOutbox(options =>
    {
        options.Schema = "myapp";      // Default: "pulse"
        options.TableName = "Events";  // Default: "OutboxMessage"
    })
    .AddSqlServerOutbox(connectionString)
);

Performance Considerations

Indexing

The default schema includes optimized indexes for:

  • Pending message polling (Status, CreatedAt)
  • Completed message cleanup (Status, ProcessedAt)

Stored Procedures

Operations use stored procedures with:

  • ROWLOCK for row-level locking
  • READPAST to skip locked rows during polling
  • SET NOCOUNT ON to reduce network traffic

Batch Processing

Configure batch size based on your throughput requirements:

.AddOutbox(processorOptions: options =>
{
    options.BatchSize = 500;                    // Messages per poll
    options.PollingInterval = TimeSpan.FromSeconds(1);
    options.EnableBatchSending = true;          // Use batch transport
})

Schema Interchangeability

This provider uses the canonical outbox schema, making it fully interchangeable with the Entity Framework provider:

  1. Development: Start with Entity Framework for rapid iteration
  2. Production: Switch to ADO.NET for maximum performance
  3. Mixed: Use both providers against the same database
// Both configurations work with the same database table
.AddSqlServerOutbox(connectionString)
// or
.AddEntityFrameworkOutbox<MyDbContext>()

Requirements

  • .NET 8.0, .NET 9.0, or .NET 10.0
  • SQL Server 2016 or later (or Azure SQL Database)
  • Microsoft.Data.SqlClient for database connectivity
  • Microsoft.Extensions.Hosting for the background processor

Documentation

For complete documentation, please visit the official documentation.

Contributing

Contributions are welcome! Please read the Contributing Guidelines before submitting a pull request.

Support

License

This project is licensed under the MIT License - see the LICENSE file for details.


Made with ❤️ by the NetEvolve Team

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.3 0 3/28/2026
0.10.11 93 3/27/2026
0.10.5 56 3/27/2026
0.7.1 63 3/25/2026