Lately, it seems that each iteration of EF Core brings fabulous new features and improvements. That has a lot do with the fact that the team has made a big investment in creating a stable base to build on. Although EF Core 7 is being released alongside .NET 7 and ASP.NET Core 7, it targets .NET 6, which is the long-term support version of .NET. So you can continue using it on a supported version of .NET for that longer term.

I've been overwhelmed in trying to choose which of its features to share with you here. There are so many that are interesting. Not only does it mean writing about them, but I also get to test them all out, which is quite a lot of fun, thanks to the fact that I don't have to do so with the goal of releasing production code.

You'll find this article filled with some of the features that will be most impactful to the bulk of dev teams as well as a few that I personally found interesting.

Although I will always refer to this version as EF Core 7, keep in mind that much of the documentation and other resources will use EF7 as its nickname. I still remember that first version of EF Core, just after EF6, which had a working name of EF7 until it became EF Core. So, I may wait until EF Core 8 to use the new nickname.

Faster and Faster!

Back in 2021, one of the biggest stories for EF Core 6 was the dramatic performance improvement for non-tracking queries. At that time, the team committed to focusing on improving the performance of other workflows in EF Core 7. And true to their word, there was a lot of work done on updates that EF Core sends to the database.

Shay Rojansky, who has become “the performance guy” on the EF team, has explored the many nooks and crannies within SQL sent to the database and other related areas, discovering many points at which efficiencies could be applied. Some of the inefficiencies he discovered hailed back to the early days of Entity Framework. In an EF Core Community Standup earlier this year, Shay walked us through a fascinating look at the discoveries he'd made and the tunings he applied. Each tuning may have only sped things up a small amount, but they do add up!

