Investigating the performance benefits of EF Core 6.0 compiled models feature
Tags: dotnet, csharp, performance, entityframeworkcore
Published: 2022-05-20 10:21:40

With Microsoft's fifth preview of Entity Framework Core 6.0 released last year, Compiled Models were introduced for the first time. The focus, here, was to speed up cold startup times using this feature. 

“Compiled models will allow the generation of a compiled form of the EF model. This will provide both better startup performance, as well as generally better performance when accessing the model.” — [Plan for Entity Framework Core 6.0](https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-6.0/plan)

This article will explore this feature in more detail - where it fits in EF Core, how it works and the benefits it offers. We will evaluate these benefits via our own investigation, which draws on a database from one of our existing projects. The database in question contains many entities, tables and relationships, making it a perfect use case for testing out the effectiveness of compiled models.  

## 1. EF Core 6.0

EF Core is an Object/Relational Mapping (O/RM) framework for .NET applications that developers use to access a database. The framework is an enhancement to ADO.NET that provides developers with an automated way to access and store data in a database. 

Described as being “open-source, lightweight and extensible”, the main difference between EF Core and its predecessor is that this updated version is compatible across Linux and OSX as well as Windows. 

This framework fits between your domain and database, providing a feature called a DbContext. This feature represents a connection to a database and can be used to query and save instances of your entities; it acts as a link between the domain model and the database, allowing developers to fluidly configure how mappings work. 

Entity Framework is responsible for opening a connection to the database, and can subsequently perform read/write actions on the dataset via interacting with items in a given DbSet.

It used to be that all mapping had to be done manually - a timely and drawn out process. In order to read data from the database, a developer would have to create a connection, exit the computer command, and then remember to close the connection. Today, with the introduction of ORM, this is no longer necessary, so software development teams can with greater efficiency.

### 1.1. EF Core 6.0 Features
The EF Core framework offers many additional advantages, such as the ability to write strongly typed LINQ queries as well as work with data using objects of domain-specific classes instead of the underlying database tables and columns (where this data is stored).

You can add, modify and remove data using your Dbcontext. The SaveChanges method allows you to commit any changes made to your data.

Asynchronous saving is also provided in EF Core with the SaveChangesAsync() method, which prevents threads from being interrupted whilst changes are being written to the database.

Optimistic Concurrency is also defaulted to prevent overwriting of changes made by another user after data is retrieved from the database. By doing so, you are ensuring that the record being updated or deleted still contains the same values as it did when the process started.

