Skip to content

NativeAOT support#2252

Merged
jzabroski merged 15 commits into
fluentmigrator:mainfrom
GerardSmit:aot
May 28, 2026
Merged

NativeAOT support#2252
jzabroski merged 15 commits into
fluentmigrator:mainfrom
GerardSmit:aot

Conversation

@GerardSmit

Copy link
Copy Markdown
Contributor

Fixes #2231

AI Disclaimer
The first commit (937b960) and this text was handwritten.
The merge commit and last commit were made together with Copilot; to revert most of the changes that I made in the first commit 😅

The rest of the description is also put together with Copilot so it's easier to understand (English is my second language) 😉
All decisions were made and validated by me, so if anything needs to be changed feel free to ask.


Why AOT Didn't Work Before

FluentMigrator previously relied entirely on runtime reflection to find migration types:

  1. Assembly scanningAddFluentMigratorCore() registered AssemblySource and AssemblyTypeSource, which use AppDomain.GetAssemblies() and Assembly.GetExportedTypes() to discover all migration types at runtime. The .NET trimmer cannot statically determine which types will be loaded through these code paths, so it may strip required types from the output.

  2. Missing [RequiresUnreferencedCode] annotations — None of the reflection-heavy APIs carried trimming annotations. Trimmed builds would silently produce incorrect binaries with no compiler warnings.

  3. Anonymous-type overloadsRow(object anonymousType), Set(object), etc. use GetProperties() at runtime to read the object''s properties. The trimmer removes property metadata unless explicitly preserved.

  4. Assembly loading from disk and GACReflectionBasedDbFactory falls back to loading provider assemblies from the file system and the GAC. Both mechanisms are completely unavailable in NativeAOT.

  5. ValidationContext in the expression validatorDefaultMigrationExpressionValidator relies on System.ComponentModel.DataAnnotations, which is not trimming-safe.


What We Changed

New Abstractions

Type Description
ITypeSource New interface with a single GetTypes() method. Decouples migration type discovery from assembly scanning.
AssemblyTypeSource Implements ITypeSource by scanning IAssemblySource. Annotated [RequiresUnreferencedCode] — preserves existing runtime behaviour but not AOT-safe.
ArrayTypeSource Implements ITypeSource from an explicit Type[]. No reflection — AOT-safe.
EmptyAssemblySource An IAssemblySource that returns no assemblies. Used when assembly loading is intentionally excluded.
TypeSourceConventionSetAccessor An IConventionSetAccessor that resolves convention sets from an ITypeSource instead of assembly scanning.

New APIs

Member Description
IServiceCollection.AddFluentMigratorSlim() Sets up all runner services without the assembly-loader infrastructure. AOT-safe. Named after ASP.NET Core's WebApplication.CreateSlimBuilder(), which follows the same convention of providing a minimal, AOT-friendly setup.
IMigrationRunnerBuilder.WithTypes(params Type[]) Register migration types explicitly. Use together with AddFluentMigratorSlim() as the AOT entry point.

Usage examples

Default (reflection-based, not AOT-safe) — unchanged:

// Discovers migrations from assemblies at runtime.
// Produces an IL2026 warning in trimmed builds.
services.AddFluentMigratorCore()
    .ConfigureRunner(rb => rb
        .AddSQLite()
        .WithGlobalConnectionString("Data Source=:memory:")
        .ScanIn(typeof(MyMigration).Assembly).For.Migrations());

AOT-safe (explicit type registration):

// No IL2026 warning. Types are known at compile time.
services.AddFluentMigratorSlim()
    .ConfigureRunner(rb => rb
        .AddSQLite()
        .WithGlobalConnectionString("Data Source=:memory:")
        .WithTypes(typeof(Migration_001_CreateUsers), typeof(Migration_002_AddEmail)));

Trimming Annotations Added