Although most of these tweaks are under the covers and you will benefit from them without having to take any action, I would like to highlight some of them for you. However, if you do want to geek out on these changes, I highly recommend watching the standup video here on YouTube (

Reducing Round Trips to the Database

Some of Shay's discoveries were nuances that I hadn't paid attention to. An interesting one is a drawback of EF Core's default transaction behavior. As you may know, EF Core wraps every command sent in SaveChanges inside a database transaction so that if one fails, they'll all roll back. If you only have one command being sent, the calls for the transaction aren't needed because there aren't other commands involved. Therefore, when SaveChanges involves only a single change, rather than sending the three commands (BEGIN TRANSACTION, the change command, and then COMMIT), EF Core 7 only sends the change command, cutting the chattiness down from three commands to one. And when comparing EF Core 6 to EF Core 7 where the database was on a remote server, Shay measured a 45% improvement on the SaveChanges call. Granted this was only a change from about 8 ms to 4 ms, but those do add up in a production application. This is a great example of the types of tweaks made to the updates.

Another tweak is related to inserts. You may be aware of another pattern that's been around since the beginning of EF, and which continued through EF Core, and that's that INSERT commands have always been paired with a SELECT to return the database-generated value of any primary or foreign key. These new keys were then applied to the related objects that EF was inserting. Although this doesn't require EF Core to make an additional call to the database, it does force the database to execute an additional command. Now with EF Core 7, the SQL Server provider compresses all of those into a single command by using an OUTPUT in the INSERT command, rather than an extra command to SELECT.

In other words, instead of this multi-command message from EF Core 6:

INSERT INTO [People] ([Name])
    VALUES (@p0);
    SELECT [PersonId]
    FROM [People]
    AND [PersonId] = scope_identity();

EF Core 7 sends this:

INSERT INTO [People] ([Name])
    VALUES (@p0);

In the case of a single INSERT being sent where EF Core 7 won't wrap this in a transaction, the OUTPUT clause also removes the need for a transaction that was needed around the composed INSERT plus SELECT.

The improvements are not limited to when SaveChanges only sends a single change. Batched commands are also streamlined. Not only do they also lose the explicit BEGIN and COMMIT for transactions, but they're also expressed in a more efficient way.

And if you've ever used EF Core's HiLo feature, parent/child inserts can really benefit from it. Wait what? HiLo? Yeah, me too. I had completely forgotten about this feature, introduced in EF Core 3, to ask SQL Server to pre-generate a bunch of primary keys that EF Core caches and pushes into INSERT commands as needed. Here's the documentation for the UseHiLo method: If you've cached keys with HiLo, EF Core can use them for parent child inserts without having to first send the parent INSERT command in order to get the new primary key for the parent to use as the foreign key for the child object(s). This means that these inserts can now be sent as a batch, which reduces the save from four round trips in EF Core 6 to a single round trip in EF Core 7.

In addition to the video I linked to above, Shay Rojansky's blog post about EF Core 7 Preview 4 details many of these improvements. I'm a big fan of how he builds a story around the changes asking, “what about this?” and “what about that?” The blog post is here:

Some Other Notable Performance Enhancements

Speaking of batched commands, you may recall that EF Core batches commands that are sent with SaveChanges. Based on performance analysis by the team when first designing the feature, the SQL Server provider only batched commands if there were at least four being sent. That's been changed so that the minimum number of commands to batch is now two.

If you use lazy loading via the proxy generation workflow, there are also major performance improvements here. A user reported that enabling lazy loading proxies on a context was creating a huge performance problem for the model builder. That meant that the very first database interaction performed for that context in an application instance was taking an inordinate amount of time. The team changed how the model responded to proxy generation, which resulted in a complete reduction of that extra time caused by UseLazyLoadingProxies. Arthur Vickers relays the EF Core 6 vs. EF Core 7 timings on a complex model in this GitHub issue comment: The chart shows that in EF Core 6, the model in question took 13 times longer to generate when UseLazyLoadingProxies was enabled. In EF Core 7, the time for model generation was equal with or without the proxy method.

Finally, Bulk Updates and Deletes

On the topic of updates, EF Core 7 brings another long-requested feature for pushing changes to the database: bulk updates. How long? The GitHub issue ( was opened in 2014.

Since the beginning of EF time, if you wanted to update a row in the database, you first had to query it, apply the changes to the object, and then call SaveChanges. If you wanted to delete a row, it's a similar workflow: retrieve the object, change its state to deleted, and then call SaveChanges.

Users have long wanted to be able to express something similar to a LINQ query to push the changes directly to the database - something that's more like how you can express updates and deletes in SQL.

After a lot of conversations with the community, the team decided to design this via ExecuteDelete and ExecuteUpdate methods that are appended to LINQ queries in the same way that you'd apply a LINQ execution method. And these are executed immediately, not stored in the change tracker awaiting a call to SaveChanges.

You would write a delete method like this:

.Where(p => p.PersonId == 1).ExecuteDelete();

Although I'm only deleting a single row, you can write your expression so that the delete affects multiple rows.

Update is a little more complicated because it enables you to specify multiple updates to occur. Each change is encapsulated in a SetProperty method. Therefore, ExecuteUpdate expects SetProperty expressions and each SetProperty expression expects an expression with the property and the value. Here, I'm applying perhaps not the most brilliant logic to assume that Lehrman is always a misspelling of my last name and my relatives'.

 .Where(p => p.LastName == "Lehrman")
 .ExecuteUpdate (s => s.SetProperty(c =>c.LastName, c =>"Lerman"));

The resulting SQL is as clear as if you'd written it yourself.

SET [p].[LastName] = N'Lerman'
FROM [People] AS [p]
WHERE [p].[LastName] = N'Lehrman'

These are simple examples, but the team has worked out variations to handle relationships, inheritance, and other more complex scenarios. Check the documentation for more detailed examples.

Mapping Entity Properties to Database JSON Columns

Storing JSON data in a relational database is usually a matter of storing the objects as some flavor of text or char in the database. For example, I may have an nvarchar Measurements column in my People table with JSON data that looks like this:


Most RDBMs, including SQL Server, have a way to query JSON-formatted data as JSON, not as text. I can write the TSQL to query for specific elements: Here I only want the HeightCM data, within the Measurements column:

SELECT personid, firstname,lastname,
    as HeightCM
FROM people

In my .NET solution, I can create a Measurements type:

public class Measurements
    public int HeightCM { get; set; }
    public int ShoeUK { get; set; }

I can use JSON conversion anytime I want to store the data from type as a JSON string in my Person class. That way I can work with a tidy class in C# and still have my JSON formatted text stored in the database.

    Person.Measurements = JsonSerializer.Serialize
        (new Measurements{HeightCM=188,ShoeUK=7});

When I retrieve Measurements in any query, I'll have to deserialize it back into the Measurements type to work with it in my code.

It's already cumbersome to serialize and deserialize, but worse yet is querying with LINQ. You have to query the string, not the type. But how? How can you use a string query method to retrieve just the HeightCM, or worse, to retrieve all people whose HeightCM is greater than 180? You can't do that with LINQ. Either you have to retrieve more data than you want and then apply the filter in the client-side code, or you have to send raw SQL, or perhaps use views or stored procedures.

Because of this, direct support for JSON columns has been a highly requested feature for EF Core. Finally with EF Core 7, it had risen to the top of the to-do list and thanks to work done by Maurycy Markowski on the EF Core team, it's supported in this version. According to Markowski, the feature is pretty basic in this version, but it provides the framework for deeper implementation in the future.

The keys to this support lay in the combination of leveraging EF Core-owned types and the database providers translating queries into SQL that reflects how their database queries JSON data.

This also means that you now have another way of persisting value objects with EF Core. Owned entities have given you a path for storing value objects in a relational database where the properties of the value object get split out into additional columns in the table along with the type that “owns” that property. Now the value object can be more neatly encapsulated into a JSON object in a single database column.

You need to apply two mappings to the Person.Measurements property in OnModeling. The OwnsOne mapping has an overload that allows you to further specify the relationship of the owned property using an OwnedNavigationBuilder. This builder has new overload allowing you to specify that the property is a JSON column.

  .OwnsOne(p=> p.Measurements,
          jb => { jb.ToJson(); });

Here I add two new person objects:

var personA = new Person
    FirstName = "Maurycy",
    LastName = "Markowski",
    Measurements = new Measurements { HeightCM = 188, ShoeUK = 8 }};
var personB = new Person
    FirstName = "Katrina",
    LastName = "Jones",
    Measurements = new Measurements { HeightCM = 170, ShoeUK = 7 }};

