How to Improve Entity Framework Performance Using Complex Types

Let’s examine Entity Framework Core’s Complex Types and how to use them to enhance the efficiency of our.NET applications in this article.

Without further ado, let’s get started.

What Are Complex Types in EF Core?

When working with data in our applications, one of the most frequent tasks we face in.NET is mapping objects that have multiple values but no identity to our database. In Domain-Driven-Design (DDD), in particular, we come across such objects in their immutable form and call them value objects.

EF Core team introduced complex types in EF 8 to make it easier for us to map these objects to our databases.

These kinds are never tracked by EF as a distinct entity; instead, it always tracks them as properties of the main entity that owns them. EF adds the values of our complex type to various columns in the owning entity’s table whenever we add an instance of the owning entity to our database.

Let’s show all of these in code now before delving too far into the theory.

Prepare EF Core Environment

To illustrate this, consider an application in which users are modeled using an entity that contains both their names and corresponding addresses.

In this application, each address is represented as a concrete type without a primary key property, and user names are represented as strings.

Therefore, we can use EF Core to model each user’s address as a complex type for our database interactions.

Let’s first define the Address type in order to do this:

public record Address(string Street, string City, string State, string PostalCode, string Country);

We specify a positional record as our address type in this instance. This makes our Address type an ideal value object because it enables us to leverage the implicit equality definitions and immutability of records.

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.

Everything is now configured and operational. Let’s now examine how to set up our Address record as a complex type property of our User entity.

How to Configure a Complex Type

In EF Core, complex types can be configured in two different ways.

Utilizing data annotations is the first strategy:

[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);

The first thing we do is modify the AppDbContext class’s OnModelCreating() function. Next, we specify that the Address property is a complex type member of our User entity within it by using the ModelBuilder object.

Well done! We’ll continue using this second strategy.

Now that we have a migration defined, let’s proceed to define our database’s initial structure:

dotnet ef migrations add CreateComplexTypesDatabase

With that, we can now create our database:

dotnet ef database update

EF instantly creates our database and adds it to the base directory of our project after we run this command.

Let’s examine our database creation query to see how our complex type (Address) is mapped to its owning entity (User):

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 can observe that our User entity is given its own single table by the EF. Columns for the Id, UserName, and every property in the Address complex type are included in this table. This indicates that EF handles our complex type as a component of another entity rather than treating it as a stand-alone entity.

Now that we have that established, let us advance our demonstration. Let’s examine how to store intricate types in our database.

Saving Complex Types

EF Core allows us to save complex type objects in the same manner as regular entities. They must be a part of an owning entity only this once.

To illustrate, let’s write a SaveComplexType() function:

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();
}

Initially, we initialize a User object (our complex type) with the address and name of the user. Next, we add the user to our Users table and save the database modifications using an instance of our AppDbContext class.

After using this method, let’s examine the SQL that was produced:

INSERT INTO "Users" ("UserName", "Address_City", "Address_Country",
      "Address_PostalCode", "Address_State", "Address_Street")
      VALUES (@p0, @p1, @p2, @p3, @p4, @p5)
      RETURNING "Id";

Our user is now added to the Users table by the SQL statement. The SQL statement inserts the value of each property of our user’s Address complex type into the appropriate column in the Users table.

That is all there is to it. A complex type can be saved without the need for any unique syntax or techniques. It needs to be saved in the database and added to the owning entity.

Querying Complex Types

Now that we’ve discussed two popular querying operations involving complex types, let’s move on.

Retrieving the Owning Entity of a Complex Type

Let’s first examine what occurs when we attempt to obtain the primary entity that contains 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

Once more, we observe that we only use one table for this retrieval (our Users table). Our member of complex type does not have a distinct table.

Let’s now see how to project our complex type from the query results and retrieve it from the Users table.

Projecting a Complex Type and Getting It Out of 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 obtain a collection of anonymous types containing all of our users’ addresses and Ids by using the Select() method. Then, we retrieve the particular anonymous type that corresponds to our desired user using the SingleAsync() method. We finally give the Address property back.

Let’s examine the SQL query that this process generates:

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 retrieves the specified User object from our database by making a direct query to our Users table.

Next, we retrieve this user and use the query results to return our complex type. As a result, by employing this technique, we can improve the query’s performance by retrieving the needed address from a single table rather than having to access several entity tables.

Conclusion

This article has covered the definition of C# complex types and their function in persisting objects to databases that have multiple values but no identity.

We saw from our discussion that our .NET applications’ data insertion and query performance can be significantly enhanced by the use of complex types.

But we must always keep in mind that these performance improvements are situational and contingent upon our particular use case.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *