dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.82k stars 3.2k forks source link

NullReferenceException when trying to optimize a DbContext with --nativeaot #35219

Open IanRawley opened 5 days ago

IanRawley commented 5 days ago

I currently have an app using EF Core with the database functions spread across several projects:

Data Model project, containing all model objects that will be found in the database
DbContext project, containing the context and model configuration
Migrations project.
Factory project, responsible for instantiating a DbContext with the appropriate provider / file IO / migration options
Application service project, which grabs an instance from the factory project and presents methods to the app for retrieving specific query results

With the release of EF Core 9, I'd like to precompile the model and queries with Native AOT, to hopefully boost performance on Android. Given the above separation of responsibilities, I felt it made the most sense to try and compile the model into another assembly, to be referenced as needed. However when trying to optimize the context from an effectively empty project (contains only an IDesignTimeDbContextFactory) the dotnet tools throw an NRE as seen below:

dotnet ef dbcontext optimize --context FieldStorageContext --nativeaot --verbose
Using project 'D:\source\repos\<REDACTED>\src\FieldStorageCompiledModel\FieldStorageCompiledModel.csproj'.
Using startup project 'D:\source\repos\<REDACTED>\src\FieldStorageCompiledModel\FieldStorageCompiledModel.csproj'.
dotnet msbuild /target:GetEFProjectMetadata "/property:EFProjectMetadataFile=C:\Users\<REDACTED>\AppData\Local\Temp\tmpyfettc.tmp" /verbosity:quiet /nologo D:\source\repos\<REDACTED>\src\FieldStorageCompiledModel\FieldStorageCompiledModel.csproj
dotnet msbuild /target:GetEFProjectMetadata "/property:EFProjectMetadataFile=C:\Users\<REDACTED>\AppData\Local\Temp\tmpzhjdqb.tmp" /verbosity:quiet /nologo D:\source\repos\<REDACTED>\src\FieldStorageCompiledModel\FieldStorageCompiledModel.csproj
Build started...
dotnet build D:\source\repos\<REDACTED>\src\FieldStorageCompiledModel\FieldStorageCompiledModel.csproj /verbosity:quiet /nologo /p:PublishAot=false /p:EFOptimizeContext=false

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:01.06