Once I've added them to the context and called SaveChanges with this mapping in place, the Measurements data is compressed into JSON and stored into the Measurements column (Figure 1).

Figure 1: Measurements values are stored as JSON in the nvarchar column.
Figure 1: Measurements values are stored as JSON in the nvarchar column.

And because Measurements is a type in my system, I can construct queries that are aware of its properties.

var tallpeople = context.People

The real magic comes in EF Core and the provider's ability to transform this into SQL. In this case, TSQL:

SELECT [p].[PersonId], [p].[FirstName], [p].[LastName],
FROM [People] AS [p]
WHERE CAST(JSON_VALUE ([p].[Measurements],'$.HeightCM') AS int)>180

More patterns related to this are supported, including collections and layers of objects (e.g., grandchildren) that are stored as tiered JSON documents in the nvarchar column. Check the documentation for further examples.

Mapping Stored Procedures Just Like EF6

In the original Entity Framework, you had the ability to map stored procedures to entities. When you called SaveChanges, as long as you followed the basic rules, EF called your stored procedures, pushing in the parameters rather than generating its own SQL. Bringing this feature to EF Core has been on the back burner for quite some time but now, with more critical features out of the way, the team has implemented this capability into EF Core 7.

In EF, I recall some convoluted UI for doing this mapping, although I have zero impetus to pull out one of the old 1000-page EF books I wrote to remind myself how that worked.

It's much simpler in EF Core 7. There are simple and discoverable FluentAPI mappings called InsertUsingStoredProcedure, UpdateUsingStoredProcedure, and DeleteUsingStoredProcedure that you apply to an entity in OnModelCreating.

Each method takes a string to identity the procedure name and a StoredProcedureBuilder that's comprised of one or more parameters where you identify the entity properties that align with the parameters via matching names. Each method is constructed a bit differently based on its nature.

As an example, here is a simple insert stored procedure:

    @personid int OUT,
    @firstname nvarchar(100),
    @lastname nvarchar(100)
    INSERT into [People] (FirstName, LastName)
        Values (@firstname, lastname);
    SELECT @personid = SCOPE_IDENTITY();