The following members now carry [RequiresUnreferencedCode] on the NET build target, correctly flagging reflection usage to the trimmer:

  • IInsertDataSyntax.Row(object) / Rows(params object[]) — anonymous type reflection
  • InsertDataExpressionBuilder.Row(object) / Rows(params object[])
  • UpdateDataExpressionBuilder.Set(object) / Where(object)
  • MigrationSource constructor (when no explicit ITypeSource is provided)
  • MaintenanceLoader constructor (when no explicit ITypeSource is provided)
  • AssemblySourceConventionSetAccessor
  • AssemblyLoaderFromFile
  • RuntimeHost.FindAssemblies() / FindAssemblies(string)
  • ReflectionBasedDbFactory.TryCreateFactory() / FindAssembliesInGac()

Projects Targeting net8.0

All runner packages now target net8.0 with <IsTrimmable>true</IsTrimmable>, except FluentMigrator.Runner.Jet. The Jet provider uses System.Data.OleDb, which itself is not trimmable or NativeAOT-compatible, making a net8.0 target impractical.


Breaking Changes

  1. [RequiresUnreferencedCode] on anonymous-type API methods
    Callers using Row(object anonymousType), Rows(params object[]), Set(object), Where(object) in a trimmed build will now receive an analyzer warning (IL2026). Switch to the IDictionary<string, object> overloads to avoid this.

  2. MaintenanceLoader constructor signature
    An optional ITypeSource typeSource = null parameter was added. Existing DI-registered usage is unaffected; only direct instantiation (new MaintenanceLoader(...)) may need updating.

  3. MigrationSource now implements IMigrationSource
    Previously, MigrationSource only implemented IFilteringMigrationSource. It now also implements IMigrationSource to support DI registrations that request the non-filtering interface.


Database Provider Support

Every provider factory uses Type.GetType("FullTypeName, AssemblyName") with a string literal. The trimmer recognises this pattern and preserves the referenced type. The TryGetInstance helper carries the necessary [DynamicallyAccessedMembers] annotation. When the provider assembly is statically referenced, this path is fully trim-safe.

The fallback paths (AppDomain scanning, file loading, GAC) are not NativeAOT-compatible, but they are already guarded by RuntimeFeature.IsDynamicCodeSupported and are skipped at runtime in an AOT context.

Provider Factory path Fully trimmed
All providers Type.GetType(literal) is trim-safe when the driver assembly is a static reference ❌ Processor/generator code not yet audited
All providers ⚠️ Fallback paths (AppDomain/file/GAC) are runtime-guarded — not called in AOT

What Is Still Missing for Full AOT Support

1. Source generator

Consumers must manually call WithTypes(typeof(Migration1), ...). A Roslyn Incremental Generator that scans for [Migration], [Profile], and [Maintenance] attributes at compile time and emits the WithTypes call would make AOT adoption seamless.

2. Trimming-safe expression validator

DefaultMigrationExpressionValidator uses System.ComponentModel.DataAnnotations.ValidationContext, which is not trimming-safe. In a trimmed build, validation is currently skipped via a RuntimeFeature.IsDynamicCodeSupported guard. A proper attribute-based validator would restore validation in AOT builds.

3. Remaining reflection in the runner

The following areas have not yet been audited for AOT:

  • MigrationRunner (activating migration instances via reflection)
  • ProfileLoader
  • VersionTableMetaDataAccessor
  • TypeFinder and related convention helpers that use Type.GetCustomAttributes

4. Provider-specific code

Each provider's processor and generator code may contain additional reflection that has not yet been annotated. The net8.0 target and <IsTrimmable>true</IsTrimmable> are now in place; a full trimming audit per provider is still needed. The factory pattern is already correct — the remaining work is in the per-provider implementation classes.

5. AOT integration test

An end-to-end test that publishes a minimal AOT binary and runs a migration (e.g. against SQLite) is needed to validate the complete stack. NUnit does not support NativeAOT, so a test runner switch (e.g. to TUnit) is required before this can be implemented.

GerardSmit and others added 3 commits July 16, 2025 01:07
- Accept all NativeAOT (HEAD) versions of files
- Delete LegacyExtensions.cs deprecated file
- Preserve NativeAOT attributes and trimming support
- Include main's new features (Postgres security labels, etc.)
…ons, net8.0 targets

## New Abstractions

- ITypeSource: new interface with GetTypes(), decouples type discovery from
  assembly scanning
