Today, the Entity Framework Core team announces the fifth preview release of EF Core 6.0. This release includes the first iteration of compiled models. If startup time for your application is important and your EF Core model contains hundreds or thousands of entities, properties, and relationships, this is one release you don’t want to ignore.
TL;DR;
- Compiled models dramatically reduce startup time for your application.
- The models are generated (similar to how migrations are) so they should be refreshed whenever your model changes.
- Some features are not currently supported by compiled models, so be aware of the limitations when you try them out.
Background
How does 10x performance sound to you? Our team created a sample project with a DbContext
that contains 449 entity types, 6,390 properties and 720 relationships. I wrote a console app that loops several times, creates a new instance of a DbContext
and loads a set of entities with no filters or ordering. The start-up time for the first run consistently takes around two seconds on my laptop, with subsequent cached instances weighing in at about 1.5 seconds. Here’s the output from a run:
$ dotnet run -c Release
Model has:
449 entity types
6390 properties
720 relationships
Instantiating context...
It took 00:00:02.1603163.
Instantiating context...
It took 00:00:01.6268628.
Instantiating context...
It took 00:00:01.7144346.
Instantiating context...
It took 00:00:01.6090380.
Instantiating context...
It took 00:00:01.7049987.
After testing the baseline application, I used the new EF Core tools Command Line Interface (CLI) feature to optimize the DbContext
:
dotnet ef dbcontext optimize -output-dir MyCompiledModels --namespace MyCompiledModels
The tool gave me instructions to add a single line of code to my DbContext
configuration:
options.UseModel(MyCompiledModels.BlogsContextModel.Instance);
I made the update and re-ran the code to receive a 10x performance gain with the initial model taking 257ms to complete. The cached model reduced additional calls to just 10ms.
$ dotnet run -c Release
Model has:
449 entity types
6390 properties
720 relationships
Instantiating context...
It took 00:00:00.2573627.
Instantiating context...
It took 00:00:00.0132345.
Instantiating context...
It took 00:00:00.0119556.
Instantiating context...
It took 00:00:00.0101717.
Instantiating context...
It took 00:00:00.0139057.
A peek at the query pipeline
EF Core performs quite a bit of work to get from your application to returning the first result of the first query your application processes. Let’s break down the following two statements and go “behind the scenes” to see what happens.
using var myContext = new MyContext();
var results = myContext.MyWidgets.ToList();
DbContext instantiation
The first step is creating an instance of the context. The first time a DbContext
is created, EF Core will create and compile delegates to set the table properties you expose by using DbSet<Entity>
. This simply creates the delegates to set the properties so you can query them right away.
Performance tip: you can avoid the overhead of
DbSet
initialization by using an alternate approach such as thecontext.Set<Entity>()
API call.
DbContext (lazy) initialization
After the DbContext
is created, EF Core “goes to sleep” until you use it. The first time you use a context by accessing one of its APIs (such as navigating an entity and returning results), the context is initialized. This will run the OnConfiguring
method to establish the proper provider and database connections as well as other settings. For example, this is the perfect place to use the simple logging feature by calling the new LogTo
extension on the options builder.
Service provider
EF Core uses a service-based architecture and has an internal dependency injection framework. This provider is built internally but is designed to work with external DI solutions such as the service provider in ASP.NET Core.
Performance tip: much of the overhead described so far can be mitigated by using context pooling. This enables a pool of reusable context instances that are already initialized.
Model building
To understand how a domain object (C# class) relates to the tables and relationships in the database, EF Core builds an internal model that represents all the types, properties, constraints, and relationships that it finds in your DbContext
. This is a metadata model and includes the call to OnModelCreating
that can be overridden to provide fluent configuration of the model.
Query compilation
A major reason why developers use EF Core is its ability to parse Language Integrated Queries (LINQ) into the database dialect. This is an advanced stage because it involves traversing a potentially complex expression tree and translating it into SQL. Something trivial like a projection:
var projection = myQuery.Select(obj => new { id = obj.EntityId, name = obj.Identifier });
Seems easy enough to translate:
SELECT EntityId, Identifier FROM ...
But what about something more complicated, like this?
var pairs = (from a1 in context.Attendees
from a2 in context.Attendees
where a1.Id != a2.Id
select new
{
a1 = a1.Id,
a1LastName = a1.LastName,
a1FirstName = a1.FirstName,
a2 = a2.Id,
a2LastName = a2.LastName,
a2FirstName = a2.FirstName,
sessionCount =
a1.Sessions.Select(s => s.Id)
.Intersect(a2.Sessions.Select(s => s.Id)).Count()
}).OrderByDescending(shared => shared.sessionCount)
.Take(5);
This is ultimately parsed into native SQL, intersection and all. The first time that EF Core encounters a query, it parses the query to determine which parts are dynamic. It then compiles the static parts of the query and parameterizes the dynamic aspects to expedite translation into SQL by using a SQL template.
Run the query
Finally! The query is now run. To avoid the overhead of performing these steps every time, EF Core caches the delegates for DbSet
properties, the internal service provider, the constructed model, and the compiled query. This results in much faster performance after the queries are successfully run the first time.
You can visualize these steps using the following diagram (note the cache boxes have strike-through to show they are disabled for our benchmark tests):
Although most of the pipeline is already streamlined, model compilation was an area we knew could improve.
A note on source generators. The approach the team chose is to provide a command that generates the source code files that you can then incorporate into your project to build the compiled model. We are often asked why we didn’t choose source generators. The answer is that source generators run as user code inside the Visual Studio process. EF Core must build and run the context to obtain information about the model. If an exception is thrown as part of the process, this could potentially force Visual Studio to hang or crash.
As with most technology, compiled models do have trade-offs. Let’s look at the pros and cons.
Pros and cons
The pros should be clear. As your model grows larger, your startup time remains fast. Here is a comparison of startup time between compiled and non-compiled models based on the size of the model.
Here are some cons to consider:
- Global query filters are not supported.
- Lazy loading proxies are not supported.
- Change tracking proxies are not supported.
- Custom IModelCacheKeyFactory implementations are not supported.
- The model must be manually synchronized by regenerating it any time the model definition or configuration change.
Tip: if supporting any of these features is critical to your success, please find the issue and upvote it or add your comments and thoughts, or file a new issue to let us know.
Now you’ve learned the background. How do you get started?
In conclusion
To start using compiled models today, reap the performance benefits and have the opportunity to provide us with feedback before we release the final EF Core 6.0 version, start by grabbing the latest preview (instructions are below) and installing the latest EF Core CLI. The new tool command looks like this (all parameters are optional):
dotnet ef dbcontext optimize -c MyContext -o MyFolder -n My.Namespace
Inside the NuGet package manager console you can use this:
Optimize-DbContext -Context MyContext -OutputDir MyFolder -Namespace My.Namespace
The tool will instruct you to add a line like this to your options configuration:
opts.UseModel(My.Namespace.MyContextModel.Instance);
We hope you benefit from this new feature and can provide us with early feedback. Check out the EF Core 6.0 plan. In addition to other work, the team has prioritized a number of Azure Cosmos DB provider features. Please upvote the features that are important to you and share any feedback you may have! Other features in the preview 5 release will be posted in EF Core 6.0 What’s New.
How to get EF Core 6.0 previews
EF Core is distributed exclusively as a set of NuGet packages. For example, to add the SQL Server provider to your project, you can use the following command using the dotnet tool:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 6.0.0-preview.5.21301.9
This following table links to the preview 5 versions of the EF Core packages and describes what they are used for.
Package | Purpose |
---|---|
Microsoft.EntityFrameworkCore | The main EF Core package that is independent of specific database providers |
Microsoft.EntityFrameworkCore.SqlServer | Database provider for Microsoft SQL Server and SQL Azure |
Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite | SQL Server support for spatial types |
Microsoft.EntityFrameworkCore.Sqlite | Database provider for SQLite that includes the native binary for the database engine |
Microsoft.EntityFrameworkCore.Sqlite.Core | Database provider for SQLite without a packaged native binary |
Microsoft.EntityFrameworkCore.Sqlite.NetTopologySuite | SQLite support for spatial types |
Microsoft.EntityFrameworkCore.Cosmos | Database provider for Azure Cosmos DB |
Microsoft.EntityFrameworkCore.InMemory | The in-memory database provider |
Microsoft.EntityFrameworkCore.Tools | EF Core PowerShell commands for the Visual Studio Package Manager Console; use this to integrate tools like scaffolding and migrations with Visual Studio |
Microsoft.EntityFrameworkCore.Design | Shared design-time components for EF Core tools |
Microsoft.EntityFrameworkCore.Proxies | Lazy-loading and change-tracking proxies |
Microsoft.EntityFrameworkCore.Abstractions | Decoupled EF Core abstractions; use this for features like extended data annotations defined by EF Core |
Microsoft.EntityFrameworkCore.Relational | Shared EF Core components for relational database providers |
Microsoft.EntityFrameworkCore.Analyzers | C# analyzers for EF Core |
We also published the 6.0 preview 5 release of the Microsoft.Data.Sqlite.Core provider for ADO.NET.
Thank you from the team
A big thank you from the EF team to everyone who has used EF over the years!
Arthur Vickers | Andriy Svyryd | Brice Lambson | Jeremy Likness |
Maurycy Markowski | Shay Rojansky | Smit Patel |
Thank you to our contributors!
We are grateful to our amazing community of contributors. Our success is founded upon the shoulders of your efforts and feedback. If you are interested in contributing but not sure how or would like help, please reach out to us! We want to help you succeed. We would like to publicly acknowledge and thank these contributors for investing in the success of EF Core 6.0.
Is there a way to find out how many properties/relationships there are in a model? I was super excited to get this working after my
net6.0
/EFCore6 upgrade which occurred today, but after implementing it my startup time isn’t impacted all that much. Super bummed and wondering if I have overlooked something. I double-checked the directions twice. Thrice, even. 😛Jeremy, thanks for such an amazing article.
This request is totally out of the scope, but I would love to have a good deep reading about why Compiled Models is necessary. I mean, I d love to “fully and really” understand it!
Thanks!
Minor issue, but in the complex LINQ query, there is this typo:
a1FirstName = a2.FirstName,
Obviously, it should be a1.FirstName
Great catch! Updated.
By change tracking proxies is that the ability to pull down an entity change it and have it automatically save it when you call SaveChanges on the context or something else? This seems very promising for performance.
Not exactly. Here is the documentation for change tracking proxies. In essence it enables you to create a POCO and generate a proxy that implements INotifyPropertyChanged for data-binding purposes.
Would you be able to change the colours for the lines in the line graph please? They’re very difficult to tell apart.
I replaced with a higher contrast image. Please let me know if that works for you.
Thanks for this feedback. I’ll work on getting a high contrast version posted.
I had to deal recently with an EF Core 5 project, for a web application. I was called into action because the project took about 26 hours to load some CSV files (yes, some of them were humungous). I ended up rewriting all those insertions and search with stored procedures. The whole 26 hours shrinked into 6 minutes.
The problem: no support at all for dealing with stored procedures in EF. I had to write them manually, create parameters manually and bind it to objects manually. And, of course, I just used good old ADO.NET DBCommands and the like.
One problem solved, and then another showed its ugly face. Querying all those rows and composing a very simple metric was another nightmare in terms of speed.
So, I’m honestly asking the team: what is EF Core good for? Insertions are not its thing. Dapper is faster, even after all the tweeking. When you need a stored procedure, you’re a voice calling in the wilderness. Right now, my opinion is that EF Core is just a toy for OOP and DDD talibans, for floral games and the like. But it’s not useful for real life tough and mean projects. Am I wrong, please?
Ian,
Thank you for your feedback. What does your ideal support for stored procedures look like? We welcome you to upvote any existing request or submit a new one for consideration so that we can improve that experience.
For your query issue, where did you find the speed bottleneck? Was it translation to the SQL from LINQ, was the resulting query not materialized the way that you expected, or was it something else? If you are able to file details in an issue we can see if there is a way to address your scenario.
Regards,
Jeremy
It would be perfect if we could replace CRUD operations with stored procedure calls. But I could even do better with some tooling for dealing with store procs. I wrote a procedure with 125 parameters, and I had to create and bind manually all those parameters.
EF Core Power Tools can map stored procedures for you!
In terms of trying out the Preview 5 implementation of this in our codebases – and since this will persist compiled models that we commit to our repos – can we count on the persistence format being stable / backwards-compatible once the full EFC 6 is released? Or could we potentially be generating compiled models which would not be usable by future RTM releases?
Hi,
Like other generated code, the expectation is that you would have to regenerate the model at a minimum each minor release. There is an extensibility point being made so that you can customize the model without having to rewrite the code every time the compiled model changes. The extensibility API is expected to be backwards compatible through future releases.
Jeremy