Build succeeded.
dotnet exec --depsfile D:\source\repos\<REDACTED>\src\FieldStorageCompiledModel\bin\Debug\net8.0\FieldStorageCompiledModel.deps.json --additionalprobingpath "C:\Users\<REDACTED>\.nuget\packages" --additionalprobingpath "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages" --runtimeconfig D:\source\repos\<REDACTED>\src\FieldStorageCompiledModel\bin\Debug\net8.0\FieldStorageCompiledModel.runtimeconfig.json "C:\Users\<REDACTED>\.nuget\packages\dotnet-ef\9.0.0\tools\net8.0\any\tools\netcoreapp2.0\any\ef.dll" dbcontext optimize --context FieldStorageContext --nativeaot --assembly D:\source\repos\<REDACTED>\src\FieldStorageCompiledModel\bin\Debug\net8.0\FieldStorageCompiledModel.dll --project D:\source\repos\<REDACTED>\src\FieldStorageCompiledModel\FieldStorageCompiledModel.csproj --startup-assembly D:\source\repos\<REDACTED>\src\FieldStorageCompiledModel\bin\Debug\net8.0\FieldStorageCompiledModel.dll --startup-project D:\source\repos\<REDACTED>\src\FieldStorageCompiledModel\FieldStorageCompiledModel.csproj --project-dir D:\source\repos\<REDACTED>\src\FieldStorageCompiledModel\ --root-namespace FieldStorageCompiledModel --language C# --framework net8.0 --nullable --working-dir D:\source\repos\<REDACTED>\src\FieldStorageCompiledModel --verbose
NativeAOT support is experimental and can change in the future.
Using assembly 'FieldStorageCompiledModel'.
Using startup assembly 'FieldStorageCompiledModel'.
Using application base 'D:\source\repos\<REDACTED>\src\FieldStorageCompiledModel\bin\Debug\net8.0'.
Using working directory 'D:\source\repos\<REDACTED>\src\FieldStorageCompiledModel'.
Using root namespace 'FieldStorageCompiledModel'.
Using project directory 'D:\source\repos\<REDACTED>\src\FieldStorageCompiledModel\'.
Remaining arguments: .
Finding DbContext classes...
Finding IDesignTimeDbContextFactory implementations...
Found IDesignTimeDbContextFactory implementation 'FieldStorageContextFactory'.
Found DbContext 'FieldStorageContext'.
Finding DbContext classes in the project...
Using DbContext factory 'FieldStorageContextFactory'.
Using context 'FieldStorageContext'.
Finding design-time services referenced by assembly 'FieldStorageCompiledModel'...
Finding design-time services referenced by assembly 'FieldStorageCompiledModel'...
No referenced design-time services were found.
Finding design-time services for provider 'Microsoft.EntityFrameworkCore.Sqlite'...
Using design-time services from provider 'Microsoft.EntityFrameworkCore.Sqlite'.
Finding IDesignTimeServices implementations in assembly 'FieldStorageCompiledModel'...
No design-time services were found.
Your target project 'FieldStorageCompiledModel' doesn't match the assembly containing 'FieldStorageContext' - 'FieldStorage'. This is not recommended as it will cause the compiled model to not be discovered automatically.
Consider changing your target project to the DbContext project by using the Package Manager Console's Default project drop-down list, by executing "dotnet ef" from the directory containing the DbContext project or by supplying it with the '--project' option.
'FieldStorageContext' disposed.
System.NullReferenceException: Object reference not set to an instance of an object.
   at Microsoft.EntityFrameworkCore.Query.Internal.LinqToCSharpSyntaxTranslator.GetUnsafeAccessorName(MemberInfo member)
   at Microsoft.EntityFrameworkCore.Scaffolding.Internal.CSharpRuntimeModelCodeGenerator.RegisterPrivateAccessor(IPropertyBase property, Boolean forMaterialization, Boolean forSet, String namespace, BidirectionalDictionary`2 unsafeAccessorClassNames, Dictionary`2 unsafeAccessorTypes, Dictionary`2& memberAccessReplacements)
   at Microsoft.EntityFrameworkCore.Scaffolding.Internal.CSharpRuntimeModelCodeGenerator.RegisterPrivateAccessors(IPropertyBase property, String namespace, BidirectionalDictionary`2 unsafeAccessorClassNames, Dictionary`2 unsafeAccessorTypes, Dictionary`2 memberAccessReplacements)
   at Microsoft.EntityFrameworkCore.Scaffolding.Internal.CSharpRuntimeModelCodeGenerator.GenerateModel(IModel model, CompiledModelCodeGenerationOptions options)
   at Microsoft.EntityFrameworkCore.Scaffolding.Internal.CompiledModelScaffolder.ScaffoldModel(IModel model, String outputDir, CompiledModelCodeGenerationOptions options)
   at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.ScaffoldCompiledModel(String outputDir, String modelNamespace, DbContext context, String suffix, Boolean nativeAot, IServiceProvider services, ISet`1 generatedFileNames)
   at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.Optimize(String outputDir, String modelNamespace, String suffix, Boolean scaffoldModel, Boolean precompileQueries, DbContext context, Boolean optimizeAllInAssembly, Boolean nativeAot, List`1 generatedFiles, HashSet`1 generatedFileNames)
   at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.Optimize(String outputDir, String modelNamespace, String contextTypeName, String suffix, Boolean scaffoldModel, Boolean precompileQueries, Boolean nativeAot)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OptimizeContextImpl(String outputDir, String modelNamespace, String contextType, String suffix, Boolean scaffoldModel, Boolean precompileQueries, Boolean nativeAot)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OptimizeContext.<>c__DisplayClass0_0.<.ctor>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
Object reference not set to an instance of an object.

Trying a similar setup using the basic Blog/Post tutorial model generates without issue, so the problem seems to be something specific to this App's model rather than the splitting into multiple assemblies. How can I get more information in order to better narrow down the cause and produce a minimal reproduction?

EF Core version: 9.0.0 Database provider: Microsoft.EntityFrameworkCore.Sqlite Target framework: .NET 8 Operating system: Windows 11 IDE: Visual Studio 2022 17.12.1

roji commented 5 days ago

How can I get more information in order to better narrow down the cause and produce a minimal reproduction?

Thanks for reporting this! What kind of information do you think you need? If you have a project on-hand which causes the exception, can you try stripping it down until you get a minimal repro which you can post? Or alternatively, you can try starting from scratch with an empty project, and add stuff to it until you manage to reproduce the error.

IanRawley commented 4 days ago

How can I get more information in order to better narrow down the cause and produce a minimal reproduction?

Thanks for reporting this! What kind of information do you think you need? If you have a project on-hand which causes the exception, can you try stripping it down until you get a minimal repro which you can post? Or alternatively, you can try starting from scratch with an empty project, and add stuff to it until you manage to reproduce the error.

The only way I can see an NRE happening here is if GetUnsafeAccessorName() is being called with a null MemberInfo. If I could figure out what entity was being processed I could narrow down what about it causes the problem, and then replicate it.

Unfortunately the existing solution is too big to pare down at this point, so I'm going to have to start with a basic model and start adding bits till it breaks.

IanRawley commented 4 days ago

@roji I finally figured it out and have been able to create a minimal reproduction. You can find it here: https://github.com/IanRawley/MinimalOptimizeNRERepo.git

The problem is this pattern on a model for what will be navigation collections:

private List<Post> _children = new();
public IReadOnlyCollection<Post> Posts => _children;

Specifically, if the name of the private backing field doesn't "match" the public facing name, optimize throws an NRE. If you change _children to _posts, or Posts to Children, the problem goes away. This only happens with the --nativeaot flag.

IanRawley commented 3 days ago

An addendum, it seems the problem is any backing field that doesn't match the public member name. It doesn't have to be a navigation collection.