Announcing Entity Framework Core 6.0 Preview 5: Compiled Models

Jeremy Likness

Jeremy

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 the context.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):

EF Core initialization steps, Announcing Entity Framework Core 6.0 Preview 5

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.

Startup time by model size, Announcing Entity Framework Core 6.0 Preview 5

Here are some cons to consider:

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!

ajcvickers Arthur Vickers AndriySvyryd Andriy Svyryd Brice Lambson JeremyLikness Jeremy Likness
maumar Maurycy Markowski roji Shay Rojansky smitpatel 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.

AkinSabriCam alexernest alexpotter10 Ali-YousefiTelori
#1 #1 #1 #1, #2
alireza-rezaee andrejs86 AndrewKitu ardalis
#1 #1 #1 #1
CaringDev carlreid carlreinke cgrevil
#1, #2 #1, #2 #1, #2 #1
cgrimes01 cincuranet dan-giddins dannyjacosta
#1 #1, #2, #3, #4 #1 #1, #2
dennisseders DickBaker ErikEJ fagnercarvalho
#1, #2, #3, #4, #5, #6 #1 #1, #2, #3, #4, #5, #6, #7, #8, #9, #10, #11, #12 #1, #2
FarshanAhamed filipnavara garyng Geoff1900
#1 #1, #2 #1, #2, #3 #1
gfoidl Giorgi GitHubPang gurustron
#1, #2 #1, #2, #3, #4 #1 #1
hez2010 HSchwichtenberg jaliyaudagedara jantlee
#1, #2 #1 #1, #2 #1
jeremycook jing8956 joakimriedel joaopgrassi
#1 #1 #1, #2 #1, #2
josemiltonsampaio KaloyanIT khalidabuhakmeh khellang
#1 #1, #2, #3, #4 #1, #2 #1
koenbeuk kotpal larsholm lauxjpn
#1 #1 #1, #2 #1, #2
leonardoporro lexkazakov mariuz marodev
#1 #1 #1 #1, #2
MartinWestminster Marusyk MattKomorcec MaxG117
#1 #1, #2, #3, #4, #5, #6, #7, #8, #9, #10, #11, #12, #13, #14, #15, #16 #1, #2 #1
mefateah meggima mguinness michalczerwinski
#1 #1, #2 #1 #1, #2, #3, #4, #5, #6
mrlife msawczyn MSDN-WhiteKnight natashanikolic
#1, #2, #3, #4 #1 #1 #1
nmichels nschonni OKTAYKIR OOberoi
#1, #2 #1, #2, #3, #4 #1 #1
Oxyrus pkellner ptupitsyn ralmsdeveloper
#1 #1 #1 #1, #2
RaymondHuy riscie SergerGood Shirasho
#1, #2, #3, #4, #5, #6, #7, #8 #1, #2 #1, #2, #3, #4, #5, #6, #7, #8, #9, #10 #1
SimonCropp stevendarby Strepto teo-tsirpanis
#1, #2 #1, #2 #1, #2 #1
the-wazz tkp1n Tomkaa umitkavala
#1, #2 #1, #2 #1, #2 #1, #2, #3, #4
uncheckederror Varorbc vincent1405 vonzshik
#1 #1 #1, #2 #1, #2, #3, #4
vytotas wdesgardin wmeints yesmey
#1 #1, #2 #1, #2 #1, #2, #3, #4, #5, #6

13 comments

Leave a comment

  • Marcel Bradea
    Marcel Bradea

    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?

    • Jeremy Likness
      Jeremy LiknessMicrosoft employee

      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

  • Avatar
    Ian Marteens

    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?

    • Jeremy Likness
      Jeremy LiknessMicrosoft employee

      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

      • Avatar
        Ian Marteens

        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.

  • Jamie Saunders
    Jamie Saunders

    Would you be able to change the colours for the lines in the line graph please? They’re very difficult to tell apart.

  • Avatar
    Greg Gacura

    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.

  • Avatar
    George Handlin

    Minor issue, but in the complex LINQ query, there is this typo:

    a1FirstName = a2.FirstName,

    Obviously, it should be a1.FirstName