Understanding Complex Types in Entity Framework: A Complete Guide

Entity Framework Core (EF Core) is a handy tool for database operations in .NET applications. It is also so powerful that you will find everything you can do conventionally in SQL queries with easy LINQ methods. Today, we will explore an essential feature of Efcore that may help you organize the project models in a reusable manner - Complex types. We will dive deeper into complex types and see how you can leverage the repetitive column fields with them. 

What is a complex type?

A complex type is a set of properties that group related properties as a single unit. This complex type provides a reusable model for mapping columns within a table without having its own table or identity. You can group the properties that represent conceptually related data as a complex type and use it in any database model where that representation is required. The Complex type is flattened to create columns of the table where you use it.

Let's understand the feature with coding examples. Suppose you define a type to represent contact information using PhoneNumber and Email. We will create a complex type to make it reusable, as such information is necessary for multiple tables. 

We will see a generic example of how the Complex type eliminates field redundancy. Later, we will see how to use the feature step-by-step. I will also show you different ways to work with complex types. 

Continuing our use case of contact information

public class ContactInfo
{
    public string PhoneNumber { get; set; }
    public string Email { get; set; }
}

Using it in database models

public class Customer
{
    public int  Id { get; set; }
    public string Name { get; set; }
    public ContactInfo ContactInfo { get; set; }
}

public class Employee
{
    public int Id { get; set; }
    public string FullName { get; set; }
    public ContactInfo ContactInfo { get; set; }
}

public class Vendor
{
    public int Id { get; set; }
    public string CompanyName { get; set; }
    public ContactInfo ContactInfo { get; set; }
}

Theid SQL queries will look like

CREATE TABLE Customers (
    Id INT PRIMARY KEY,
    Name NVARCHAR(MAX),
    ContactInfo_PhoneNumber NVARCHAR(MAX),
    ContactInfo_Email NVARCHAR(MAX)
);

CREATE TABLE Employees (
    Id INT PRIMARY KEY,
    FullName NVARCHAR(MAX),
    ContactInfo_PhoneNumber NVARCHAR(MAX),
    ContactInfo_Email NVARCHAR(MAX)
);

CREATE TABLE Vendors (
    Id INT PRIMARY KEY,
    CompanyName NVARCHAR(MAX),
    ContactInfo_PhoneNumber NVARCHAR(MAX),
    ContactInfo_Email NVARCHAR(MAX)
);

And adding any record will look like

var customer = new Customer 
{ 
    Name = "Damien Martyn", 
    ContactInfo = new ContactInfo 
    { 
        PhoneNumber = "111-331-394", 
        Email = "martyn.crew@util.com" 
    }
}; 

dbContext.Customers.Add(customer); 
dbContext.SaveChanges();

The insertion query will be

INSERT INTO Customers (Name, ContactInfo_PhoneNumber, ContactInfo_Email)
VALUES ('Damien Martyn’, '111-331-394', 'martyn.crew@util.com');

You knew the basics of complex types. Dive deeper to see the step-by-step creation of the feature

Example 1: Complex type with Owned annotation 

Step 1: Prepare your project with the necessary packages.

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package  Microsoft.EntityFrameworkCore.SqlServer // in case you are using MsSql

Step 2: Create a complex type

public record ContactInfo(
    string PhoneNumber,
    string Email
);

Step 3: Define the ContactInfo property in a database entity

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ContactInfo ContactInfo { get; set; }
}

public class Employee
{
    public int Id { get; set; }
    public string FullName { get; set; }
    public ContactInfo ContactInfo { get; set; }
}

public class Vendor
{
    public int Id { get; set; }
    public string CompanyName { get; set; }
    public ContactInfo ContactInfo { get; set; }
}

Step 4: Configure Models in the DbContext

using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Employee> Employees { get; set; }
    public DbSet<Vendor> Vendors { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Configuring the ContactInfo owned type explicitly (optional)
        modelBuilder.Entity<Customer>()
            .OwnsOne(c => c.ContactInfo);

        modelBuilder.Entity<Employee>()
            .OwnsOne(e => e.ContactInfo);

        modelBuilder.Entity<Vendor>()
            .OwnsOne(v => v.ContactInfo);

        base.OnModelCreating(modelBuilder);
    }
}

OwnsOne configures the complex type ContactInfo with each table. 

Step 5: Create migrations 

dotnet ef migrations add AddContactInfo

Apply it

