While the EF Core 2.0 release deserves a lot of criticism, there are also a lot of things to like about it. In this article we touch on some of the highlights from this release.
Table Splitting
A common criticism of ORMs in general is that they tend to be inefficient with the data they request. By default most ORMs perform what’s known as a “SELECT *” query, in which the database is asked for every column even if the application only needs a small subset.
The traditional way to handle this in EF is via a .Select clause in the query that limits the generated SQL to only the needed columns. However this requires either a 3rd party mapping library (e.g. AutoMapper) or by explicitly listing each desired column into a projected object. Since the latter is rather tedious and error prone, developers in a hurry often skip that step. This can result in poor performing queries because the database can’t use covering indexes.
Another problem is that projected objects cannot participate in CRUD operations. They have to either be mapped back to an entity or used in a raw SQL call.
A better way to create narrow queries in EF Core is the use of “table splitting”. This is where you map multiple classes to the same table. Using this in EF Core requires “an identifying relationship (where foreign key properties form the primary key) must be configured between all of the entity types sharing the table”.
Global Filters
EF Core has plugged a major feature gap found in EF, the inability to reliably create global filters.
Global filters allow developers to universally apply an additional filter to all queries that hit a particular table. This is primarily used in soft-delete scenarios, where you don’t want to return rows that have been flagged as deleted but not yet physically removed from the database. Global filters are not a new concept, you see the concept in NHibernate, Tortuga Chain, and other ORMs.
In EF Core, this is implemented using the below code in the OnModelCreating event.
modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);
This syntax does raise some concerns because it needs to be repeated for each and every table that supports the IsDeleted flag. This could be error-prone if you have a lot of tables, but is still significantly better than trying to remember the check in every query.
Global filters can access fields declared on the DbContext object. This means you can use them for advanced scenarios such as multi-tenancy. Extreme care must be taken when doing this with context pools (see below).
Two limitations are mentioned in the documentation:
- Navigation references are not allowed. This feature may be added based on feedback.
- Filters can only be defined on the root Entity Type of a hierarchy.
Context Pools
While significantly cheaper than database connections, creating DbContext objects can still be a performance drain. The ideal solution would be to make DbContext thread-safe, but the design of EF doesn’t allow for that.
To mitigate this for ASP.NET Core applications, EF Core now offers DbContext pooling. This relies on ASP.NET Core’s dependency injection framework.
A warning in the documentation notes that context pools are not compatible with using global filters for multi-tenancy.
Avoid using DbContext Pooling if you maintain your own state (e.g. private fields) in your derived DbContext class that should not be shared across requests. EF Core will only reset the state that is aware of before adding a DbContext instance to the pool.
Since this could easily be solved by a “reset” method or event called by the DI framework, hopefully this will be corrected in a future version.
Scalar Functions
A defining feature of the relational database server is the ability to execute code where the data resides. When used judiciously, this can dramatically improve performance over transporting data to the application before processing.
EF Core 2 adds support for this by exposing scalar functions. Scalar functions are defined in EF models using empty static functions tagged with the DbFunction attribute. The LINQ provider will detect the use of these and generate SQL that uses the matching function on the server.
Currently EF Core only supports scalar functions in this manner. The far more powerful table valued functions are still not available at this time unless you use raw SQL.
String interpolation in raw SQL methods
A very interesting feature is how string interpolation works with EF Core. Rather than simply converting it into a String.Format call, EF Core will emit a parameterized SQL string. This is critical for avoiding SQL injection attacks.
var city = "Redmond";
context.Customers.FromSql($"SELECT * FROM Customers WHERE City = {city}");
SELECT * FROM Customers WHERE City = @p0
We can probably expect the various micro ORMs to adopt this technique as well.
Explicitly Compiled Queries
Queries can now be cached in the form of delegates (i.e. function pointers), usually defined by an anonymous method. Beyond improving performance, this is a convenient way to refer to a query in multiple places. The delegate accepts a DbContext object as a parameter, so it can be used within a multi-statement transaction.
Do note that some caching occurs automatically so the expected performance gains are modest.
Although in general EF Core can automatically compile and cache queries based on a hashed representation of the query expressions, this mechanism can be used to obtain a small performance gain by bypassing the computation of the hash and the cache lookup, allowing the application to use an already compiled query through the invocation of a delegate.
These new features don’t come without a price. In part 3 of our EF Core series we’ll look at the breaking changes in EF Core 2.0.