Skip to content

Instantly share code, notes, and snippets.

@Klotzi111
Last active October 12, 2025 21:47
Show Gist options
  • Select an option

  • Save Klotzi111/15be45fdb772b10a82faab77efae06d2 to your computer and use it in GitHub Desktop.

Select an option

Save Klotzi111/15be45fdb772b10a82faab77efae06d2 to your computer and use it in GitHub Desktop.
using System.Collections.Concurrent;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
public sealed class StronglyTypedIdValueConverterSelector : ValueConverterSelector
{
// The dictionary in the base type is private, so we need our own one here.
private readonly ConcurrentDictionary<(Type ModelClrType, Type ProviderClrType), ValueConverterInfo> _converters = new();
public StronglyTypedIdValueConverterSelector(ValueConverterSelectorDependencies dependencies) : base(dependencies)
{ }
public override IEnumerable<ValueConverterInfo> Select(Type modelClrType, Type? providerClrType = null)
{
var baseConverters = base.Select(modelClrType, providerClrType);
foreach (var converter in baseConverters)
{
yield return converter;
}
// Extract the "real" type T from Nullable<T> if required
var underlyingModelType = UnwrapNullableType(modelClrType);
var underlyingProviderType = providerClrType is null ? null : UnwrapNullableType(providerClrType);
// Try and get a nested class with the expected name.
var converterType = underlyingModelType.GetNestedType("EfCoreValueConverter");
var targetType = GetCompatibleConverterTargetType(converterType);
if (targetType is not null && (underlyingProviderType is null || underlyingProviderType == targetType))
{
var converterInfo = GetValueConverterInfoForConverterType(modelClrType, underlyingModelType, converterType!, targetType);
if (converterInfo is not null)
{
yield return converterInfo.Value;
}
}
}
private ValueConverterInfo? GetValueConverterInfoForConverterType(Type modelClrType, Type underlyingModelType, Type converterType, Type targetType)
{
return _converters.GetOrAdd(
(underlyingModelType, targetType),
k =>
{
// Create an instance of the converter whenever it's requested.
Func<ValueConverterInfo, ValueConverter> factory =
info => (ValueConverter)Activator.CreateInstance(converterType, info.MappingHints)!;
// Build the info for our strongly-typed ID => TId converter
return new ValueConverterInfo(modelClrType, targetType, factory);
}
);
}
private static Type? GetValueConverterTProviderType(Type type)
{
// check if the type is 'ValueConverter' then extract the TProvider type
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ValueConverter<,>))
{
return type.GetGenericArguments()[1];
}
return null;
}
private static Type? GetCompatibleConverterTargetType(Type? converterType)
{
if (converterType is null)
{
return null;
}
var ret = GetValueConverterTProviderType(converterType);
if (ret is not null)
{
return ret;
}
var baseType = converterType.BaseType;
while (baseType is not null && baseType != typeof(object))
{
ret = GetValueConverterTProviderType(baseType);
if (ret is not null)
{
return ret;
}
baseType = baseType.BaseType;
}
return null;
}
private static Type UnwrapNullableType(Type type)
{
return Nullable.GetUnderlyingType(type) ?? type;
}
}
@Klotzi111
Copy link
Author

Klotzi111 commented Oct 12, 2025

How to use:
StronglyTypedId:

public readonly struct OrderId 
{
    public class EfCoreValueConverter : ValueConverter<OrderId, Guid> { ... }
}

Configure:

services.AddDbContext<ApplicationDbContext>(options =>
  options
    .ReplaceService<IValueConverterSelector, StronglyTypedIdValueConverterSelector>() // add this line
    .UseSqlServer(...);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment