Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

53 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ModelSync

ModelSync

NuGet CI License: MIT .NET Standard 2.0

ModelSync keeps database structure close to your .NET code without turning the project into an ORM. It can generate provider-specific table SQL from attributed C# models, compare models with a live database, run ordered SQL migrations, synchronize stored procedures, and leave a readable migration report behind.

Framework-owned SQL has one authority: Core. Provider packages supply structured capabilities, identifier rules, mappings, connections, and thin execution adapters. Database create/reset plans and provider-specific drop behavior are compiled in Core; provider runners execute those plans without maintaining a second SQL implementation.

It is a good fit for Dapper, ADO.NET, hand-written SQL, and services where schema changes should remain visible in code review.

ModelSync is more than a DDL generator. Its migration runner can execute reviewed table, stored procedure, trigger, seed, and custom SQL artifacts with ordering, history, hashes, locking, and transaction policies. This is a SQL-first data migration model: data transformations belong in versioned seed or CustomSql scripts rather than EF-style C# Up/Down classes.

Database relationships are represented as schema metadata, not ORM navigation properties. Primary and composite keys, unique constraints, indexes, foreign keys, defaults, and checks can be planned and compared where the selected provider supports them. ModelSync does not provide Include, change tracking, LINQ translation, or automatic object-graph persistence.

Current package version: 1.5.1

Release status: This repository is prepared for the stable 1.5.1 package contract. Read the 1.5.1 release notes and the 1.5.0 to 1.5.1 migration guide before upgrading a production migration job.

Find The Right Tool

I need to... Use
Generate or execute table DDL from one model TableGenerator
Compare attributed models with a live database ModelSynchronizer
Run ordered table, procedure, trigger, seed, and custom SQL files MigrationRunner
Synchronize procedure files directly StoredProcedureSynchronizer
Validate, preview, and run migrations from a terminal or CI modelsync CLI

Start with the full usage guide when choosing between these workflows.

Packages

Package Purpose
UmbrellaFrame.ModelSync.SqlServer SQL Server and Azure SQL
UmbrellaFrame.ModelSync.MySql MySQL and MariaDB
UmbrellaFrame.ModelSync.PostgreSQL PostgreSQL
UmbrellaFrame.ModelSync.SQLite SQLite
UmbrellaFrame.ModelSync.Oracle Oracle preview: table DDL and safe model comparison
UmbrellaFrame.ModelSync.Analyzers Compile-time model checks
UmbrellaFrame.ModelSync.Cli Migration CLI and Markdown/JSON reports

Provider packages pull UmbrellaFrame.ModelSync.Core automatically.

dotnet add package UmbrellaFrame.ModelSync.Core --version 1.5.1
dotnet add package UmbrellaFrame.ModelSync.SqlServer --version 1.5.1
dotnet add package UmbrellaFrame.ModelSync.MySql --version 1.5.1
dotnet add package UmbrellaFrame.ModelSync.PostgreSQL --version 1.5.1
dotnet add package UmbrellaFrame.ModelSync.SQLite --version 1.5.1
dotnet add package UmbrellaFrame.ModelSync.Oracle --version 1.5.1
dotnet add package UmbrellaFrame.ModelSync.Analyzers --version 1.5.1

Oracle remains a preview provider. Its migration runner, stored procedure synchronization, reset, and native lock features are not production-ready yet. See the provider support matrix before adopting it.

Oracle note: the preview package is published for evaluation, but its support surface is intentionally smaller than the four stable providers.

The modelsync CLI currently runs migrations for SQL Server, MySQL, MariaDB, PostgreSQL, and SQLite. Oracle is not exposed through the CLI until its migration runner reaches production-ready status.

Five-Minute Table Example

using UmbrellaFrame.ModelSync.Core;
using UmbrellaFrame.ModelSync.SqlServer;

[SqlServerTableName("Products")]
public sealed class Product
{
    [SqlServerColumnType(SqlServerColumnType.INT)]
    [SqlServerColumnPrimaryKey(isAutoIncrement: true)]
    public int Id { get; set; }

    [SqlServerColumnType(SqlServerColumnType.NVARCHAR, "200")]
    [SqlServerColumnNotNull]
    [SqlServerColumnIndex("IX_Products_Name")]
    public string Name { get; set; } = string.Empty;

    [SqlServerColumnType(SqlServerColumnType.DECIMAL, "18,2")]
    [SqlServerColumnDefault("0")]
    [SqlServerColumnCheck("Price >= 0")]
    public decimal Price { get; set; }
}