Data from Web Framework Benchmarks shows that EF Core 6.0 now performs [70% faster](https://www.techempower.com/benchmarks/#section=data-r20) than its predecessor, which further highlights the all round improvements in this latest update. 

## 2. Compiled Models
Compiled Models is a feature that has been introduced in version 5.0 with the intention of improving cold startup times for models. This feature is particularly useful when you are dealing with a large model with many properties and relationships. 

Initially, EF Core interrogates the database to understand these relationships in more detail. Compiled Models enable you to carry out this process statically during development. Using a very simple command, you can analyse your model and generate code that reduces cold startup speed.

`dotnet ef dbcontext optimize -output-dir MyCompiledModels --namespace MyCompiledModels`

This command will allow you to optimise the DbContext and generate a Compiled Model.  When EF Core compiles the model, it finds your configuration and creates an instance in your DbContext. A model is generated from this instance, and the compiled model code is then generated using this model.

This command works in a similar way to when you’re adding and generating a migration. For this reason, Microsoft advises that the model is refreshed whenever your model is changed. 

From here, we can tell our code that we want to use our model with this function: 

`options.UseModel(MyCompiledModels.BlogsContextModel.Instance);`

Now that we have some idea of how to get set up with Compiled Models, let's evaluate some of the benefits that this feature can bring. 

### 2.2 Compiled Models Benefits
For many users operating with a reasonable size model, Compiled Models don’t necessarily offer any significant improvements. However, if you are in the process of building your model and working toward executing your first query, then Compiled Models offer the great advantage of reducing cold startup times for your application — a benefit that remains constant as your model grows bigger. 

The same evaluation applies to Model Size. Functionality here is more suited to models with lots of entity types, properties, and relationships which would otherwise operate very slowly. For example, Microsoft conducted an investigation on a model with 449 entity types, 6,390 properties and 720 relationships, where they reported [10x faster](https://devblogs.microsoft.com/dotnet/announcing-entity-framework-core-6-0-preview-5-compiled-models/) startup times using Compiled Models (see image below). 

![Startup time by model size chart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zqdllub536p1vzztlwsf.jpg) Image from [Microsoft](https://devblogs.microsoft.com/dotnet/announcing-entity-framework-core-6-0-preview-5-compiled-models/)

Using Compiled Models also offers the advantage that you don't have to constantly change the model during development. Providing the model stays the same, you can run the application without compiling it — a great timesaver when it comes to loop time. 

However, this point also alludes to a disadvantage with Compiled Models; each time your model changes, you need to manually synchronise it.

So compiled models are not necessarily useful for everyone, but for those big big models, it promises great benefits for cold startup time. To put this to the test, the next part of this article will investigate a database from one of Audacia’s existing projects — a database with several hundred tables and relationships.  

The intention, here, is to investigate the performance benefit of upgrading an existing project to EF Core 6 & using compiled models, for a project with a large schema.

## 3. Investigation
In order to select an appropriate project for a case study, we needed to find a project with a large enough DbContext, which is already running on .NET 6. We selected a few projects that fulfil these criteria, and found a marked improvement on the projects with more entities & relationships set up. This leads you to think that the larger your schema, the more benefit you would get from this feature.

The selected project was one with over 300 entities, the vast majority of which correspond to its own table in SQL. 

By following the steps outlined in part 2, we enabled compiled models on this project and were ready to test the impact.

### 3.1. Example Application
It was easy enough to write a small amount of code to assess whether this improves start up performance. With the below code, we set up a stand-alone console application in the the solution, which does the following:

1. Creates a `DbContextOptionsBuilder` for the context which contains the entities mentioned above.
2. Tells the builder to use the compiled model generated by the `optimize` command.
3. Instantiate the context.

```csharp
Console.WriteLine("Starting application");

for (var i = 0; i < 5; i++)
{
   var stopWatch = new Stopwatch();
   stopWatch.Start();
   var builder = new DbContextOptionsBuilder<DatabaseContext>()
           .UseSqlServer("***")
           .UseModel(DatabaseContextModel.Instance);

   using var context = new DatabaseContext(builder.Options)
   Console.WriteLine($"{nameof(DatabaseContext)} instantiated after
   {stopWatch.Elapsed:c}");
   
   stopWatch.Stop();
}
```

### 3.2. The Results 
In order to test the difference, we ran this program before we created the compiled model, which meant omitting `.UseModel(DatabaseContextModel.Intance)`. This produced the following results:

```
Starting application

DatabaseContext instantiated after 00:00:02.1330204

DatabaseContext instantiated after 00:00:00.0006707

DatabaseContext instantiated after 00:00:00.0001553

DatabaseContext instantiated after 00:00:00.0002751

DatabaseContext instantiated after 00:00:00.0000814
```

As you can see, the first time it’s instantiated takes far longer than the subsequent iterations. This demonstrates the difference between a cold start (first instantiation) and a warm start (subsequent instantiations). This is typical of the first time an EF Core DbContext starts up, and is something that Compiled Models aims to reduce.

Now, setting up Compiled Models and telling the builder to use it results in the following:

```
Starting application

DatabaseContext instantiated after 00:00:00.4312566

DatabaseContext instantiated after 00:00:00.0070515

DatabaseContext instantiated after 00:00:00.0001587

DatabaseContext instantiated after 00:00:00.0000955

DatabaseContext instantiated after 00:00:00.0000988
```

We’ve therefore reduced our startup time fourfold, from over two seconds to under half a second. It’s encouraging to see this working in practice on one of our projects. We would expect this to only improve should more entities and relationships be added to the DbContext.

As a result of this, having knowledge of when/how your application starts up can be really important. For example, if you’re on some sort of consumption-based plan in a serverless architecture in which applications shut down automatically, you might find that your application does a cold start each time it’s invoked. Reducing this startup time could therefore have a large financial benefit as it would spend less time instantiating the context.

## 4. Conclusion
Compiled Models is a new EF Core 6 feature which vastly improves the startup times of DbContexts with large schemas. This would be a very useful tool for legacy systems with a lot of information in a single DbContext. However, one could make the argument that once you have a DbContext with this number of entities, problems like initial start up time wouldn’t be your main concern, as having that amount of information in a single data store presents its own risks.

At Audacia, we’re constantly looking at ways we can improve our new and existing applications. Features like Compiled Models would be a great incentive for a large application to upgrade to .NET 6, and demonstrates Microsoft’s appetite for continuous improvements.

## Acknowledgements
This article was written by [Jack Gill](https://uk.linkedin.com/in/jack-gill-028895156) and produced in collaboration with Principal Consultant [Owen Lacey](https://audacia.co.uk/technical-blog/team-stories-owen-lacey-principal-consultant) who provided his expert insight for the investigation.