Although Entity Framework Core (EF Core) 2.1 is a minor release on top of EF Core 2.0, it seems that with this version, EF Core has turned the corner regarding core features, advanced features, and stability. This article looks at some of the new features and improvements in 2.1 and begins by taking a look at the bigger picture of EF Core's goals, why it's moving in the direction that it's going, and where we hope to see EF Core in the future.

Revisiting the EF Core Vision

EF Core has always been about trying to hit a sweet spot between a rich object/database mapper experience while still trying to be simple, lightweight, down to the metal, extensible, and less tied to relational database concepts than other existing products. And it's cross-platform, too! EF Core follows several tenets:

  • You shouldn't have to pay for complexity that you're not going to use.
  • You should be able to target different data stores while still using some of the same basic concepts.
  • Complex abstractions shouldn't get in the way of accessing the full capabilities of the underlying store.

EG6 is a very complex O/RM, at least in terms of the mappings that it supports and how the mapping is implemented. That had some consequences, for example, in terms of memory usage, startup time, and even in the difficulty to evolve the codebase and add new features. That's why EF Core started much simpler. EF Core's approach to inheritance is a good example of this, supporting only Table per Hierarchy (TPH). There are no Table per Type (TPT), no Table per Concrete Type) TPC, and no hybrid inheritance mappings.

As EF Core evolves, some of the features that were present in EG6 are starting to appear. You might interpret this as EF Core catching up with its big brother. But when you take a closer look at the features, you see that they have different inflections and personalities. And it's unlikely that EF Core will ever support everything that EG6 did. Doing so would incur the cost of becoming as complex as EF6.

You can read the list of “guiding principles” the EF team has used in the initial development of EF Core at https://github.com/aspnet/EntityFrameworkCore/wiki/guiding-principles. One of those principles that may be surprising to you is “EF Core prioritizes features based on their individual merit rather than EG6 parity.”

Some examples:

  • FromSql, introduced in EF Core 1, is a lot like SqlQuery in EG6, but is composable, meaning that additional LINQ operators are applied after it can be translated to SQL.
  • Owned entities, which arrived in EF Core 2, are a lot like complex types in EG6, but they have stronger semantics for aggregates and, in the future, they'll support collections.
  • Query types, one of EF Core 2.1's new features, enable projecting SQL queries into types that aren't mapped to a table, but they also enable many other scenarios, including mapping to tables without keys.
  • Lazy loading returns in EF Core 2.1, but uses a different (non-monolithic) architecture, and tries to fix some of the problems of the past, for example: being unsure if your entities are capable of lazy loading or not, or forgetting to make the navigation properties virtual.
  • GroupBy translation debuts in EF Core 2.1 and it works differently from EG6 in some cases. One example there is that it can generate HAVING clauses in the SQL translation.
  • Data Seeding also returns to the fold in EF Core 2.1 but instead of the imperative model of EG6, it uses a declarative model in which seed data is part of the model.
  • System.Transactions support, which was in EF since the very first version, appears in EF Core 2.1. That's finally possible because it was added in .NET Core 2 and is now supported in some of the most important ADO.NET providers.
  • Change tracking events (Tracked and StateChanged) debut in EF Core 2.1.

There are also features that have received a completely new treatment and are far superior to their EG6 counterparts.

  • The set of types you can use in properties in EG6 was fixed. In EF Core 1, the set of types was borrowed directly from the underlying database provider. EF Core 2.1 enables an even richer set of types through value conversions.
  • Alongside lazy loading, the EF team has enabled parameter constructors in entities. Some of us are very happy to no longer need to justify adding private parameterless constructors into Domain-Driven Design models just to make EF happy!
  • EF Core 2.1 includes code analyzers that detect possibly unsafe usage of FromSql and ExecuteSqlCommand.

Another convenient change is that the .NET Core Command Line Interface (CLI) commands (usually employed to manage migrations and to reverse engineering classes from an existing database) have now been moved to the .NET Core SDK, so you no longer need to manually edit project files to have them included.

Exploring Features with Code

Now let's take a closer look at some of the new features. I'll use a simple model of Team and TeamMember to demonstrate the features.

public class Team
{
    public int TeamId { get; set; }
    public string Name { get; set; }
    public string TwitterAlias { get; set; }
    public List<TeamMember> Members { get; set;}
}
public class TeamMember
{
    public int TeamMemberId { get; set; }
    public string Name { get; set; }
    public string Role { get; set; }
    public int TeamId { get; set; }
}

The samples presume that you're using the SQL Server database provider.

EF6 Parity Features

Lazy loading and LINQ's GroupBy support were some of the most (loudly) requested features from Entity Framework to be enabled in EF Core 2.1.

Lazy Loading

