With the release of .NET 8, we also see the release of Entity Framework Core 8 and a bounty of new features. One of my favorite new features is Complex Types. When data modeling with Entity Framework Core, we may unnecessarily add tables to our database schema because “that’s just how database modeling works”. This can lead to table sprawl, decreased insert performance, and increased query times.
In this post, we’ll explore how to use Complex Types in Entity Framework Core 8 to reduce the number of tables in our schema, simplify inserts, and increase query performance.
What are Complex Types?
Entity Framework Core has long had the concept of “owned types”, which are properties dependent on their parent object. Depending on your data model, these pieces of information only make sense within the context of additional data. For example, in a hypothetical domain, a physical address may only make sense when related to a customer. Otherwise, it is an arbitrary piece of information. Let’s look at how you may model this.
public class Customer
{
public int Id { get; set; }
public required string Name { get; set; }
public required Address Address { get; set; }
}
As you may notice, the Address
property is used within the Customer
definition. Folks familiar with EF Core might
assume that there would be an Addresses
table, and in previous versions of EF Core, that would have been the case.
In EF Core 8, the modeling process lets us map an Address
directly to columns within a Customers
table.
create table main.Customers
(
Id INTEGER not null
constraint PK_Customers
primary key autoincrement,
Name TEXT not null,
Address_City TEXT not null,
Address_Country TEXT not null,
Address_Line1 TEXT not null,
Address_Line2 TEXT,
Address_PostCode TEXT not null
);
According to the initial release notes, Complex types have certain characteristics:
- Types are not identified or tracked by a key value.
- Must only exist as part of the entity and not directly have a
DbSet
- Can be value or reference types (records or classes).
- Can share the same instance across multiple properties* (be careful).
The final bullet states that the same instance is only shared during the manipulation process of in-memory objects. Once the data is read back out, the newly tracked objects will be different instances. This is important because you may encounter unexpected issues if you’re operating under false assumptions.
So, how do you implement a complex type?
[ComplexType]
public class Address
{
public required string Line1 { get; set; }
public string? Line2 { get; set; }
public required string City { get; set; }
public required string Country { get; set; }
public required string PostCode { get; set; }
}
You add a ComplexType
attribute, of course. Again, note that this class does not have any key.
Saving Complex Types
Using a complex type is as you’d expect. Let’s take a look at a quick usage sample.
Database db = new();
// Complex Type storage of Address
var customer = new Customer
{
Name = "Khalid Abuhakmeh",
Address = new()
{
Line1 = "1 Fantasy Lane",
City = "Los Angeles",
Country = "USA",
PostCode = "90210",
}
};
db.Customers.Add(customer);
await db.SaveChangesAsync();
When we look at the Insert
SQL statement, we can see a straightforward command.
INSERT INTO "Customers" ("Name", "Address_City", "Address_Country", "Address_Line1", "Address_Line2", "Address_PostCode")
VALUES (@p0, @p1, @p2, @p3, @p4, @p5)
That’s one less table needed for our insert statements. That’s great news! What about querying the same model?
Let’s perform the query db.Customers.FirstOrDefault()
and see what we get.
SELECT "c"."Id", "c"."Name", "c"."Address_City", "c"."Address_Country", "c"."Address_Line1", "c"."Address_Line2", "c"."Address_PostCode"
FROM "Customers" AS "c"
LIMIT 1
Cool! Querying a single table is great news again.
We can even write LINQ queries like you’d expect from any previous DbContext
.
var result = await db
.Customers
.Where(x => x.Id == customer.Id)
.Select(x => x.Address)
.FirstOrDefaultAsync();
The previous LINQ statement produces the following SQL.
SELECT "c"."Address_City", "c"."Address_Country", "c"."Address_Line1", "c"."Address_Line2", "c"."Address_PostCode"
FROM "Customers" AS "c"
WHERE "c"."Id" = @__customer_Id_0
LIMIT 1
This is awesome.
A quick note about performance claims, it’s important to test any optimization and performance improvements in your own codebase. While generally less tables involved in a transaction are better, there are scenarios that can be less performant. For example, if you use Complex Types to create a monsterous 500+ column table, then you might want to reconsider your approach and it might be less performant than inserting into multiple tables.
Here’s the DbContext
for completeness.
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace EntityFrameworkCoreEight;
public class Database : DbContext
{
public DbSet<Customer> Customers => Set<Customer>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlite("Data Source= database.db")
.LogTo(Console.Write);
}
public class Customer
{
public int Id { get; set; }
public required string Name { get; set; }
public required Address Address { get; set; }
}
[ComplexType]
public class Address
{
public required string Line1 { get; set; }
public string? Line2 { get; set; }
public required string City { get; set; }
public required string Country { get; set; }
public required string PostCode { get; set; }
}
Conclusion
Complex types allow you to reduce table sprawl, increase insert performance, and speed up query times. There are many opportunities to review existing schemas and optimize your database. It’s still important to realize you might be dealing with reference objects, so be careful about how you assign and modify objects that may be shared. There are still some outstanding issues with Complex Types, but they are not critical show-stoppers. One of the issues is support for inheritance. It is planned for future versions, but I can live without it now. Despite the minor issues, this is an excellent addition to Entity Framework Core 8.
I hope you enjoyed this blog post, and as always, cheers.