StrongOf.EntityFrameworkCore 2.1.8

Requires NuGet 2.12 or higher.

dotnet add package StrongOf.EntityFrameworkCore --version 2.1.8
                    
NuGet\Install-Package StrongOf.EntityFrameworkCore -Version 2.1.8
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="StrongOf.EntityFrameworkCore" Version="2.1.8" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="StrongOf.EntityFrameworkCore" Version="2.1.8" />
                    
Directory.Packages.props
<PackageReference Include="StrongOf.EntityFrameworkCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add StrongOf.EntityFrameworkCore --version 2.1.8
                    
#r "nuget: StrongOf.EntityFrameworkCore, 2.1.8"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package StrongOf.EntityFrameworkCore@2.1.8
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=StrongOf.EntityFrameworkCore&version=2.1.8
                    
Install as a Cake Addin
#tool nuget:?package=StrongOf.EntityFrameworkCore&version=2.1.8
                    
Install as a Cake Tool

StrongOf.EntityFrameworkCore

Entity Framework Core value converters for StrongOf strongly-typed primitives. Seamlessly persist strong types like UserId, Email, or Amount to and from the database - without losing type safety.

Installation

dotnet add package StrongOf.EntityFrameworkCore

Quick Start

// Your domain types
public sealed class UserId(Guid value) : StrongGuid<UserId>(value);
public sealed class Email(string value) : StrongString<Email>(value);
public sealed class Amount(decimal value) : StrongDecimal<Amount>(value);

// Your entity
public class Order
{
    public UserId Id { get; set; } = null!;
    public Email CustomerEmail { get; set; } = null!;
    public Amount Total { get; set; } = null!;
}

Usage

The best approach is to register converters once in ConfigureConventions, so every property of that type is automatically converted - no per-property configuration needed:

public class AppDbContext : DbContext
{
    public DbSet<Order> Orders => Set<Order>();

    protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
    {
        // Register once - applies to ALL properties of these types across ALL entities
        configurationBuilder.Properties<UserId>()
            .HaveConversion<StrongOfValueConverter<UserId, Guid>>();

        configurationBuilder.Properties<Email>()
            .HaveConversion<StrongOfValueConverter<Email, string>>();

        configurationBuilder.Properties<Amount>()
            .HaveConversion<StrongOfValueConverter<Amount, decimal>>();
    }
}

This is the recommended approach. You register each strong type exactly once, and EF Core applies the converter everywhere that type appears - in any entity, in any DbSet. No per-property calls to HasConversion or HasStrongOfConversion are needed.

Option 2: Per-Property Configuration with HasStrongOfConversion

If you prefer explicit per-property control (e.g., only certain properties should be converted, or different column types):

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>(entity =>
    {
        entity.Property(e => e.Id)
              .HasStrongOfConversion<UserId, Guid>();

        entity.Property(e => e.CustomerEmail)
              .HasStrongOfConversion<Email, string>()
              .HasMaxLength(256);

        entity.Property(e => e.Total)
              .HasStrongOfConversion<Amount, decimal>()
              .HasPrecision(18, 2);
    });
}

Option 3: Direct Converter Instance

You can also instantiate a converter directly, e.g. for use in custom model building logic:

StrongOfValueConverter<UserId, Guid> converter = new();

// Use with HasConversion directly
modelBuilder.Entity<Order>()
    .Property(e => e.Id)
    .HasConversion(converter);

Supported Types

Strong Type Base Database Type Converter
StrongGuid<T> uniqueidentifier / uuid StrongOfValueConverter<T, Guid>
StrongString<T> nvarchar / text StrongOfValueConverter<T, string>
StrongInt32<T> int StrongOfValueConverter<T, int>
StrongInt64<T> bigint StrongOfValueConverter<T, long>
StrongDecimal<T> decimal StrongOfValueConverter<T, decimal>
StrongChar<T> nchar(1) / char(1) StrongOfValueConverter<T, char>
StrongDateTime<T> datetime2 / timestamp StrongOfValueConverter<T, DateTime>
StrongDateTimeOffset<T> datetimeoffset / timestamptz StrongOfValueConverter<T, DateTimeOffset>