There are two ways to implement lazy loading in EF Core starting with 2.1. For those new to EF, lazy loading allows EF Core to retrieve related data, as needed, without you writing additional queries. The first uses proxies, as had always been done with Entity Framework. However, as the proxy logic isn't a core feature of EF Core, it's encapsulated in its own package, Microsoft.EntityFrameworkCore.Proxies, which you'll need to add to your project. You'll also need to signal to your DbContext that you want the proxies to be used, which you must do with the DbContext options. If this is part of an ASP.NET Core application, you can specify it in the AddDbContext method of the startup files Configure method.

services.AddDbContext<TeamsContext>(
    optionsBuilder => optionsBuilder
        .UseLazyLoadingProxies()
        .UseSqlServer(myConnectionString));

If you're configuring your DbContext in its OnConfiguring override, the syntax is:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseLazyLoadingProxies().UseSqlServer(myConnectionString);
}

With lazy loading enabled on the context, any virtual navigation property is overridden under the covers by the proxy at run time.

That means that in order for the Members property of Team to be lazy loaded along with a Team object, you'll need to change its declaration to virtual:

public virtual List<TeamMember> Members { get; set; }

Something that's different from earlier versions of EF is that it can't be selective about which navigation properties can be lazy loaded. You must mark every navigation property in your model as virtual. This is a side-effect of EF Core helping you remember to mark the properties as virtual. Perhaps it will be more lenient in the future.

Now, any reference to the Members property on an existing instance of a Team triggers EF Core to retrieve the members for that object. Even this example requesting the count of the members populates the members and then returns the count.

var team = context.Teams.Find(101);
var memberCount = team.Members.Count();

If you have enabled lazy loading but the model builder can't find any virtual navigation properties, it alerts you. The usual caveats that you've always had with lazy loading still apply:

  • The context needs to be available and in scope in order for it to perform this task
  • Lazy loading is an all-or-nothing means of retrieving related data. If the navigation is a collection, not a reference, it loads all of the related items in that collection from the database.
  • EF Core uses a flag to determine whether related data has been loaded and won't reload even if the data in the database has changed since the first load.
  • Roundtrips to the database are implicit and it's easy to end up executing N+1 queries.
  • Lazy loading is a sync/blocking I/O and can take an arbitrary amount of time. If you want to keep things asynchronous, use eager or explicit loading.

Proxies can be messy at runtime and you may prefer not to use them. Or you may not be able to use proxies: There are .NET implementations that need to know all types at compile time, like UWP and Xamarin, and therefore don't allow emitting new types at runtime. The alternate way to use lazy loading in EF Core 2.1 – which doesn't rely on proxies – is a completely new mechanism with two ways to achieve it. I'll explain the feature and those details that originally confused me, and let you look at the detailed code in the EF Core docs (https://docs.microsoft.com/en-us/ef/core/querying/related-data/lazy) to see how to implement it.

The first way is to let EF Core inject one of the key objects from the new Microsoft.EntityFrameworkCore.Abstractions package, an implementation of the ILazyLoader service, into your entity. This requires setting up your entity constructor to accept the ILazyLoader type as a parameter and then calling its Load method in the getter of the navigation property to be lazy loaded. There's an alternate way to allow EF Core to inject the ILazyLoader service without making any reference to its assemblies and reducing coupling with anything related to EF Core. That's by using a System.Action object as a parameter of the constructor with the parameter named lazyLoader, a current naming requirement. EF Core sees that and injects the instance.

private Team(Action<object, string> lazyLoader)

LINQ GroupBy Translation

GroupBy is an important query operation. Up until EF Core 2.1, the LINQ GroupBy method was never translated to SQL and was always evaluated in memory on query results. EF Core 2.1 brings the first wave of translation into SQL, although there's still more to go.

Here's an example of grouping on a single column and projecting the group key along with an aggregate:

context.TeamMembers
    .GroupBy(m => m.TeamId)
    .Select(g => new { TeamId = g.Key, Count = g.Count() })
    .ToList();

The SQL sent to the database reflects that the group by is translated

SELECT [m].[TeamId], COUNT(*) AS [Count]
FROM [TeamMembers] as [m]
GROUP BY [m].[TeamId]

The results are a pair of anonymous types with requested properties and of course, the correct values, as shown in Figure 1.

Figure 1: The results of the GroupBy query
Figure 1: The results of the GroupBy query