- AssemblyTypeSource: ITypeSource backed by IAssemblySource, annotated
  [RequiresUnreferencedCode]
- ArrayTypeSource: ITypeSource backed by an explicit Type[], AOT-safe
- EmptyAssemblySource: IAssemblySource that returns no assemblies
- TypeSourceConventionSetAccessor: resolves convention sets from ITypeSource

## New APIs

- AddFluentMigratorSlim(): AOT-safe DI setup without assembly-loader
  infrastructure, named after ASP.NET Core's WebApplication.CreateSlimBuilder
- AddFluentMigratorCoreWithoutAssemblyLoader(): marked [Obsolete], delegates
  to AddFluentMigratorSlim()
- WithTypes(params Type[]): register migration types explicitly

## Trimming Annotations

Added [RequiresUnreferencedCode] to:
- IInsertDataSyntax.Row/Rows (anonymous type overloads)
- InsertDataExpressionBuilder.Row/Rows
- UpdateDataExpressionBuilder.Set/Where (anonymous type overloads)
- MigrationSource constructor (fallback to AssemblyTypeSource)
- MaintenanceLoader constructor (fallback to AssemblyTypeSource); restored
  IOptions<TypeFilterOptions> parameter and namespace filtering
- AssemblySourceConventionSetAccessor
- AssemblyLoaderFromFile
- RuntimeHost.FindAssemblies
- ReflectionBasedDbFactory.TryCreateFactory / FindAssembliesInGac
Added #pragma IL2026 suppressions where [RequiresUnreferencedCode] on an
override would cause IL2046 (ReflectionBasedDbFactory.CreateFactory).

## Other fixes

- Reverted VersionPrefix from 8.0.0 to 6.0.0 (Global.props)
- Fixed .NET 8 Enumerable.Reverse() ambiguity
- MigrationSource now implements IMigrationSource in addition to
  IFilteringMigrationSource
- Fixed IL2091 in OracleRunnerBuilderExtensions (DynamicallyAccessedMembers
  on generic type parameter T in three DI registration helpers)
- Removed invalid [DynamicallyAccessedMembers] on Type[] parameters

## net8.0 targets

All runner packages now target net8.0 with <IsTrimmable>true</IsTrimmable>,
except FluentMigrator.Runner.Jet (System.Data.OleDb is not trimmable).

Co-authored-by: Copilot <[email protected]>
Copilot AI review requested due to automatic review settings February 20, 2026 14:04
@github-actions

github-actions Bot commented Feb 20, 2026

Copy link
Copy Markdown

Test Results

0 tests  ±0   0 ✅ ±0   0s ⏱️ ±0s
0 suites ±0   0 💤 ±0 
0 files   ±0   0 ❌ ±0 

Results for commit 7b6c344. ± Comparison against base commit 6df7ac8.

♻️ This comment has been updated with latest results.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds comprehensive NativeAOT support to FluentMigrator, addressing issue #2231. The PR introduces new AOT-compatible abstractions and APIs while maintaining backward compatibility with existing reflection-based workflows.

Changes:

  • Introduces ITypeSource abstraction with AOT-safe (ArrayTypeSource) and reflection-based (AssemblyTypeSource) implementations
  • Adds AddFluentMigratorSlim() and WithTypes() APIs as AOT-compatible alternatives to AddFluentMigratorCore() and ScanIn()
  • Applies [RequiresUnreferencedCode] and [DynamicallyAccessedMembers] attributes throughout for proper trimming analysis
  • Updates all runner packages (except Jet) to target net8.0 with <IsTrimmable>true</IsTrimmable>
  • Modifies database provider factories to use Type.GetType() with explicit type factory delegates for AOT compatibility

Reviewed changes

