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.

To download the source code for this article, you can visit our GitHub repository.

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.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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:

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:

          "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")

    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.



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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!