I once inherited a codebase where every value object was an EF Core Owned Type. The C# models looked clean, but the main Orders table in SQL Server had over 150 columns. Queries were timing out, and simple reports were a nightmare to build.

The culprit? A well-intentioned feature used in the wrong way.

Owned Types are a great tool, but if you don’t understand how they translate to SQL Server, you can create a maintenance headache that’s hard to untangle. After using them in production for years, I’ve learned where they shine and where they just cause pain.

This is my practical take for anyone using EF Core 8+ with SQL Server.

What’s actually happening under the hood?

Owned Types let you group related properties in your C# code without creating a separate table in the database. Think of an Address object on a Customer. In your code, it’s a neat, self-contained object. In the database, EF Core flattens it right into the Customers table.

It’s a way to keep your domain model expressive without cluttering your database schema with tiny tables and foreign keys for things that don’t need their own identity.

The simplest example

Here’s the classic Address value object stored with a Customer.

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

[Owned]
public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string PostalCode { get; set; }
}

While the [Owned] attribute works, I always use the Fluent API in production for more control over things like column names and lengths.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>()
        .OwnsOne(c => c.BillingAddress, a =>
        {
            a.Property(p => p.Street).HasMaxLength(200);
            a.Property(p => p.City).HasMaxLength(100);
            a.Property(p => p.PostalCode).HasMaxLength(20);
        });
}

How SQL Server sees it

Here’s the sneaky part. To SQL Server, there is no Address object. EF Core generates a single, flat table:

CREATE TABLE Customers (
    Id INT PRIMARY KEY IDENTITY,
    Name NVARCHAR(200) NOT NULL,
    BillingAddress_Street NVARCHAR(200),
    BillingAddress_City NVARCHAR(100),
    BillingAddress_PostalCode NVARCHAR(20)
);

See that BillingAddress_Street column? That _ naming convention is the default. This is critical to remember when you’re writing raw SQL queries or adding indexes.

Here’s when they work great

I reach for Owned Types in a few specific situations:

  1. Truly Cohesive Data: An Address is the perfect example. Street, City, and PostalCode almost always travel together. Another great one is a Money type with Amount and Currency properties. You rarely query for just the currency without the amount.
  2. Avoiding Pointless Joins: If you have 2-3 properties that you always need with the parent entity, making them an Owned Type saves you a JOIN. For a small amount of data, this is a clean performance win.
  3. Improving the Domain Model: It keeps your C# code tidy. You can pass around an Address object instead of five separate string parameters. This makes the code’s intent much clearer.

This one burned me once in production: we had Money objects scattered as PriceAmount and PriceCurrency columns all over the place. Refactoring them into an Owned Type eliminated dozens of joins in our pricing engine, and we saw a measurable speed-up on product listing pages.

And here’s when they’re a bad idea

This is where developers often get into trouble. Avoid Owned Types when:

  1. The Object Might Be Shared: If an Address needs to be used by both a Customer and a Vendor, it’s not an owned type anymore. It’s a shared entity that needs its own table and primary key. Don’t try to force it.
  2. You’re Creating Super-Wide Tables: SQL Server performance can suffer with extremely wide rows. Adding three or four complex Owned Types to one entity can easily push you past 50 columns. This impacts I/O, as SQL Server has to read more data from disk for every query.
  3. You Might Need to Query it Independently: We once used an Owned Type for audit fields (CreatedBy, CreatedAt, ModifiedBy, ModifiedAt). It seemed smart at first. But then the business wanted reports on “all records modified by user X last month.” The queries to scan every table and check the owned columns were a disaster. It was a painful, two-sprint refactor to pull that data out into its own structure.
  4. You’re Nesting Them Deeply: EF Core lets you put an Owned Type inside another Owned Type. Just don’t. The column names become Address_Geolocation_Latitude, and the generated SQL gets weird fast. Keep it one level deep.

The SQL Server performance gotchas

Remember, an Owned Type makes your table wider. If you SELECT * (which EF Core often does), you’re pulling back all those extra columns, whether you need them or not.

The most common mistake is forgetting to index the properties inside the Owned Type. If your users are frequently searching for customers by postal code, you absolutely need an index on that flattened column.

modelBuilder.Entity<Customer>()
    .OwnsOne(c => c.BillingAddress, a =>
    {
        // ... other properties
        a.HasIndex(p => p.PostalCode)
         .HasDatabaseName("IX_Customers_PostalCode"); // I like explicit names
    });

Always check your query plans in SQL Server Management Studio (SSMS). If you see a costly table scan happening, you probably forgot an index on an owned property.

Don’t guess, just test it

You should always have a simple integration test to prove your mappings work as expected against a real database (like SQL Server LocalDB).

[Test]
public async Task Customer_WithBillingAddress_SavesAndLoadsCorrectly()
{
    // Arrange
    var customer = new Customer
    {
        Name = "Acme Corp",
        BillingAddress = new Address
        {
            Street = "123 Main St",
            City = "Seattle",
            PostalCode = "98101"
        }
    };

    // Act
    context.Customers.Add(customer);
    await context.SaveChangesAsync();
    context.ChangeTracker.Clear(); // Ensure we fetch from DB

    // Assert
    var saved = await context.Customers.FirstAsync();
    Assert.IsNotNull(saved.BillingAddress);
    Assert.AreEqual("Seattle", saved.BillingAddress.City);
}

The Final Takeaway: When would I use it?

So, here’s my rule of thumb.

I always use Owned Types for:

  • Small, immutable value objects (2-5 properties).
  • Data that has no identity or meaning outside of its parent (e.g., Money, DateRange).
  • When I know for a fact I will never need to query that object on its own or share it between other entities.

I actively avoid Owned Types for:

  • Anything that might become a shared entity later. If there’s even a 10% chance, I’ll just create a separate table from day one. Migrating is painful.
  • Large collections of properties (more than 5-7). This is a code smell that you’re probably hiding a real entity.
  • Generic concepts like audit trails or tags. These almost always need to be queried independently down the line.

Owned Types are a storage optimization that makes your C# code cleaner. But it’s an optimization. Make sure it fits your query patterns before you commit, or you’ll be paying for it later.

References

FAQ

What are EF Core Owned Types?

EF Core Owned Types are value objects stored in the same SQL Server table as their parent entity, allowing you to group related columns without separate tables.

When should I use EF Core Owned Types?

Use them for small, cohesive value objects like addresses or money types that are always queried with the parent entity.

When should I avoid EF Core Owned Types?

Avoid them for wide tables, reusable entities across aggregates, or when you may need to query the data independently in the future.

Do EF Core Owned Types impact SQL Server performance?

Yes. They can create wider rows, which increases I/O. Always index frequently queried owned properties and monitor execution plans.

Can I migrate from Owned Types to separate tables later?

Yes, but it requires schema changes and data migration scripts. Plan ahead if future flexibility is likely.

About the Author

@CodeCrafter is a software engineer who builds real-world systems , from resilient ASP.NET Core backends to clean, maintainable Angular frontend. With 11+ years in production development, he shares what actually works when shipping software that has to last.