Concepts & pipeline
This page goes over the core concepts in NSchema in more detail.
Domain model
Section titled “Domain model”NSchema works around a simple domain model of schemas, tables, columns, indexes, constraints, and views. The model is designed to be flexible enough to represent the features of any relational database, while still being simple and intuitive to work with.
These models are used to represent both the desired state (what you want) and the current state (what the database has), so they can be compared symmetrically and transformed as needed.
Because they’re just .NET objects, the schema can be constructed in any way you like. The usual source is a SQL DDL file (see Defining schemas), but you could just as easily generate it from code, or even construct it from the database itself.
Pipeline
Section titled “Pipeline”This section goes over the high-level pipeline steps that every NSchema application flows through. Most stages have an interface you can swap or extend; these are covered in more detail below and in Extension points.
Planning
Section titled “Planning”This section of the pipeline is where the migration plan is generated. It runs on every
execution, even for an Apply, so that stale plans aren’t accidentally applied.
- Resolve desired schemas. Load the target schema(s) from one or more registered sources.
- Combine desired schemas. Combine the desired schemas into a single database schema.
- Validate the desired schema. Run any registered schema policies to validate things like missing foreign keys, tables without columns, etc.
- Read current state. Load the current schema from the live database or the state store, depending on the operation and what’s configured.
- Compare schemas. The current and desired schemas are compared to produce a
MigrationPlan. - Transform the plan. Any custom transformations are applied to the plan. This is where actions are reordered to respect dependencies, or where custom actions are injected.
- Validate the plan. Validate the plan using any registered policies. If configured, the built-in destructive actions policy will error on any destructive actions.
- Compile the plan. The migration plan is compiled into an executable unit of work.
Applying
Section titled “Applying”This section runs only for an Apply operation. It takes the compiled plan and executes it
against the database.
- Execute the migration. Takes the compiled migration from the Planning phase, and executes it against the target.
- State capture. After a successful apply, the resulting schema is captured to the state store (if configured) so that future plans can be generated against it.
Refresh
Section titled “Refresh”The Refresh operation captures the current live schema to the state store without doing any
planning or applying. This is useful for recording drift that happened between applies, or for
initializing the state store with the current schema.
Schema providers
Section titled “Schema providers”Desired state is declared through one or more ISchemaProvider implementations. Multiple
desired providers are supported and will be combined into a single schema, merging declared
schemas of the same name. This lets you organize schemas by feature or bounded context and
have them merged at runtime.
Current state is accessed through ICurrentSchemaProvider, which wraps two optional sources:
- Online. The live database, registered via
UseCurrentSchema<T>()or a provider package likeUseCurrentSchemaPostgres(...). - Offline. A persisted snapshot, enabled automatically when a
ISchemaStateStoreis registered viaUseStateStore<T>().
Plan operations prefer the offline source when available (so planning works without a
database connection); Apply always reads from the live database.
Schema scope
Section titled “Schema scope”The ISchemaProvider.GetSchema(...) method takes an optional list of schema names to read.
When null or empty, the provider is expected to return its full schema. This allows for
scoping to a subset of schemas when needed.
By default, the scope of a migration is equal to the full set of schemas returned by the
registered desired providers, but it can also be scoped explicitly per run by setting the
Schemas argument on the operation (e.g. app.Plan(new PlanArguments { Schemas = ["app"] })).
Schema policies
Section titled “Schema policies”Schema policies are used to validate the desired schema before any comparison or planning is done. This is where you can enforce naming conventions, required columns, banned types, or any other rules you want to apply to your schema.
Schema policies are implemented using ISchemaPolicy and are registered with
AddSchemaPolicy<T>(). If the policy returns any errors, execution will halt, preventing bad
schemas from being applied.
Schema comparison
Section titled “Schema comparison”The schema comparer is responsible for taking the current and desired schemas and producing a migration plan. The migration plan takes the form of a list of actions to perform, such as creating or dropping tables, adding or removing columns, etc.
The default comparer supports all the core features of the domain model, but you can replace it
with your own implementation of ISchemaComparer if you have special requirements.
Diff policies
Section titled “Diff policies”Diff policies validate the structured diff between the current and desired schema before a plan is built. This is where you enforce rules about what kinds of changes are allowed — for example, preventing destructive actions like dropping tables or columns (the built-in destructive-action policy lives here).
Diff policies are implemented using IDiffPolicy and registered with AddDiffPolicy<T>(). If
any policy returns errors, execution will halt, preventing bad changes from being applied.
SQL generation and execution
Section titled “SQL generation and execution”Once the plan is validated, NSchema turns it into SQL and, for an apply, runs it. These are two separate steps, so a plan can be previewed without a live connection:
ISqlGeneratorturns the plan into aSqlPlan. This is pure string-building, so the SQL preview works offline; it’s typically implemented in database providers likeNSchema.Postgresand registered withAddSqlGenerator<T>(). Each generator declares aDialect; register several and choose one per run withWithDialect(...).ISqlExecutorruns theSqlPlanagainst the database, applying the configured transaction mode. It is the only online step.
The rendered preview is produced by ISqlPlanRenderer (default DefaultSqlPlanRenderer),
which the reporter owns — register a custom one to change the preview format, mirroring how
IDiffRenderer controls the diff output.