⚠️ EF Core LINQ Limitations

EF Core value converters work transparently for basic CRUD operations (insert, update, delete, read). However, there are important LINQ query limitations you should be aware of.

1. No Translation of Custom Methods or Properties

EF Core cannot translate methods or properties on your strong types into SQL. Only the underlying primitive value is known to the database.

// ❌ Will NOT work - EF Core cannot translate .Value into SQL
var users = await db.Users
    .Where(u => u.Id.Value == someGuid)
    .ToListAsync();

// ✅ Works - compare against another strong type instance
UserId targetId = new(someGuid);
var users = await db.Users
    .Where(u => u.Id == targetId)
    .ToListAsync();

2. Equality and Comparison

EF Core translates ==, !=, and comparison operators correctly when both sides are the same strong type (the converter unwraps both to the primitive for SQL generation):

UserId targetId = new(someGuid);

// ✅ Works - EF Core unwraps both sides
var user = await db.Users
    .FirstOrDefaultAsync(u => u.Id == targetId);

// ✅ Works - ordering is translated correctly
var sorted = await db.Users
    .OrderBy(u => u.Id)
    .ToListAsync();

3. No Contains with Mixed Types

When using Contains with a list of values, the list must be of the strong type, not the primitive:

Guid[] rawGuids = [guid1, guid2, guid3];
List<UserId> userIds = UserId.From(rawGuids)!;

// ❌ Does NOT work - cannot mix Guid[] with UserId property
var users = await db.Users
    .Where(u => rawGuids.Contains(u.Id))
    .ToListAsync();

// ✅ Works - same types on both sides
var users = await db.Users
    .Where(u => userIds.Contains(u.Id))
    .ToListAsync();

4. No SQL Translation for ToString() or Formatting

Calls to ToString(), IFormattable.ToString(format, provider), or any string formatting on strong types are not translatable to SQL:

// ❌ Will throw or evaluate client-side
var emails = await db.Users
    .Select(u => u.Email.ToString())
    .ToListAsync();

// ✅ Select the strong type, then format in memory
var users = await db.Users.ToListAsync();
var emails = users.Select(u => u.Email.Value).ToList();

5. Aggregations Work on Numeric Types

EF Core correctly unwraps numeric strong types for aggregate functions:

// ✅ Works - EF Core unwraps Amount to decimal for SUM
decimal? total = await db.Orders
    .SumAsync(o => (decimal?)o.Total);

Note: You may need to cast to the nullable primitive type (as shown above) because EF Core aggregate methods expect the underlying type, not the strong type.

6. General Rule of Thumb

Operation Works? Notes
Insert / Update / Delete Fully transparent
Where with == / != Both sides must be the same strong type
OrderBy / ThenBy Translated to SQL
Contains (same type) List and property must be the same strong type
Contains (mixed types) Use StrongType.From(...) to convert the list
.Value in LINQ Not translatable - use strong type comparisons
ToString() in LINQ Not translatable - format client-side
Aggregations (Sum, Avg) ⚠️ Cast to nullable primitive may be required
Custom methods in LINQ No SQL translation for user-defined methods

Best Practices

  1. Use ConfigureConventions to register converters once - avoid repetitive per-property configuration.
  2. Compare strong types against strong types in LINQ, not against raw primitives.
  3. Convert lists before querying - use StrongType.From(rawValues) to create a List<TStrong> for Contains queries.
  4. Format and transform client-side - select entities first, then access .Value or call .ToString() in memory.
  5. Test your queries - always verify that your LINQ queries translate to SQL correctly by checking the generated SQL (e.g., via logging or ToQueryString()).
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed.  net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.1.8 71 2/28/2026