The StoredProcedureBuilder for an insert starts with the procedure name and then the lambda for the StoredProcedureBuilder. For the parameters, I used lambdas to express each property of Person that maps to the parameters and did so in the order expected by the procedure. Additionally, I used an overload to further specify the first parameter - that it's an output parameter and because the parameter name doesn't match the property name, I specify that the name is “id”. You may already have noticed this:

        spbuilder => spbuilder
            .HasParameter(p => p.PersonId, pb => pb.IsOutput().HasName("id"))
            .HasParameter(p => p.FirstName)
            .HasParameter(p => p.LastName)

If you're also mapping the update and delete, you can compose them together. Another improvement over the way this was in EF is that you aren't required to supply all of the mappings in order for any of them to work. For example, if I only have a mapping for updates, that procedure is used and EF Core composes SQL for inserts and deletes.

There are additional methods that you can use with the StoredProcedureBuilder besides HasParameter: HasOriginalParameter, HasResultColumn, and HasRowsAffectedParameter.

Enabling Value Generation on Value Converters Used for Key Properties

This is an important change to EF Core 7 for developers following practices learned from Domain-Driven Design (like me!). Let me start by explaining what this means. By now you know what key properties are in EF Core. Most often you see a key defined as an int or a GUID.

public class Person
    public int PersonId { get; set; }

Int is commonly used for relational databases that can generate those integers for you. GUIDs give you more control over keys on the client side. You can generate them at the same time you create new objects without waiting on the database to provide those values for you. In the scenario above, EF Core creates a temporary value for PersonId (seen only by EF Core's internals) while awaiting that database-generated value. You can at least make the setter private to protect from developers accidentally setting the PersonId property to some random value, which could cause a conflict in the database. There are ways around that protection. Imagine that you have so many people in your database that you run out of ints and decide to switch to GUIDs. That's a difficult change to make so far into your application's history.

A common practice, especially among developers following guidance and practices from Domain-Driven Design, is to create a value object that you use as the type for the key property.

Here's an example of a new type I created and named EntityKey that's then used as the type for the Person class' PersonId:

public class EntityKey
    public EntityKey(int id) => Id = id;
    public int Id { get; private set; }
public class Person
    public EntityKey PersonId { get; set; }
    . . .

This gives some nice advantages. For example, if I need to change the EntityKey Id property to a GUID, it won't impact the Person type at all. The Person type doesn't care about how EntityKey is implemented. Read more about some advantages of using Value Objects for key properties at Nick Chamberlain's blog post here:

Back to EF Core. EF Core knows how to handle ints and GUIDS as keys but it doesn't know how to store your custom-generated EntityKey type. Value converters, introduced in EF Core 3, provide what looks like a possible solution. You can tell EF Core that when it's time to save a Person object, it should use the Id property of the PersonId property as the value to persist. And when querying Person types, EF Core should take the int that's stored in the table and create an EntityKey from it (using that constructor defined in EntityKey) then set that as the value of PersonId. All this is defined in this HasConversion-fluent API method.

    .Property(c => c.PersonId)
        v => v.Id,
        v => new EntityKey(v))

It's a brilliant solution, but up through EF Core 6, EF Core couldn't combine its value-generation capabilities with the conversion. Steve Smith, my brainy co-conspirator on the Pluralsight course Domain-Driven Design Fundamentals, brought this up way back in 2018 in this GitHub issue: EF Core complained, saying that it doesn't have a value generator for EntityKey and the error message suggested that you should set the key's value in code.

Perhaps you've figured out where this is leading: EF Core 7 now supports the combination of value converters with value generation. You must have that ValueGeneratedOnAdd() method or you'll get a runtime exception saying that the ChangeTracker isn't able to track an EntityKey type.

Back to the Future: Intercepting Object Materialization and Other New Interceptors, Too

Why “back to the future”? For you long-time users of Entity Framework, before EF4 gave you POCO support and the DbContext, you had a more tightly coupled way of implementing EF using an ObjectContext. Through that API, you had access to an ObjectMaterialized event handler that allowed you to inject your own rules and logic as the object was being created from query results. If you wanted to access it when using EF6 DbContext, you'd have to drill into the low-level ObjectContext. But since EF Core, there is no ObjectContext and you've never had a way to override the behavior.

