Fluens.Migrations
0.9.1
dotnet add package Fluens.Migrations --version 0.9.1
NuGet\Install-Package Fluens.Migrations -Version 0.9.1
<PackageReference Include="Fluens.Migrations" Version="0.9.1" />
<PackageVersion Include="Fluens.Migrations" Version="0.9.1" />
<PackageReference Include="Fluens.Migrations" />
paket add Fluens.Migrations --version 0.9.1
#r "nuget: Fluens.Migrations, 0.9.1"
#:package Fluens.Migrations@0.9.1
#addin nuget:?package=Fluens.Migrations&version=0.9.1
#tool nuget:?package=Fluens.Migrations&version=0.9.1
Fluens.Migrations
Provider-neutral FluentMigrator base classes and helpers for inbox/outbox tables, dead letters, saga tables, HiLo sequences, and auditable/deletable columns. Zero Fluens dependencies.
Installation
dotnet add package Fluens.Migrations
# Plus exactly one provider package:
dotnet add package Fluens.Migrations.PostgreSql
# or
dotnet add package Fluens.Migrations.SqlServer
Usage
Runner registration
The provider is opted into from the AddFluens callback on FluentMigrator's own ConfigureRunner pipeline —
never through a bespoke top-level method. rb.AddPostgres() / rb.AddSqlServer() (FluentMigrator's own
provider registration) and .AddFluens(f => f.AddPostgres()) / .AddFluens(f => f.AddSqlServer()) (the
Fluens conventions opt-in) are distinct extension targets — no ambiguity:
services.AddFluentMigratorCore()
.ConfigureRunner(rb => rb
.AddPostgres() // FluentMigrator's own provider registration
.WithGlobalConnectionString(connectionString)
.ScanIn(typeof(CreateOrdersSchema).Assembly).For.Migrations()
.AddFluens(f => f.AddPostgres())); // Fluens provider-conventions opt-in
Migration classes
Every FluensMigration subclass needs a forwarding constructor — Conventions is resolved by FluentMigrator's
own constructor-injection support when the runner instantiates the migration, so a single, unchanged
subclass creates the same schema on both providers:
public class CreateOrdersSchema : FluensMigration
{
public CreateOrdersSchema(IMigrationProviderConventions conventions) : base(conventions) { }
public override void Up()
{
Create.Schema("orders");
Create.Table("orders").InSchema("orders")
.WithColumn("id").AsGuid().PrimaryKey()
.WithColumn("number").AsString(50).NotNullable()
.WithColumn("total").AsDecimal().NotNullable()
.WithAuditableAndDeletableColumns()
.WithVersionColumn();
CreateMessagingTables("orders"); // outbox + inbox + dead_letters
CreateHiLoSequence("orders_hilo", "orders");
}
public override void Down()
{
DeleteMessagingTables("orders");
DeleteHiLoSequence("orders_hilo", "orders");
Delete.Table("orders").InSchema("orders");
}
}
Helpers: WithAuditableColumns(), WithDeletableColumn(), WithAuditableAndDeletableColumns(), WithVersionColumn(),
CreateOutboxTable(), CreateOutboxDeliveryTable(), CreateInboxTable(), CreateIngressOutboxTable(), CreateDeadLettersTable(),
CreateSagaTable(), CreateMessagingTables(), CreateHiLoSequence(), DeleteOutboxTable(), DeleteOutboxDeliveryTable(),
DeleteInboxTable(), DeleteIngressOutboxTable(), DeleteDeadLettersTable(), DeleteSagaTable(), DeleteMessagingTables(),
DeleteHiLoSequence(). Upgrade helpers for already-deployed schemas: AddMintSeqColumns(),
AddDeadLetterModuleNameColumn() (see Upgrading an existing database below).
Messaging schema (ADR 20260530000001 / ADR 20260530000002 / ADR 20260729000001)
All four messaging tables carry the serialized envelope blob (non-nullable; ADR 20260530000001 — replaces the
bare context column) plus the denormalized routing columns (partition_key, priority, and on outbox
also available_at) so the partial pending indexes can sort / filter on real columns. Each table has a
partial pending index emitted via Conventions.ApplyFilter(<index>.WithOptions(), "<predicate>") — the
registered provider package's .Filter(...) extension — so the hot fetch scans only in-flight rows:
outbox— the terminal marker issent_at(renamed from the legacypublished_atflag; ADR 20260530000002). Message-TTL columnsexpires_at(non-nullable, computed from(available_at ?? created_at)at write) andexpired_at(nullable, set in place on TTL expiry — the row is never deleted, only marked). Also carriesmint_seq(bigint, non-nullable, default0— a provider-neutral monotonic mint-time key derived fromid's embedded GUIDv7 timestamp; ADR 20260729000001). Partialix_outbox_pending(WHERE sent_at IS NULL) over(sent_at, priority DESC, mint_seq).inbox— composite primary key(message_id, handler_type)(one row per subscribing handler). Also carriesmint_seq(same shape asoutbox, derived frommessage_id) and addsnext_retry_at(stage-2 retry schedule). Partialix_inbox_pending(WHERE processed_at IS NULL) over(handler_type, partition_key, priority DESC, mint_seq).ingress_outbox— primary keymessage_id(boundary dedup). Partialix_ingress_pending(WHERE processed_at IS NULL) overreceived_at.dead_letters— adds the owningmodule_name(string(100), NOT NULL, no default), the why-columnshandler_type,partition_key,failure_code,exception_type,attempt_history(Conventions.JsonColumnType—jsonbon PostgreSQL,nvarchar(max)on SQL Server), and the replay markerreplayed_at. Two module-leading indexes serve the per-module operator queries (query, replay, housekeeping):ix_dead_letters_module_failed_aton (module_nameASC,failed_atDESC) andix_dead_letters_module_replayed_aton (module_nameASC,replayed_atASC);DeleteDeadLettersTabledrops both, then the table.
dead_letters.module_name is the deliberate exception to "an added column carries a default": in a freshly
created table it is required and has no default, because every dead letter must record the module that owns it
(ADR 20261006184926). Tables created before these columns existed are brought up to date with the upgrade
helpers below.
Upgrading an existing database
Every schema change to a Fluens table ships a protected upgrade helper on FluensMigration. Call it from your
own numbered migration - Fluens never runs a migration itself and keeps no version table, so you stay the owner
of your migration history. Each helper is listed in the changelog under the release that needs it, and a host
that skips one is refused at startup by the messaging schema verification (see the Fluens.Messaging README).
AddMintSeqColumns(schema)- foroutboxandinboxtables created before 0.7.7. Adds thebigintmint_seqcolumn (non-null, default0) to<schema>.outboxand<schema>.inboxand rebuildsix_outbox_pendingandix_inbox_pending. Call it once per module schema.AddDeadLetterModuleNameColumn(moduleNamesBySchema, schema = "shared")- for adead_letterstable created before 0.8.5. Adds themodule_namecolumn, backfills every row (a row whoseoriginal_schemais a key of the map gets the mapped module name, every other row keeps itsoriginal_schema), makes the column required and createsix_dead_letters_module_failed_atandix_dead_letters_module_replayed_at.
public class UpgradeFluensSchema : FluensMigration
{
public UpgradeFluensSchema(IMigrationProviderConventions conventions) : base(conventions) { }
public override void Up()
{
AddMintSeqColumns("orders");
AddMintSeqColumns("billing");
// original_schema -> owning module name
AddDeadLetterModuleNameColumn(new Dictionary<string, string>
{
["orders"] = "orders",
["billing"] = "billing",
});
}
public override void Down() { }
}
Give the migration the next version number in your own sequence. A helper is never removed or reshaped once published, so a migration that calls it keeps working on every later release.
Saga table (ADR 20261006184927)
CreateSagaTable(tableName, schema) creates the SagaData base schema for a Fluens.Messaging.Sagas saga and
returns the table syntax, so you chain your business columns straight on. The base columns are id (Guid,
primary key), created_at (NOT NULL), completed_at (nullable) and version, whose type comes from
Conventions.SagaVersionColumnType because the column type EF Core uses for the uint concurrency token differs
per provider (bigint on both PostgreSQL and SQL Server). Three indexes back the SQL-side saga operator queries:
ix_<tableName>_active—created_at DESC, partialWHERE completed_at IS NULL(viaConventions.ApplyFilter)ix_<tableName>_created_at—created_at DESCix_<tableName>_completed_at—completed_at ASC
public override void Up()
{
CreateSagaTable("order_fulfillment", "orders")
.WithColumn("order_id").AsGuid().NotNullable()
.WithColumn("customer_email").AsString(200).NotNullable()
.WithColumn("payment_captured").AsBoolean().NotNullable();
}
public override void Down() => DeleteSagaTable("order_fulfillment", "orders");
DeleteSagaTable drops the three indexes, then the table. The indexes come from this helper only — the EF model of
a consumer's saga table declares none.
CreateOutboxDeliveryTable(schema) creates the per-destination delivery table (one row per
(outbox_id, destination_app) pair) with a unique ix_outbox_delivery_outbox_dest index on
(outbox_id, destination_app) for re-fan-out idempotency, an ix_outbox_delivery_destination_app index, and
the partial ix_delivery_pending (WHERE delivered_at IS NULL AND expired_at IS NULL) over
(expires_at, destination_app); CreateMessagingTables creates it alongside outbox/inbox in the module
schema. CreateIngressOutboxTable(schema = "shared") creates the single staging table into which a
peer-delivered batch is written atomically before acking.
Provider-specific column types
Use Fluens.Migrations.PostgreSql or Fluens.Migrations.SqlServer for provider-specific column types —
independent of which IMigrationProviderConventions is registered for the run:
using Fluens.Migrations.PostgreSql; // AsCitext(), AsTimestampTz()
Create.Table("customers").InSchema("orders")
.WithColumn("id").AsInt32().PrimaryKey().Identity()
.WithColumn("email").AsCitext().NotNullable() // case-insensitive text
.WithColumn("created_at").AsTimestampTz().NotNullable(); // timestamp with time zone
using Fluens.Migrations.SqlServer; // AsNVarCharMax(), AsDateTimeOffset2()
Create.Table("customers").InSchema("orders")
.WithColumn("id").AsInt32().PrimaryKey().Identity()
.WithColumn("notes").AsNVarCharMax().Nullable() // unbounded Unicode text
.WithColumn("created_at").AsDateTimeOffset2().NotNullable(); // timestamp with time zone
Helpers: Fluens.Migrations.PostgreSql.AsCitext() / .AsTimestampTz(); Fluens.Migrations.SqlServer.AsNVarCharMax() / .AsDateTimeOffset2().
License
This project is licensed under the MIT License.
| 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
- FluentMigrator (>= 8.0.1)
- FluentMigrator.Runner.Core (>= 8.0.1)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Fluens.Migrations:
| Package | Downloads |
|---|---|
|
Fluens.Migrations.PostgreSql
PostgreSQL provider for Fluens.Migrations — jsonb attempt_history column type, citext/timestamptz column helpers, and FluentMigrator.Postgres partial-index filters. Opt in via rb.AddFluens(f => f.AddPostgres()). |
|
|
Fluens.Migrations.SqlServer
SQL Server provider for Fluens.Migrations — nvarchar(max) attempt_history column type, datetimeoffset column helper, and FluentMigrator.SqlServer partial-index filters. Opt in via rb.AddFluens(f => f.AddSqlServer()). |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.9.1 | 33 | 10/13/2026 |
| 0.9.0 | 33 | 10/13/2026 |
| 0.8.7 | 76 | 10/12/2026 |
| 0.8.6 | 72 | 10/12/2026 |
| 0.8.5 | 104 | 10/7/2026 |
| 0.8.4 | 102 | 10/6/2026 |
| 0.8.3 | 103 | 10/6/2026 |
| 0.8.2 | 163 | 10/5/2026 |
| 0.8.1 | 221 | 9/15/2026 |
| 0.8.0 | 115 | 9/15/2026 |
| 0.7.11 | 113 | 9/15/2026 |
| 0.7.10 | 124 | 9/13/2026 |
| 0.7.9 | 120 | 9/11/2026 |
| 0.7.8 | 125 | 9/10/2026 |
| 0.7.7 | 126 | 9/10/2026 |
| 0.7.6 | 181 | 7/1/2026 |
| 0.7.5 | 153 | 6/22/2026 |
| 0.7.4 | 162 | 6/18/2026 |
| 0.7.2 | 136 | 6/18/2026 |
| 0.7.1 | 150 | 6/18/2026 |