MaksIT.HAMode 1.0.6

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

MaksIT.HAMode

Reusable high-availability runtime coordination library for MaksIT services.

Line Coverage Branch Coverage Method Coverage

Packages

  • MaksIT.HAMode (single NuGet package, single assembly)
    • MaksIT.HAMode.Extensions.ServiceCollectionExtensions
    • MaksIT.HAMode.Abstractions namespace:
      • IRuntimeInstanceId
      • IRuntimeLeaseService
      • IRuntimeLeaseConnectionProvider (root marker interface)
      • IRuntimeLeaseConnectionStringProvider
      • IRuntimeLeaseRedisConnectionProvider
      • IRuntimeLeaseEtcdConnectionProvider
      • RuntimeInstanceIdProvider
    • MaksIT.HAMode.PostgreSql.RuntimeLeaseServiceNpgsql
    • MaksIT.HAMode.Redis.RuntimeLeaseServiceRedis
    • MaksIT.HAMode.Etcd.RuntimeLeaseServiceEtcd

Target framework

  • net10.0

Design goals

  • Keep runtime coordination reusable across products.
  • Keep application configuration ownership in host projects.
  • Keep lease persistence implementation swappable and SOLID-compliant.

PostgreSQL lease table contract

RuntimeLeaseServiceNpgsql expects:

  • table: configurable via IRuntimeLeaseConnectionStringProvider.Schema and IRuntimeLeaseConnectionStringProvider.Table
    • defaults: public.app_runtime_leases
  • columns:
    • lease_name (text, PK)
    • holder_id (text)
    • version (bigint)
    • acquired_at_utc (timestamptz)
    • expires_at_utc (timestamptz)

If the configured table does not exist, PostgreSQL lease operations return an explicit internal error explaining which table is missing.

Usage examples

Install package

<ItemGroup>
  <PackageReference Include="MaksIT.HAMode" />
</ItemGroup>

Shared runtime instance id

using MaksIT.HAMode.Extensions;

builder.Services.AddHAModeRuntimeInstanceId();

PostgreSQL backend

using MaksIT.HAMode.Abstractions;
using MaksIT.HAMode.Extensions;

// Host project contract.
public interface IMyPgLeaseConfiguration : IRuntimeLeaseConnectionStringProvider;

// Host project concrete configuration.
public sealed class MyPgLeaseConfiguration : IMyPgLeaseConfiguration {
  public required string ConnectionString { get; init; }
  public string Schema { get; init; } = "ha";
  public string Table { get; init; } = "runtime_leases";
}

IMyPgLeaseConfiguration pgConfiguration = new MyPgLeaseConfiguration {
  ConnectionString = "<your-connection-string>"
};

builder.Services.AddHAModePostgreSql(pgConfiguration);

If you already manage a pooled PostgreSQL client in the host, pass the shared NpgsqlDataSource:

var dataSource = new NpgsqlDataSourceBuilder("<your-connection-string>").Build();
builder.Services.AddHAModePostgreSql(pgConfiguration, dataSource);

Redis backend

using MaksIT.HAMode.Abstractions;
using MaksIT.HAMode.Extensions;

// Host project contract.
public interface IMyRedisLeaseConfiguration : IRuntimeLeaseRedisConnectionProvider;

// Host project concrete configuration.
public sealed class MyRedisLeaseConfiguration : IMyRedisLeaseConfiguration {
  public required string Configuration { get; init; }
  public string KeyPrefix { get; init; } = "my-app/runtime-leases:";
}

IMyRedisLeaseConfiguration redisConfiguration = new MyRedisLeaseConfiguration {
  Configuration = "<your-redis-connection-string>"
};

builder.Services.AddHAModeRedis(redisConfiguration);

If you already manage a shared Redis client in the host, pass the same IConnectionMultiplexer:

var multiplexer = await ConnectionMultiplexer.ConnectAsync("<your-redis-connection-string>");
builder.Services.AddHAModeRedis(redisConfiguration, multiplexer);

Redis is schema-less, so there is no table bootstrap requirement; lease keys are isolated by KeyPrefix.

