Neils Schoenfelder
Neils Schoenfelder

Reputation: 455

Constraining generic types by extrinsic functionality

Background:

I am working with an organization that has an ever-growing collection of data types that they need to extract data from. I have no ability to change these data types. Some are machine-generated from XML files provided by other organizations; some are controlled by intransigent internal dev teams; and some are so old that no one is willing to change them in any way for fear that it will destabilize the entire Earth and cause it to crash into the sun. These classes don't share any common interface, and don't derive from any common type other than object. A few sample classes are given below for illustration purposes:

    public class Untouchable
    {
        public string Data;
    }

    public class Unchangeable
    {
        public int Info;
    }

The good news is that most of the time, I can use canned functionality to get at least some of the data from instances of the various classes. Unfortunately, most of these classes also have weird and specialized data that needs class-specific logic to extract data from. Also, information often needs to persist inside of the data extractors because the data objects I'm pulling data from have "interactions" (don't ask).

I have created an abstract generic Extractor<T> class to serve as a repository of common methodology, and an IExtractor<T> interface to serve as a convenient handle to access functionality. I also have a few specific (de-generic?) implementations of this class that can extract information from the business objects built from some of the data types. Here's some sample code to illustrate:

    public interface IExtractor<T>
    {
        string ExtractionOne(T something);
        string ExtractionTwo(T something);
    }

    public abstract class Extractor<T> : IExtractor<T>
    {
        private string internalInfo; // Certain business logic requires us to keep internal info over multiple objects that we extract data from.
        protected Extractor() { internalInfo="stuff"; }

        public virtual string ExtractionOne(T something)
        {
            return "This manipulation is generally applicable to most conceivable types.";
        }

        public abstract string ExtractionTwo(T something); // This DEFINITELY needs to be overridden for each class T
    }

    public class UntouchableExtractor : Extractor<Untouchable>
    {
        public UntouchableExtractor() : base() { }

        public override string ExtractionTwo(Untouchable something)
        {
            return something.Data;
        }
    }

    public class UnchangeableExtractor : Extractor<Unchangeable>
    {
        public UnchangeableExtractor() : base() { }

        public override string ExtractionTwo(Unchangeable something)
        {
            return something.Info.ToString();
        }
    }

I don't yet support all of the available data types, but management wants to roll out the data extractor to end-users using a command-line interface. They're telling me that we should start extracting the data we can extract, and get to the rest later. Support for the many unmodifiable types will be added by me and by and other programmers as time permits, and the end-users are expected to work around our latency. This actually makes sense in our real-world setting, so just go with it.

The Problem:

The list of data types that we want to pull information from is very large. Maintaining an explicit list of supported types in code will be tricky and error prone -- especially if we find any problems with specific data extractors and need to revoke support until we fix some bugs.

I would like to support the large and changing list of supported data types from a single entry point that dynamically determines the "right version" of IExtractor<> to use based on a passed in dynamic dataObject. If there is no class that implements the IExtractor<> to support the given dataObject, than an error should be thrown.

What Doesn't Work:

I tried taking a dynamic thing and using typeof(Extractor<>).MakeGenericType(thing.GetType()) to create an instance of Extractor<Untouchable> or Extractor<Unchangeable>, but those are abstract classes, so I can't use Activator.CreateInstance() to build an instance of those classes. The core issue with this approach is that it's unfortunately looking for a class of the form Extractor<> instead of an interface of the form IExtractor<>.

I considered putting extension methods like IExtractor<T> BuildExtractor(this T something) in some class, but I'm nervous about running into some business logic called BuildExtractor that already exists in one of these untouchable classes. This may be an unhealthy level of paranoia, but that's where I'm at.

Where I need help:

I'd welcome any suggestions for how I can create a single entrypoint for an unconstrained collection of classes. Thanks in advance.

Upvotes: 0

Views: 131

Answers (2)

Pavel Anikhouski
Pavel Anikhouski

Reputation: 23238

The following snippet will create the concrete instance of Extractor<T> class and dynamically invokes a method of this instance

var test = new Unchangeable();
var baseType = typeof(Extractor<>).MakeGenericType(test.GetType());
var extractorType = Assembly.GetExecutingAssembly()
    .GetTypes().FirstOrDefault(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(baseType));
if (extractorType != null)
{
    dynamic extractor = Activator.CreateInstance(extractorType);
    string result = extractor?.ExtractionTwo(test);
}

Of course, it's simplified, you can pass a specific instance of Unchangeable or Untouchable class and restrict assembly types scanning (and get all types only once).

The disadvantage here is that you have to pay attention to ExtractionOne and ExtractionTwo signatures, since they are invoked on dynamic object

