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:

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.