dotnet ef database update

The generated SQL will be 

CREATE TABLE Customers (
    Id INT PRIMARY KEY,
    Name NVARCHAR(MAX),
    ContactInfo_PhoneNumber NVARCHAR(MAX),
    ContactInfo_Email NVARCHAR(MAX)
);

CREATE TABLE Employees (
    Id INT PRIMARY KEY,
    FullName NVARCHAR(MAX),
    ContactInfo_PhoneNumber NVARCHAR(MAX),
    ContactInfo_Email NVARCHAR(MAX)
);

CREATE TABLE Vendors (
    Id INT PRIMARY KEY,
    CompanyName NVARCHAR(MAX),
    ContactInfo_PhoneNumber NVARCHAR(MAX),
    ContactInfo_Email NVARCHAR(MAX)
);

Step 6: Use it

Now, you can use it to insert the data. 

using var dbContext = new AppDbContext();
var customer = new Customer
{
    Name = "Wong",
    ContactInfo = new ContactInfo("123-456-7890", "wong@wh.com")
};

dbContext.Customers.Add(customer);
dbContext.SaveChanges();

Output

Example 2: Complex Type with Owned Annotation

EF Core offers Owned annotation to mark a C# class as a complex type. Owned annotation is an alternative to the HasOnce configuration

using System.ComponentModel.DataAnnotations.Schema;

[Owned]
public record ContactInfo(
    string PhoneNumber,
    string Email
);

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ContactInfo ContactInfo { get; set; }
}

public class Employee
{
    public int Id { get; set; }
    public string FullName { get; set; }
    public ContactInfo ContactInfo { get; set; }
}

public class Vendor
{
    public int Id { get; set; }
    public string CompanyName { get; set; }
    public ContactInfo ContactInfo { get; set; }
}

The owned annotation specifies the class as a complex type. This approach is the most advanced and concise and does not require any other configuration. However, If you still want to define these fields, such as adding IsRequired() or HasMaxLength, you must specify explicitly in the Fluent API. Data annotations like [Required] and [StringLength] won't be applied unless you configure them in the model builder.

Example 3: Complex Type using ComplexType annotation

This annotation is mainly used in earlier versions of the Entity Framework. EFCore prefers the Owned annotation because of its concise and modern approach. ComplexType requires explicit Fluent API configuration, while Owned automatically treats the class as owned and eliminates the need for some Fluent API setup. This code will help if you are working with legacy Entity Framework versions.

Step 1: Define the complex type

using System.ComponentModel.DataAnnotations.Schema;

[ComplexType]
public class ContactInfo
{
    public string PhoneNumber { get; set; }
    public string Email { get; set; }
}

Step 2: Usage in the db models

This step will be the same as earlier examples

Step 3: Configuration of DbContext

using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Configure the Customer entity
        modelBuilder.Entity<Customer>(entity =>
        {
            entity.ToTable("Customers");

            // Configure the complex type ContactInfo
            entity.OwnsOne(c => c.ContactInfo, contact =>
            {
                contact.Property(c => c.PhoneNumber).HasColumnName("PhoneNumber").IsRequired();
                contact.Property(c => c.Email).HasColumnName("Email").IsRequired();
            });
        });

        base.OnModelCreating(modelBuilder);
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("YourConnectionStringHere"); // Replace connection string
        base.OnConfiguring(optionsBuilder);
    }
}

All the other steps are the same as other examples.

Advantages of Complex types

Using complex types edges you in a few ways. Defining a standard unit provides reusability and encapsulation in modeling the database, especially if the representation is required in many tables. It also adds simplicity to the code base with encapsulating fields. Complex units are stored flattened in the parent table instead of defining a separate table that would need additional joins. If you introduce complex types after understanding your models, you can leverage this high maintainability feature where you have only one place to update. Adding related fields to one model can achieve the DRY (Don't Repeat Yourself) principle.

Conclusion 

Entity framework is a powerful ORM (Object-Relational Mapping ) tool for database operations in .NET applications. Most developers opt for it over other options because of its vast features and easy LINQ support. Complex Type is one of the reusability features that allows you to group related types at one model rather than repeating it in multiple classes. Also known as value types, complex types do not have any table or own identity, but they are flattened in the table they associate with. Complex types provide the solution for DRY in such cases and improve maintainability and reusability. Elimination of additional tables enhances database queries and operations. I offered different ways to use it in your applications and leverage this EFCore member.