Copilot reviewed 70 out of 70 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/FluentMigrator/FluentMigrator.csproj Adds net8.0 target with IsTrimmable=true
src/FluentMigrator/Builders/**/*.cs Adds RequiresUnreferencedCode to anonymous type methods
src/FluentMigrator.Runner.Core/**/*.csproj Adds net8.0 target with IsTrimmable=true
src/FluentMigrator.Runner.Core/FluentMigratorServiceCollectionExtensions.cs Implements AddFluentMigratorSlim() and obsoletes AddFluentMigratorCoreWithoutAssemblyLoader()
src/FluentMigrator.Runner.Core/MigrationRunnerBuilderExtensions.cs Adds WithTypes() method for explicit type registration
src/FluentMigrator.Runner.Core/Initialization/ITypeSource.cs New abstraction for type discovery
src/FluentMigrator.Runner.Core/Initialization/ArrayTypeSource.cs AOT-safe type source implementation
src/FluentMigrator.Runner.Core/Initialization/AssemblyTypeSource.cs Reflection-based type source implementation
src/FluentMigrator.Runner.Core/Initialization/TypeSourceConventionSetAccessor.cs Convention set accessor using ITypeSource
src/FluentMigrator.Runner.Core/Initialization/EmptyAssemblySource.cs Empty assembly source for AOT scenarios
src/FluentMigrator.Runner.Core/Initialization/MigrationSource.cs Updated to accept ITypeSource parameter
src/FluentMigrator.Runner.Core/MaintenanceLoader.cs Updated to accept ITypeSource parameter
src/FluentMigrator.Runner.Core/Processors/ReflectionBasedDbFactory.cs Adds TryCreateFromPreloadedType() for AOT path
src/FluentMigrator.Runner.{SQLite,Postgres,Oracle,MySql,Hana,Firebird,Db2,Snowflake,Redshift}/**/*.cs Updates provider factories with Type.GetType() delegates
src/FluentMigrator.Abstractions/Validation/DefaultMigrationExpressionValidator.cs Adds RuntimeFeature.IsDynamicCodeSupported guard
src/FluentMigrator.Abstractions/Expressions/DeleteForeignKeyExpression.cs Refactors validation logic
src/FluentMigrator.Abstractions/Exceptions/**/*.cs Marks serialization constructors obsolete for .NET

Comment thread src/FluentMigrator.Runner.Core/Initialization/ArrayTypeSource.cs Outdated
Comment thread src/FluentMigrator.Runner.Db2/Processors/Db2/iSeries/Db2ISeriesDbFactory.cs Outdated
Comment thread src/FluentMigrator.Runner.Db2/Processors/Db2/Db2DbFactory.cs Outdated
Comment thread src/FluentMigrator.Runner.Core/FluentMigratorServiceCollectionExtensions.cs Outdated
Comment thread src/FluentMigrator.Runner.Db2/Processors/Db2/iSeries/Db2ISeriesDbFactory.cs Outdated
Comment thread src/FluentMigrator.Runner.Core/Initialization/TypeSourceConventionSetAccessor.cs Outdated
Comment thread src/FluentMigrator.Runner.Db2/Processors/Db2/Db2DbFactory.cs Outdated
Comment thread src/FluentMigrator.Runner.Core/Processors/ReflectionBasedDbFactory.cs Outdated

@GerardSmit GerardSmit left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review; I still need to make some changes. But the beginning is here.

Comment thread src/FluentMigrator.Abstractions/Validation/DefaultMigrationExpressionValidator.cs Outdated
Comment thread src/FluentMigrator.Runner.Core/Infrastructure/RuntimeHost.cs
Comment thread src/FluentMigrator.Runner.Core/Processors/ReflectionBasedDbFactory.cs Outdated
Comment thread src/FluentMigrator.Runner.Core/FluentMigratorServiceCollectionExtensions.cs Outdated
@GerardSmit
GerardSmit marked this pull request as draft February 20, 2026 14:16
@jzabroski

Copy link
Copy Markdown
Collaborator

Superficially, this makes sense, but it's a lot to review, so it might take me a couple weeks before I merge it. @PhenX if you have time to look it over, I would appreciate it.

