Compiler dynamiquement une classe dans le Code App tout en pré-compilant le reste du Projet / Bibliothèque

ASP.NET a des dossiers d'application spéciaux comme App_Code:

contient le code source pour les classes partagées et les objets d'affaires (par exemple, ..cs, et .vb fichiers) que vous souhaitez compiler dans le cadre de votre demande. Dans un projet de site web compilé dynamiquement, ASP.NET compile le code dans le dossier App_Code sur la demande initiale de votre application. Les éléments de ce dossier sont alors recompilés lorsque des changements sont détecter.

le problème, c'est que je construis une application web, pas un site web compilé dynamiquement. Mais j'aimerais pouvoir stocker les valeurs de configuration directement dans C#, plutôt que de servir via un XML et avoir à lire pendant Application_Start et de les stocker dans HttpContext.Current.Application

donc j'ai le code suivant dans /App_Code/Globals.cs:

namespace AppName.Globals
{
    public static class Messages
    {
        public const string CodeNotFound = "The entered code was not found";
    }
}

ce Qui pourrait être n'importe où dans l'application comme ceci:

string msg = AppName.Globals.Messages.CodeNotFound;

le but est d'être capable de stocker toutes les littérales dans un zone configurable qui peut être mise à jour sans recompiler toute l'application.

je peux utiliser le .cs fichier définir son action de compilation pour compiler, mais, ce faisant, des bandes App_Code/Globals.cs depuis ma sortie.

Q:Est-il possible d'identifier parties d'un projet qui devraient compiler dynamiquement tout en permettant au reste du projet précompilé?


  • Si je mets le action de construirecontent - le .cs fichier sera copié dans le bin et compilé à l'exécution. Toutefois, dans ce cas, il n'est pas disponible au moment de la conception.
  • Si je mets le action de construirecompile - je peux accéder aux objets de la même manière que n'importe quelle autre classe compilée pendant design/runtime, mais il sera retiré du dossier /App_Code une fois publié. Je peut encore placer dans le répertoire de sortie à l'aide de Copy Always, mais les classes déjà compilées semblent prendre la priorité donc je ne peux pas pousser les changements de configuration sans redéployer toute l'application.

App_Code Content vs Compile

14
demandé sur KyleMit 2017-12-21 18:49:13

3 réponses

Aperçu Des Problèmes

nous devons surmonter deux problèmes différents ici:

  1. la première est d'avoir un fichier unique qui peut être compilé au moment de la compilation et aussi recompilé au moment de l'exécution.
  2. la seconde est de résoudre les deux versions différentes de cette classe créée par la résolution du premier problème afin que nous puissions réellement les utiliser.

Problème 1-Compilation de Schrödinger

Le premier problème est essayer d'obtenir une classe qui est à la fois compilé et non pas compilé. Nous avons besoin de le compiler au moment de la conception afin que les autres sections de code soient au courant de son existence et puissent utiliser ses propriétés avec un fort typage. Mais normalement, le code compilé est retiré de la sortie de sorte qu'il n'y a pas plusieurs versions de la même classe provoquant des conflits de noms.

Dans tous les cas, nous besoin pour compiler la classe au départ, mais il y a deux options pour persister une re-compilable copie:

  1. Ajouter le fichier à App_Code, qui est compilé à l'exécution par défaut, mais défini comme Build Action = Compiler donc, c'est au moment de la conception.
  2. Ajouter une classe ordinaire de fichier, qui est compilé au moment de la conception par défaut, mais il Copier vers le Répertoire de Sortie = Copy, donc il y a une chance que nous puissions l'évaluer à l'exécution aussi bien.

problème 2 - DLL auto-imposée L'enfer

à tout le moins, c'est une tâche délicate à charger sur le compilateur. Tout code qui consomme une classe doit avoir une garantie qu'il existe au moment de la compilation. Tout ce qui est compilé dynamiquement, que ce soit via App_Code ou autrement, fera partie d'un assemblage entièrement différent. Donc, produire une classe identique est traité plus comme une image de cette classe. Le type sous-jacent pourrait être le même, mais ce n'est une pipe.

