Last active
October 12, 2025 21:47
-
-
Save Klotzi111/15be45fdb772b10a82faab77efae06d2 to your computer and use it in GitHub Desktop.
Helper to use StronglyTypedIds with EntityFramework as if they were the value directly. Actually working version of: https://andrewlock.net/strongly-typed-ids-in-ef-core-using-strongly-typed-entity-ids-to-avoid-primitive-obsession-part-4/#creating-a-custom-valueconverterselector-for-strongly-typed-ids
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
How to use:
StronglyTypedId:
Configure: