Let's explore the Entity Framework (EF) ValueConverter feature. ValueConverter, as the name suggests, provides the ability to transform values just before they're stored in the database, and again when reading them back. This adds a handy layer of control to how we handle our data, especially when dealing with sensitive information.

Consider the scenario where we're storing some sensitive employee data, such as monthly salaries, in the employee_compensation table. We want to encrypt this information to keep it hidden from anyone who has direct access to the database. By controlling the application logic and data flow, we can ensure that only a select group of individuals can view the actual decrypted values. This seems like a pretty good scenario where we could use a ValueConverter to encrypt the salary value before storing it in the database and decrypt it when reading it from the database. Let's take a closer look.

Firstly, we'll need an encryption key for the encryption and decryption processes. This key can be stored in the application configuration, most likely populated from the secrets store.

public class EncryptionKey
{
    public const string Section = nameof(EncryptionKey);
    
    public string Value { get; set; } = default!;
}

Next, we register this value via dependency injection, making it available later through IOptionsMonitor.

services.Configure<EncryptionKey>(
    configuration.GetSection(EncryptionKey.Section));

In the configuration sample, configuration is of IConfiguration type, and the EncryptionKey section refers to the "EncryptionKey" key section within our application configuration instance.

Now, let's implement our custom ValueConverter, called EncryptedDecimalValueConverter.

internal sealed class EncryptedDecimalValueConverter
    : ValueConverter<decimal, byte[]>
{
    public EncryptedDecimalValueConverter(
        IOptionsMonitor<EncryptionKey> options)
        : base(
            value => Encrypt(value, options.CurrentValue),
            value => Decrypt(value, options.CurrentValue))
    {
    }

    private static byte[] Encrypt(decimal value, EncryptionKey key)
    {
        using SymmetricAlgorithm aes = Aes.Create();
        using HashAlgorithm hash = SHA256.Create();

        aes.Mode = CipherMode.CBC;
        aes.Padding = PaddingMode.PKCS7;
        aes.Key = hash.ComputeHash(Encoding.UTF8.GetBytes(key.Value));

        ICryptoTransform encryptor = aes.CreateEncryptor();

        using var memoryStream = new MemoryStream();
        using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
        using (var streamWriter = new StreamWriter(cryptoStream))
        {
            streamWriter.Write(data);
        }

        var encryptedData = memoryStream.ToArray();
        return aes.IV.Concat(encryptedData).ToArray();
    }

    private static decimal Decrypt(byte[] value, EncryptionKey key)
    {
        using var aes = Aes.Create();
        using var hash = SHA256.Create();

        var iv = new byte[aes.IV.Length];
        var value = new byte[data.Length - iv.Length];

        Array.Copy(data, iv, iv.Length);
        Array.Copy(data, iv.Length, value, 0, value.Length);

        aes.Mode = CipherMode.CBC;
        aes.Padding = PaddingMode.PKCS7;
        aes.Key = hash.ComputeHash(Encoding.UTF8.GetBytes(key.Value));
        aes.IV = iv;

        ICryptoTransform decryptor = aes.CreateDecryptor();

        using var memoryStream = new MemoryStream(value);
        using var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);
        using var streamReader = new StreamReader(cryptoStream);

        var decryptedString = streamReader.ReadToEnd();
        return decimal.Parse(decryptedString);
    }
}

We then register our converter via dependency injection and inject it into our EmployeeCompensationConfiguration class, which contains the table mapping and definition of our EmployeeCompensation entity.

services.AddSingleton<EncryptedDecimalValueConverter>();

internal class EmployeeCompensation
{
    public long Id { get; set; }
    public long EmployeeId { get; set; }
    public decimal Salary { get; set; }
}

internal sealed class EmployeeCompensationConfiguration
    : IEntityTypeConfiguration<EmployeeCompensation>
{
    private readonly EncryptedDecimalValueConverter _converter;

    public EmployeeCompensationConfiguration(
        EncryptedDecimalValueConverter converter)
    {
        _converter = converter;
    }

    public void Configure(EntityTypeBuilder<EmployeeCompensation> entity)
    {
        entity.ToTable("employee_compensation");

        entity
            .HasKey(e => e.Id)
            .HasName("employee_compensation_id_key");

        entity
            .Property(e => e.EmployeeId)
            .IsRequired()
            .HasColumnType("bigint");
        
        entity
            .Property(e => e.Salary)
            .IsRequired()
            .HasColumnType("bytea")
            .HasConversion(_converter);
    }
}

With everything set up, each time an employee's salary is stored in the employee_compensation table, it gets encrypted. And, when someone with proper authorization accesses this table through the application logic, the salary is decrypted.

That wraps up our brief exploration of the EF ValueConverter feature. I'm curious to know about the real-world scenarios where you've used a value converter. Feel free to share your experiences in the comments below.