Entity Framework Core - Setting Value Converter generically

Steven Day picture Steven Day · Oct 24, 2018 · Viewed 7.2k times · Source

I'm currently trialing Entity Framework Core 2.1 with a view to using it in the company I work for's business applications. I've got most of the way in implementing Value Converters in my test project but my existing knowledge base has let me down at the last hurdle!

What I'm trying to do

My understanding is that for enum values, the built in type converters can convert from the enum value to the string equivalent (EnumToStringConverter) or from the enum value to it's numerical representation (EnumToNumberConverter). However we use a custom string value to represent the enum in our database, so I have written a custom EnumToDbStringEquivalentConvertor to do this conversion and the database string value is specified as an attribute on each of the enum values in my model.

The code is as follows:

Model

public class User
{
    [Key] public int ID { get; set; }
    public EmployeeType EmployeeType { get; set; }
}

public enum EmployeeType
{
    [EnumDbStringValue("D")]
    Director,
    [EnumDbStringValue("W")]
    Weekly,
    [EnumDbStringValue("S")]
    Salaried
}

DataContext

public class MyDataContext : DbContext
{
    public DbSet<User> Users { get; set; }

    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
            foreach (var property in entityType.GetProperties())
            {
                if (property.ClrType.IsEnum)
                {
                    property.SetValueConverter(new EnumToDbStringEquivalentConvertor<EmployeeType>());
                }
             }
         }
    }
}

Value Converter

public class EnumToDbStringEquivalentConvertor<T> : ValueConverter<T, string>
{
    public EnumToDbStringEquivalentConvertor(ConverterMappingHints mappingHints = null) : base(convertToProviderExpression, convertFromProviderExpression, mappingHints)
    { }

    private static Expression<Func<T, string>> convertToProviderExpression = x => ToDbString(x);
    private static Expression<Func<string, T>> convertFromProviderExpression = x => ToEnum<T>(x);

    public static string ToDbString<TEnum>(TEnum tEnum)
    {
        var enumType = tEnum.GetType();
        var enumTypeMemberInfo = enumType.GetMember(tEnum.ToString());
        EnumDbStringValueAttribute enumDbStringValueAttribute = (EnumDbStringValueAttribute)enumTypeMemberInfo[0]
            .GetCustomAttributes(typeof(EnumDbStringValueAttribute), false)
            .FirstOrDefault();

        return enumDbStringValueAttribute.StringValue;
    }

    public static TEnum ToEnum<TEnum>(string stringValue)
    {
        // Code not included for brevity
    }
}

This code (I'm glad to say) seems to be working without any issues.

My problem

The documentation around value converters seems to suggest the way we assign them in the OnModelCreating method is to physically assign each individual type converter to each individual property in the model. I don't want to have to do this - I want my model to be the driver. I'll implement this later but, for now, in the current version of the code I'm looping through the entity types in my model, checking the 'IsEnum' property value and then assigning the value converter at that point.

My problem is that the SetValueConverter extension method that I'm using requires me to pass it a new instance of EnumToDbStringEquivalentConvertor, which in my example is hard coded to be EnumToDbStringEquivalentConvertor which works. However I don't want that to be hardcoded - I want to pass the entity type's ClrType.

I have used reflection to create generic types and generic methods before but I can't seem to find the right code to get this working.

This:

public class MyDataContext : DbContext
{
    public DbSet<User> Users { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            foreach (var property in entityType.GetProperties())
            {
                if (property.ClrType.IsEnum)
                {
                    var converterType = typeof(EnumToDbStringEquivalentConvertor<>);
                    var genericConverterType = converterType.MakeGenericType(property.ClrType);

                    MethodInfo setValueConverterMethodInfo = typeof(MutablePropertyExtensions).GetMethod("SetValueConverter");
                    setValueConverterMethodInfo.Invoke(property,
                            new object[] { property, Activator.CreateInstance(genericConverterType) });
                }
             }
         }
    }
}

gives me an error of "System.MissingMethodException: 'No parameterless constructor defined for this object.'" on the GetModel method in Microsoft.EntityFrameworkCore.Infrastructure

So my question is can anyone advise me of how I can pass my value converter generically to EF Core's 'SetValueConveter' method?

Thank you in advance for your assistance.

Answer

Ivan Stoev picture Ivan Stoev · Oct 24, 2018

You are almost there. The problem is this code

Activator.CreateInstance(genericConverterType)

which tries to find and invoke parameterless constructor of your converter class. But your class constructor do have parameter, although optional. Pptional parameters are just compiler sugar, when using reflection you should pass them explicitly.

So you need to use the CreateInstance overload accepting params object[] args and pass null for mappingHints.

Also, there is no need to call SetValueConverter via reflection - it's part of the public API.

The working code could be like this:

if (property.ClrType.IsEnum)
{
    var converterType = typeof(EnumToDbStringEquivalentConvertor<>)
        .MakeGenericType(property.ClrType);    
    var converter = (ValueConverter)Activator.CreateInstance(converterType, (object)null);
    property.SetValueConverter(converter);
}