Until now. Huzzah! You finally have this capability with EF Core 7 by way of interceptors. Interceptors were another great feature of EF6 that took some time to find their way into EF Core 3. Now EF Core 7 adds a slew of new interceptors to allow you to add your own logic to low-level actions. These interceptors allow you to:

  • Override object materialization
  • Modify the LINQ expression tree
  • Affect how optimistic concurrency is handled
  • Tap into additional points in the lifecycle of connections and commands
  • Muck with query result sets

Arthur Vickers details the various new interceptors in his blog post at

I'll focus here on the object materialization interceptor, aka the IMaterializationInterceptor. This interceptor allows you to tap into the pipeline before and after materialization. In other words, once EF Core has instantiated the object but hasn't yet pushed the query result values into it. I'll dig a little deeper into this interceptor, which should also give you an idea of how you can do the same with the other interceptors.

There are four interception points in this interceptor; before and after the new instance is created and, once created, before and after the instance is initialized.

Let's take a look at each of these methods.

I've created a class that implements the IMaterializationInterceptor interface (see Listing 1) and implemented all four methods without adding any of my own logic, so each returns either the InterceptionResult or the entity by default.

Listing 1: The IMaterializationInterceptor interface's returns

public class MyMaterializationInterceptor: IMaterializationInterceptor
    public InterceptionResult<object> CreatingInstance(
        MaterializationInterceptionData materializationData,
        InterceptionResult<object> result)=>result;

    public object CreatedInstance(
        MaterializationInterceptionData materializationData,
        object entity) =>entity;

    public InterceptionResult InitializingInstance(
        MaterializationInterceptionData materializationData,
        object entity, InterceptionResult result)=> result;

    public object InitializedInstance(
        MaterializationInterceptionData materializationData,
        object entity) => entity;

The value of the HasResult property of the result returned by CreatingInstance is false but it's materializationData object has access to the DbContext instance and the EntityType, which gives you the ability to affect anything within those objects. All of the methods expose the materializationData object.

When CreatingInstance is hit, you have access to the instance of the object being created, although its properties have not yet been populated. And it's this entity that's returned by default from the method.

