Value Convertors
Transform values between C# types and database representations on every read and write operation.
Overview
A value convertor sits between a C# property and the database column. It is called automatically on every INSERT, UPDATE, and SELECT — you never invoke it manually. The convertor can change both the direction and the type of the value, so the C# type and the column type do not need to match.
Common uses include encrypting sensitive columns, storing enums as strings, serializing collections to a delimited column, normalizing text casing, or adapting any C# type to a compatible database type.
IDbValueConvertor Interface
public interface IDbValueConvertor<TFrom>
{
object? ConvertToDbValue(TFrom? value); // C# → DB (called on INSERT / UPDATE)
TFrom? ConvertFromDbValue(object? dbValue); // DB → C# (called on SELECT)
}TFrom is the declared C# property type. ConvertToDbValue receives the in-memory value and must return whatever the database driver expects. ConvertFromDbValue receives the raw value from the driver and must return the C# representation.
| Method | Direction | When called |
|---|---|---|
ConvertToDbValue(TFrom?) |
C# → DB | INSERT, UPDATE |
ConvertFromDbValue(object?) |
DB → C# | SELECT |
Requirements:
- The convertor class must have a public, parameterless constructor. The source generator instantiates it with
new TConvertor(). - The convertor does not persist instance state across calls; do not rely on fields surviving between rows or queries.
- The
ConvertFromDbValueparameter is the raw value returned byIDataRecord.GetValue(i). For most column types this is the CLR type Npgsql maps to (string,int,Guid, etc.).
Applying a Convertor
Place [ValueConvertor(typeof(TConvertor))] on the property you want to convert:
[ValueConvertor(typeof(LowerCaseConvertor))]
public string Email { get; set; }The value type of the property does not need to match the column type. For example, a List<string> property maps cleanly to a TEXT column if the convertor handles serialization and deserialization.
Examples
Lowercase Normalizer
Stores text in lowercase on every write; reads it back as-is (already lowercase in the database).
using Socigy.OpenSource.DB.Core.Convertors;
public class LowerCaseConvertor : IDbValueConvertor<string>
{
public object? ConvertToDbValue(string? value)
=> value?.ToLowerInvariant();
public string? ConvertFromDbValue(object? dbValue)
=> dbValue?.ToString();
}[Table("user_logins")]
public partial class UserLogin
{
[PrimaryKey, Default(DbDefaults.Guid.Random)]
public Guid Id { get; set; }
[ValueConvertor(typeof(LowerCaseConvertor))]
public string Username { get; set; }
public string? PasswordHash { get; set; }
}AES Encryption
Encrypts a string before persisting and decrypts on read. The database column stores TEXT (base-64 ciphertext).
public class AesConvertor : IDbValueConvertor<string>
{
public object? ConvertToDbValue(string? value)
=> value is null ? null : Encrypt(value);
public string? ConvertFromDbValue(object? dbValue)
=> dbValue is null ? null : Decrypt(dbValue.ToString()!);
private static string Encrypt(string plain) { /* AES encrypt + base-64 */ return plain; }
private static string Decrypt(string cipher) { /* base-64 + AES decrypt */ return cipher; }
}[ValueConvertor(typeof(AesConvertor))]
public string SocialSecurityNumber { get; set; }Enum as String
Stores the enum member name as text, making database values human-readable and safe across enum value reorderings.
public class StatusConvertor : IDbValueConvertor<OrderStatus>
{
public object? ConvertToDbValue(OrderStatus? value)
=> value?.ToString().ToLowerInvariant();
public OrderStatus? ConvertFromDbValue(object? dbValue)
=> dbValue is null
? null
: Enum.Parse<OrderStatus>(dbValue.ToString()!, ignoreCase: true);
}[ValueConvertor(typeof(StatusConvertor))]
public OrderStatus Status { get; set; } = OrderStatus.Active;Enum.Parse will throw a clear exception rather than silently mapping to a wrong value.List to CSV String
Maps a List<string> property to a single TEXT column by joining with a separator on write and splitting on read.
public class CsvConvertor : IDbValueConvertor<List<string>>
{
public object? ConvertToDbValue(List<string>? value)
=> value is null ? null : string.Join(',', value);
public List<string>? ConvertFromDbValue(object? dbValue)
=> dbValue is null
? null
: dbValue.ToString()!.Split(',').ToList();
}[ValueConvertor(typeof(CsvConvertor))]
public List<string> Tags { get; set; } = new();TEXT (or equivalent). The migration tool uses the column attribute for the DDL type — the convertor only affects runtime values, not schema generation.When Convertors Run
| Operation | Method called |
|---|---|
| INSERT | ConvertToDbValue |
| UPDATE | ConvertToDbValue |
| SELECT | ConvertFromDbValue |
Convertors do not affect the DDL emitted by the migration tool. Define the column type to match what ConvertToDbValue returns, not the C# property type.