nous en avons deux options: utilisez une interface ou un passage entre les assemblages:

  1. si nous utilisons un interface, nous pouvons le compiler avec la construction initiale et tous les types dynamiques peuvent implémenter cette même interface. De cette façon, nous comptons en toute sécurité sur quelque chose qui existe au moment de la compilation, et notre classe créée peut être échangée en toute sécurité comme propriété de sauvegarde.

  2. Si nous types de coulée à travers les assemblages, c'est il est important de noter que toute utilisation existante dépend du type qui a été compilé à l'origine. Nous aurons donc besoin de saisir les valeurs du type dynamique et appliquer ces valeurs de propriété au type original.

Apple Class

Réponses Existantes

Par evk j'aime bien l'idée de l'interrogation AppDomain.CurrentDomain.GetAssemblies() au démarrage pour vérifier les nouvelles assemblées/classes. Je vais vous avouer que l'utilisation d'une interface est probablement une façon conseillée d'unifier les classes précompilées/compilées dynamiquement, mais j'aimerais idéalement avoir un seul fichier/classe qui puisse être simplement relu s'il change.

Par S. Deepika j'aime bien l'idée de compiler dynamiquement à partir d'un fichier, mais ne voulez pas avoir à déplacer les valeurs d'un projet distinct.

Décision App_Code

App_Code déverrouiller la possibilité de construire deux versions de la même classe, mais c'est effectivement dur de modifier soit un après publication comme nous le verrons. .cs le fichier situé dans ~ / App_Code / sera compilé dynamiquement lorsque l'application sera lancée. Ainsi, dans Visual Studio, nous pouvons construire la même classe deux fois en l'ajoutant à App_Code et en définissant le Action De ConstruireCompiler.

construisez Action et copiez Sortie:

Build Action and Copy Output

quand nous déboguons localement, tout .les fichiers cs seront intégrés dans l'assemblage du projet et le fichier physique dans ~/App_Code sera également intégré.

Nous pouvons identifier deux types comme ceci:

// have to return as object (not T), because we have two different classes
public List<(Assembly asm, object instance, bool isDynamic)> FindLoadedTypes<T>()
{
    var matches = from asm in AppDomain.CurrentDomain.GetAssemblies()
                  from type in asm.GetTypes()
                  where type.FullName == typeof(T).FullName
                  select (asm,
                      instance: Activator.CreateInstance(type),
                      isDynamic: asm.GetCustomAttribute<GeneratedCodeAttribute>() != null);
    return matches.ToList();
}

var loadedTypes = FindLoadedTypes<Apple>();

types compilés et dynamiques:

Compiled and Dynamic Types

C'est vraiment proche de la résolution le problème n ° 1. Nous avons accès aux deux types chaque fois que l'application fonctionne. Nous pouvons utiliser la version compilée au moment de la conception et tout changement au fichier lui-même sera automatiquement recompilé par IIS dans une version à laquelle nous pouvons accéder à l'exécution.

le problème est apparent cependant une fois que nous sortons du mode de débogage et essayons de publier le projet. Cette solution repose sur la construction par IIS de la App_Code.xxxx montage dynamique, et qui repose sur le .fichier cs se trouvant dans le dossier racine App_Code. Toutefois, lorsqu'un .cs le fichier est compilé, il est automatiquement retiré du projet publié, pour éviter le scénario exact que nous essayons de créer (et gérer délicatement). Si le fichier était laissé en place, il produirait deux classes identiques, ce qui créerait des conflits de noms chaque fois que l'une ou l'autre serait utilisée.

nous pouvons essayer de forcer sa main en compilant le fichier dans l'assemblage du projet et aussi en copiant le fichier dans le répertoire de sortie. Mais App_Code ne fonctionne pas tout ce qui est magique à l'intérieur de ~ / bin/App_Code/. Il ne fonctionnera qu'au niveau de la racine ~ / App_Code/

App_Code Compilation Source:

App_Code Compilation Source

avec chaque publication, Nous pourrions couper et coller manuellement le dossier App_Code généré à partir de la corbeille et le replacer au niveau de la racine, mais c'est au mieux précaire. Peut-être pourrions-nous automatiser cela pour construire des événements, mais nous essaierons quelque chose autre...

Solution

compiler + (Copier vers la sortie et compiler manuellement le fichier)

