2

I'm using the following generic function to determine whether a class implements a specified interface:

private static bool HasFieldType<TEntity, TInterface>()
{
    return typeof(TInterface).IsAssignableFrom(typeof(TEntity));
}

This works fine for the majority of the time.

However, I now have an interface which has a generic parameter:

public interface IStatusField<TEnum> where TEnum : System.Enum
{
    TEnum Status { get; set; }
}

And this causes the HasFieldType function to break with an unexpected use of unbound generic name error.

Ideally, I want to call the function like:

if (HasFieldType<TEntity, IStatusField<>>()) 
{
    // builder is an EntityTypeBuilder instance
    builder.Property("Status")
        .HasMaxLength(255)
        .HasConversion(new EnumToStringConverter<>());
}

But this won't work as I'm not specifying the generic type for both the IStatusField<> or the EnumToStringConverter<>.

Is there any way around this?

UPDATE

This code forms part of a generic base IEntityTypeConfiguration class as follows:

public abstract class EntityTypeConfiguration<TPrimaryKey, TEntity> : IEntityTypeConfiguration<TEntity> where TEntity : Entity<TPrimaryKey>
{
    public void Configure(EntityTypeBuilder<TEntity> builder)
    {
        builder.HasKey(e => e.Id);

        builder.Property(e => e.Id)
            .IsRequired()
            .HasMaxLength(13)
            .HasValueGenerator<PrimaryKeyValueGenerator>();

        // Apply the generic interface properties
        builder.ApplyInterfaceFields<TPrimaryKey, TEntity>();

        // Apply any additional configuration
        this.OnConfigure(builder);
    }

    protected abstract void OnConfigure(EntityTypeBuilder<TEntity> builder);
}

// In an extension class, I have
public static void ApplyInterfaceFields<TPrimaryKey, TEntity>(this EntityTypeBuilder<TEntity> builder) where TEntity : Entity<TPrimaryKey>
{
    // Check other implementations (removed for brevity)

    // IStatusField implementation
    if (HasFieldType<TEntity, IStatusField<>())
    {
        builder.Property("Status")
            .HasMaxLength(255)
            .HasConversion(new EnumToStringConverter<>());
    }

}

At the point of checking for IStatusField implementation, I know nothing about the generic type specified. I think this may be the bigger problem...

weblar83
  • 681
  • 11
  • 32
  • Why not overload `HasFieldType` to take a generic parameter `TInterface`? – Anoop R Desai Mar 19 '20 at 16:12
  • Unfortunately I can't do this. I'll update my question with more details as to why – weblar83 Mar 19 '20 at 16:13
  • Why can't you simply use `where TEntity : TInterface` with your `HasFieldType` method? Probably I missed something here. – janw Mar 19 '20 at 16:25
  • It's a mismatch between the fact that the entity's type args are in scope here, however what the builder needs is the _field_ type info to supply to `IStatusField` and `EnumToStringConverter`. Those names *must* be resolved, at the end of the day – Josh E Mar 19 '20 at 16:26
  • @weblar83 - see my answer for getting the generic type arguments and using them to construct an instance of a generic type – Josh E Mar 19 '20 at 17:57

2 Answers2

0

Ok, so I've managed to punch my way around the problem.

It needs a bit of tidying and some error checking but in the crudest form:

private static bool HasFieldType<TEntity>(Type interfaceType)
{
    var interfaces = typeof(TEntity).GetTypeInfo().ImplementedInterfaces;

    // Iterate through the interfaces
    foreach (var intf in interfaces)
    {
        // Compare interface names
        if (intf.FullName.Contains(interfaceType.FullName))
        {
            return intf.IsAssignableFrom(typeof(TEntity));
        }
    }

    return false;
}

Which now enables this to work:

// IStatusField implementation
if (HasFieldType<TEntity>(typeof(IStatusField<>)))
{
    builder.Property("Status")
        .HasMaxLength(255)
        .HasConversion<string>();
}

I can just use the built-in automatic string-to-enum conversions from EF to do the grunt work.

weblar83
  • 681
  • 11
  • 32
0

Instead of trying to wrestle with resolving generic type arguments from nothing, you might consider approaching it from the opposite direction, by getting a list of interfaces implemented by TEntity, filtering it to search for an IStatusField. Once you've located the field, you can get its' generic type arguments and pass those to your EnumToStringConverter:

var statusField = typeof(TEntity)
    .GetInterfaces()
    .FirstOrDefault(x => x.Name.StartsWith("IStatusField"));

Value given TEntity : IStatusField<ConsoleColor>:

statusField.GenericTypeArguments = [ typeof(System.Color) ]

From there though you're not done; you must still construct an instance of the generic type EnumToStringConverter<System.Color>. This is rather simple and outlined here.

Edit: I realized that because you'd be invoking a constructor, it's not quite the same. Here's how you'd accomplish this:

var statusField = typeof(TEntity)
            .GetInterfaces()
            .FirstOrDefault(x => x.Name.StartsWith("IStatusField"));

        if (statusField != null)
        {

            var enumType = statusField.GenericTypeArguments[0]; // get the IStatusField<T> value

            // get the default constructor after supplying generic type arg
            var converterType = typeof(EnumToStringConverter<>)
                .MakeGenericType(enumType)
                .GetConstructors()[0];

            // invoke the constructor. Note the null (optional) param
            dynamic converter = converterType.Invoke(new Object[1]);

                builder.Property("Status")
                    .HasMaxLength(255)
                    .HasConversion(converter);
        }
Josh E
  • 7,390
  • 2
  • 32
  • 44