Since Entity Framework Core 5.0 is the major release, it contains many new features in the release. EF Core 5.0 release contains many breaking changes which are mainly for the API improvement or the behavioral changes related to the existing applications.
In Entity Framework 5.0, one of the main features is the Many – to – Many relationships without explicitly mapping the join table. In the previous version of EF Core, we need to define the third entity to establish the many-to-many relationship. As per the example, if we want to establish a many-to-many relationship between Author Entity and Blogs Entity, then we need to define three entities – Author, Blog, and AuthorBlog. But in EF Core 5.0, it is not necessary to define the third entity i.e. AuthorBlog. Now, we can define the Many-to-Many relationship between the Author and Blogs entities. To define the many to many relationships between these two entities, we need to define the mention entity class, as shown below:
In the above example, the Author contains a collection of Blogs and the Blog contains a collection of Authors. In Entity Framework 5.0, this recognizes as a many-to-many relationship by convention. So, the DBContext class as per the above entities will look like this:
- public class EFCore5RelationshipsExamplesDbContext : DbContext
- {
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- { optionsBuilder.UseSqlServer(@"Server=(localdb)\MSSQLLocalDB;Database=TestDB;Trusted_Connection=True;MultipleActiveResultSets=true");
- }
-
- public DbSet<Author> Authors { get; set; }
- public DbSet<Blog> Blogs { get; set; }
- }
Split Queries
From the Entity Framework Core 3.0, EF Core always populates a single SQL query for each LINQ query. This normally ensures the consistency of the data returned within the constraint of the transaction mode. But, this can become very slow when the query uses Include or a projection to bring back multiple related records or collections. But in EF Core 5.0, now allows a single LINQ query with include related collections can be split into multiple SQL queries. This can significantly improve the performance of the application. Entity Framework now allows us to specify that to mention in the LINQ query either it should split into multiple SQL queries or not. If the split is mentioned, then instead of JOINs, split queries generate an additional SQL query for each included collection:
- using (var context = new BloggingContext())
- {
- var blogs = context.Blogs
- .Include(blog => blog.Posts)
- .AsSplitQuery()
- .ToList();
- }
It will produce the following SQL queries:
- SELECT [b].[Id] as BlogId, [b].[Name]
- FROM [Blogs] AS [b]
- ORDER BY [b].[BlogId]
-
- SELECT [p].[Id] as AuthorId, [p].[BlogId], [p].[Title], [b].[BlogId]
- FROM [Blogs] AS [b]
- INNER JOIN [Author] AS [p] ON [b].[BlogId] = [p].[BlogId]
- ORDER BY [b].[BlogId]
We can also configure the split queries as a default in our application context:
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- {
- optionsBuilder
- .UseSqlServer( @"Server=(localdb)\MSSQLLocalDB;Database=TestDB;Trusted_Connection=True;ConnectRetryCount=0",
- o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
- }
Logging & Diagnostics
Entity Framework Core 5.0 also introduces a simple and easy way to set up the logging via the new LogTo method. Through this method, we can log the messages to the console include all SQL generated by EF Core. With the help of this method, we can generate logs from the EF Core application without any special kind of configuration related to the external logging framework.
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- => optionsBuilder.LogTo(Console.WriteLine);
There are multiple overloads available that can be used in different use cases:
- Set the minimum log level
- Example: .LogTo(Console.WriteLine, LogLevel.Information)
- Filter for only specific events:
- Example: .LogTo(Console.WriteLine, new[] {CoreEventId.ContextInitialized, RelationalEventId.CommandExecuted})
- Filter for all events in specific categories:
- Example: .LogTo(Console.WriteLine, new[] {DbLoggerCategory.Database.Name}, LogLevel.Information)
- Use a custom filter over event and level:
- Example: .LogTo(Console.WriteLine, (id, level) => id == RelationalEventId.CommandExecuting)
Table – per – type (TPT) mapping
In general, Entity Framework maps as inheritance hierarchy of .NET types into a single database type. It is normally known as table-per-hierarchy (TPH) mapping. But in EF Core 5.0, allows us to map each .NET class type within an inheritance hierarchy into a different database table, it is known as table-per-type (TPT) mapping. Table-per-type inheritance normally uses a separate table in the database to maintain data for non-inherited properties and key properties for each type in the inheritance hierarchy.
- Table-per-Type (TPT) is normally representing inheritance relationships as a relational foreign key in the table.
- Every class and subclass including abstract classes has its table.
- The table for subclasses contains columns only for each non-inherited property along with a primary key that is also a foreign key of the base class table.
Let's consider the following simple class model with a mapped hierarchy:
- public class Person
- {
- public int Id { get; set; }
- public string FullName { get; set; }
- }
-
- public class Student: Person
- {
- public DateTime EnrollmentDate { get; set; }
- }
-
- public class Teacher: Person
- {
- public DateTime JoiningDate { get; set; }
- }
By default, Entity Framework will map this with a single table:
- CREATE TABLE [dbo].[People] (
- [Id] INT IDENTITY (1, 1) NOT NULL,
- [FullName] NVARCHAR (MAX) NULL,
- [Discriminator] NVARCHAR (MAX) NOT NULL,
- [EnrollmentDate] DATETIME2 (7) NULL,
- [JoiningDate] DATETIME2 (7) NULL,
- CONSTRAINT [PK_People] PRIMARY KEY CLUSTERED ([Id] ASC)
- );
In the Table-per-Type mapping pattern, all the entity types are mapped with individual tables. Properties that belong solely to a base type or derived type are stored in a table that maps to that type. Entity types can be mapped to different tables using mapping attributes.
- [Table("People")]
- public class Person
- {
- public int Id { get; set; }
- public string FullName { get; set; }
- }
-
- [Table("Students")]
- public class Student : Person
- {
- public DateTime EnrollmentDate { get; set; }
- }
-
- [Table("Teachers")]
- public class Teacher : Person
- {
- public DateTime JoiningDate { get; set; }
- }
Now, mapping each entity type to a different table will instead result in one table per type.
- CREATE TABLE [dbo].[People] (
- [Id] INT IDENTITY (1, 1) NOT NULL,
- [FullName] NVARCHAR (MAX) NULL,
- CONSTRAINT [PK_People] PRIMARY KEY CLUSTERED ([Id] ASC)
- );
-
- CREATE TABLE [dbo].[Students] (
- [Id] INT NOT NULL,
- [EnrollmentDate] DATETIME2 (7) NOT NULL,
- CONSTRAINT [PK_Students] PRIMARY KEY CLUSTERED ([Id] ASC),
- CONSTRAINT [FK_Students_People_Id] FOREIGN KEY ([Id]) REFERENCES [dbo].[People] ([Id])
- );
-
- CREATE TABLE [dbo].[Teachers] (
- [Id] INT NOT NULL,
- [JoiningDate] DATETIME2 (7) NOT NULL,
- CONSTRAINT [PK_Teachers] PRIMARY KEY CLUSTERED ([Id] ASC),
- CONSTRAINT [FK_Teachers_People_Id] FOREIGN KEY ([Id]) REFERENCES [dbo].[People] ([Id])
- );
Flexible Entity Mapping
In Entity Framework, Entity types are mainly mapped to the tables or view so that EF Core will pull the records of the table or view when querying for that type. In EF Core 5.0, now we have additional mapping options, in which we can map an entity with a SQL query (known as defining query) or a table-valued function (TVF).
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder.Entity<Post>().ToSqlQuery(@"SELECT Id, Name, Category, BlogId FROM posts");
-
- modelBuilder.Entity<Blog>().ToFunction("BlogsReturningFunction");
- }
Table-valued function or TVF can also be mapped with a .NET method rather than to a DBSet. Now, in EF Core 5.0, it is possible to map an entity with a view when we perform a query, as shown below:
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder
- .Entity<Blog>()
- .ToTable("Blogs")
- .ToView("BlogsView");
- }
Shared – type entity types with Property Bags
In Entity Framework Core 5.0, we can define or mapped the same CLR type to multiple different entity types, such types are normally known as shared-type entity types. While we are using any CLR type with this feature, .NET Dictionary offers a particularly compelling use-case which we can call property-bags. These entities can be used to perform query or update operations just like other normal entity types with their own, dedicated CLR types. Normally, entity types that contain only indexer properties are called property bag entity types. These types of entities don’t have any shadow properties. Currently, only Dictionary<string, object> is only supported as a property bag entity type. It needs to be defined as a shared entity type with a unique name and the related DBSet property must be implemented using Set call.
- public class MyContext : DbContext
- {
- public DbSet<Dictionary<string, object>> Blogs => Set<Dictionary<string, object>>("Blog");
-
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder.SharedTypeEntity<Dictionary<string, object>>(
- "Blog", bb =>
- {
- bb.Property<int>("BlogId");
- bb.Property<string>("Url");
- bb.Property<DateTime>("LastUpdated");
- });
- }
- }
Required 1:1 Dependents
Entity Framework Core always allows us to model the entity types which can only appear on navigation properties of other entity types. These are known as owned entity types. The entity contains an owned entity type is known as the owner. This was most apparent when using owned entities, as all the owned entity's columns were created as nullable in the database, even if they were configured as required in the model. But now, in EF Core 5.0, navigation to an owned entity can be configured as a required dependency. This way, when we migrate that entity to the database level, it creates NOT NULL columns in the database table.
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder.Entity<Author>(b =>
- {
- b.OwnsOne(e => e.HomeAddress,
- b =>
- {
- b.Property(e => e.City).IsRequired();
- b.Property(e => e.Postcode).IsRequired();
- });
- b.Navigation(e => e.HomeAddress).IsRequired();
- });
- }
08. DBContextFactory – In Entity Framework Core 5.0, Microsoft introduced AddDbContextFactory and AddPooledDbContextFactory to register a factory for creating DbContext instances in the application's dependency injection (D.I.) container. This can be useful when the application code needs to create and dispose of context instances manually.
- services.AddDbContextFactory<AuthorDbContext>(b =>
- b.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=TestDB"));
At this point, application services such as ASP.NET Core controllers can then be injected with IDbContextFactory<TContext>, and use to instantiate context instances.
- public class MyController
- {
- private readonly IDbContextFactory<SomeDbContext> _contextFactory;
-
- public MyController(IDbContextFactory<AuthorDbContext> contextFactory)
- => _contextFactory = contextFactory;
-
- public void DoSomeThing()
- {
- using (var context = _contextFactory.CreateDbContext())
- {
-
- }
- }
Conclusion
Entity Framework Core 5.0 has been released with many new features and functionality. In this article, we discuss the overview and features of Entity Framework. We also discussed the overview of the Entity Framework Core, its supported database providers, and key new release functionalities in EF Core 5.0. In the next article, we will discuss some of the new releases in detail along with examples. Any suggestions or feedback or queries related to this article are most welcome.