évitons le dossier App_Code car il ajoutera des conséquences involontaires.

il suffit de créer un nouveau dossier nommé Config et ajouter une classe qui stockera les valeurs que nous voulons pouvoir modifier dynamiquement:

~/Config/AppleValues.cs:

public class Apple
{
    public string StemColor { get; set; } = "Brown";
    public string LeafColor { get; set; } = "Green";
    public string BodyColor { get; set; } = "Red";
}

encore une fois, nous voulons aller aux propriétés du fichier ( F4) et configuré pour compiler et copie à la sortie. Cela nous donnera une deuxième version du fichier que nous pourrons utiliser plus tard.

nous consommerons cette classe en l'utilisant dans une classe statique qui expose les valeurs de n'importe où. Cela permet de séparer les préoccupations, en particulier entre la nécessité de compiler dynamiquement et statiquement accès.

~/Config/GlobalConfig.cs:

public static class Global
{
    // static constructor
    static Global()
    {
        // sub out static property value
        // TODO magic happens here - read in file, compile, and assign new values
        Apple = new Apple();
    }

    public static Apple Apple { get; set; }
}

et nous pouvons l'utiliser comme ceci:

var x = Global.Apple.BodyColor;

ce que nous tenterons de faire à l'intérieur du constructeur statique, c'est seed Apple avec les valeurs de notre classe dynamique. Cette méthode sera appelée une fois chaque fois que l'application est redémarrée, et toute modification au dossier bin déclenche automatiquement le recyclage du pool d'applications.

En bref, voici ce que nous voulons accomplir à l'intérieur de l'constructeur:

string fileName = HostingEnvironment.MapPath("~/bin/Config/AppleValues.cs");
var dynamicAsm = Utilities.BuildFileIntoAssembly(fileName);
var dynamicApple = Utilities.GetTypeFromAssembly(dynamicAsm, typeof(Apple).FullName);
var precompApple = new Apple();
var updatedApple = Utilities.CopyProperties(dynamicApple, precompApple);

// set static property
Apple = updatedApple;

fileName - le chemin du fichier peut être spécifique à l'endroit où vous comme pour la déployer, mais note qu'à l'intérieur d'une méthode statique, vous devez utiliser HostingEnvironment.MapPath au lieu de Server.MapPath

BuildFileIntoAssembly - en termes de chargement de l'assemblage à partir d'un fichier, j'ai adapté le code des docs sur CSharpCodeProvider et cette question sur Comment charger une classe à partir d'une .fichier cs. Aussi, plutôt que de combattre les dépendances, j'ai juste donné le accès du compilateur à tous les assemblages qui se trouvaient actuellement dans le domaine de L'application, comme sur la compilation originale. Il y a probablement un moyen de le faire avec moins de frais généraux, mais c'est un coût unique qui s'en soucie.

CopyProperties - pour mapper les nouvelles propriétés sur l'ancien objet, j'ai adapté la méthode dans cette question sur comment appliquer automatiquement des valeurs de propriétés d'un objet à un autre du même type? qui utilise la réflexion pour décomposer les deux objets et itérer sur chaque propriété.

Utilitaires.cs

voici le code source complet pour les méthodes Utility d'en haut

public static class Utilities
{

    /// <summary>
    /// Build File Into Assembly
    /// </summary>
    /// <param name="sourceName"></param>
    /// <returns>https://msdn.microsoft.com/en-us/library/microsoft.csharp.csharpcodeprovider.aspx</returns>
    public static Assembly BuildFileIntoAssembly(String fileName)
    {
        if (!File.Exists(fileName))
            throw new FileNotFoundException($"File '{fileName}' does not exist");

        // Select the code provider based on the input file extension
        FileInfo sourceFile = new FileInfo(fileName);
        string providerName = sourceFile.Extension.ToUpper() == ".CS" ? "CSharp" :
                              sourceFile.Extension.ToUpper() == ".VB" ? "VisualBasic" : "";

        if (providerName == "")
            throw new ArgumentException("Source file must have a .cs or .vb extension");

        CodeDomProvider provider = CodeDomProvider.CreateProvider(providerName);

        CompilerParameters cp = new CompilerParameters();

        // just add every currently loaded assembly:
        // https://stackoverflow.com/a/1020547/1366033
        var assemblies = from asm in AppDomain.CurrentDomain.GetAssemblies()
                         where !asm.IsDynamic
                         select asm.Location;
        cp.ReferencedAssemblies.AddRange(assemblies.ToArray());

        cp.GenerateExecutable = false; // Generate a class library
        cp.GenerateInMemory = true; // Don't Save the assembly as a physical file.
        cp.TreatWarningsAsErrors = false; // Set whether to treat all warnings as errors.

        // Invoke compilation of the source file.
        CompilerResults cr = provider.CompileAssemblyFromFile(cp, fileName);

        if (cr.Errors.Count > 0)
            throw new Exception("Errors compiling {0}. " +
                string.Join(";", cr.Errors.Cast<CompilerError>().Select(x => x.ToString())));

        return cr.CompiledAssembly;
    }