The core issue with this approach is that it's unfortunately looking for a class of the form Extractor<> instead of an interface of the form IExtractor<>.

This snippet can help you to look through types using IExtrator<> interface

var baseType = typeof(IExtractor<>).MakeGenericType(typeof(Unchangeable));
var extractorType = Assembly.GetExecutingAssembly()
    .GetTypes().FirstOrDefault(t => t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom(t));

Upvotes: 1

NetMage
NetMage

Reputation: 26917

Combining some code from StackOverflow and my own testing, I suggest using Reflection to find all types implementing an interface:

public static class TypeExt {
    public static bool IsBuiltin(this Type aType) => new[] { "/dotnet/shared/microsoft", "/windows/microsoft.net" }.Any(p => aType.Assembly.CodeBase.ToLowerInvariant().Contains(p));
    public static IEnumerable<Type> ImplementingTypes(this Type interfaceType, bool includeAbstractClasses = false, bool includeStructs = false, bool includeSystemTypes = false, bool includeInterfaces = false) =>
        AppDomain.CurrentDomain.GetAssemblies()
                               .SelectMany(a => a.GetLoadableTypes())
                               .Distinct()
                               .Where(aType => (includeAbstractClasses || !aType.IsAbstract) &&
                                               (includeInterfaces ? aType != interfaceType : !aType.IsInterface) &&
                                               (includeStructs || !aType.IsValueType) &&
                                               (includeSystemTypes || !aType.IsBuiltin()) &&
                                               interfaceType.IsAssignableFrom(aType) &&
                                               aType.GetInterfaces().Contains(interfaceType));
}

public static class AssemblyExt {
    //https://stackoverflow.com/a/29379834/2557128
    public static IEnumerable<Type> GetLoadableTypes(this Assembly assembly) {
        if (assembly == null)
            throw new ArgumentNullException("assembly");
        try {
            return assembly.GetTypes();
        } catch (ReflectionTypeLoadException e) {
            return e.Types.Where(t => t != null);
        }
    }

}

Reflection can be quite slow, and in my testing, getting all loaded types was the slowest part, so I added caching of the loaded types and the implementing types, but this does mean you will need to refresh the loaded types if you dynamically load assemblies:

public static class TypeExt {
    public static bool IsBuiltin(this Type aType) => new[] { "/dotnet/shared/microsoft", "/windows/microsoft.net" }.Any(p => aType.Assembly.CodeBase.ToLowerInvariant().Contains(p));

    static Dictionary<Type, HashSet<Type>> FoundTypes = null;
    static List<Type> LoadableTypes = null;

    public static void RefreshLoadableTypes() {
        LoadableTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetLoadableTypes()).ToList();
        FoundTypes = new Dictionary<Type, HashSet<Type>>();
    }

    public static IEnumerable<Type> ImplementingTypes(this Type interfaceType, bool includeAbstractClasses = false, bool includeStructs = false, bool includeSystemTypes = false, bool includeInterfaces = false) {
        if (FoundTypes != null && FoundTypes.TryGetValue(interfaceType, out var ft))
            return ft;
        else {
            if (LoadableTypes == null)
                RefreshLoadableTypes();

            var ans = LoadableTypes
                       .Where(aType => (includeAbstractClasses || !aType.IsAbstract) &&
                                       (includeInterfaces ? aType != interfaceType : !aType.IsInterface) &&
                                       (includeStructs || !aType.IsValueType) &&
                                       (includeSystemTypes || !aType.IsBuiltin()) &&
                                       interfaceType.IsAssignableFrom(aType) &&
                                       aType.GetInterfaces().Contains(interfaceType))
                       .ToHashSet();

            FoundTypes[interfaceType] = ans;

            return ans;
        }
    }
}

public static class AssemblyExt {
    //https://stackoverflow.com/a/29379834/2557128
    public static IEnumerable<Type> GetLoadableTypes(this Assembly assembly) {
        if (assembly == null)
            throw new ArgumentNullException("assembly");
        try {
            return assembly.GetTypes();
        }
        catch (ReflectionTypeLoadException e) {
            return e.Types.Where(t => t != null);
        }
    }
}

Once you have one of these, you can make a factory method that takes a dynamic object:

public static class ImplementingFactory {
    public static Type ExtractorType(dynamic anObject) {
        Type oType = anObject.GetType();
        var iType = typeof(IExtractor<>).MakeGenericType(oType);
        var ans = iType.ImplementingTypes().FirstOrDefault();
        if (ans == null)
            throw new Exception($"Unable to find IExtractor<{oType.Name}>");
        else
            return ans;
    }
}

Upvotes: 1

Related Questions