var generator = new SqlServerTableGenerator(connectionString);
var sql = generator.GenerateSqlServerTable<Product>(ifNotExists: true);

Console.WriteLine(sql);              // review before execution
await generator.CreateTablesAsync();

Generate...Table<T>() generates and caches SQL. CreateTablesAsync() executes the cached statements. Index SQL can be reviewed separately with GenerateIndexSql<T>().

Safe Schema Changes

Additive operations are explicit:

await generator.AddColumnAsync<Product>("Stock");

Operations that may lose data require visible approval:

var allow = DestructiveOperationOptions.Allow();

generator.DropColumn<Product>("LegacyCode", allow);
generator.AlterColumnType<Product>("Price", allow);
await generator.DropTablesAsync(allow);

ModelSync never treats this approval as permission for arbitrary automatic destructive model changes. Live synchronization still reports drop, rename, narrowing, and risky nullability changes instead of silently applying them.

Live Model Synchronization

Use a model synchronizer when the database already exists and you want a reviewable diff:

var options = new SqlServerModelSyncOptions
{
    ConnectionString = connectionString,
    DefaultSchema = "app",
    HistorySchema = "sec",
    ReportUnmappedTables = false
};

var result = await SqlServerModelSynchronizer
    .FromAssemblies(options, typeof(Product).Assembly)
    .CompareAsync(cancellationToken);

// Inspect AutomaticOperations, ManualOperations, SkippedOperations,
// and BlockedOperations before applying anything.
await result.ApplyAsync(cancellationToken);

For SQL Server, MySQL/MariaDB, PostgreSQL, and SQLite, model-sync mutation now uses the same migration lifecycle as registered scripts: deterministic ordering, provider locking, transaction policy, history/hash tracking, and structured execution results. CompareAsync() remains read-only.

options.TransactionPolicy = MigrationTransactionPolicy.Auto;
options.LockOptions.Enabled = true;

var execution = await result.ApplyWithResultAsync(cancellationToken);

Review execution.Items before treating a deployment as complete. Oracle preview still uses its limited direct execution path because its migration runner and native lock are not production-ready.

Table policies can mix ownership in the same run:

options.DefaultTableMode = ModelSyncTableMode.ManualOnly;
options.TablePolicies
    .ForType<AuditLog>(ModelSyncTableMode.ApplySafeChanges)
    .ForTable("legacy", "OldOrders", ModelSyncTableMode.Ignore);

ManualOnly changes are reported but never executed. ApplySafeChanges never authorizes destructive changes.

For CI or pull-request review, render the comparison without executing it:

var markdown = ModelSyncPlanMarkdownReport.Create(result);
var json = ModelSyncPlanJsonReport.Create(result);

Changed default and check expressions are reported as review-required drift and are never silently replaced. Report SQL is bounded and literal values are redacted.

Schema evidence and advanced metadata

The 1.5 package line also closes several correctness and reporting defects:

Area Previous behavior Corrected behavior
Check introspection A discovered table check could be copied to unrelated columns Only the deterministic CK_<Table>_<Column> constraint is attached to its matching column
Check convergence CREATE TABLE emitted an unnamed inline check, so a second comparison could plan it again Generated checks are named consistently; SQLite also reads them from the stored table definition
Drift evidence A model snapshot could be compared with a live snapshot even though they represent different evidence verify-drift rejects incompatible model-to-live comparisons
Report safety Plan reasons and fallback exception messages could retain SQL literal or secret-like text JSON, Markdown, SQL previews, reasons, and fallback errors use centralized bounded redaction
Provider normalization Catalog formatting differences could appear as schema changes Supported quote, parenthesis, decimal, cast, referential-action, and identity variations are normalized before comparison
Analyzer identity Look-alike user attribute names could satisfy mapping rules Rules resolve real ModelSync symbols and base types across every provider
CLI option safety Unknown, duplicate, or conflicting options could be accepted silently Invalid option sets fail before comparison or execution; dry-run SQL is bounded and redacted
SQLite check lookup A longer constraint name could match a shorter deterministic-name prefix Only an exact quoted constraint name is accepted

See the 1.5.0 release notes for the underlying root causes, provider coverage, and validation evidence.

Canonical snapshot contracts are available for tooling through SchemaSnapshotFactory; snapshots are evidence, not automatic rollback instructions.

Create deterministic model and live-database evidence from the same synchronizer:

var synchronizer = SqlServerModelSynchronizer
    .FromAssemblies(options, typeof(Product).Assembly);

