Entity Framework Core Advanced Modeling
Explore sophisticated techniques for mapping your object model to a relational database with EF Core.
This section dives into advanced topics for modeling your entities when using Entity Framework Core. We'll cover scenarios that go beyond basic entity and property mapping, enabling you to handle complex database structures and relationships effectively.
Complex Types (Owned Entities)
Complex types, also known as owned entities, allow you to represent entities that don't have their own primary key and are always owned by another entity. This is useful for scenarios like mapping an address to a customer.
Consider an entity that has a set of properties that conceptually belong together but don't need to be their own table. EF Core allows you to define these as owned entities, typically in the OnModelCreating
method using the Fluent API:
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
}
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public Address ShippingAddress { get; set; }
public Address BillingAddress { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Customer>().OwnsOne(c => c.ShippingAddress);
modelBuilder.Entity<Customer>().OwnsOne(c => c.BillingAddress);
}
This configuration will typically result in the address properties being mapped into the same table as the Customer
, with columns like ShippingAddress_Street
, ShippingAddress_City
, etc.
TPH, TPT, and TPC Inheritance
EF Core supports three primary strategies for mapping inheritance hierarchies to relational databases:
- Table-Per-Hierarchy (TPH): The most common strategy. All types in an inheritance hierarchy are mapped to a single table. A discriminator column is used to distinguish between the different entity types.
- Table-Per-Type (TPT): Each type in the hierarchy is mapped to its own table. The base type table contains the common properties, and derived type tables contain their specific properties and a foreign key back to the base type table.
- Table-Per-Concrete Type (TPC): Each concrete type in the hierarchy is mapped to its own table. Base type properties are duplicated in each derived type table. Abstract types are not mapped to a table.
Configuring Inheritance Strategies
You can configure these strategies using the Fluent API:
// Table-Per-Hierarchy (Default)
modelBuilder.Entity<Animal>().HasDiscriminator<string>("Discriminator");
// Table-Per-Type
modelBuilder.Entity<Dog>().ToTable("Dogs");
modelBuilder.Entity<Cat>().ToTable("Cats");
// Table-Per-Concrete Type
modelBuilder.Entity<Car>().ToTable("Cars");
modelBuilder.Entity<Truck>().ToTable("Trucks");
Choose the strategy that best fits your performance and schema requirements. TPH is generally simpler and can perform better for queries involving the base type, while TPT and TPC offer more normalized schemas but can increase query complexity and join overhead.
Owned Types with Collections
You can also have collections of owned types. For instance, a Company
might have multiple Locations
, where each Location
is an owned entity.
public class Location
{
public string Street { get; set; }
public string City { get; set; }
}
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
public List<Location> Locations { get; set; } = new List<Location>();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Company>().OwnsMany(c => c.Locations);
}
When using OwnsMany
, EF Core will create a separate table for the Locations
, including a foreign key back to the Company
table.
Shadow Properties
Shadow properties are properties that are not defined in your entity classes but are defined in the EF Core model. They are often used for tracking information like creation dates or last modified dates that are managed by the database or EF Core itself.
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>().Property<DateTime>("LastModified");
}
You can then use DbContext.Entry(entity).Property("PropertyName").CurrentValue
to access or set these shadow properties.
Value Objects
Value objects are objects that represent a descriptive aspect of the domain but do not have their own identity. They are typically immutable and are compared by their values. EF Core allows you to map these by treating them as owned entities, often with a dedicated column for each property or by using a single complex column type if the database provider supports it.
public class Money
{
public decimal Amount { get; private set; }
public string Currency { get; private set; }
// Constructor and comparison logic
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public Money Price { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>().OwnsOne(p => p.Price);
}
Note: When dealing with complex value objects that might be better represented as a single database column (e.g., JSON or a custom type), you may need to implement custom converters using EF Core's value conversion features.
Shared Types and Table Splitting
Shared Types: In some scenarios, you might have entities that share the same underlying database table. This is less common with modern EF Core but can be achieved using modelBuilder.Entity<EntityType>()
with shared type entities.
Table Splitting: This is a more practical approach where properties of a single entity are mapped to multiple columns in multiple tables. This is often used to normalize a large entity or to separate sensitive data.
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public ProductDetails Details { get; set; }
}
public class ProductDetails
{
public int ProductId { get; set; } // Primary key and foreign key
public string Description { get; set; }
public int StockLevel { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>().HasKey(p => p.Id);
modelBuilder.Entity<Product>().HasOne(p => p.Details).WithOne().HasForeignKey<ProductDetails>(pd => pd.ProductId);
modelBuilder.Entity<ProductDetails>().ToTable("ProductDetails");
modelBuilder.Entity<Product>().ToTable("Products");
}
Tip: Carefully consider the trade-offs between schema normalization and query performance when choosing advanced modeling techniques. For most common scenarios, default EF Core conventions and basic Fluent API configurations will suffice. Reserve these advanced features for specific needs.
By mastering these advanced modeling techniques, you can build robust and efficient data access layers with Entity Framework Core that accurately reflect the complexities of your domain model and database.