You can find detailed samples of each of each supported scenario in the GitHub issue “Relational: Support translating GroupBy() to SQL” (https://github.com/aspnet/EntityFrameworkCore/issues/2341). Here's an overview:

  • Group by a single column or by multiple columns where those columns are expressed as an anonymous type. For example, if there were also a Role property in teamMember, you could group by TeamId and Role as GroupBy(m=>new{m.TeamId,m.Role}).
  • GroupBy can be translated when it's been appended to a complex query.
  • Group by a constant or a variable.
  • Group by scalar properties of a related type.
  • Select into a known type.
  • Order or filter on a key or aggregate after GroupBy

Also from the GitHub issue, here's list of scenarios that aren't supported in EF Core 2.1:

  • Grouping on an entity (e.g., a reference navigation property)
  • Projecting non-aggregate scalar subqueries after grouping, e.g., FirstOrDefault()
  • Making groups of multiple entityTypes using anonymous types
  • Using Key/Aggregate values after GroupBy in joins

EF Core Gets Out of the Way of Your Domain Design

Anything that you can do to avoid modifying your business classes in order to accommodate EF Core's mapping rules makes EF Core more appealing to many developers. To this end, there are two notable improvements in EF Core 2.1: value conversions and the ability for EF Core to materialize results without an available parameterless constructor.

Value Conversions

The new value conversion feature allows you to have more control over mapping CLR types to database types. EF has always been limited by the fact that you could only map types if they were natively supported by the database provider. In EF Core 2.1, you can use the types you want in your model without being constrained by the database mapping support and you can define a converter to take care of identifying the proper database type as well as when saving and querying data. There are also a number of pre-defined converters built in for some common scenarios. Let's take a look at one of these: TimeSpanToTicksConverter.

TimeSpan maps by default to SQL Server's Time type, which represents the duration since midnight. But developers who wanted to truly store it as a duration, not a time, especially if that duration is more than 24 hours, had to come up with clever strategies. Now you can easily store a TimeSpan duration as a bigint in the database and retrieve that value back into the TimeSpan type.

Here's a new TimeSpan property, TypicalCommuteTime, in the TeamMember class. Its value is set by the method, CalculateCommuteTime.

public TimeSpan TypicalCommuteTime { get; private set; }

public void CalculateCommuteTime(DateTime start, DateTime end)
{
    TypicalCommuteTime = end.Subtract(start);
}

By default, the SQL Server provider creates this new column as a Time type. Ticks in .NET are represented as an Int64, which in SQL Server is a bigint. It's possible to use the HasColumnType(“bigint”) mapping with the HasConversion with the .NET type and not worry about what the database type is.

modelBuilder.Entity<TeamMember>()
    .Property(e => e.TypicalCommuteTime)
    .HasConversion<System.Int64>();

EF Core creates a bigint column for you when defining a migration as well as when creating SQL, so you can easily save and query TeamMember objects with the TimeSpan value without needing to be involved with the conversion.

Another built-in converter that will be of interest to many developers is EnumToStringConverter, providing the ability to finally store enums as strings.

Check out the Value Conversion details in the docs at https://docs.microsoft.com/core/modeling/value-conversions to learn more about the built-in converters and creating your own custom conversions. Also, pay attention to the Limitations section in the doc that discusses lack of support for nulls and other things.

Materialize Entities That Have Parameterized Constructors

Another change in EF Core 2.1 should make anyone who's ever added a constructor to their entity classes happy. EF and EF Core have never been able to materialize results without the presence of a parameterless constructor. If you create a constructor with parameters, that overrides the default parameterless constructor inherited from System.Object.

The fix has always been to add a private parameterless constructor to your entities, an annoying concession for developers who don't want to design their entities to satisfy the data persistence layer. Now EF Core can materialize results if the only available constructors have parameters. If there are multiple constructors, EF Core uses the narrower one to materialize the object.

Data Annotation Attribute for Owned Types

If you like to use Data Annotations, be aware that there's now a data annotation provided in the Abstractions package, Owned, to mark properties that are owned types. This aligns with the IsOwned fluent mapping introduced in EF Core 2.0.

Data Seeding

Data seeding by way of migrations has returned in EF Core 2.1. This is a fresh take on the workflow. In this iteration, you define the seed data as part of the model in the DbContext's OnModelConfiguring method. For example:

modelBuilder.Entity<Team>().HasData(new Team
{
    TeamId = 101,
    Name = "Entity Framework",
    TwitterAlias = "efmagicunicorns"
});

You can also add multiple entities as an array:

modelBuilder.Entity<Team>().HasData(new Team[]
{
    new Team
    {
        TeamId = 101,
        Name = "Entity Framework",
        TwitterAlias = "efmagicunicorns"
    },
    new Team
    {
        TeamId = 102,
        Name = ".NET",
        TwitterAlias = "dotnet"
    }
});

There are some important points to note about how this works.

  • You must specify the key value even if you're seeding a database that generates those keys. EF Core ensures that your values are inserted.
  • In the HasData method, you can only specify data for a single type. If you want to also add TeamMembers, you must do that via modelBuilder.Entity<TeamMember>.HasData().
  • When you add a migration after defining HasData, new MigrationBuilder methods are used to insert that data. The migration's Down method removes that same data.
  • The seed data is only inserted into the database when you call the migrations' update database command, rather than at runtime.
  • If you're using the InMemory provider for testing, EnsureCreated reads the HasData method and seeds your InMemory database.

That last point about working with the InMemory provider is important. It's a nice benefit, so you don't have to always write extra code to populate the InMemory context in your tests. Pay attention, in case your tests require different data.

Check the documentation (https://docs.microsoft.com/ef/core/modeling/data-seeding) for other nuances about data seeding.

Richer Data Models with Query Types

EF Core 2.1 brings you Query types, which enable some scenarios that are critical to many developers as well as a great deal of the software you're building. Query types are a completely new idea in EF Core and didn't exist in any version of Entity Framework. What's interesting is that with this feature, it's the first time the DbContext can do something useful with objects without requiring keys.

Remember that every entity in your model must have a key. A query type is a type that's recognized by DbContext for the sole purpose of executing queries and pushing the results into objects. Although the DbContext is aware of these types, there are three important traits for you to understand. Query types:

  • Don't require a key property
  • Aren't tracked by the ChangeTracker
  • May or may not be mapped to tables in the database

Some of the most important scenarios that query types enable include mapping to database views and tables with no primary key, returning FromSql queries into non-entity types (something we were able to do in EG6), mapping to tables that don't have a primary key, and mapping to queries defined with the new ToQuery mapping (a.k.a. defining queries).

Database view mapping is probably the most requested capability. The documentation provides a nice walk through for this, so here are some highlights. To map a database view, you'll need:

  • A view in the database
  • A class that matches the schema
  • A QueryType mapping in the DbContext that includes the new ToView method to specify the name of the view:
modelBuilder.Query<TeamMemberCount>().ToView("View_TeamMemberCounts");
  • Optionally, a DbQuery property, which is like a DbSet, in the DbContext class. This example assumes that the class is named TeamMemberCount:
public DbQuery<TeamMemberCount> TeamMemberCounts { get; set; }

If you choose not to define the DbQuery, you can use the dynamic syntax when writing your query. You'll see an example of this below with FromSql.

Now let's look at the FromSql support that query types enable. Here's an example of a stored procedure that takes a team ID as a parameter and returns the name and commute time of each team member. Surely that's easy enough to do with a LINQ projection, but given the simple model, I'll have to use this contrived example:

CREATE PROCEDURE [dbo].GetCommuteTimes
        @teamId int
AS
    SELECT Name, TypicalCommuteTime
    FROM TeamMembers
    WHERE TeamId = @teamId

You'll need a class that matches the results:

public class TeamCommute
{
    public string Name { get; set; }
    public TimeSpan TypicalCommuteTime { get; set; }
}

And you'll need to let the model be aware that this class can be used as a query type. In OnModelCreating, you only need to refer to the query type to achieve this:

modelBuilder.Query<TeamCommute>();

Notice that I'm using the TimeSpan in the TeamCommute class just as I did above in the TeamMember. You can use the same mapping to ensure that EF Core knows how to translate the System.Tick (an Int64) to the TimeSpan property. Append that to the Query method:

modelBuilder.Query<TeamCommute>()
    .Property(e => e.TypicalCommuteTime)
    .HasConversion<System.Int64>();

Now you can use the non-entity type with a FromSql query that calls the stored procedure. This query uses a dynamic context.Query<TeamCommute>() rather than defining the DbQuery in the TeamContext class.

var teamlist = context.Query<TeamCommute>()
    .FromSql("EXEC GetCommuteTimes {0}",101)
    .ToList();

Note that the documentation at https://docs.microsoft.com/ef/core/modeling/query-types says that you can return arbitrary types from the FromSql method. The term arbitrary doesn't mean any undefined class. You'll have to pre-define the class and configure it in the model, as described above.

The EF Core You've Been Waiting For

There are even more excellent features in EF Core 2.1. Here's a quick list of some of the more notable changes.

  • There are new pipeline hooks with the Tracked and StateChanged events on DbContext.ChangeTracker.
  • System.Transactions is again supported so you can customize how transactions are handled.
  • Developers have long complained about the inability to control how database columns are ordered; now migrations honor the order of properties in your entities when generating the code that creates the table for the first time.
  • Derived types are now possible with the eager loading Include method.
  • For apps where you're working with the .NET Core CLI and EF Core's command line tools, they've now moved to the .NET Core SDK so you won't have to add the DotNEtCliToolReference into your csproj files.

The attention being paid to EF Core in this version demonstrates that there's no question about EF Core being production ready. There may yet be a particular feature that you're still waiting for, but for the majority of projects, EF Core 2.1 has come to the point where you can start calling it feature-rich and ready for prime time.