Résoudre le type à partir du nom de classe dans un autre assembly

87

J'ai une méthode où j'ai besoin de résoudre le type d'une classe. Cette classe existe dans un autre assembly avec l'espace de noms similaire à:

MyProject.Domain.Model

J'essaye d'effectuer les opérations suivantes:

Type.GetType("MyProject.Domain.Model." + myClassName);

Cela fonctionne très bien si le code qui exécute cette action est dans le même assembly que la classe dont j'essaye de résoudre le type, cependant, si ma classe est dans un assembly différent, ce code échoue.

Je suis sûr qu'il existe un bien meilleur moyen d'accomplir cette tâche, mais je n'ai pas eu beaucoup d'expérience avec la résolution d'assemblys et la traversée des espaces de noms pour résoudre le type de classe que je recherche. Des conseils ou astuces pour accomplir cette tâche plus gracieusement?

Brandon
la source

Réponses:

171

Vous devrez ajouter le nom de l'assembly comme ceci:

Type.GetType("MyProject.Domain.Model." + myClassName + ", AssemblyName");

Pour éviter toute ambiguïté ou si l'assembly est situé dans le GAC, vous devez fournir un nom d'assembly complet comme celui-ci:

Type.GetType("System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
Sandor Drieënhuizen
la source
Excellent, je savais qu'il me manquait quelque chose de mineur comme inclure l'assemblage. Cette solution a fonctionné pour mes besoins. Merci.
Brandon
10
Et pour ceux qui s'occupent de la sérialisation: Pour obtenir le nom qualifié d'assembly, il y a la propriété Type.AssemblyQualifiedName
Michael Wild
1
Si le type est un List <T>, où T est une classe personnalisée, comment spécifiez-vous 2 assemblys? C'est-à-dire l'assembly mscorlib pour System.Collections.Generic.List et la bibliothèque qui contient T?
Simon Green
@SimonGreen: Vous devrez probablement le construire en utilisant listType.MakeGenericType(itemType). Les deux variables de type peuvent être construites en utilisant Type.GetType()comme dans ma réponse.
Sandor Drieënhuizen
object.Assembly.ToString () Peut être utilisé pour obtenir également l'assembly complet.
zezba9000
6

Cette solution universelle est destinée aux personnes qui ont besoin de charger des types génériques à partir de références externes dynamiques par AssemblyQualifiedName, sans savoir de quel assemblage proviennent toutes les pièces de type générique provenant:

    public static Type ReconstructType(string assemblyQualifiedName, bool throwOnError = true, params Assembly[] referencedAssemblies)
    {
        foreach (Assembly asm in referencedAssemblies)
        {
            var fullNameWithoutAssemblyName = assemblyQualifiedName.Replace($", {asm.FullName}", "");
            var type = asm.GetType(fullNameWithoutAssemblyName, throwOnError: false);
            if (type != null) return type;
        }

        if (assemblyQualifiedName.Contains("[["))
        {
            Type type = ConstructGenericType(assemblyQualifiedName, throwOnError);
            if (type != null)
                return type;
        }
        else
        {
            Type type = Type.GetType(assemblyQualifiedName, false);
            if (type != null)
                return type;
        }

        if (throwOnError)
            throw new Exception($"The type \"{assemblyQualifiedName}\" cannot be found in referenced assemblies.");
        else
            return null;
    }

    private static Type ConstructGenericType(string assemblyQualifiedName, bool throwOnError = true)
    {
        Regex regex = new Regex(@"^(?<name>\w+(\.\w+)*)`(?<count>\d)\[(?<subtypes>\[.*\])\](, (?<assembly>\w+(\.\w+)*)[\w\s,=\.]+)$?", RegexOptions.Singleline | RegexOptions.ExplicitCapture);
        Match match = regex.Match(assemblyQualifiedName);
        if (!match.Success)
            if (!throwOnError) return null;
            else throw new Exception($"Unable to parse the type's assembly qualified name: {assemblyQualifiedName}");

        string typeName = match.Groups["name"].Value;
        int n = int.Parse(match.Groups["count"].Value);
        string asmName = match.Groups["assembly"].Value;
        string subtypes = match.Groups["subtypes"].Value;

        typeName = typeName + $"`{n}";
        Type genericType = ReconstructType(typeName, throwOnError);
        if (genericType == null) return null;

        List<string> typeNames = new List<string>();
        int ofs = 0;
        while (ofs < subtypes.Length && subtypes[ofs] == '[')
        {
            int end = ofs, level = 0;
            do
            {
                switch (subtypes[end++])
                {
                    case '[': level++; break;
                    case ']': level--; break;
                }
            } while (level > 0 && end < subtypes.Length);

            if (level == 0)
            {
                typeNames.Add(subtypes.Substring(ofs + 1, end - ofs - 2));
                if (end < subtypes.Length && subtypes[end] == ',')
                    end++;
            }

            ofs = end;
            n--;  // just for checking the count
        }

        if (n != 0)
            // This shouldn't ever happen!
            throw new Exception("Generic type argument count mismatch! Type name: " + assemblyQualifiedName);  

        Type[] types = new Type[typeNames.Count];
        for (int i = 0; i < types.Length; i++)
        {
            try
            {
                types[i] = ReconstructType(typeNames[i], throwOnError);
                if (types[i] == null)  // if throwOnError, should not reach this point if couldn't create the type
                    return null;
            }
            catch (Exception ex)
            {
                throw new Exception($"Unable to reconstruct generic type. Failed on creating the type argument {(i + 1)}: {typeNames[i]}. Error message: {ex.Message}");
            }
        }

        Type resultType = genericType.MakeGenericType(types);
        return resultType;
    }

Et vous pouvez le tester avec ce code (application console):

    static void Main(string[] args)
    {
        Type t1 = typeof(Task<Dictionary<int, Dictionary<string, int?>>>);
        string name = t1.AssemblyQualifiedName;
        Console.WriteLine("Type: " + name);
        // Result: System.Threading.Tasks.Task`1[[System.Collections.Generic.Dictionary`2[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Collections.Generic.Dictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Nullable`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
        Type t2 = ReconstructType(name);
        bool ok = t1 == t2;
        Console.WriteLine("\r\nLocal type test OK: " + ok);

        Assembly asmRef = Assembly.ReflectionOnlyLoad("System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
        // Task<DialogResult> in refTypeTest below:
        string refTypeTest = "System.Threading.Tasks.Task`1[[System.Windows.Forms.DialogResult, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
        Type t3 = ReconstructType(refTypeTest, true, asmRef);
        Console.WriteLine("External type test OK: " + (t3.AssemblyQualifiedName == refTypeTest));

        // Getting an external non-generic type directly from references:
        Type t4 = ReconstructType("System.Windows.Forms.DialogResult, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", true, asmRef);

        Console.ReadLine();
    }

Je partage ma solution pour aider les personnes ayant le même problème que moi (pour désérialiser TOUT type de chaîne qui pourrait être définie à la fois partiellement ou dans son ensemble dans un assemblage référencé en externe - et les références sont ajoutées dynamiquement par l'utilisateur de l'application).

J'espère que cela aide n'importe qui!

PW
la source
2

Semblable à l'OP, j'avais besoin de charger un sous-ensemble limité de types par nom (dans mon cas, toutes les classes étaient dans un seul assembly et implémentaient la même interface). J'ai eu beaucoup de problèmes étranges en essayant de l'utiliser Type.GetType(string)contre un assemblage différent (même en ajoutant le AssemblyQualifiedName comme mentionné dans d'autres articles). Voici comment j'ai résolu le problème:

Usage:

var mytype = TypeConverter<ICommand>.FromString("CreateCustomer");

Code:

    public class TypeConverter<BaseType>
        {
            private static Dictionary<string, Type> _types;
            private static object _lock = new object();

            public static Type FromString(string typeName)
            {
                if (_types == null) CacheTypes();

                if (_types.ContainsKey(typeName))
                {
                    return _types[typeName];
                }
                else
                {
                    return null;
                }
            }

            private static void CacheTypes()
            {
                lock (_lock)
                {
                    if (_types == null)
                    {
                        // Initialize the myTypes list.
                        var baseType = typeof(BaseType);
                        var typeAssembly = baseType.Assembly;
                        var types = typeAssembly.GetTypes().Where(t => 
                            t.IsClass && 
                            !t.IsAbstract && 
                            baseType.IsAssignableFrom(t));

                        _types = types.ToDictionary(t => t.Name);
                    }
                }
            }
        }

De toute évidence, vous pouvez modifier la méthode CacheTypes pour inspecter tous les assemblys dans AppDomain, ou toute autre logique qui correspond mieux à votre cas d'utilisation. Si votre cas d'utilisation autorise le chargement de types à partir de plusieurs espaces de noms, vous souhaiterez peut-être modifier la clé du dictionnaire pour utiliser le type à la FullNameplace. Ou si vos types n'héritent pas d'une interface commune ou d'une classe de base, vous pouvez supprimer <BaseType>et modifier la méthode CacheTypes pour utiliser quelque chose comme.GetTypes().Where(t => t.Namespace.StartsWith("MyProject.Domain.Model.")

EverPresent
la source
1

Chargez d'abord l'assemblage, puis le type. ex: DLL d'assemblage = Assembly.LoadFile (PATH); DLL.GetType (typeName);

azulay7
la source
0

Pouvez-vous utiliser l'une des méthodes standard?

typeof( MyClass );

MyClass c = new MyClass();
c.GetType();

Sinon, vous devrez ajouter des informations au Type.GetType sur l'assembly.

Jerod Houghtelling
la source
0

Approche courte et dynamique utilisant la AssemblyQualifiedNamepropriété -

Type.GetType(Type.GetType("MyProject.Domain.Model." + myClassName).AssemblyQualifiedName)

Prendre plaisir!

Simonbor
la source
10
Si Type.GetType ("MyProject.Domain.Model." + MyClassName) échoue, comment l'encapsuler dans un autre appel GetType peut-il empêcher cela?
Kaine
1
Dans tous les cas, vous pouvez l'envelopper dans un bloc try catch avec une exception System.NullReferenceException. Beaucoup plus susceptible de se tromper dans ceci - "MyProject.Domain.Model.ClassName, ClassName, Version = 2.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089" puis dans ce - "MyProject.Domain.Model." ...
simonbor
1
@Kaine Je ne suis pas sûr de ce que voulait dire simonbor, mais si vous utilisez GetType (). AssemblyQualifiedName lors de l'ÉCRITURE de la chaîne, vous n'avez pas à vous en soucier lorsque vous utilisez la chaîne pour résoudre un type.
Sergio Porres