var modelSnapshot = synchronizer.CreateModelSnapshot(sourceCommit: gitCommit);
var liveSnapshot = await synchronizer.CreateDatabaseSnapshotAsync(
    asBaseline: true,
    sourceCommit: gitCommit,
    cancellationToken);

var json = SchemaSnapshotSerializer.CreateJson(liveSnapshot);

Snapshot creation is read-only. asBaseline labels the evidence; it does not authorize rollback or database mutation.

Use verify-drift with an approved live-database baseline and a later live-database snapshot from the same provider and scope. Model and live snapshots serve different review purposes; the CLI rejects model-to-live hash comparisons instead of returning a misleading drift result. ModelSync-created check constraints use deterministic CK_<Table>_<Column> names so supported providers, including SQLite, converge on the next comparison. A differently named custom check remains review-required because ModelSync cannot safely infer that arbitrary expressions are semantically equivalent.

When a property was deliberately renamed, declare its previous database name explicitly:

[DbPreviousColumnName("OldCode")]
public string Code { get; set; } = string.Empty;

ModelSync reports a single review-only RenameColumn operation when OldCode exists and Code does not. It never guesses renames from similar names and never executes a rename automatically. Ambiguous hints are reported as unsupported.

Composite indexes and foreign keys can be described at table level:

[DbTableIndex(
    "IX_Orders_Tenant_Code",
    "TenantId", "Code",
    IsUnique = true,
    IncludedColumns = new[] { "DisplayName" },
    Filter = "Code IS NOT NULL",
    SortDirections = new[] { DbIndexSortDirection.Ascending, DbIndexSortDirection.Descending })]
[DbTableForeignKey(
    "FK_Orders_Tenants",
    typeof(Tenant),
    new[] { "TenantId", "Region" },
    new[] { "Id", "Region" },
    OnDelete = DbReferentialAction.Cascade)]
public sealed class OrderSchema
{
    // Provider-specific column attributes remain unchanged.
}

These declarations feed the Model Synchronizer canonical plan. Provider capabilities decide whether SQL can be generated; unsupported include, filter, sort, foreign-key, or referential-action combinations are blocked rather than silently simplified. Filter expressions are reviewed SQL and must never contain untrusted input.

Ordered SQL Migrations

var runner = new SqlServerMigrationRunner(connectionString);

runner.RegisterScriptFile("Database/Scripts/Tables/001_CreateProducts.sql");
runner.RegisterScriptFile("Database/Scripts/StoredProcedures/010_GetProducts.sql");
runner.RegisterScriptFile("Database/Scripts/Triggers/020_ProductAudit.sql");
runner.RegisterScriptFile("Database/Scripts/Seeds/030_DefaultProducts.sql");
runner.RegisterScriptFile("Database/Scripts/CustomSql/999_AfterSetup.sql");

var plan = await runner.CompareRegisteredAsync(cancellationToken); // read-only
var result = await runner.RunWithResultAsync(cancellationToken);

Execution order is Tables -> StoredProcedures -> Triggers -> Seeds -> CustomSql. History tables and SQL hashes make repeated runs predictable. Application-supplied SQL remains trusted project content; ModelSync does not attempt to prove arbitrary SQL safe.

CLI: Preview First, Apply Deliberately

dotnet tool install --global UmbrellaFrame.ModelSync.Cli --version 1.5.1

Keep connection strings out of process arguments:

export MODELSYNC_CONNECTION_STRING='Data Source=modelsync-preview.db'

modelsync validate --scripts ./Database/Scripts

modelsync run \
  --provider sqlite \
  --connection-env MODELSYNC_CONNECTION_STRING \
  --scripts ./Database/Scripts \
  --dry-run

modelsync run \
  --provider sqlite \
  --connection-env MODELSYNC_CONNECTION_STRING \
  --scripts ./Database/Scripts \
  --apply \
  --report-md ./artifacts/modelsync-report.md \
  --report-json ./artifacts/modelsync-report.json

--apply is required for mutation. --connection remains available for compatibility but can expose secrets in process listings. Ctrl+C is propagated to migration operations.

Controlled Database Reset

Reset is intentionally difficult to trigger accidentally:

var options = new MigrationRunnerOptions
{
    ResetDatabase = true,
    ResetOptions = new DatabaseResetOptions
    {
        Enabled = true,
        Approval = DestructiveOperationOptions.Allow(),
        ExpectedDatabaseName = "AppDb",
        EnvironmentName = "Development",
        AllowedEnvironments = new[] { "Development" },
        BackupBeforeReset = true,             // SQL Server only
        BackupDirectory = @"C:\SqlBackups"
    }
};

