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.
elmah.io: Error logging and Uptime Monitoring for your web apps
This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.
See how we can help you monitor your website for crashes Monitor your website