etcd backend

using MaksIT.HAMode.Abstractions;
using MaksIT.HAMode.Extensions;

// Host project contract.
public interface IMyEtcdLeaseConfiguration : IRuntimeLeaseEtcdConnectionProvider;

// Host project concrete configuration.
public sealed class MyEtcdLeaseConfiguration : IMyEtcdLeaseConfiguration {
  public required string Endpoints { get; init; } // ex: http://etcd:2379
  public string? Username { get; init; }
  public string? Password { get; init; }
  public string KeyPrefix { get; init; } = "my-app/runtime-leases/";
}

IMyEtcdLeaseConfiguration etcdConfiguration = new MyEtcdLeaseConfiguration {
  Endpoints = "http://etcd:2379",
  Username = null,
  Password = null
};

builder.Services.AddHAModeEtcd(etcdConfiguration);

If you already manage a shared etcd client in the host, pass the same EtcdClient:

var etcdClient = new EtcdClient("http://etcd:2379");
builder.Services.AddHAModeEtcd(etcdConfiguration, etcdClient);

etcd is key-space based, so there is no table bootstrap requirement; lease keys are isolated by KeyPrefix.

Runtime acquire/release flow

using MaksIT.HAMode.Abstractions;

public sealed class BootstrapHostedService(
  IRuntimeLeaseService leaseService,
  IRuntimeInstanceId runtimeInstance,
  ILogger<BootstrapHostedService> logger
) : IHostedService {
  public async Task StartAsync(CancellationToken cancellationToken) {
    var holder = runtimeInstance.InstanceId;
    var acquired = await leaseService.TryAcquireAsync(
      leaseName: "my-app-bootstrap",
      holderId: holder,
      ttl: TimeSpan.FromSeconds(30),
      cancellationToken: cancellationToken);

    if (!acquired.IsSuccess || !acquired.Value) {
      logger.LogInformation("Another replica owns bootstrap lease.");
      return;
    }

    try {
      // Run single-replica bootstrap logic here.
    }
    finally {
      await leaseService.ReleaseAsync("my-app-bootstrap", holder, CancellationToken.None);
    }
  }

  public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

Integration in MaksIT.Vault

1) Add package reference

In MaksIT.Vault.Engine.csproj:

<ItemGroup>
  <PackageReference Include="MaksIT.HAMode" />
</ItemGroup>

Switching backend uses a different service registration (no package change needed).

2) Keep Vault interfaces stable with adapters

// Vault Engine contract remains unchanged for consumers.
using SharedRuntimeLeaseService = MaksIT.HAMode.Abstractions.IRuntimeLeaseService;
namespace MaksIT.Vault.Engine.Infrastructure;
public interface IRuntimeLeaseService : SharedRuntimeLeaseService;
// Vault host contract remains unchanged for controllers/hosted services.
namespace MaksIT.Vault.Engine.RuntimeCoordination;
public interface IRuntimeInstanceId : MaksIT.HAMode.Abstractions.IRuntimeInstanceId;

3) Adapter for connection/config ownership

using MaksIT.HAMode.Abstractions;
using SharedRuntimeLeaseServiceNpgsql = MaksIT.HAMode.PostgreSql.RuntimeLeaseServiceNpgsql;

public sealed class RuntimeLeaseServiceNpgsql(
  IVaultEngineConfiguration config,
  ILogger<SharedRuntimeLeaseServiceNpgsql> logger
) : IRuntimeLeaseService {
  private sealed class VaultLeaseConnection(IVaultEngineConfiguration cfg) : IRuntimeLeaseConnectionStringProvider {
    public string ConnectionString => cfg.ConnectionString;
  }

  private readonly SharedRuntimeLeaseServiceNpgsql _inner = new(new VaultLeaseConnection(config), logger);

  public Task<MaksIT.Results.Result<bool>> TryAcquireAsync(string leaseName, string holderId, TimeSpan ttl, CancellationToken ct = default) =>
    _inner.TryAcquireAsync(leaseName, holderId, ttl, ct);

  public Task<MaksIT.Results.Result> ReleaseAsync(string leaseName, string holderId, CancellationToken ct = default) =>
    _inner.ReleaseAsync(leaseName, holderId, ct);
}