System databases are rejected. SQL Server reset runs before the native migration lock is acquired, then readiness, infrastructure, history, and scripts continue under the normal lock. Backup paths are evaluated from the SQL Server service account's filesystem.

MySQL/MariaDB and PostgreSQL use the same approval and expected-database checks. PostgreSQL executes session termination, DROP DATABASE, and CREATE DATABASE as separate administrative commands because these operations cannot share one command pipeline. Live integration tests reset each supported engine twice and verify that tables, seeds, history, readiness, and locking converge after recreation.

ResetDatabase = true requires ResetOptions.Enabled = true, explicit reset approval, and ExpectedDatabaseName. Legacy DestructiveOptions.Allow() alone is intentionally rejected for database reset. Failed migration reports include bounded diagnostics and redact SQL string literal values from failed-batch previews.

Use database reset from a single deployment job or local development runner. Do not let multiple application instances start with ResetDatabase = true; the target database can be dropped before the provider-native target lock exists.

Provider Support

Feature SQL Server MySQL/MariaDB PostgreSQL SQLite Oracle preview
Table and index DDL Yes Yes Yes Yes Yes
Safe live model sync Yes Yes Yes Yes Partial
Ordered migration runner Yes Yes Yes Yes No
Stored procedures Yes Yes Yes No No
Native migration lock Yes Yes Yes SQLite write lock No
Controlled DB reset Yes Yes Yes Limited No

The detailed and tested limitations live in the provider support matrix.

Important Boundaries

  • ModelSync is not an ORM and does not provide LINQ, entity tracking, or runtime CRUD.
  • Provider-specific raw default/check attributes and migration scripts contain reviewed SQL. Never build them from untrusted user input. The legacy Core attributes remain available only for 1.x source compatibility; new models should use provider-specific attributes.
  • Prefer a deployment-time migration job. If migrations run during application startup, keep provider-native locking enabled.
  • Compare APIs are read-only. Infrastructure is created only by explicit mutation APIs.
  • Published NuGet versions are immutable. Older packages are not overwritten or unlisted to hide migration work.

Documentation

Topic Document
Start here Documentation index
Complete usage Full Usage Guide
Provider details Provider guides · Support matrix
Migration runner Migration runner
Live synchronization Model Synchronizer
CLI and reports CLI guide · Reporting
CLI and scaffolder direction Roadmap
Maturity roadmap General recommendation roadmap
Current release 1.5.1 notes · Migration guide

Versioning, Release Notes and Migration Guides

Start with the release notes before upgrading. Behavior-sensitive changes are paired with a focused migration guide, while the complete history remains in CHANGELOG.md.

Development continues on main, but stable NuGet releases are intentionally grouped into meaningful 3-6 week cycles. Feature work goes through preview and RC channels; migration-sensitive minor releases normally remain in RC for at least seven days. Critical security, data-loss, and migration-correctness fixes may ship sooner as patches. Documentation-only changes do not create a package version. See the versioning and compatibility policy.

Build And Test

dotnet restore ModelSync.sln
dotnet build ModelSync.sln -c Release --no-restore
dotnet test ModelSync.sln -c Release --no-build --filter "Category!=Integration"
dotnet run --project tools/UmbrellaFrame.ModelSync.RepositoryChecks -- verify-all

Live provider tests use compose.integration.yml and explicit local-only credentials. Start that environment with docker compose -f compose.integration.yml up -d; do not reuse its credentials outside tests.

The opt-in UmbrellaFrame.ModelSync.ScaleTest suite creates one million rows on SQL Server, MySQL, MariaDB, PostgreSQL, SQLite, and Oracle. It measures live schema comparison, safe column and index additions, idempotent re-runs, destructive-change blocking, and migration history where the provider supports it. The regular provider integration suite remains the authority for reset, lock, routine, trigger, seed, constraint, and foreign-key behavior. Run the scale suite locally after starting Docker:

$env:MODELSYNC_RUN_SCALE_INTEGRATION = "1"
dotnet test UmbrellaFrame.ModelSync.ScaleTest/UmbrellaFrame.ModelSync.ScaleTest.csproj -c Release --filter "Category=Scale"

The same matrix runs weekly and on demand through the Million Row Scale Tests GitHub Actions workflow. Scale timings are environment evidence, not a production latency guarantee.

About

Provider-aware schema and SQL migration toolkit for .NET. Generate DDL from attributed C# models, compare live schemas, run ordered migrations, and synchronize routines where supported. Production providers: SQL Server, MySQL/MariaDB, PostgreSQL, SQLite; Oracle is preview.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages