Skip to main content

Value Object Converters

Namespace: RA.Utilities.Data.EntityFramework.Conventions

The Conventions namespace provides EF Core value converters that persist the value objects of RA.Utilities.Core.ValueObjects as columns. Each converter derives from ValueConverter<TValueObject, string>.

🎯 Purpose​

Value objects guarantee validity in memory, but EF Core does not know how to store them. A converter supplies two expressions:

  • Write — store the normalized Value (or, for Money, the JSON representation).
  • Read — construct the value object through its constructor, re-running validation on materialization.

That means invalid data already in the database fails loudly when read instead of entering your domain unchecked.

Available converters​

ConverterValue objectStored asRead-side validation
EmailConverterEmailNormalized Value string, e.g. user@example.comnew Email(value)
CurrencyConverterCurrencyISO 4217 code, e.g. EURnew Currency(value)
SsnConverterSSNNormalized string, e.g. J01234567Rnew SSN(value)
IbanConverterIBANNormalized IBAN, e.g. GB82WEST12345698765432new IBAN(value)
MoneyConverterMoneyJSON string, e.g. {"Amount":19.99,"Currency":"EUR"}JsonSerializer.Deserialize<Money>(value)
v10.2.0

IbanConverter was added in v10.2.0 alongside the IBAN value object. The other converters are unchanged.

🚀 How to Use​

Apply a converter per property​

using Microsoft.EntityFrameworkCore.Metadata.Builders;
using RA.Utilities.Data.EntityFramework.Conventions;

public sealed class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
public void Configure(EntityTypeBuilder<Customer> builder)
{
builder.Property(c => c.Email).HasConversion(new EmailConverter());
builder.Property(c => c.Ssn).HasConversion(new SsnConverter());
builder.Property(c => c.Iban).HasConversion(new IbanConverter());
builder.Property(c => c.PreferredCurrency).HasConversion(new CurrencyConverter());
}
}

Apply converters model-wide​

If every property of a given value-object type should use the same converter, configure it once in ConfigureConventions:

using RA.Utilities.Core.ValueObjects;
using RA.Utilities.Data.EntityFramework.Conventions;

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Properties<Email>().HaveConversion<EmailConverter>();
configurationBuilder.Properties<Currency>().HaveConversion<CurrencyConverter>();
configurationBuilder.Properties<SSN>().HaveConversion<SsnConverter>();
configurationBuilder.Properties<IBAN>().HaveConversion<IbanConverter>();

// Money has two supported mappings; choose one:
configurationBuilder.Properties<Money>().HaveConversion<MoneyConverter>();
}

Per-property configuration takes precedence, so you can still map an individual Money property with HasMoney() instead.

Store Money as a single JSON column​

builder.Property(w => w.Balance).HasConversion(new MoneyConverter());

MoneyConverter uses System.Text.Json with default options. Because Money is a record with Amount and Currency, the stored JSON is:

{ "Amount": 19.99, "Currency": "EUR" }

Store Money as two columns​

Use HasMoney() instead of MoneyConverter when amounts must be queried or aggregated in SQL:

modelBuilder.Entity<Wallet>().HasMoney(w => w.Balance); // Balance_Amount + Balance_Currency

💡 Choosing a Money mapping​

MappingColumnsBest for
MoneyConverterOne string column with JSONSimple persistence; value used as a whole
HasMoney(){Property}_Amount (decimal) + {Property}_Currency (string)SQL filtering, sorting, indexing, and aggregation on the amount

Do not configure both for the same property.

🧠 Summary​

Value converters persist validated value objects without giving up their guarantees: writes store the normalized representation, and reads re-run constructor validation. Apply them per property or model-wide, and pick the Money mapping that matches how the data is queried.