Indiko.Hosting.Abstractions
2.6.1
dotnet add package Indiko.Hosting.Abstractions --version 2.6.1
NuGet\Install-Package Indiko.Hosting.Abstractions -Version 2.6.1
<PackageReference Include="Indiko.Hosting.Abstractions" Version="2.6.1" />
<PackageVersion Include="Indiko.Hosting.Abstractions" Version="2.6.1" />
<PackageReference Include="Indiko.Hosting.Abstractions" />
paket add Indiko.Hosting.Abstractions --version 2.6.1
#r "nuget: Indiko.Hosting.Abstractions, 2.6.1"
#:package Indiko.Hosting.Abstractions@2.6.1
#addin nuget:?package=Indiko.Hosting.Abstractions&version=2.6.1
#tool nuget:?package=Indiko.Hosting.Abstractions&version=2.6.1
Indiko.Hosting.Abstractions
Core hosting abstractions for the Building Blocks framework. This package provides the foundation that every Indiko-based application is built on: the IBlock lifecycle contract, BlockBase<TOptions> base class, BaseHostBootstraper<T, TBaseStartup> orchestration, and the supporting utilities that operate before the DI container is built.
Table of Contents
- Features
- Installation
- Quick Start
- Block Lifecycle
- BlockBase<TOptions>
- Flexible Host Builder Configuration
- HostStartupOptions
- BlockLoadOrder Attribute
- Pre-DI Utilities
- AssemblyLoader and BlockManager
- Best Practices
- Related Packages
- Target Framework
- License
Features
| Feature | Description |
|---|---|
IBlock lifecycle interface |
Five-phase contract covering pre-DI initialization, host builder configuration, service registration, middleware pipeline, and pre-run async hooks |
BlockBase<TOptions> |
Abstract base class that wires options binding, validation, and override hooks out of the box |
Typed ConfigureBuilder overload |
Override ConfigureBuilder(IHostBuilder, TOptions) to receive already-resolved options without manual binding boilerplate |
| Fluent host builder actions | Register Action<IHostBuilder> lambdas on the bootstrapper with .ConfigureHostBuilder(b => ...) — no subclassing required |
Virtual ConfigureHostBuilder |
Subclass the bootstrapper and override ConfigureHostBuilder(IHostBuilder) for access to Configuration before the DI container is built |
HostStartupOptions |
Base options class for startup classes; drives EnableForwardedHeaderOptions and ForceHttps |
[BlockLoadOrder(int)] |
Attribute that controls the order in which blocks are discovered and invoked |
BlockManager |
Static orchestrator that discovers, instantiates, and invokes all IBlock implementations in order |
AssemblyLoader |
Scans loaded assemblies and the dependency graph for concrete IBlock types at startup |
ConfigurationUtil |
Pre-DI helper that delegates to a configuration block or falls back to the standard appsettings.json / environment variable chain |
LogUtil |
Pre-DI helper that delegates to a logging block or falls back to a simple console logger |
HostBuilderExtension.UseStartup<T> |
Extension method that wires a startup class into IHostBuilder.ConfigureServices using expression-compiled constructor injection |
IRequestMetadataService |
Contract for services that surface per-request tenant and user context |
Installation
dotnet add package Indiko.Hosting.Abstractions
Quick Start
1. Write a block
using Indiko.Blocks.Common.Abstractions;
using Indiko.Blocks.Common.Abstractions.Attributes;
using Indiko.Blocks.Common.Abstractions.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
// Options class — section name in appsettings.json must match the class name: "CacheBlock"
public class CacheBlockOptions : BlockOptions
{
public string ConnectionString { get; set; } = "localhost:6379";
public int DefaultExpirySeconds { get; set; } = 300;
}
[BlockLoadOrder(10)]
public sealed class CacheBlock : BlockBase<CacheBlockOptions>
{
public CacheBlock(IConfiguration configuration, ILogger logger)
: base(configuration, logger) { }
public override void ConfigureServices(IServiceCollection services)
{
base.ConfigureServices(services); // binds CacheBlockOptions into DI
services.AddStackExchangeRedisCache(o =>
o.Configuration = Options.ConnectionString);
}
}
2. Write a startup class
using Indiko.Hosting.Abstractions;
using Indiko.Hosting.Abstractions.Interfaces;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class MyStartup : HostingStartup
{
public MyStartup(IHostEnvironment environment, IConfiguration configuration)
: base(environment, configuration) { }
public override void ConfigureServices(IServiceCollection services)
{
base.ConfigureServices(services); // invokes all blocks' ConfigureServices
services.AddSingleton<IMyService, MyService>();
}
}
3. Run it
// Program.cs
using Indiko.Hosting.Abstractions;
class Program
{
static async Task<int> Main(string[] args)
=> await MyBootstrapper.Instance.RunAsync<MyStartup>(args);
}
public sealed class MyBootstrapper : BaseHostBootstraper<MyBootstrapper, IHostingStartup>
{
// Nothing else needed for the simplest case.
}
4. Add appsettings.json
{
"ServiceName": "my-service",
"CacheBlock": {
"Enabled": true,
"ConnectionString": "redis-host:6379",
"DefaultExpirySeconds": 600
},
"HostStartupOptions": {
"EnableForwardedHeaderOptions": true,
"ForceHttps": false
}
}
Block Lifecycle
Every IBlock implementation participates in five ordered phases. BlockManager drives all phases; blocks are sorted by [BlockLoadOrder] before any phase begins.
Application start
│
▼
1. Initialize<TType>(object context)
─ Pre-DI. ConfigurationUtil uses IConfigurationBlock here.
─ LogUtil uses ILoggingBlock here.
─ Return value is the result type (e.g. IConfiguration, ILoggerFactory).
│
▼
2. ConfigureBuilder(IHostBuilder)
─ Pre-DI. Modify the IHostBuilder before it is built.
─ Kestrel, Windows Service, custom DI containers go here.
│
▼
IHost is built — DI container is now available
│
▼
3. ConfigureServices(IServiceCollection)
─ Register services, options, and middleware components.
─ BlockBase binds TOptions into the DI options pipeline here.
│
▼
4. Configure(IApplicationBuilder, IServiceProvider, IHostEnvironment)
─ Configure the middleware pipeline.
─ Called from the startup class, typically driven by the hosting layer.
│
▼
5. PreRunAsync(IServiceProvider)
─ Async hook. Seed data, warm caches, run migrations here.
─ Called after the host is built, before host.RunAsync().
│
▼
Host running
Phase responsibilities at a glance
| Phase | DI available | Typical use |
|---|---|---|
Initialize |
No | Provide IConfiguration or ILoggerFactory before DI |
ConfigureBuilder |
No | Kestrel options, Windows Service, custom host configuration |
ConfigureServices |
No (registration only) | Register services, bind options, add middleware features |
Configure |
Yes (via serviceProvider) |
Build middleware pipeline, use registered services |
PreRunAsync |
Yes | Migrations, cache warm-up, health pre-checks |
BlockBase<TOptions>
BlockBase<TOptions> is the abstract base class all production blocks derive from. It handles options binding, validation, and override hooks automatically.
Constructor signature
Every concrete block must provide this constructor — BlockManager requires it:
public MyBlock(IConfiguration configuration, ILogger logger)
: base(configuration, logger) { }
Options binding
TOptions must implement IBlockOptions (which defines bool Enabled { get; set; }). The base class:
- Calls
GetModuleOptionsConfigSection()— returnsconfiguration.GetSection(GetType().Name)by default. - Calls
config.Get<TOptions>()to bind the section. - Calls
OptionsConfigure(options)so you can apply in-code adjustments. - Calls
OptionsPostConfigure(options)which executes any storedOverrideModuleOptionsaction.
Options are refreshed in the constructor, in ConfigureBuilder, and in ConfigureServices.
Defining options
public class EmailBlockOptions : BlockOptions
{
[Required]
public string SmtpHost { get; set; }
[Range(1, 65535)]
public int SmtpPort { get; set; } = 587;
public bool UseSsl { get; set; } = true;
}
{
"EmailBlock": {
"Enabled": true,
"SmtpHost": "smtp.example.com",
"SmtpPort": 587,
"UseSsl": true
}
}
OptionsConfigure — in-code defaults and adjustments
Override OptionsConfigure to apply logic that cannot live in JSON, for example deriving values or enforcing invariants:
protected override void OptionsConfigure(EmailBlockOptions options)
{
if (string.IsNullOrWhiteSpace(options.SmtpHost))
options.SmtpHost = "fallback-smtp.internal";
// Enforce SSL in production regardless of config file
if (Environment?.EnvironmentName == "Production")
options.UseSsl = true;
}
OverrideModuleOptions — caller-side overrides
A caller can store an override action before the block runs. This is applied last, after OptionsConfigure, so it wins:
var block = new EmailBlock(configuration, logger);
block.OverrideModuleOptions(o => o.SmtpHost = "test-smtp.internal");
This is primarily used by test harnesses or integration scaffolding that needs to redirect block configuration without touching appsettings files.
GetModuleOptionsConfigSection — custom section name
By default the section name equals the class name. Override to change it:
protected override IConfiguration GetModuleOptionsConfigSection()
=> Configuration.GetSection("Email"); // reads "Email" instead of "EmailBlock"
Data Annotations validation
BlockBase.ConfigureServices calls optionsBuilder.ValidateDataAnnotations(). Any [Required], [Range], [MinLength], etc. attributes on TOptions are enforced automatically when the options are first resolved from DI.
Protected members available in blocks
| Member | Type | Description |
|---|---|---|
Configuration |
IConfiguration |
Full application configuration |
Options |
TOptions |
Current options instance |
Logger |
ILogger |
Logger created before DI via LogUtil |
ServiceName |
string |
Value of "ServiceName" from configuration |
Environment |
IHostEnvironment |
Startup environment populated from env vars |
Flexible Host Builder Configuration
Three independent layers let you configure the IHostBuilder without forcing everything into a single override point.
Layer 1 — Typed ConfigureBuilder overload (inside a block)
Override the typed overload instead of the plain one to receive options that are already resolved, including any OverrideModuleOptions actions:
public sealed class KestrelBlock : BlockBase<KestrelBlockOptions>
{
public KestrelBlock(IConfiguration configuration, ILogger logger)
: base(configuration, logger) { }
// Typed overload — options are already bound and post-configured
protected override void ConfigureBuilder(IHostBuilder hostBuilder, KestrelBlockOptions options)
{
hostBuilder.ConfigureWebHost(web =>
web.ConfigureKestrel(kestrel =>
{
kestrel.ListenAnyIP(options.HttpPort);
if (options.HttpsPort > 0)
kestrel.ListenAnyIP(options.HttpsPort, l => l.UseHttps());
}));
}
}
The base ConfigureBuilder(IHostBuilder) implementation refreshes options then delegates to this overload, so you never need to call both.
Layer 2 — BaseHostBootstraper fluent actions and virtual override
Fluent — no subclassing needed
Chain .ConfigureHostBuilder(...) calls directly on the bootstrapper instance. All actions are applied after all block ConfigureBuilder calls and after the virtual override:
// Program.cs
await new MyBootstrapper()
.ConfigureHostBuilder(b => b.UseWindowsService())
.ConfigureHostBuilder(b => b.ConfigureWebHost(web =>
web.ConfigureKestrel(o => o.ListenAnyIP(8080))))
.RunAsync<MyStartup>(args);
Multiple calls accumulate in order:
var bootstrapper = new MyBootstrapper();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
bootstrapper.ConfigureHostBuilder(b => b.UseWindowsService());
else
bootstrapper.ConfigureHostBuilder(b => b.UseSystemd());
bootstrapper.ConfigureHostBuilder(b => b.UseDefaultServiceProvider(o =>
o.ValidateScopes = true));
return await bootstrapper.RunAsync<MyStartup>(args);
Virtual override — subclass with Configuration access
When the host builder configuration depends on values read from IConfiguration, override ConfigureHostBuilder in a subclass. The Configuration property is available before the DI container is built:
public sealed class MyBootstrapper : BaseHostBootstraper<MyBootstrapper, IHostingStartup>
{
protected override void ConfigureHostBuilder(IHostBuilder builder)
{
var port = Configuration.GetValue<int>("Kestrel:Port", 5000);
var useHttps = Configuration.GetValue<bool>("Kestrel:UseHttps", false);
builder.ConfigureWebHost(web =>
web.ConfigureKestrel(kestrel =>
{
kestrel.ListenAnyIP(port);
if (useHttps)
kestrel.ListenAnyIP(port + 1, l => l.UseHttps());
}));
}
}
Execution order within CreateHostBuilder
1. BlockManager.ConfigureBlock<IBlock>(x => x.ConfigureBuilder(builder), ...)
(all blocks, sorted by [BlockLoadOrder])
2. ConfigureHostBuilder(builder) ← virtual override
3. foreach action in _hostBuilderActions ← fluent .ConfigureHostBuilder(...)
Layer 3 — HostStartupOptions (startup class flags as virtual properties)
See HostStartupOptions below. Flags that were previously abstract bool properties are now backed by options loaded from appsettings, while still supporting the traditional override pattern.
HostStartupOptions
HostStartupOptions is the base options class for startup behavior. It lives in the "HostStartupOptions" section of appsettings.json.
public class HostStartupOptions
{
public bool EnableForwardedHeaderOptions { get; set; }
public bool ForceHttps { get; set; }
}
Configuring from appsettings.json
{
"HostStartupOptions": {
"EnableForwardedHeaderOptions": true,
"ForceHttps": false
}
}
Traditional override pattern (still supported)
Concrete startup classes in higher-level packages (e.g., Indiko.Hosting.Web) expose these as virtual properties. Existing subclasses that use override continue to work unchanged:
public class MyStartup : WebStartup
{
public MyStartup(IConfiguration config, IWebHostEnvironment env)
: base(config, env) { }
protected override bool EnableForwardedHeaderOptions => true;
protected override bool ForceHttps => true;
}
Options-driven pattern (new)
When you do not override the properties, the base class reads them from HostStartupOptions bound from appsettings. This lets you control startup behavior purely from configuration without code changes:
{
"HostStartupOptions": {
"EnableForwardedHeaderOptions": true,
"ForceHttps": true
}
}
Extending HostStartupOptions
Derive a richer options class for your own startup hierarchy:
public class MyStartupOptions : HostStartupOptions
{
public string[] AllowedOrigins { get; set; } = [];
public bool EnableSwagger { get; set; }
}
BlockLoadOrder Attribute
[BlockLoadOrder(int order)] is applied to IBlock implementations to control the order in which BlockManager invokes them across all lifecycle phases.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class BlockLoadOrderAttribute : Attribute
{
public int Order { get; }
public BlockLoadOrderAttribute(int order) => Order = order;
}
Ordering rules
- Blocks without the attribute receive
int.MaxValueand run last. - Lower numbers run earlier.
- Blocks with equal order values run in undefined relative order.
- Ordering is applied once at startup and applies to every lifecycle phase.
Common order conventions
| Order | Block type | Reason |
|---|---|---|
-1 |
Configuration blocks (e.g., AppSettingsConfigurationBlock) |
Must run first — all other blocks need IConfiguration |
0 or omitted |
Most feature blocks | Default; no ordering dependency |
1 |
Logging blocks (e.g., SerilogBlock) |
After configuration, before everything else |
10+ |
Application feature blocks | After infrastructure is established |
100+ |
Blocks with runtime dependencies on earlier blocks | Explicit late ordering |
Examples
// Configuration block — must run before all others
[BlockLoadOrder(-1)]
public sealed class AppSettingsConfigurationBlock : BlockBase, IConfigurationBlock
{
public AppSettingsConfigurationBlock(IConfiguration cfg, ILogger log)
: base(cfg, log) { }
public override TType Initialize<TType>(object context = default)
{
// Returns IConfiguration instance
return new AppsettingsConfigurationBuilder().Build() as TType;
}
}
// Logging block — after configuration
[BlockLoadOrder(1)]
public sealed class SerilogBlock : BlockBase<SerilogBlockOptions>
{
public SerilogBlock(IConfiguration cfg, ILogger log) : base(cfg, log) { }
public override void ConfigureBuilder(IHostBuilder hostBuilder, SerilogBlockOptions options)
{
hostBuilder.UseSerilog((ctx, cfg) => cfg.ReadFrom.Configuration(ctx.Configuration));
}
}
// Feature block — standard order
[BlockLoadOrder(10)]
public sealed class CacheBlock : BlockBase<CacheBlockOptions>
{
public CacheBlock(IConfiguration cfg, ILogger log) : base(cfg, log) { }
// ...
}
// No attribute — runs after all explicitly ordered blocks
public sealed class SwaggerBlock : BlockBase<SwaggerBlockOptions>
{
public SwaggerBlock(IConfiguration cfg, ILogger log) : base(cfg, log) { }
// ...
}
Pre-DI Utilities
These utilities operate before the DI container is built and are used internally by BaseHostBootstraper and HostingStartup. They can also be used directly in bootstrapping code.
ConfigurationUtil
Builds an IConfiguration instance. It first checks whether a block implementing IConfigurationBlock exists and delegates to it. If no configuration block is found, it falls back to the standard chain:
appsettings.json
appsettings.{DOTNET_ENVIRONMENT}.json (or ASPNETCORE_ENVIRONMENT)
appsettings.{MachineName}.json
Environment variables
Command-line arguments
// Used internally by BaseHostBootstraper.Configuration property:
IConfiguration configuration = ConfigurationUtil.CreateConfiguration();
LogUtil
Creates an ILogger<TTarget>. Delegates to an ILoggingBlock if one is registered. Falls back to a simple console logger (Debug level in DEBUG builds, Information in Release):
ILogger<MyBootstrapper> logger = LogUtil.CreateLogger<MyBootstrapper>(configuration);
AssemblyLoader and BlockManager
AssemblyLoader (internal)
Scans AppDomain.CurrentDomain.GetAssemblies() and the DependencyContext graph to locate all concrete, non-abstract types that implement IBlock. Results are cached after the first scan. Ordering by [BlockLoadOrder] is applied at scan time.
Custom assembly filters can be registered on BlockManager.ModuleCompatibilityCheck to restrict which assemblies are scanned:
// Only scan assemblies whose names start with "MyCompany."
BlockManager.ModuleCompatibilityCheck.Add(
t => t.Assembly?.GetName().Name?.StartsWith("MyCompany.") == true);
BlockManager
Static class that drives the lifecycle. Two overloads:
ConfigureBlock<TModule>(Action<IBlock>, IConfiguration, ILogger) — invokes an action on every discovered IBlock. Used for ConfigureBuilder, ConfigureServices, and PreRunAsync phases.
ConfigureBlock<TModule, TResult>(Func<TModule, TResult>, IConfiguration, ILogger) — invokes a function on the first IBlock that satisfies TModule and returns its result. Used for Initialize (e.g., to retrieve IConfiguration from a configuration block).
Block instances are created fresh for each invocation via expression-compiled constructors, which avoids reflection overhead in the hot path.
Best Practices
One block per concern
Each block should own exactly one infrastructure concern: caching, storage, messaging, authentication. Blocks that do too much become hard to test and reorder.
Always call base.ConfigureServices
BlockBase.ConfigureServices binds and validates TOptions. Skipping the call means your options are never registered into the DI options pipeline:
public override void ConfigureServices(IServiceCollection services)
{
base.ConfigureServices(services); // must come first
services.AddSingleton<IMyService, MyService>();
}
Use [BlockLoadOrder] intentionally
Only apply the attribute when your block has a genuine dependency on another block's output. Assigning arbitrary low numbers creates hidden ordering contracts. The general guidance:
- Configuration blocks:
-1 - Logging blocks:
1 - Everything else: omit the attribute unless you have a specific reason
Prefer the typed ConfigureBuilder overload
Overriding ConfigureBuilder(IHostBuilder, TOptions) is safer than overriding ConfigureBuilder(IHostBuilder) directly, because options are already refreshed and post-configured before the typed overload is called.
Keep PreRunAsync idempotent
The host may restart in some environments. Operations in PreRunAsync (migrations, seeding) should be safe to run more than once.
Validate options eagerly
Add [Required] and [Range] attributes to your options class. BlockBase already calls ValidateDataAnnotations(). To additionally validate at startup rather than on first use, call ValidateOnStart() explicitly in ConfigureServices:
public override void ConfigureServices(IServiceCollection services)
{
base.ConfigureServices(services);
services.AddOptions<MyBlockOptions>()
.ValidateOnStart();
}
Use OverrideModuleOptions only in tests
OverrideModuleOptions bypasses appsettings entirely. In production code, prefer environment-specific appsettings files. Reserve OverrideModuleOptions for integration test fixtures:
// In an integration test
var block = new EmailBlock(testConfiguration, NullLogger.Instance);
block.OverrideModuleOptions(o =>
{
o.SmtpHost = "localhost";
o.SmtpPort = 2525;
});
Do not use Instance singleton when you need fluent actions
BaseBootstrapper.Instance returns a lazy singleton. Fluent .ConfigureHostBuilder(...) calls mutate the instance state. For testability or when you need independent bootstrapper instances per test, use new MyBootstrapper() directly:
// Good for tests — fresh instance, no shared state
await new MyBootstrapper()
.ConfigureHostBuilder(b => b.ConfigureAppConfiguration(
cfg => cfg.AddInMemoryCollection(testSettings)))
.RunAsync<MyStartup>(args);
Related Packages
| Package | Role |
|---|---|
Indiko.Hosting.Web |
Web API hosting built on this package |
Indiko.Hosting.Mvc |
MVC hosting built on this package |
Indiko.Hosting.BlazorServer |
Blazor Server hosting built on this package |
Indiko.Hosting.Gateway |
API Gateway hosting built on this package |
Indiko.Blocks.Common.Abstractions |
IBlock, BlockBase<TOptions>, [BlockLoadOrder] |
Indiko.Blocks.Common.Management |
BlockManager, AssemblyLoader |
Indiko.Blocks.Configuration.AppSettings |
AppSettings configuration block ([BlockLoadOrder(-1)]) |
Indiko.Blocks.Configuration.Consul |
Consul configuration block |
Indiko.Common.Runtime.Abstractions |
BaseBootstrapper, IBaseStartup |
Target Framework
.NET 10
License
MIT — see LICENSE in the repository root.
| Product | Versions 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. |
-
net10.0
- Indiko.Blocks.Common.Management (>= 2.6.1)
- Indiko.Blocks.Configuration.Abstractions (>= 2.6.1)
- Indiko.Blocks.Logging.Abstractions (>= 2.6.1)
- Indiko.Common.Runtime.Abstractions (>= 2.6.1)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.6)
- Microsoft.Extensions.DependencyModel (>= 10.0.6)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.6)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.6)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on Indiko.Hosting.Abstractions:
| Package | Downloads |
|---|---|
|
Indiko.Hosting.BlazorServer
Building Blocks Hosting Blazor Server |
|
|
Indiko.Hosting.Web
Building Blocks Hosting Web |
|
|
Indiko.Hosting.Gateway
Building Blocks Hosting Gateway |
|
|
Indiko.Hosting.Mvc
Building Blocks Hosting MVC |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.6.1 | 36 | 4/18/2026 |
| 2.6.0 | 39 | 4/17/2026 |
| 2.5.1 | 106 | 4/14/2026 |
| 2.5.0 | 132 | 3/30/2026 |
| 2.2.18 | 145 | 3/8/2026 |
| 2.2.17 | 111 | 3/8/2026 |
| 2.2.16 | 125 | 3/8/2026 |
| 2.2.15 | 116 | 3/7/2026 |
| 2.2.13 | 111 | 3/7/2026 |
| 2.2.12 | 116 | 3/7/2026 |
| 2.2.10 | 116 | 3/6/2026 |
| 2.2.9 | 114 | 3/6/2026 |
| 2.2.8 | 117 | 3/6/2026 |
| 2.2.7 | 118 | 3/6/2026 |
| 2.2.5 | 117 | 3/6/2026 |
| 2.2.3 | 118 | 3/6/2026 |
| 2.2.2 | 111 | 3/6/2026 |
| 2.2.1 | 112 | 3/6/2026 |
| 2.2.0 | 115 | 3/6/2026 |
| 2.1.4 | 125 | 3/2/2026 |