Why Value Conversions Exist
A value converter lets EF Core translate a .NET type your domain wants to use (like an enum, a custom Money struct, or a Uri) into a database-primitive type it can store (like a string or int), and back again when reading. You register one with .HasConversion(v => v.ToString(), v => Enum.Parse<OrderStatus>(v)) or the shorthand .HasConversion<string>() for enums, so your C# model stays expressive without forcing an ugly primitive-typed column onto your domain classes.
Cricket analogy: A value converter is like a translator converting a bowler's speed reading from km/h (the domain-friendly unit commentators use) into raw radar-gun mph data stored in the stadium's system, converting back and forth without either side needing to change.
Writing a Value Converter
For anything beyond simple enum-to-string mapping, you write a ValueConverter<TModel, TProvider> explicitly - for example, converting a Money value object into a decimal column, or a List<string> of tags into a comma-separated string with new ValueConverter<List<string>, string>(v => string.Join(',', v), v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()). EF Core applies the converter transparently on every read and write, so LINQ queries against the property still work as long as you're comparing against the converted (provider-side) value.
Cricket analogy: Writing a custom ValueConverter<Money, decimal> is like a scorer manually defining how to convert a complex 'overs bowled' figure (like 4.3 overs) into a single decimal balls-count for spreadsheet analysis, and back again for the printed scorecard.
Owned Types for Value Objects
An owned type - configured with [Owned] or modelBuilder.Entity<Order>().OwnsOne(o => o.ShippingAddress) - lets you model a value object like Address (with Street, City, PostalCode) as a genuine class with its own identity-free semantics, while EF Core maps its properties as columns on the owner's table (Order) by default, prefixed like ShippingAddress_City. Unlike a full entity, an owned type has no separate identity or DbSet - it only exists as part of its owner and is loaded and saved together with it.
Cricket analogy: An owned type like Address is like a player's 'batting stance details' (grip, guard position) that only make sense attached to that specific player's record - they're not tracked as an independent roster entry the way a full player entity would be.
Value converters and owned types solve different problems: use a converter when a single property needs a different database representation than its C# type; use an owned type when a group of properties together form a cohesive value object with its own behavior and no independent identity.
Owned Type Collections and Table Splitting
Owned types can also be collections (OwnsMany), which EF Core maps to a separate table with a shadow FK back to the owner (e.g., Order owning many OrderNote value objects), or you can force an owned reference type onto its own table with .ToTable("Addresses") instead of the default column-prefixed inline mapping - useful when the owned type has many properties and you want to avoid a wide, sparsely-populated owner table.
Cricket analogy: OwnsMany is like a player's record having a repeatable collection of 'injury history' entries stored in their own linked table, rather than cramming every injury into extra columns on the player's single row - similar to owning many OrderNote value objects.
public enum OrderStatus { Pending, Shipped, Delivered, Cancelled }
public class Order
{
public int Id { get; set; }
public OrderStatus Status { get; set; }
public Address ShippingAddress { get; set; } = null!;
public List<OrderNote> Notes { get; set; } = new();
}
public class Address
{
public string Street { get; set; } = null!;
public string City { get; set; } = null!;
public string PostalCode { get; set; } = null!;
}
public class OrderNote
{
public string Text { get; set; } = null!;
public DateTime CreatedAt { get; set; }
}
modelBuilder.Entity<Order>(builder =>
{
builder.Property(o => o.Status).HasConversion<string>();
builder.OwnsOne(o => o.ShippingAddress, sa =>
{
sa.Property(a => a.PostalCode).HasMaxLength(20);
});
builder.OwnsMany(o => o.Notes, n =>
{
n.ToTable("OrderNotes");
n.WithOwner().HasForeignKey("OrderId");
});
});Value converters that allocate or do non-trivial work on every read/write (like JSON serialization) can noticeably slow down large query result sets - benchmark before applying a converter broadly, and consider ValueComparer configuration for owned collection properties to avoid EF Core reporting false-positive change tracking.
- Value converters translate between a C# type and a database-primitive type on every read and write.
- HasConversion<string>() is the common shorthand for storing enums as readable text.
- Custom ValueConverter<TModel, TProvider> instances handle more complex transformations like value objects or collections.
- Owned types (OwnsOne/OwnsMany) model value objects with no independent identity or DbSet.
- By default, an owned reference type's properties are mapped as prefixed columns on the owner's table.
- OwnsMany maps to a separate table with a shadow FK back to the owner by default.
Practice what you learned
1. What does a value converter do in EF Core?
2. Which Fluent API method configures a value object like Address as an owned type on Order?
3. By default, how are an owned reference type's properties mapped to the database?
4. What is the shorthand Fluent API call to store an enum property as its string name instead of an integer?
5. How does OwnsMany() map a collection of owned value objects by default?
Was this page helpful?
You May Also Like
Entity Classes and Conventions
Learn how EF Core discovers and maps plain C# classes to database tables using convention-based modeling, including primary keys, shadow properties, and backing fields.
Data Annotations vs Fluent API
Compare EF Core's two configuration approaches - inline Data Annotations and the more powerful Fluent API - and learn when each is the right tool.
One-to-Many Relationships
Learn how EF Core models one-to-many relationships through navigation properties and foreign keys, including required vs optional relationships and delete behavior.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics