In this article, let’s explore what Complex Types are in Entity Framework Core and how we can use them to improve the performance of our .NET applications.
Without further ado, let’s get started.
What Are Complex Types in EF Core?
To begin, let’s first understand what complex types are and why they were added to Entity Framework (EF) Core.
In .NET, mapping objects that contain multiple values but no identity to our database is a common task that we encounter when working with data in our applications. Particularly, we encounter such objects in their immutable form in Domain-Driven-Design (DDD) and refer to them as value objects.
To make it easy for us to map these objects to our databases, the EF Core team introduced complex types in EF 8.
These types are always properties of a main entity, and EF tracks them as part of that owning entity, never as a separate entity. Whenever we add an instance of the owning entity to our database, EF adds the values of our complex type to different columns in the owning entity’s table.
Now, before we go too deep into the theory, let’s demonstrate all these in code.
Prepare EF Core Environment
For this, let’s imagine we have an application where we model our users using an entity that stores their names and their corresponding addresses.
In this app, we represent users’ names as strings, while we represent each address as a concrete type that doesn’t have a primary key property.
So, we can model each user’s address as a complex type for our database interactions using EF Core.
To do this, let’s first define the Address
type:
public record Address(string Street, string City, string State, string PostalCode, string Country);
Here, we define our Address
type as a positional record. This allows us to take advantage of the immutability of records and their implicit equality definitions, thereby making our Address
type an ideal value object.
Next, let’s define our User
entity:
public class User { public int Id { get; set; } public required string UserName { get; set; } public required Address Address { get; set; } }
Our User
class contains an identity, a UserName
property, and an Address
property.
With that, everything is set and ready for use, Next, let’s explore how to configure our Address
record as a complex type property of our User
entity.
How to Configure a Complex Type
There are two ways of configuring a complex type in EF Core.
The first approach involves making use of data annotations:
[ComplexType] public record Address(string Street, string City, string State, string PostalCode, string Country);
While for the second approach, we can utilize the EF Core fluent API:
protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.Entity<User>().ComplexProperty(u => u.Address);
We first override the OnModelCreating()
method in our AppDbContext
class. Then within it, we use the ModelBuilder
object to specify that the Address
property is a complex type member of our User
entity.
Nice! We will stick with this second approach.
Moving on, let’s now create a migration that defines the initial structure of our database:
dotnet ef migrations add CreateComplexTypesDatabase
With that, we can now create our database:
dotnet ef database update
Once we execute this command, EF immediately creates our database and adds it to our project’s base directory.
To see how our complex type (Address
) is mapped to its owning entity (User
), let’s inspect our database creation query:
CREATE TABLE "Users" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY AUTOINCREMENT, "UserName" TEXT NOT NULL, "Address_City" TEXT NOT NULL, "Address_Country" TEXT NOT NULL, "Address_PostalCode" TEXT NOT NULL, "Address_State" TEXT NOT NULL, "Address_Street" TEXT NOT NULL );
Here, we see that the EF creates a single table for our User
entity. This table includes columns for the Id
, UserName
, and each property in the Address
complex type. From this, we can see that EF does not treat our complex type as a separate entity, but rather it handles it as part of another entity.
With that, let’s now take our demonstration a step further. Let’s see how to save complex types to our database.
Saving Complex Types
We can save a complex type object the same way we save normal entities with EF Core. Only this time, they have to be part of an owning entity.
Let’s demonstrate this by creating a SaveComplexType()
method:
public async Task SaveComplexType() { var user = new User() { UserName = "Luther", Address = new Address("Slessor Way", "Bendel", "Rivers", "Nigeria", "504103") }; _context.Users.Add(user); await _context.SaveChangesAsync(); }
First, we initialize a User
object with a user’s name and address (our complex type). Then, we use an instance of our AppDbContext
class to add the user to our Users
table and save the changes to the database.
Now, once we invoke this method, let’s check the generated SQL:
INSERT INTO "Users" ("UserName", "Address_City", "Address_Country", "Address_PostalCode", "Address_State", "Address_Street") VALUES (@p0, @p1, @p2, @p3, @p4, @p5) RETURNING "Id";
Here, the SQL statement adds our user to the Users
table. For each property of our user’s Address
complex type, the SQL statement inserts its value into the corresponding column in the Users
table.
It’s as simple as that. Saving a complex type doesn’t require any special technique or syntax. We must add it to its owning entity and save it to the database.
Querying Complex Types
Moving on, let’s now consider two common querying operations that involve complex types that we can perform.
Retrieving the Owning Entity of a Complex Type
First, let’s look at what happens when we try to retrieve the main entity that houses a complex type:
public async Task<User> GetComplexTypeOwningEntity(int id) => await _context.Users.FirstAsync(user => user.Id == id);
Here, we use the FirstAsync()
method to retrieve a user. Let’s check the generated SQL:
SELECT "u"."Id", "u"."UserName", "u"."Address_City", "u"."Address_Country", "u"."Address_PostalCode", "u"."Address_State", "u"."Address_Street" FROM "Users" AS "u" WHERE "u"."Id" = @__id_0
Again, we see that we perform this retrieval from a single table (our Users
table). Our complex type member does not have a separate table.
Next up, let’s see how to retrieve our complex type from the Users
table by projecting it from our query results.
Retrieving a Complex Type by Projecting It From a Query
For this, let’s define a GetComplexTypeFromOwningEntity()
method:
public async Task<Address> GetComplexTypeFromOwningEntity(int id) { var query = await _context.Users .Select(u => new { u.Id, u.Address }) .SingleAsync(user => user.Id == id); return query.Address; }
Here, we first use the Select() method to get a collection of anonymous types containing the Ids and addresses of all our users. Then, with the SingleAsync()
method, we retrieve the specific anonymous type corresponding to our desired user. Finally, we return the Address
property.
Let’s check the SQL query that this method produces:
SELECT "u"."Id", "u"."UserName", "u"."Address_City", "u"."Address_Country", "u"."Address_PostalCode", "u"."Address_State", "u"."Address_Street" FROM "Users" AS "u" WHERE "u"."Id" = @__id_0
As we can see, this method directly queries our Users
table to retrieve the specified User
object from our database.
Then, after we get this user, we return our complex type from the query results. Therefore, using this method, we retrieve the desired address from a single table without accessing multiple entity tables, thereby improving the performance of our query.
Awesome!
Conclusion
In this article, we’ve examined what complex types are in C# and their role when we want to persist objects with multiple values, but no identity to our databases.
From our discussion, we saw that using complex types can greatly improve the data insertion and query performance of our .NET applications.
However, we should always remember that these performance gains are not always absolute, and depend on our specific use case.