4) DI registration in Program.cs

builder.Services.AddSingleton<MaksIT.Vault.Engine.RuntimeCoordination.IRuntimeInstanceId, RuntimeInstanceIdProvider>();
builder.Services.AddSingleton<MaksIT.Vault.Engine.Infrastructure.IRuntimeLeaseService, RuntimeLeaseServiceNpgsql>();

Integration in MaksIT.CertsUI

The same pattern applies to MaksIT.CertsUI.Engine.

1) Add package reference

In MaksIT.CertsUI.Engine.csproj:

<ItemGroup>
  <PackageReference Include="MaksIT.HAMode" />
</ItemGroup>

2) Keep CertsUI interfaces stable with adapters

using SharedRuntimeLeaseService = MaksIT.HAMode.Abstractions.IRuntimeLeaseService;
namespace MaksIT.CertsUI.Engine.Infrastructure;
public interface IRuntimeLeaseService : SharedRuntimeLeaseService;
namespace MaksIT.CertsUI.Engine.RuntimeCoordination;
public interface IRuntimeInstanceId : MaksIT.HAMode.Abstractions.IRuntimeInstanceId;

3) Adapter for CertsUI configuration

using MaksIT.HAMode.Abstractions;
using SharedRuntimeLeaseServiceNpgsql = MaksIT.HAMode.PostgreSql.RuntimeLeaseServiceNpgsql;

public sealed class RuntimeLeaseServiceNpgsql(
  ICertsEngineConfiguration config,
  ILogger<SharedRuntimeLeaseServiceNpgsql> logger
) : IRuntimeLeaseService {
  private sealed class CertsLeaseConnection(ICertsEngineConfiguration cfg) : IRuntimeLeaseConnectionStringProvider {
    public string ConnectionString => cfg.ConnectionString;
  }

  private readonly SharedRuntimeLeaseServiceNpgsql _inner = new(new CertsLeaseConnection(config), logger);

  public Task<MaksIT.Results.Result<bool>> TryAcquireAsync(string leaseName, string holderId, TimeSpan ttl, CancellationToken ct = default) =>
    _inner.TryAcquireAsync(leaseName, holderId, ttl, ct);

  public Task<MaksIT.Results.Result> ReleaseAsync(string leaseName, string holderId, CancellationToken ct = default) =>
    _inner.ReleaseAsync(leaseName, holderId, ct);
}

4) DI registration in Program.cs

builder.Services.AddSingleton<MaksIT.CertsUI.Engine.RuntimeCoordination.IRuntimeInstanceId, RuntimeInstanceIdProvider>();
builder.Services.AddSingleton<MaksIT.CertsUI.Engine.Infrastructure.IRuntimeLeaseService, RuntimeLeaseServiceNpgsql>();

Backend switching strategy

Use the same app-level interface (IRuntimeLeaseService) and swap only adapter implementation:

  • PostgreSQL: MaksIT.HAMode.PostgreSql.RuntimeLeaseServiceNpgsql
  • Redis: MaksIT.HAMode.Redis.RuntimeLeaseServiceRedis
  • etcd: MaksIT.HAMode.Etcd.RuntimeLeaseServiceEtcd

This keeps hosted services and domain workflows unchanged in both maksit-vault and maksit-certs-ui.

Local pack

dotnet pack .\src\MaksIT.HAMode.slnx -c Release

The command emits a single MaksIT.HAMode .nupkg and .snupkg.

Product Compatible and additional computed target framework versions.
.NET 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.6 53 6/28/2026
1.0.5 73 6/28/2026 1.0.5 is deprecated.
1.0.4 110 6/20/2026
1.0.3 60 6/20/2026
1.0.2 62 6/20/2026
1.0.1 56 6/20/2026
1.0.0 100 6/20/2026

See CHANGELOG.md in the package and repository releases.