Next, the InitializingInstance method gets hit immediately after the property values have been created but not yet populated. It returns an InterceptionResult (note that this one isn't generic) that has one read-only property, IsSurpressed, which is false. Query results overwrite any changes you make to the entity properties here. It's best to make those changes in the next method. However, if you have unmapped properties, any values you apply to them here will remain. In his Preview 7 blog post referenced above, Vickers uses an example of a property for audit data to note when the data was retrieved from the database. He then populates that Retrieved property in the InitializingInstance method.

Finally, there's the InitializedInstance that receives the populated entity object as a parameter.

Keep in mind that if you have related objects or an owned type, such as the Measurements type from the JSON column support example above, those will be materialized separately. Therefore, if you affect the Measurements property of a Person as the Person is being materialized, that property will be overwritten when the Measurements object is being materialized.

This interceptor isn't solely for modifying results or entities. You might use it to trigger application events or other relevant actions. But like any tool, whether for coding or building a doghouse, take care in how you apply it. As Khalid Abuhakmeh warns in a blog post about EF Core 5 interceptors (, you should be careful about adding resource-intensive logic in any of the interceptors, as well as triggering unwanted side effects.

Support for Database Specific Aggregate Functions

EF Core is designed to enable common features across databases. EF Core 7 now allows database providers to expose provider-specific aggregates that the provider knows how to translate into their own flavor of SQL. You'll find these in the EF Functions extension that has already been exposing methods such as SQL Server's CONTAINS, RANDOM, and a number of date functions.

Thanks to the change in EF Core 7, the SQL Server provider adds string.Join, string.Concat, and some methods for you statistics seekers, methods to translate to some TSQL functions I've never used in my very lengthy career: StandardDeviationSample (STDEV in TSQL), StandardDeviationPopulation (STDEVP), VarianceSample (VAR), VariancePopulation (VARP). SQLite also benefits from string.Join and string.Concat.

Shay Rojansky is not only a member of the EF Core team but also a long-time maintainer of PostgreSQL providers for .NET, EF, and EF Core. In addition to working on the other functions, he has added quite a few aggregate methods to the PostgreSQL provider for EF Core for strings such as filtering and ordering, JSON, arrays, ranges, and some statistics as well. For the curious, learn more in this GitHub pull request discussion:

You may wonder how string.Join is new. You've always been able to write a query like this:

context.People.Select(p=>string.Join(",",p.FirstName, p.LastName)).ToList();

That returns a list of joined names such as:

Julie, Lerman
Shay, Rojansky

What's new here is that you can use them in GroupBy expressions. Here, for example, is a query where I want to group by LastName and then create a comma-delimited list of the first names in that group.

Var groupedpeople = context.People
    .GroupBy(p => p.LastName)
    .Select(surname => new
        Last = surname.Key,
        firstnames = string.Join(",",surname.Select(p => p.FirstName))

Given that I've seeded the database with three people, two with the last name of Jones, here are the results of this query:

Jones: Katrina,Serena
Markowski: Maurycy

Shay discusses and demonstrates a number of the new aggregate features for EF Core 7 in the August 25, 2022 Community Standup here on YouTube:

More EF 6 Parity

In each iteration of EF Core, the team works toward bringing more parity with features we loved and relied on from EF6. Here are some that have been implemented for EF Core 7.

Entity Splitting

Entity splitting is a mapping that allows you to persist properties of a single entity across multiple tables or views.

There are Fluent API and data annotation mappings for this. Here's an example of mapping a few properties from the Person type into a separate table called PeopleLastNames using the new SplitToTable method. I'm letting convention take care of naming the core table. For views, there's a method called SplitToView.

    .SplitToTable("PeopleLastNames", s => s.Property(p => p.LastName));

This mapping creates a second table with its own PersonId column that's a primary key as well as a foreign key pointing back to PersonId in the People table (Figure 2).

Figure 2: Entity Splitting creates a separate table for specified properties.
Figure 2: Entity Splitting creates a separate table for specified properties.

You can specify multiple properties to split out by using an expression function in the lambda as follows:

      s => { s.Property(p => p.LastName);
             s.Property(p => p.FirstName);

With an eye always on persisting classes that are designed following Domain-Driven Design, entity splitting also means that you now have a variety of ways to persist value objects:

  • As separate columns in the same table as the host entity
  • As a JSON document in a single column of the host entity's table
  • As a separate table with individual columns for each property of the value object along with the primary key column
  • As a separate table with the primary key column and a single column containing a representative JSON document

In the future, EF Core will let you more easily use value conversions to store value objects ( but the added flexibility in EF Core 7 via JSON column support and table splitting combined with owned-type support continue to allow you to persist value objects in relational stores.

EF Core 7 as a Part of Distributed Transactions

If you used EF6 or earlier versions, you may be familiar with the support for including EF's SaveChanges in Windows' distributed transactions. A limitation in .NET Core has prevented this for some time, but once again, Rojansky came to the rescue and fixed this in the dotnet runtime repository. You can read about this change at I haven't written about EF and distributed transactions in a long time (! But now, with or without EF Core in the mix, you can combine transactions from a variety of systems in a single transaction.

Table Per Concrete Type (TPC) Mapping

TPC was always “the red-headed step-child” of inheritance mappings. In EF Core, as it was in EF, TPC was the last to be implemented and has been overlooked by many developers. Vickers reminds you that it's a much better strategy than the more popular Table Per Type (TPT). TPC was supported in EF6. EF Core arrived with Table per Hierarchy (all columns across an inheritance hierarchy in a single table). Then EF Core 5 brought you TPT (where the unique properties of each derived type are stored in their own tables). Now, finally, TPC has arrived with EF Core 7. TPC stores each complete derived type in its own table. To learn more, check out the EF Core 7 Preview 5 announcement blog post that reviews pros and cons of these various mappings (

Define Your Own Scaffolding Rules with T4 Templates

Do you remember T4 templates? They're yet another language syntax to learn but don't worry, it's not YAML. Templates are the underpinnings of how EF Core scaffolding is able to reverse-engineer databases into a DbContext and entity classes. Back in the days of yore when EF was not EF Core, you could use T4 templates to customize how to build your models from your databases. This is really handy when you find yourself adding (or removing) the same code time and time again after scaffolding a database.

Brice Lambson was then, as he is now, the go-to guy for T4 templating on the EF team. He and other team members showed off the work he's done on this feature in this April 2022 Community Standup (, if you want to see some great demos of how you can generate your finely tuned DbContext and entity classes when scaffolding databases. There's tooling for doing this in Visual Studio 2022 (written by Brice) and command line tools where you can tell EF Core scaffolding to use your templates instead of the default. I wasn't surprised to see Arthur Vickers create a customization on DbSet. Arthur isn't a big fan of the null bang (!) used to satisfy null reference settings.

Public virtual DbSet<Person> People { get; set; } = null!;

Instead, he customized the template to use his preferred pattern by removing the getter and setter and just returning an instance of the DbSet.

Public virtual DbSet<Person> People => Set<Person>();

Override EF Core's Conventions with Your Own

EF Core 6 brought the ability to apply bulk configurations, which is wonderful. You can override the ConfigureConventions to apply things like HaveColumnType to all properties in the model that are strings, instead of doing it per property in each relevant entity.

But there was something else from EF6 that we've been hoping for: a more sweeping way to affect conventions. That's finally come to EF Core 7 and in fact, creating the ConfigureConventions method in EF Core 6 was part of the preparation for this feature, referred to as “public conventions” because the conventions are now publicly exposed.

With the conventions now public, you can now remove or replace built-in conventions as well as add completely new ones.

The set of built-in conventions is exposed in the ModelConfigurationBuilder that's passed into the Configure Conventions method. You can even take a look at them by drilling into the configurationBuilders Conventions property.

Protected override void ConfigureConventions
  (ModelConfigurationBuilder configurationBuilder)
    var cs = configurationBuilder.Conventions;

The various conventions are grouped. For example, the ForeignKeyAddedConventions contains a collection of six conventions, one of which is the KeyDiscoveryConvention.

You can remove a convention, which also means that if you don't know what you're doing, you could really mess up your data model. But let's say you want database tables to match the names of the entity classes, not the DbSets. You could remove the TableNameFromDbSetConvention.


Or perhaps you have a configuration rule that needs to be applied in all of your DbContexts in all of your apps. Perhaps all strings for your SQL Server database should default to nvarchar(200), rather than nvarchar(max). You can create a convention for that in a class, add it to your project, and then add it to the configuration builder.

Adding conventions is a little more complicated because different conventions are applied at different stages of model building and these stages are identified via different interfaces. Quite often, it's simplest to add your conventions when the model is finished with applying conventions by implementing the IModelFinalizingConvention. Do keep in mind that mappings are always applied after conventions, so you might have mappings that override your custom conventions.

There's a new class to define a custom MaxLengthConvention limiting text-based data columns to 200. I can reuse this class across many DbContexts in one or more solutions.

My MaxStringLength200Convention class (Listing 2) employs what might be a familiar pattern of searching the metadata for strings, and then setting the HasMaxLength mapping for those strings.

Listing 2: The MaxStringLength200Convention class

public class MaxStringLength200Convention :IModelFinalizingConvention
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder,
    IConventionContext<IConventionModelBuilder> context)
        foreach (var property in modelBuilder.Metadata.GetEntityTypes()
          .SelectMany(entityType =>
                property => property.ClrType == typeof(string))))

With the class in place, you can now add the convention. The Add method takes a lambda but as you don't need to reference the lambda in the expression: You can simply use an underscore as the lambda variable.

Here's the updated ConfigureConventions method:

protected override void ConfigureConventions(
    ModelConfigurationBuilder configurationBuilder)
      .Remove(typeof (TableNameFromDbSetConvention));
      .Add(_ => new MaxStringLength200Convention());

So Much More to Explore

It's never possible to include all of the changes in a single article and there are so many more improvements and new features in EF Core 7 that have caught my eye. The EF Core team has created a lot of great resources and documentation that I highly recommend checking out. In addition to the docs at, there are a lot of great details to glean from their GitHub repository ( The bi-weekly updates ( have very detailed lists of changes and, of course, filtering on milestones is also very useful. But also be aware that you can move to EF Core 7 just for the performance benefits without having to worry much about breaking changes. The list of breaking changes is short and you can view it at