- Fix copyright years to 2026 in all new files (ITypeSource, ArrayTypeSource, EmptyAssemblySource, AssemblyTypeSource, TypeSourceConventionSetAccessor)
- Add descriptive messages to [Obsolete] attributes in ReflectionBasedDbFactory, Db2DbFactory, and Db2ISeriesDbFactory
- Add Net.IBM.* fallback entries in Db2DbFactory and Db2ISeriesDbFactory for backward compatibility
- Remove unused 'options' variable in FluentMigratorServiceCollectionExtensions.cs
- Restore primary constructor syntax for MigrationRunnerBuilder and ConnectionlessProcessorAccessor
- Revert Rider-specific change in AssemblyLoaderFromFile.cs (restore unqualified exception type references)
- Restore XML doc comments on private methods in RuntimeHost.cs
- Restore XML doc comments on class and methods in ReflectionBasedDbFactory.cs
- Change [CanBeNull] back to [NotNull] for TryCastInstance value parameter

Co-authored-by: Copilot <[email protected]>
Comment thread src/FluentMigrator.Abstractions/FluentMigrator.Abstractions.csproj Outdated
Comment thread src/FluentMigrator.Runner.Firebird/FluentMigrator.Runner.Firebird.csproj Outdated
Comment thread src/FluentMigrator.Runner.Core/MaintenanceLoader.cs
private static readonly TestEntry[] _testEntries =
{
new TestEntry("Net.IBM.Data.DB2.iSeries", "Net.IBM.Data.DB2.iSeries.iDB2Factory"),
new TestEntry("Net.IBM.Data.DB2.iSeries", "Net.IBM.Data.DB2.iSeries.iDB2Factory", () => Type.GetType("Net.IBM.Data.DB2.iSeries.iDB2Factory, Net.IBM.Data.DB2.iSeries")),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you test this? What if Type.GetType returns null?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

…t.props, restore TestEntry docs

- Create TrimAot.props at repo root to centralize net8.0/net9.0/net10.0
  targeting and IsTrimmable settings (PhenX suggestion)
- Update all 13 affected .csproj files to import TrimAot.props instead
  of duplicating the per-framework IsTrimmable block in each file
- Restore removed XML doc comments for TestEntry class, constructor,
  AssemblyName and DBProviderFactoryTypeName properties (PhenX/jzabroski)
- Add XML doc comments for new TypeFactory property (jzabroski)

Co-authored-by: Copilot <[email protected]>
Comment thread src/FluentMigrator.Runner.Db2/FluentMigrator.Runner.Db2.csproj Outdated
Comment thread src/FluentMigrator.Runner.Core/Infrastructure/DefaultMigrationConventions.cs Outdated
Comment thread TrimAot.props Outdated
Comment thread TrimAot.props
@jzabroski

Copy link
Copy Markdown
Collaborator

@GerardSmit See my latest comments. I think this is close to complete. My main ask is that we think about a way to signal ReflectionBasedDbFactory should no longer be used, and add a separate PR to address the dynamically accessed members in DefaultMigrationConvention.cs so that we can ship a version of FluentMigrator that warns about those interfaces going away and being replaced by virtual interfaces. Let me know if I misunderstood the concern about DefaultMigrationConvention, though. I just think if the simple fix is to stage deprecation of features with something AOT compatible, that is preferred vs. building a lot of extra machinery to maintain long-term.

GerardSmit and others added 3 commits March 6, 2026 20:01
Introduce IMigrationRunnerTagConventions interface with proper method-based
TypeHasTags and TypeHasMatchingTags members that carry DynamicallyAccessedMembers
annotations for AOT/trimming support.

- Mark Func<> tag properties on IMigrationRunnerConventions as [Obsolete]
- Implement IMigrationRunnerTagConventions on DefaultMigrationRunnerConventions
  and MigrationRunnerConventions via explicit interface implementation
- Update HasRequestedTags extension to prefer new interface with fallback
- Register IMigrationRunnerTagConventions in DI
- Update tests to use new interface and suppress obsolete warnings

Co-authored-by: Copilot <[email protected]>
Introduce AotSupport.IsDynamicCodeSupported as a testable wrapper that:
- Returns RuntimeFeature.IsDynamicCodeSupported on .NET (with AggressiveInlining)
- Returns true on .NET Framework (no AOT concern)
- Returns false when built with -p:TestingAot=true (TESTING_AOT define)

Replace direct RuntimeFeature.IsDynamicCodeSupported calls in:
- DefaultMigrationExpressionValidator.Validate()
- ReflectionBasedDbFactory (factory creation)
- AssemblySourceVersionTableMetaDataAccessor.GetAssemblyTypes()

Add ILLink.Descriptors.xml to preserve [Required] attributes in trimmed/AOT builds:
- FluentMigrator.Abstractions (27 expression/model types)
- FluentMigrator.Extensions.Postgres (2 types)
- FluentMigrator.Extensions.MySql (1 type)
- FluentMigrator.Extensions.SqlServer (1 type)

Add TUnit-based NativeAOT smoke tests (net10.0) covering:
- Validation ([Required] and IValidatableObject)
- SQL generation (SQLite)
- Dependency injection (AddFluentMigratorSlim)

Update GitHub Actions to run AOT smoke tests.

Co-authored-by: Copilot <[email protected]>
@jzabroski

Copy link
Copy Markdown
Collaborator

Trying to help push blockers here forward. Assigned #2256 to Copilot.

@jzabroski jzabroski added this to the 9.0.0 milestone Mar 12, 2026
@GerardSmit

Copy link
Copy Markdown
Contributor Author

Sorry, hectic week 🙏

I did some work on it. I got the validations working together with https://github.com/dotnet/runtime/blob/main/docs/workflow/trimming/ILLink-files.md and made some tests with TUnit (so I'm 100% sure validations work).

@cristiano-linvix

Copy link
Copy Markdown

Hi guys, I hope you're all doing well!
Thank you for the excellent work!

Is there a plan to release this?

@GerardSmit

Copy link
Copy Markdown
Contributor Author

Most of the work is done; it works but there is still one thing I'm reconsidering (look at my last comment).

@jzabroski

Copy link
Copy Markdown
Collaborator

My general policy is to try to avoid responding to "updates?" questions, as they just waste time giving status updates versus actually doing work reviewing PRs, testing changes, etc. I appreciate the interest, though, and do intend to incorporate this PR soon.

@jzabroski

Copy link
Copy Markdown
Collaborator

@GerardSmit I do want to merge this PR but I am stuck on #2257 and the best path for creating a migration bridge to minimize impact to users. Let me know your thoughts.

@jzabroski

Copy link
Copy Markdown
Collaborator

@GerardSmit I read about platform-specific CLI Tool support that was added as of the .NET 10 SDK - https://learn.microsoft.com/en-us/dotnet/core/tools/rid-specific-tools#:~:text=Package%20.,of%20the%20following%20MSBuild%20properties: - Thanks to your changes, we can ship a thinner FluentMigrator.DotNet.CLI tool! Let me know if you're interested in incorporating that. I also asked Copilot about it here, as it's a blocker for adopting Hana in FluentMigrator.DotNet.CLI: #2276

@GerardSmit

Copy link
Copy Markdown
Contributor Author

I've commented in #2257 (review)


About the FluentMigrator.DotNet.CLI tool; personally I'm not using it but if I understand it correctly; if this PR is okay, then we can just add RuntimeIdentifiers and set TrimMode and it should be good?

@jzabroski
jzabroski marked this pull request as ready for review May 28, 2026 17:22
@jzabroski

Copy link
Copy Markdown
Collaborator

@GerardSmit Merging this today. I know I made you wait because of the backward compatibility concerns. I think this one is just going to have to have a bright clear indication in the release notes. I try really hard not to break users existing code bases. Hope you understand.

@GerardSmit

Copy link
Copy Markdown
Contributor Author

No worries, completely understandable.

I was wondering, maybe enabling https://learn.microsoft.com/en-us/dotnet/fundamentals/apicompat/package-validation/overview can help with this?

@jzabroski

Copy link
Copy Markdown
Collaborator

create a issue. im always for best practices. I think once Hana libs are gone from source control, Claire Novotny's idempotent builds approach will be easier too.

@jzabroski
jzabroski merged commit 8f0460b into fluentmigrator:main May 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Does FluentMigrator support AOT?

5 participants