In Domain-Driven Design (DDD), a Value Object is an object that has no identity of its own (no Primary Key) and is entirely defined by its properties. A classic example is a StreetAddress. If you and I have the exact same address string, we live in the same house. EF Core supports Value Objects via Owned Entities.
First, we create the C# class for the Address. Notice it has NO Id property.
public class Address
{
public string Street { get; private set; }
public string City { get; private set; }
public string ZipCode { get; private set; }
public Address(string street, string city, string zipCode)
{
Street = street;
City = city;
ZipCode = zipCode;
}
}
We use this Address type directly inside our User class.
public class User
{
public int Id { get; set; }
public string Name { get; set; }
// The complex Value Object. It is fundamentally Owned by the User.
public Address HomeAddress { get; set; }
}
If we try to run a Migration now, EF Core will panic because it doesn't know how to map Address. We must use the OwnsOne() configuration.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>(entity =>
{
// Tell EF Core that Address is completely dependent on User
entity.OwnsOne(u => u.HomeAddress, addressMap =>
{
// By default, EF Core "Table Splits". It physically flattens the Address
// properties into the main 'Users' SQL table!
addressMap.Property(a => a.Street).HasColumnName("Home_Street");
addressMap.Property(a => a.City).HasColumnName("Home_City");
});
});
}
Users containing: [Id], [Name], [Home_Street], [Home_City].
Address object!
Q: "Why should we use an Owned Entity (Table Splitting) to store an Address, rather than just serializing the Address object to JSON and saving it in an NVARCHAR column?"
Architect Answer: "Queryability and Indexing. If you serialize the Address into a JSON string, SQL Server loses native relational awareness of those fields (unless you utilize specific SQL JSON syntax and computed columns). If you try to run `_context.Users.Where(u => u.HomeAddress.City == 'Chicago')`, a JSON string mapping will crash EF Core's LINQ translator. However, because Owned Entities utilize Table Splitting, the `City` physically exists as a standard `NVARCHAR` column in the `Users` table! That means you can write standard EF Core LINQ queries against the nested object properties, and you can even apply high-performance SQL B-Tree Indexes directly onto the `Home_City` column."