NativeAOT support#2252
Conversation
- 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]>
There was a problem hiding this comment.
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
ITypeSourceabstraction with AOT-safe (ArrayTypeSource) and reflection-based (AssemblyTypeSource) implementations - Adds
AddFluentMigratorSlim()andWithTypes()APIs as AOT-compatible alternatives toAddFluentMigratorCore()andScanIn() - Applies
[RequiresUnreferencedCode]and[DynamicallyAccessedMembers]attributes throughout for proper trimming analysis - Updates all runner packages (except Jet) to target
net8.0with<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 |
GerardSmit
left a comment
There was a problem hiding this comment.
Self-review; I still need to make some changes. But the beginning is here.
|
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]>
| 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")), |
There was a problem hiding this comment.
Did you test this? What if Type.GetType returns null?
There was a problem hiding this comment.
There is a null check here: https://github.com/fluentmigrator/fluentmigrator/pull/2252/changes#diff-b00a3e2a8a1a0f447587cdea2ad4d2c8b4cb6ccc039de03f94720b6d10822c9eR230-R236
If it's null, it falls back to the original code.
…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]>
|
@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. |
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]>
|
Trying to help push blockers here forward. Assigned #2256 to Copilot. |
|
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). |
|
Hi guys, I hope you're all doing well! Is there a plan to release this? |
|
Most of the work is done; it works but there is still one thing I'm reconsidering (look at my last comment). |
|
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. |
|
@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. |
|
@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 |
|
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? |
|
@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. |
|
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? |
|
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. |
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:
Assembly scanning —
AddFluentMigratorCore()registeredAssemblySourceandAssemblyTypeSource, which useAppDomain.GetAssemblies()andAssembly.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.Missing
[RequiresUnreferencedCode]annotations — None of the reflection-heavy APIs carried trimming annotations. Trimmed builds would silently produce incorrect binaries with no compiler warnings.Anonymous-type overloads —
Row(object anonymousType),Set(object), etc. useGetProperties()at runtime to read the object''s properties. The trimmer removes property metadata unless explicitly preserved.Assembly loading from disk and GAC —
ReflectionBasedDbFactoryfalls back to loading provider assemblies from the file system and the GAC. Both mechanisms are completely unavailable in NativeAOT.ValidationContextin the expression validator —DefaultMigrationExpressionValidatorrelies onSystem.ComponentModel.DataAnnotations, which is not trimming-safe.What We Changed
New Abstractions
ITypeSourceGetTypes()method. Decouples migration type discovery from assembly scanning.AssemblyTypeSourceITypeSourceby scanningIAssemblySource. Annotated[RequiresUnreferencedCode]— preserves existing runtime behaviour but not AOT-safe.ArrayTypeSourceITypeSourcefrom an explicitType[]. No reflection — AOT-safe.EmptyAssemblySourceIAssemblySourcethat returns no assemblies. Used when assembly loading is intentionally excluded.TypeSourceConventionSetAccessorIConventionSetAccessorthat resolves convention sets from anITypeSourceinstead of assembly scanning.New APIs
IServiceCollection.AddFluentMigratorSlim()WebApplication.CreateSlimBuilder(), which follows the same convention of providing a minimal, AOT-friendly setup.IMigrationRunnerBuilder.WithTypes(params Type[])AddFluentMigratorSlim()as the AOT entry point.Usage examples
Default (reflection-based, not AOT-safe) — unchanged:
AOT-safe (explicit type registration):
Trimming Annotations Added
The following members now carry
[RequiresUnreferencedCode]on theNETbuild target, correctly flagging reflection usage to the trimmer:IInsertDataSyntax.Row(object)/Rows(params object[])— anonymous type reflectionInsertDataExpressionBuilder.Row(object)/Rows(params object[])UpdateDataExpressionBuilder.Set(object)/Where(object)MigrationSourceconstructor (when no explicitITypeSourceis provided)MaintenanceLoaderconstructor (when no explicitITypeSourceis provided)AssemblySourceConventionSetAccessorAssemblyLoaderFromFileRuntimeHost.FindAssemblies()/FindAssemblies(string)ReflectionBasedDbFactory.TryCreateFactory()/FindAssembliesInGac()Projects Targeting
net8.0All runner packages now target
net8.0with<IsTrimmable>true</IsTrimmable>, exceptFluentMigrator.Runner.Jet. The Jet provider usesSystem.Data.OleDb, which itself is not trimmable or NativeAOT-compatible, making anet8.0target impractical.Breaking Changes
[RequiresUnreferencedCode]on anonymous-type API methodsCallers using
Row(object anonymousType),Rows(params object[]),Set(object),Where(object)in a trimmed build will now receive an analyzer warning (IL2026). Switch to theIDictionary<string, object>overloads to avoid this.MaintenanceLoaderconstructor signatureAn optional
ITypeSource typeSource = nullparameter was added. Existing DI-registered usage is unaffected; only direct instantiation (new MaintenanceLoader(...)) may need updating.MigrationSourcenow implementsIMigrationSourcePreviously,
MigrationSourceonly implementedIFilteringMigrationSource. It now also implementsIMigrationSourceto 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. TheTryGetInstancehelper 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.IsDynamicCodeSupportedand are skipped at runtime in an AOT context.Type.GetType(literal)is trim-safe when the driver assembly is a static referenceWhat 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 theWithTypescall would make AOT adoption seamless.2. Trimming-safe expression validator
DefaultMigrationExpressionValidatorusesSystem.ComponentModel.DataAnnotations.ValidationContext, which is not trimming-safe. In a trimmed build, validation is currently skipped via aRuntimeFeature.IsDynamicCodeSupportedguard. 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)ProfileLoaderVersionTableMetaDataAccessorTypeFinderand related convention helpers that useType.GetCustomAttributes4. Provider-specific code
Each provider's processor and generator code may contain additional reflection that has not yet been annotated. The
net8.0target 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.