    // have to use FullName not full equality because different classes that look the same
    public static object GetTypeFromAssembly(Assembly asm, String typeName)
    {
        var inst = from type in asm.GetTypes()
                   where type.FullName == typeName
                   select Activator.CreateInstance(type);
        return inst.First();
    }


    /// <summary>
    /// Extension for 'Object' that copies the properties to a destination object.
    /// </summary>
    /// <param name="source">The source</param>
    /// <param name="target">The target</param>
    /// <remarks>
    /// https://stackoverflow.com/q/930433/1366033
    /// </remarks>
    public static T2 CopyProperties<T1, T2>(T1 source, T2 target)
    {
        // If any this null throw an exception
        if (source == null || target == null)
            throw new ArgumentNullException("Source or/and Destination Objects are null");

        // Getting the Types of the objects
        Type typeTar = target.GetType();
        Type typeSrc = source.GetType();

        // Collect all the valid properties to map
        var results = from srcProp in typeSrc.GetProperties()
                      let targetProperty = typeTar.GetProperty(srcProp.Name)
                      where srcProp.CanRead
                         && targetProperty != null
                         && (targetProperty.GetSetMethod(true) != null && !targetProperty.GetSetMethod(true).IsPrivate)
                         && (targetProperty.GetSetMethod().Attributes & MethodAttributes.Static) == 0
                         && targetProperty.PropertyType.IsAssignableFrom(srcProp.PropertyType)
                      select (sourceProperty: srcProp, targetProperty: targetProperty);

        //map the properties
        foreach (var props in results)
        {
            props.targetProperty.SetValue(target, props.sourceProperty.GetValue(source, null), null);
        }

        return target;
    }

}

Mais Pourquoi Donc?

D'accord, il y a donc d'autres façons plus conventionnelles d'atteindre le même objectif. Idéalement,on tirerait pour la Configuration de la Convention. Mais cela fournit le moyen le plus facile, le plus flexible, fortement dactylographié pour stocker les valeurs de configuration que j'ai jamais vu.

normalement les valeurs de configuration sont lues via un XML dans un processus tout aussi étrange qui repose sur des chaînes magiques et une faible Dactylographie. Nous devons appeler MapPath pour accéder à la mémoire de valeur et ensuite faire le mappage relationnel objet de XML à C#. Au lieu de cela, nous avons le type final à partir du début, et nous pouvons automatiser tout le travail ORM entre des classes identiques qui se trouvent juste être compilées contre différentes assemblées.

dans les deux cas, la sortie de rêve de ce processus est d'être capable d'écrire et de consommer C# directement. Dans ce cas, si je voulez ajouter un supplément, entièrement paramétrables, des biens, c'est aussi simple que l'ajout d'une propriété à la classe. Fait!

il sera disponible immédiatement et se recompilera automatiquement si cette valeur change sans avoir besoin de publier une nouvelle version de l'application.

L'Évolution Dynamique De La Classe Demo:

Dynamically Changing Class Demo

voici le code source complet du projet:

