{"id":40488,"date":"2022-06-14T10:17:10","date_gmt":"2022-06-14T17:17:10","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=40488"},"modified":"2024-12-13T15:21:18","modified_gmt":"2024-12-13T23:21:18","slug":"announcing-ef7-preview5","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/announcing-ef7-preview5\/","title":{"rendered":"Announcing Entity Framework 7 Preview 5"},"content":{"rendered":"<p>Entity Framework 7 (EF7) Preview 5 has shipped with support for Table-per-Concrete type (TPC) mapping. This blog post will focus on TPC. There are several other enhancements included in Preview 5, such as:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/dotnet\/efcore\/issues\/26199\">Support for AT TIME ZONE in SQL Server<\/a><\/li>\n<li>Updates to command and connection interception (<a href=\"https:\/\/github.com\/dotnet\/efcore\/issues\/23087\">#23087<\/a>, <a href=\"https:\/\/github.com\/dotnet\/efcore\/issues\/23085\">#23085<\/a>, <a href=\"https:\/\/github.com\/dotnet\/efcore\/issues\/17261\">#17261<\/a>)<\/li>\n<li>Addition of the <a href=\"https:\/\/github.com\/dotnet\/efcore\/issues\/9621\">delete behavior attribute<\/a><\/li>\n<\/ul>\n<p>Read the <a href=\"https:\/\/github.com\/dotnet\/efcore\/issues?q=is%3Aissue+milestone%3A7.0.0-preview5+is%3Aclosed+label%3Atype-enhancement\">full list of EF7 Preview 5 enhancements<\/a>.<\/p>\n<h2>Table-per-concrete-type (TPC) mapping<\/h2>\n<p>By default, EF Core maps an inheritance hierarchy of .NET types to a single database table. This is known as the table-per-hierarchy (TPH) mapping strategy. EF Core 5.0 introduced the table-per-type (TPT) strategy, which supports mapping each .NET type to a different database table. In EF Core 7.0 preview 5.0, we are excited to introduce the table-per-concrete-type (TPC) strategy. TPC also maps .NET types to different tables, but in a way that addresses some common performance issues with the TPT strategy.<\/p>\n<p>In this post, we&#8217;ll start by describing the structure of TPH, TPT, and TPC mappings, then look at how these strategies can be configured in EF Core, and finally discuss the pros and cons of each approach.<\/p>\n<h3>Mapping inheritance hierarchies<\/h3>\n<p>Consider the following object-oriented domain model:<\/p>\n<pre><code class=\"language-csharp\">public abstract class Animal\r\n{\r\n    public int Id { get; set; }\r\n    public string Species { get; set; }\r\n}\r\n\r\npublic class FarmAnimal : Animal\r\n{\r\n    public decimal Value { get; set; }\r\n}\r\n\r\npublic class Pet : Animal\r\n{\r\n    public string Name { get; set; }\r\n}\r\n\r\npublic class Cat : Pet\r\n{\r\n    public string EducationLevel { get; set; }\r\n}\r\n\r\npublic class Dog : Pet\r\n{\r\n    public string FavoriteToy { get; set; }\r\n}<\/code><\/pre>\n<p>If we are to retrieve some <code>Animal<\/code> object from the database, then we must know which type of animal it is. We don&#8217;t want to save a cat and then read it back as a dog, or vice versa. (I can tell you from experience that dogs generally don&#8217;t like to be treated as cats, and cats <em>certainly<\/em> don&#8217;t like to be treated as dogs!) So this means the type of animal&#8211;that is the actual class used when the animal was created in C#&#8211;must be saved to the database in some form.<\/p>\n<p>Further, different information is associated with each <code>Animal<\/code> object depending on its type. For example, in our model, a farm animal has some monetary value but no name, while pets are priceless and named.<\/p>\n<p>Inheritance mapping strategies (TPH, TPT, or TPC) define how this object-oriented type information and type-specific information are saved into a relational database, where inheritance is not a natural concept.<\/p>\n<h4>The TPH strategy<\/h4>\n<p>With the TPH strategy, a single table is created for all types in the hierarchy&#8211;hence the name &#8220;table-per-hierarchy&#8221;. This table contains a special column containing a &#8220;discriminator value&#8221;, which indicates the type of the object saved in each row. In addition, a column is created for every property of every type in the hierarchy. For example:<\/p>\n<pre><code class=\"language-sql\">CREATE TABLE [Animals] (\r\n    [Id] int NOT NULL IDENTITY,\r\n    [Species] nvarchar(max) NOT NULL,\r\n    [Discriminator] nvarchar(max) NOT NULL,\r\n    [Value] decimal(18,2) NULL,\r\n    [Name] nvarchar(max) NULL,\r\n    [EducationLevel] nvarchar(max) NULL,\r\n    [FavoriteToy] nvarchar(max) NULL,\r\n    CONSTRAINT [PK_Animals] PRIMARY KEY ([Id])\r\n);<\/code><\/pre>\n<p>Saving two cats, a dog, and a sheep to this table results in the following:<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left;\">Id<\/th>\n<th style=\"text-align: left;\">Species<\/th>\n<th style=\"text-align: left;\">Discriminator<\/th>\n<th style=\"text-align: left;\">Value<\/th>\n<th style=\"text-align: left;\">Name<\/th>\n<th style=\"text-align: left;\">EducationLevel<\/th>\n<th style=\"text-align: left;\">FavoriteToy<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left;\">1<\/td>\n<td style=\"text-align: left;\">Felis catus<\/td>\n<td style=\"text-align: left;\">Cat<\/td>\n<td style=\"text-align: left;\">NULL<\/td>\n<td style=\"text-align: left;\">Alice<\/td>\n<td style=\"text-align: left;\">MBA<\/td>\n<td style=\"text-align: left;\">NULL<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left;\">2<\/td>\n<td style=\"text-align: left;\">Felis catus<\/td>\n<td style=\"text-align: left;\">Cat<\/td>\n<td style=\"text-align: left;\">NULL<\/td>\n<td style=\"text-align: left;\">Mac<\/td>\n<td style=\"text-align: left;\">BA<\/td>\n<td style=\"text-align: left;\">NULL<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left;\">3<\/td>\n<td style=\"text-align: left;\">Canis familiaris<\/td>\n<td style=\"text-align: left;\">Dog<\/td>\n<td style=\"text-align: left;\">NULL<\/td>\n<td style=\"text-align: left;\">Toast<\/td>\n<td style=\"text-align: left;\">NULL<\/td>\n<td style=\"text-align: left;\">Mr. Squirrel<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left;\">4<\/td>\n<td style=\"text-align: left;\">Ovis aries<\/td>\n<td style=\"text-align: left;\">FarmAnimal<\/td>\n<td style=\"text-align: left;\">100.00<\/td>\n<td style=\"text-align: left;\">NULL<\/td>\n<td style=\"text-align: left;\">NULL<\/td>\n<td style=\"text-align: left;\">NULL<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Notice that:<\/p>\n<ul>\n<li>The value in the <code>Discriminator<\/code> column indicates the type of C# object saved<\/li>\n<li>There is a column for every property in the hierarchy<\/li>\n<li>If the property does not exist for the type of the object saved, then the value in the database for that column is null<\/li>\n<\/ul>\n<blockquote><p>The TPH strategy requires that database columns be nullable for any property not defined in the root type of the hierarchy, even if that property is required. It is possible to create a database constraint for these columns to ensure the value is non-null whenever an instance with that property is saved, but this is not done automatically by EF Core. See <a href=\"https:\/\/github.com\/dotnet\/efcore\/issues\/20931\">Issue #20931 on the EF Core GitHub repo<\/a> for more information.<\/p><\/blockquote>\n<h4>The TPT strategy<\/h4>\n<p>With the TPT strategy, a different table is created for every type in the hierarchy&#8211;hence the name &#8220;table-per-type&#8221;. The table itself is used to determine the type of the object saved, and each table contains only columns for the properties of that type. For example:<\/p>\n<pre><code class=\"language-sql\">CREATE TABLE [Animals] (\r\n    [Id] int NOT NULL IDENTITY,\r\n    [Species] nvarchar(max) NOT NULL,\r\n    CONSTRAINT [PK_Animals] PRIMARY KEY ([Id])\r\n);\r\n\r\nCREATE TABLE [FarmAnimals] (\r\n    [Id] int NOT NULL,\r\n    [Value] decimal(18,2) NOT NULL,\r\n    CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id]),\r\n    CONSTRAINT [FK_FarmAnimals_Animals_Id] FOREIGN KEY ([Id]) REFERENCES [Animals] ([Id])\r\n);\r\n\r\nCREATE TABLE [Pets] (\r\n    [Id] int NOT NULL,\r\n    [Name] nvarchar(max) NOT NULL,\r\n    CONSTRAINT [PK_Pets] PRIMARY KEY ([Id]),\r\n    CONSTRAINT [FK_Pets_Animals_Id] FOREIGN KEY ([Id]) REFERENCES [Animals] ([Id])\r\n);\r\n\r\nCREATE TABLE [Cats] (\r\n    [Id] int NOT NULL,\r\n    [EducationLevel] nvarchar(max) NOT NULL,\r\n    CONSTRAINT [PK_Cats] PRIMARY KEY ([Id]),\r\n    CONSTRAINT [FK_Cats_Pets_Id] FOREIGN KEY ([Id]) REFERENCES [Pets] ([Id])\r\n);\r\n\r\nCREATE TABLE [Dogs] (\r\n    [Id] int NOT NULL,\r\n    [FavoriteToy] nvarchar(max) NOT NULL,\r\n    CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id]),\r\n    CONSTRAINT [FK_Dogs_Pets_Id] FOREIGN KEY ([Id]) REFERENCES [Pets] ([Id])\r\n);<\/code><\/pre>\n<p>Saving the same data into this database results in the following:<\/p>\n<p>Animals table:<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left;\">Id<\/th>\n<th style=\"text-align: left;\">Species<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left;\">1<\/td>\n<td style=\"text-align: left;\">Felis catus<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left;\">2<\/td>\n<td style=\"text-align: left;\">Felis catus<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left;\">3<\/td>\n<td style=\"text-align: left;\">Canis familiaris<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left;\">4<\/td>\n<td style=\"text-align: left;\">Ovis aries<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>FarmAnimals table:<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left;\">Id<\/th>\n<th style=\"text-align: left;\">Value<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left;\">4<\/td>\n<td style=\"text-align: left;\">100.00<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Pets table:<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left;\">Id<\/th>\n<th style=\"text-align: left;\">Name<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left;\">1<\/td>\n<td style=\"text-align: left;\">Alice<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left;\">2<\/td>\n<td style=\"text-align: left;\">Mac<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left;\">3<\/td>\n<td style=\"text-align: left;\">Toast<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Cats table:<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left;\">Id<\/th>\n<th style=\"text-align: left;\">EducationLevel<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left;\">1<\/td>\n<td style=\"text-align: left;\">MBA<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left;\">2<\/td>\n<td style=\"text-align: left;\">BA<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Dogs table:<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left;\">Id<\/th>\n<th style=\"text-align: left;\">FavoriteToy<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left;\">3<\/td>\n<td style=\"text-align: left;\">Mr. Squirrel<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Notice that the data is saved in a normalized form, but that this means the information for a single object is spread across multiple tables.<\/p>\n<h4>TPC mapping<\/h4>\n<p>The TPC strategy is similar to the TPT strategy except that a different table is created for every concrete type in the hierarchy, but tables are not created for abstract types&#8211;hence the name &#8220;table-per-concrete-type&#8221;. As with TPT, the table itself indicates the type of the object saved. However, unlike TPT mapping, each table contains columns for every property in the concrete type <em>and its base types<\/em>. For example:<\/p>\n<pre><code class=\"language-sql\">CREATE TABLE [FarmAnimals] (\r\n    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalIds]),\r\n    [Species] nvarchar(max) NOT NULL,\r\n    [Value] decimal(18,2) NOT NULL,\r\n    CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id])\r\n);\r\n\r\nCREATE TABLE [Pets] (\r\n    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalIds]),\r\n    [Species] nvarchar(max) NOT NULL,\r\n    [Name] nvarchar(max) NOT NULL,\r\n    CONSTRAINT [PK_Pets] PRIMARY KEY ([Id])\r\n);\r\n\r\nCREATE TABLE [Cats] (\r\n    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalIds]),\r\n    [Species] nvarchar(max) NOT NULL,\r\n    [Name] nvarchar(max) NOT NULL,\r\n    [EducationLevel] nvarchar(max) NOT NULL,\r\n    CONSTRAINT [PK_Cats] PRIMARY KEY ([Id])\r\n);\r\n\r\nCREATE TABLE [Dogs] (\r\n    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalIds]),\r\n    [Species] nvarchar(max) NOT NULL,\r\n    [Name] nvarchar(max) NOT NULL,\r\n    [FavoriteToy] nvarchar(max) NOT NULL,\r\n    CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id])\r\n);<\/code><\/pre>\n<p>Notice that:<\/p>\n<ul>\n<li>There is no table for <code>Animal<\/code>, since it is an abstract type in the object model. Remember that C# does not allow instances of abstract types, and there is therefore no situation where one will be saved to the database.<\/li>\n<li>The mapping of properties in base types is repeated for each concrete type&#8211;for example, every table has a <code>Species<\/code> column, and both <code>Cats<\/code> and <code>Dogs<\/code> have a <code>Name<\/code> column.<\/li>\n<\/ul>\n<p>Saving the same data into this database results in the following:<\/p>\n<p>FarmAnimals table:<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left;\">Id<\/th>\n<th style=\"text-align: left;\">Species<\/th>\n<th style=\"text-align: left;\">Value<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left;\">4<\/td>\n<td style=\"text-align: left;\">Ovis aries<\/td>\n<td style=\"text-align: left;\">100.00<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Pets table:<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left;\">Id<\/th>\n<th style=\"text-align: left;\">Species<\/th>\n<th style=\"text-align: left;\">Name<\/th>\n<\/tr>\n<\/thead>\n<\/table>\n<p>Cats table:<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left;\">Id<\/th>\n<th style=\"text-align: left;\">Species<\/th>\n<th style=\"text-align: left;\">Name<\/th>\n<th style=\"text-align: left;\">EducationLevel<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left;\">1<\/td>\n<td style=\"text-align: left;\">Felis catus<\/td>\n<td style=\"text-align: left;\">Alice<\/td>\n<td style=\"text-align: left;\">MBA<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left;\">2<\/td>\n<td style=\"text-align: left;\">Felis catus<\/td>\n<td style=\"text-align: left;\">Mac<\/td>\n<td style=\"text-align: left;\">BA<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Dogs table:<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left;\">Id<\/th>\n<th style=\"text-align: left;\">Species<\/th>\n<th style=\"text-align: left;\">Name<\/th>\n<th style=\"text-align: left;\">FavoriteToy<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left;\">3<\/td>\n<td style=\"text-align: left;\">Canis familiaris<\/td>\n<td style=\"text-align: left;\">Toast<\/td>\n<td style=\"text-align: left;\">Mr. Squirrel<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Notice that, unlike with TPT mapping, all the information for a single object is contained in a single table.<\/p>\n<h3>Configuring inheritance mappings in EF Core<\/h3>\n<p>When mapping an inheritance hierarchy, all types in the hierarchy must be explicitly included in the model. This can be done by creating a <code>DbSet<\/code> property for the type on your <code>DbContext<\/code>:<\/p>\n<pre><code class=\"language-csharp\">public DbSet&lt;Animal&gt; Animals { get; set; }\r\npublic DbSet&lt;Pet&gt; Pets { get; set; }\r\npublic DbSet&lt;Cat&gt; Cats { get; set; }\r\npublic DbSet&lt;Dog&gt; Dogs { get; set; }\r\npublic DbSet&lt;FarmAnimal&gt; FarmAnimals { get; set; }<\/code><\/pre>\n<p>Or by using the <code>Entity<\/code> method in <code>OnModelCreating<\/code>:<\/p>\n<pre><code class=\"language-csharp\">protected override void OnModelCreating(ModelBuilder modelBuilder)\r\n{\r\n    modelBuilder.Entity&lt;Animal&gt;();\r\n    modelBuilder.Entity&lt;Pet&gt;();\r\n    modelBuilder.Entity&lt;Cat&gt;();\r\n    modelBuilder.Entity&lt;Dog&gt;();\r\n    modelBuilder.Entity&lt;FarmAnimal&gt;();\r\n}<\/code><\/pre>\n<blockquote><p>This is different from the legacy EF6 behavior, where derived types of mapped base types would sometimes be automatically discovered.<\/p><\/blockquote>\n<p>Nothing else needs to be done to map the hierarchy as TPH, since it is the default strategy. However, you can make this explicit by calling <code>UseTphMappingStrategy<\/code> on the base type of the hierarchy. For example:<\/p>\n<pre><code class=\"language-csharp\">modelBuilder.Entity&lt;Animal&gt;().UseTphMappingStrategy();<\/code><\/pre>\n<p>To use TPT instead, change this to <code>UseTptMappingStrategy<\/code>. For example:<\/p>\n<pre><code class=\"language-csharp\">modelBuilder.Entity&lt;Animal&gt;().UseTptMappingStrategy();<\/code><\/pre>\n<p>Likewise, <code>UseTpcMappingStrategy<\/code> is used to configure TPC:<\/p>\n<pre><code class=\"language-csharp\">modelBuilder.Entity&lt;Animal&gt;().UseTpcMappingStrategy();<\/code><\/pre>\n<p>In each case, the table name to use for each type can be configured using the <code>ToTable<\/code> builder method, or the <code>[Table]<\/code> attribute. However, this is only valid on types that are mapped to a table for the strategy being used. For example, the following code specifies the table names for TPC mapping:<\/p>\n<pre><code class=\"language-csharp\">modelBuilder.Entity&lt;Pet&gt;().ToTable(\"Pets\");\r\nmodelBuilder.Entity&lt;Cat&gt;().ToTable(\"Cats\");\r\nmodelBuilder.Entity&lt;Dog&gt;().ToTable(\"Dogs\");\r\nmodelBuilder.Entity&lt;FarmAnimal&gt;().ToTable(\"FarmAnimals\");<\/code><\/pre>\n<p>No table name can be specified for <code>Animal<\/code> because it is not mapped to its own table when using the TPC strategy. Conversely, when using the TPH strategy, only the base type (<code>Animal<\/code>) can be given a table name.<\/p>\n<blockquote><p>If multiple types in a hierarchy are given different table names, but no mapping strategy is explicitly specified, then the TPT strategy is used. This was the normal way to configure TPT prior to EF7.<\/p><\/blockquote>\n<h3>Primary keys<\/h3>\n<p>The inheritance mapping strategy chosen has consequences for how primary key values are generated and managed. Keys in TPH are easy, since each entity instance is represented by a single row in a single table. Any kind of key value generation can be used, and no additional constraints are needed.<\/p>\n<p>For the TPT strategy, there is always a row in the table mapped to the base type of the hierarchy. Any kind of key generation can be used on this row. The keys for other tables are linked to this table using foreign key constraints. For example:<\/p>\n<pre><code class=\"language-sql\">CREATE TABLE [FarmAnimals] (\r\n    [Id] int NOT NULL,\r\n    [Value] decimal(18,2) NOT NULL,\r\n    CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id]),\r\n    CONSTRAINT [FK_FarmAnimals_Animals_Id] FOREIGN KEY ([Id]) REFERENCES [Animals] ([Id])\r\n);<\/code><\/pre>\n<p>This ensures that same primary key value is used for a given entity in every table of the hierarchy.<\/p>\n<p>This gets a bit more complicated when the TPC strategy is used. First, it&#8217;s important to understand that EF Core requires that all entities in a hierarchy must have a unique key value, even if the entities have different types. So, using our example model, a <code>Dog<\/code> cannot have the same <code>Id<\/code> key value as a <code>Cat<\/code>. Second, unlike TPT, there is no common table that can act as the single place where key values live and can be generated. This means a simple <code>Identity<\/code> column cannot be used.<\/p>\n<p>For databases that support sequences, this key values can be generated by using a single sequence and referencing it in the default constraint for every table. This is the strategy used in the TPC tables shown above, where each table has the following:<\/p>\n<pre><code class=\"language-sql\">[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalIds])<\/code><\/pre>\n<p><code>AnimalIds<\/code> is a sequence created by EF Core migrations. The following model building code sets this up for SQL Server:<\/p>\n<pre><code class=\"language-csharp\">modelBuilder.HasSequence&lt;int&gt;(\"AnimalIds\");\r\n\r\nmodelBuilder.Entity&lt;Animal&gt;()\r\n    .UseTpcMappingStrategy()\r\n    .Property(e =&gt; e.Id).HasDefaultValueSql(\"NEXT VALUE FOR [AnimalIds]\");<\/code><\/pre>\n<p>The syntax for the default constraint may be different for over database systems.<\/p>\n<h3>Pros and cons of mapping strategies<\/h3>\n<p>All of the above may be interesting, but how do you decide which strategy to use?<\/p>\n<h4>TPH<\/h4>\n<p>In almost all cases, TPH mapping works just fine, which is why it is the default. People are often concerned that the table can become very wide, with many columns only sparsely populated. While this can be true, it is rarely a problem with modern database systems. Query performance with TPH is always very good, mainly because no matter what query you write, only one table is ever needed to return results.<\/p>\n<p>The most important performance differences of the different strategies stem from the SQL needed for different types of common query. To illustrate this, we will run the same three LINQ queries with each of the strategies. These queries are:<\/p>\n<ol>\n<li>A query that returns entities of all types in the hierarchy:<\/li>\n<\/ol>\n<pre><code class=\"language-csharp\">context.Animals.Where(a =&gt; a.Species.StartsWith(\"F\")).ToList();<\/code><\/pre>\n<ol start=\"2\">\n<li>A query that returns entities from a subset of types in the hierarchy:<\/li>\n<\/ol>\n<pre><code class=\"language-csharp\">context.Pets.Where(a =&gt; a.Species.StartsWith(\"F\")).ToList();<\/code><\/pre>\n<ol start=\"3\">\n<li>A query that returns only entities from a single leaf type in the hierarchy:<\/li>\n<\/ol>\n<pre><code class=\"language-csharp\">context.Cats.Where(a =&gt; a.Species.StartsWith(\"F\")).ToList();<\/code><\/pre>\n<p>When using TPH, the SQL generated is simple and efficient in all cases. To help improve the performance of these queries, consider defining an index for faster filtering on the discriminator. In some scenarios this can a source of query slowdown compared to a table scan. However, note that SQL Server avoids using an index if it isn&#8217;t highly selective. For example, for 5 discriminator values over 1 million rows, SQL server prefers a table scan. Note that adding an index will also slows down updates, which may or may not be important.<\/p>\n<p>Finally, if your database system supports it, then consider using sparse columns when the majority of rows will be null for that column.<\/p>\n<ol>\n<li>All types:<\/li>\n<\/ol>\n<pre><code class=\"language-sql\">SELECT [a].[Id], [a].[Discriminator], [a].[Species], [a].[Value], [a].[Name], [a].[EducationLevel], [a].[FavoriteToy]\r\nFROM [Animals] AS [a]\r\nWHERE [a].[Species] LIKE N'F%'<\/code><\/pre>\n<ol start=\"2\">\n<li>Subset of types:<\/li>\n<\/ol>\n<pre><code class=\"language-sql\">SELECT [a].[Id], [a].[Discriminator], [a].[Species], [a].[Name], [a].[EducationLevel], [a].[FavoriteToy]\r\nFROM [Animals] AS [a]\r\nWHERE [a].[Discriminator] IN (N'Pet', N'Cat', N'Dog') AND ([a].[Species] LIKE N'F%')<\/code><\/pre>\n<ol start=\"3\">\n<li>Leaf type:<\/li>\n<\/ol>\n<pre><code class=\"language-sql\">SELECT [a].[Id], [a].[Discriminator], [a].[Species], [a].[Name], [a].[EducationLevel]\r\nFROM [Animals] AS [a]\r\nWHERE [a].[Discriminator] = N'Cat' AND ([a].[Species] LIKE N'F%')<\/code><\/pre>\n<h4>TPT<\/h4>\n<p>The TPT strategy is rarely a good choice. It is mainly used when it is considered important that the data is stored in a normalized form, which is in turn often the case for legacy existing databases or databases managed independently from the application development team.<\/p>\n<p>The main issue with the TPT strategy is that almost all queries involve joining multiple tables because the data for any given entity instance is split across multiple tables.<\/p>\n<p>Using the same queries again, we can see that querying for entities of all types requires all five tables to be joined:<\/p>\n<pre><code class=\"language-sql\">SELECT [a].[Id], [a].[Species], [f].[Value], [p].[Name], [c].[EducationLevel], [d].[FavoriteToy], CASE\r\n    WHEN [d].[Id] IS NOT NULL THEN N'Dog'\r\n    WHEN [c].[Id] IS NOT NULL THEN N'Cat'\r\n    WHEN [p].[Id] IS NOT NULL THEN N'Pet'\r\n    WHEN [f].[Id] IS NOT NULL THEN N'FarmAnimal'\r\nEND AS [Discriminator]\r\nFROM [Animals] AS [a]\r\n    LEFT JOIN [FarmAnimals] AS [f] ON [a].[Id] = [f].[Id]\r\n    LEFT JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]\r\n    LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]\r\n    LEFT JOIN [Dogs] AS [d] ON [a].[Id] = [d].[Id]\r\nWHERE [a].[Species] LIKE N'F%'<\/code><\/pre>\n<blockquote><p>EF Core uses &#8220;discriminator synthesis&#8221; to determine which table the data comes from, and hence the correct type to use. This works because the LEFT JOIN returns nulls for the dependent ID column (the &#8220;sub-tables&#8221;) which aren&#8217;t the correct type. So for a dog, <code>[d].[Id]<\/code> will be non-null, and all the other (concrete) IDs will be null.<\/p><\/blockquote>\n<p>Querying for entities of a subset of types still requires that the base table be joined, resulting in four tables being used:<\/p>\n<pre><code class=\"language-sql\">SELECT [a].[Id], [a].[Species], [p].[Name], [c].[EducationLevel], [d].[FavoriteToy], CASE\r\n    WHEN [d].[Id] IS NOT NULL THEN N'Dog'\r\n    WHEN [c].[Id] IS NOT NULL THEN N'Cat'\r\n    WHEN [p].[Id] IS NOT NULL THEN N'Pet'\r\nEND AS [Discriminator]\r\nFROM [Animals] AS [a]\r\n    LEFT JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]\r\n    LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]\r\n    LEFT JOIN [Dogs] AS [d] ON [a].[Id] = [d].[Id]\r\nWHERE ([d].[Id] IS NOT NULL OR [c].[Id] IS NOT NULL OR [p].[Id] IS NOT NULL) AND ([a].[Species] LIKE N'F%')<\/code><\/pre>\n<p>And even querying for entities of just a single leaf type requires the tables for all the types that the leaf type derives from:<\/p>\n<pre><code class=\"language-sql\">SELECT [a].[Id], [a].[Species], [p].[Name], [c].[EducationLevel], CASE\r\n    WHEN [c].[Id] IS NOT NULL THEN N'Cat'\r\nEND AS [Discriminator]\r\nFROM [Animals] AS [a]\r\n    LEFT JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]\r\n    LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]\r\nWHERE [c].[Id] IS NOT NULL AND ([a].[Species] LIKE N'F%')<\/code><\/pre>\n<h4>TPC<\/h4>\n<p>The TPC strategy is an improvement over TPT because it ensures that the information for a given entity instance is always stored in a single table. This means the TPC strategy can be useful when the mapped hierarchy is large and has many concrete (usually leaf) types, each with a large number of properties, and where only a small subset of types are used in most queries.<\/p>\n<p>Using the same LINQ queries again, the SQL needed when querying for entities of all types is better than it was for TPT, since it requires one fewer table in the query. This is because there is no table for the abstract base type. In addition, <code>UNION ALL<\/code> is used instead of the <code>LEFT JOIN<\/code> needed for TPT. <code>UNION ALL<\/code> does not need to perform any matching between rows or de-duplication of rows, which makes it more efficient that the joins used in TPT queries.<\/p>\n<p>All that being said, when compared to the SQL for TPH, the SQL for TPC in this case is still not great:<\/p>\n<pre><code class=\"language-sql\">SELECT [t].[Id], [t].[Species], [t].[Value], [t].[Name], [t].[EducationLevel], [t].[FavoriteToy], [t].[Discriminator]\r\nFROM (\r\n    SELECT [f].[Id], [f].[Species], [f].[Value], NULL AS [Name], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'FarmAnimal' AS [Discriminator]\r\n    FROM [FarmAnimals] AS [f]\r\n    UNION ALL\r\n    SELECT [p].[Id], [p].[Species], NULL AS [Value], [p].[Name], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'Pet' AS [Discriminator]\r\n    FROM [Pets] AS [p]\r\n    UNION ALL\r\n    SELECT [c].[Id], [c].[Species], NULL AS [Value], [c].[Name], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]\r\n    FROM [Cats] AS [c]\r\n    UNION ALL\r\n    SELECT [d].[Id], [d].[Species], NULL AS [Value], [d].[Name], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]\r\n    FROM [Dogs] AS [d]\r\n) AS [t]\r\nWHERE [t].[Species] LIKE N'F%'<\/code><\/pre>\n<p>This is again the case when querying for entities of a subset of types:<\/p>\n<pre><code class=\"language-sql\">SELECT [t].[Id], [t].[Species], [t].[Name], [t].[EducationLevel], [t].[FavoriteToy], [t].[Discriminator]\r\nFROM (\r\n    SELECT [p].[Id], [p].[Species], [p].[Name], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'Pet' AS [Discriminator]\r\n    FROM [Pets] AS [p]\r\n    UNION ALL\r\n    SELECT [c].[Id], [c].[Species], [c].[Name], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]\r\n    FROM [Cats] AS [c]\r\n    UNION ALL\r\n    SELECT [d].[Id], [d].[Species], [d].[Name], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]\r\n    FROM [Dogs] AS [d]\r\n) AS [t]\r\nWHERE [t].[Species] IS NOT NULL AND ([t].[Species] LIKE N'F%')<\/code><\/pre>\n<p>But TPC is <em>much better<\/em> than TPT when querying for entities of a single leaf type, since all the information for those entities comes from a single table:<\/p>\n<pre><code class=\"language-sql\">SELECT [c].[Id], [c].[Species], [c].[Name], [c].[EducationLevel]\r\nFROM [Cats] AS [c]\r\nWHERE [c].[Species] LIKE N'F%'<\/code><\/pre>\n<p>These types of queries for single leaf types is where TPC really excels.<\/p>\n<h3>Guidance<\/h3>\n<p>In summary, the guidance for which mapping strategy to use is quite simple:<\/p>\n<ul>\n<li>If your code will mostly query for entities of a single leaf type, then use TPC. This is because:\n<ul>\n<li>The storage requirements are smaller, since there are no null columns and no discriminator.<\/li>\n<li>No index is ever needed on the discriminator column, which would slow down updates and possibly also queries. An index may not be needed when using TPH either, but that depends on various factors.<\/li>\n<\/ul>\n<\/li>\n<li>If your code will mostly query for entities of many types, such as writing queries against the base type, then lean towards TPH.\n<ul>\n<li>If your database system supports it (e.g. SQL Server), then consider using sparse columns for columns that will be rarely populated.<\/li>\n<\/ul>\n<\/li>\n<li>Use TPT only if constrained to do so by external factors.<\/li>\n<\/ul>\n<h2>Prerequisites<\/h2>\n<ul>\n<li>EF7 currently targets .NET 6.<\/li>\n<li>EF7 will not run on .NET Framework.<\/li>\n<\/ul>\n<p>EF7 is the successor to EF Core 6.0, not to be confused with <a href=\"https:\/\/github.com\/dotnet\/ef6\">EF6<\/a>. If you are considering upgrading from EF6, please read our guide to <a href=\"https:\/\/docs.microsoft.com\/ef\/efcore-and-ef6\/porting\/\">port from EF6 to EF Core<\/a>.<\/p>\n<h2>How to get EF7 previews<\/h2>\n<p>EF7 is distributed exclusively as a set of NuGet packages.\nFor example, to add the SQL Server provider to your project, you can use the following command using the dotnet tool:<\/p>\n<pre><code class=\"language-bash\">dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 7.0.0-preview.5.22302.2<\/code><\/pre>\n<p>This following table links to the preview 5 versions of the EF Core packages and describes what they are used for.<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: right;\"><strong>Package<\/strong><\/th>\n<th style=\"text-align: left;\"><strong>Purpose<\/strong><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: right;\"><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.EntityFrameworkCore\/7.0.0-preview.5.22302.2\">Microsoft.EntityFrameworkCore<\/a><\/td>\n<td style=\"text-align: left;\">The main EF Core package that is independent of specific database providers<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\"><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.EntityFrameworkCore.SqlServer\/7.0.0-preview.5.22302.2\">Microsoft.EntityFrameworkCore.SqlServer<\/a><\/td>\n<td style=\"text-align: left;\">Database provider for Microsoft SQL Server and SQL Azure<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\"><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite\/7.0.0-preview.5.22302.2\">Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite<\/a><\/td>\n<td style=\"text-align: left;\">SQL Server support for spatial types<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\"><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.EntityFrameworkCore.Sqlite\/7.0.0-preview.5.22302.2\">Microsoft.EntityFrameworkCore.Sqlite<\/a><\/td>\n<td style=\"text-align: left;\">Database provider for SQLite that includes the native binary for the database engine<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\"><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.EntityFrameworkCore.Sqlite.Core\/7.0.0-preview.5.22302.2\">Microsoft.EntityFrameworkCore.Sqlite.Core<\/a><\/td>\n<td style=\"text-align: left;\">Database provider for SQLite <em>without<\/em> a packaged native binary<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\"><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.EntityFrameworkCore.Sqlite.NetTopologySuite\/7.0.0-preview.5.22302.2\">Microsoft.EntityFrameworkCore.Sqlite.NetTopologySuite<\/a><\/td>\n<td style=\"text-align: left;\">SQLite support for spatial types<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\"><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.EntityFrameworkCore.Cosmos\/7.0.0-preview.5.22302.2\">Microsoft.EntityFrameworkCore.Cosmos<\/a><\/td>\n<td style=\"text-align: left;\">Database provider for Azure Cosmos DB<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\"><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.EntityFrameworkCore.InMemory\/7.0.0-preview.5.22302.2\">Microsoft.EntityFrameworkCore.InMemory<\/a><\/td>\n<td style=\"text-align: left;\">The in-memory database provider<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\"><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.EntityFrameworkCore.Tools\/7.0.0-preview.5.22302.2\">Microsoft.EntityFrameworkCore.Tools<\/a><\/td>\n<td style=\"text-align: left;\">EF Core PowerShell commands for the Visual Studio Package Manager Console; use this to integrate tools like <a href=\"https:\/\/docs.microsoft.com\/ef\/core\/managing-schemas\/scaffolding\">scaffolding<\/a> and <a href=\"https:\/\/docs.microsoft.com\/ef\/core\/managing-schemas\/migrations\/\">migrations<\/a> with Visual Studio<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\"><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.EntityFrameworkCore.Design\/7.0.0-preview.5.22302.2\">Microsoft.EntityFrameworkCore.Design<\/a><\/td>\n<td style=\"text-align: left;\">Shared design-time components for EF Core tools<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\"><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.EntityFrameworkCore.Proxies\/7.0.0-preview.5.22302.2\">Microsoft.EntityFrameworkCore.Proxies<\/a><\/td>\n<td style=\"text-align: left;\">Lazy-loading and change-tracking proxies<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\"><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.EntityFrameworkCore.Abstractions\/7.0.0-preview.5.22302.2\">Microsoft.EntityFrameworkCore.Abstractions<\/a><\/td>\n<td style=\"text-align: left;\">Decoupled EF Core abstractions; use this for features like extended data annotations defined by EF Core<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\"><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.EntityFrameworkCore.Relational\/7.0.0-preview.5.22302.2\">Microsoft.EntityFrameworkCore.Relational<\/a><\/td>\n<td style=\"text-align: left;\">Shared EF Core components for relational database providers<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: right;\"><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.EntityFrameworkCore.Analyzers\/7.0.0-preview.5.22302.2\">Microsoft.EntityFrameworkCore.Analyzers<\/a><\/td>\n<td style=\"text-align: left;\">C# analyzers for EF Core<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>We also published the 7.0 preview 5 release of the <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.Data.Sqlite.Core\/7.0.0-preview.5.22302.2\">Microsoft.Data.Sqlite.Core<\/a> provider for <a href=\"https:\/\/docs.microsoft.com\/dotnet\/framework\/data\/adonet\/ado-net-overview\">ADO.NET<\/a>.<\/p>\n<h2>Installing the EF7 Command Line Interface (CLI)<\/h2>\n<p>Before you can execute EF7 Core migration or scaffolding commands, you&#8217;ll have to install the CLI package as either a global or local tool.<\/p>\n<p>To install the preview tool globally, install with:<\/p>\n<pre><code class=\"language-bash\">dotnet tool install --global dotnet-ef --version 7.0.0-preview.5.22302.2 <\/code><\/pre>\n<p>If you already have the tool installed, you can upgrade it with the following command:<\/p>\n<pre><code class=\"language-bash\">dotnet tool update --global dotnet-ef --version 7.0.0-preview.5.22302.2 <\/code><\/pre>\n<p>It&#8217;s possible to use this new version of the EF7 CLI with projects that use older versions of the EF Core runtime.<\/p>\n<h2>Daily builds<\/h2>\n<p>EF7 previews are aligned with .NET 7 previews. These previews tend to lag behind the latest work on EF7. Consider using the <a href=\"https:\/\/github.com\/aspnet\/AspNetCore\/blob\/master\/docs\/DailyBuilds.md\">daily builds<\/a> instead to get the most up-to-date EF7 features and bug fixes.<\/p>\n<p>As with the previews, the daily builds require .NET 6.<\/p>\n<h2>The .NET Data Community Standup<\/h2>\n<p>The .NET data team is now live streaming every other Wednesday at 10am Pacific Time, 1pm Eastern Time, or 17:00 UTC. Join the stream to ask questions about the data-related topic of your choice, including the latest preview release.<\/p>\n<ul>\n<li><a href=\"https:\/\/aka.ms\/efstandups\">Watch our YouTube playlist<\/a> of previous shows<\/li>\n<li><a href=\"https:\/\/live.dot.net\">Visit the .NET Community Standup<\/a> page to preview upcoming shows<\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/efcore\/issues\/22700\">Submit your ideas<\/a> for a guest, product, demo, or other content to cover<\/li>\n<\/ul>\n<h2>Documentation and Feedback<\/h2>\n<p>The starting point for all EF Core documentation is <a href=\"https:\/\/docs.microsoft.com\/ef\/\">docs.microsoft.com\/ef\/<\/a>.<\/p>\n<p>Please file issues found and any other feedback on the <a href=\"https:\/\/github.com\/dotnet\/efcore\">dotnet\/efcore GitHub repo<\/a>.<\/p>\n<h2>Helpful Links<\/h2>\n<p>The following links are provided for easy reference and access.<\/p>\n<ul>\n<li><a href=\"https:\/\/aka.ms\/efstandups\">EF Core Community Standup Playlist: https:\/\/aka.ms\/efstandups<\/a><\/li>\n<li><a href=\"https:\/\/aka.ms\/efdocs\">Main documentation: https:\/\/aka.ms\/efdocs<\/a><\/li>\n<li><a href=\"https:\/\/aka.ms\/efcorefeedback\">Issues and feature requests for EF Core: https:\/\/aka.ms\/efcorefeedback<\/a><\/li>\n<li><a href=\"https:\/\/aka.ms\/efroadmap\">Entity Framework Roadmap: https:\/\/aka.ms\/efroadmap<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/efcore\/issues\/27185\">Bi-weekly updates: https:\/\/github.com\/dotnet\/efcore\/issues\/27185<\/a><\/li>\n<\/ul>\n<h2>Thank you from the team<\/h2>\n<p>A big thank you from the EF team to everyone who has used and contributed to EF over the years!<\/p>\n<p>Welcome to EF7.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Announcing EF7 Preview 5 with completed support for Table-per-Concrete Type (TPC).<\/p>\n","protected":false},"author":368,"featured_media":40489,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,859],"tags":[4,30,7265,70,71],"class_list":["post-40488","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-entity-framework","tag-net","tag-announcement","tag-announcements","tag-entity-framework","tag-entity-framework-core"],"acf":[],"blog_post_summary":"<p>Announcing EF7 Preview 5 with completed support for Table-per-Concrete Type (TPC).<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/40488","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/368"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=40488"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/40488\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/40489"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=40488"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=40488"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=40488"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}