Configuration Compilée - Github Source Code/

  • Download Link

  • 12
    répondu KyleMit 2017-12-28 15:29:24

    Vous pouvez déplacer la partie de configuration pour séparer le projet, et créer une interface commune comme (IApplicationConfiguration.ReadConfiguration) pour y accéder.

    Vous pouvez compiler le code dynamiquement au moment de l'exécution comme ci-dessous, et vous pouvez accéder aux détails de la configuration en utilisant la réflexion.

    public static Assembly CompileAssembly(string[] sourceFiles, string outputAssemblyPath)
    {
        var codeProvider = new CSharpCodeProvider();
    
        var compilerParameters = new CompilerParameters
        {
            GenerateExecutable = false,
            GenerateInMemory = false,
            IncludeDebugInformation = true,
            OutputAssembly = outputAssemblyPath
        };
    
        // Add CSharpSimpleScripting.exe as a reference to Scripts.dll to expose interfaces
        compilerParameters.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().Location);
    
        var result = codeProvider.CompileAssemblyFromFile(compilerParameters, sourceFiles); // Compile
    
        return result.CompiledAssembly;
    }
    
    4
    répondu Saravana Manikandan 2017-12-27 03:28:25

    voyons comment la compilation dynamique des fichiers dans App_Code fonctionne. Lorsque la première demande à votre application arrive, asp.net compilera les fichiers de code dans ce dossier dans assembly (si ils n'ont pas été compilés auparavant), puis chargera cet assemblage dans le domaine d'application courant de asp.net application. C'est pourquoi vous voyez votre message dans un watch - assembly a été compilé et est disponible dans le domaine app actuel. Parce qu'il a été compilé dynamiquement, bien sûr vous avez une erreur de compilation quand vous essayez de référencez - le explicitement - ce code n'est pas encore compilé, et quand il sera compilé-il pourrait avoir une structure complètement différente et le message que vous référencez pourrait tout simplement ne pas être là du tout. Il n'y a donc aucun moyen de référencer explicitement le code de l'assemblage généré dynamiquement.

    quelles options Avez-vous alors? Par exemple, vous pouvez avoir une interface pour vos messages:

    // this interface is located in your main application code,
    // not in App_Code folder
    public interface IMessages {
        string CodeNotFound { get; }
    }
    

    puis, dans votre fichier App_Code - implémentez cette interface:

    // this is in App_Code folder, 
    // you can reference code from main application here, 
    // such as IMessages interface
    public class Messages : IMessages {
        public string CodeNotFound
        {
            get { return "The entered code was not found"; }
        }
    }
    

    Et puis dans l'application principale-fournir un proxy en cherchant le domaine d'application courant pour l'assemblage avec le type qui implémente IMessage interface (une seule fois, puis de les mettre en cache) et le proxy tous les appels de ce type:

    public static class Messages {
        // Lazy - search of app domain will be performed only on first call
        private static readonly Lazy<IMessages> _messages = new Lazy<IMessages>(FindMessagesType, true);
    
        private static IMessages FindMessagesType() {
            // search all types in current app domain
            foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) {
                foreach (var type in asm.GetTypes()) {
                    if (type.GetInterfaces().Any(c => c == typeof(IMessages))) {
                        return (IMessages) Activator.CreateInstance(type);
                    }
                }
            }
            throw new Exception("No implementations of IMessages interface were found");
        }
    
        // proxy to found instance
        public static string CodeNotFound => _messages.Value.CodeNotFound;
    }
    

    cela vous permettra d'atteindre votre objectif - maintenant, lorsque vous changez de code dans App_Code Messages classe, sur demande asp.net va détruire le domaine d'application actuel (d'abord en attendant que toutes les requêtes en suspens soient terminées), puis créer un nouveau domaine app, recompiler votre Messages et charger une nouvelle domaine app (notez que cette recréation du domaine app se produit toujours lorsque vous changez quelque chose dans App_Code, et pas seulement dans cette situation particulière). Ainsi la requête suivante verra déjà la nouvelle valeur de votre message sans que vous recompiliez explicitement quoi que ce soit.

    notez que vous ne pouvez évidemment pas ajouter ou supprimer des messages (ou changer leur nom) sans recompiler l'application principale, car cela nécessitera des modifications à IMessages interface qui appartient au code de l'application principale. Si vous essayez - asp.net lancera l'erreur de compilation sur les requêtes next (et toutes les suivantes).

    j'éviterais personnellement de faire de telles choses, mais si vous êtes d'accord avec ça - pourquoi pas.

    3
    répondu Evk 2017-12-25 10:41:48