Подтвердить что ты не робот

Динамически компилировать класс в App_Code, предварительно компилируя остальную часть проекта/библиотеки

ASP.NET имеет специальные папки приложений, такие как App_Code, которые:

Содержит исходный код для общих классов и бизнес-объектов (например,..cs и .vb файлы), которые вы хотите скомпилировать как часть вашего приложения. В динамически скомпилированном проекте веб-сайта ASP.NET компилирует код в папке App_Code при первоначальном запросе вашего приложения. Элементы в этой папке затем перекомпилируются, когда обнаружены какие-либо изменения.

Проблема в том, что я создаю веб-приложение, а не динамически скомпилированный веб-сайт. Но мне бы хотелось иметь возможность сохранять значения конфигурации непосредственно на С#, а не обслуживать через XML и читать в течение Application_Start и хранить в HttpContext.Current.Application

Итак, у меня есть следующий код в /App_Code/Globals.cs:

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

Что может быть в любом месте приложения следующим образом:

string msg = AppName.Globals.Messages.CodeNotFound;

Цель состоит в том, чтобы иметь возможность хранить любые литералы в настраиваемой области, которые могут быть обновлены без повторной компиляции всего приложения.

Я могу использовать .cs файл чтобы его компилировать, но при этом выделяется App_Code/Globals.cs из моего вывода.

Q: существует ли способ идентифицировать некоторые части проекта, которые должны динамически компилировать, позволяя предварительно скомпилировать остальную часть проекта?


  • Если я установил действие build content - файл .cs будет скопирован в папку bin и скомпилирован во время выполнения. Однако в этом случае он недоступен во время разработки.
  • Если я устанавливаю действие сборки на compile - я могу получить доступ к объектам так же, как и любой другой скомпилированный класс во время разработки/времени выполнения, но он будет удален из папки /App _Code при публикации. Я все еще могу поместить его в выходной каталог через Copy Always, но уже скомпилированные классы, похоже, имеют приоритет, поэтому я не могу нажимать изменения конфигурации без повторного развертывания всего приложения.

App_Code Content vs Compile

4b9b3361

Ответ 1

Обзор проблемы

Нам нужно преодолеть две различные проблемы:

  • Первый имеет один файл, который может быть скомпилирован во время сборки, а также повторно скомпилирован во время выполнения.
  • Во-вторых, это решение двух разных версий этого класса, созданных путем решения первой проблемы, поэтому мы можем фактически использовать их.

Проблема 1 - Компиляция Шредингера

Первая проблема заключается в попытке получить класс, который скомпилирован и не скомпилирован. Нам нужно скомпилировать его во время разработки, чтобы другие разделы кода знали, что он существует, и могут использовать его свойства с сильной типизацией. Но, как правило, скомпилированный код исключается из вывода, поэтому не существует нескольких версий одного и того же класса, вызывающих конфликты именования.

В любом случае нам нужно сначала скомпилировать класс, но есть две возможности для сохранения повторно скомпилируемой копии:

  • Добавьте файл в App_Code, который по умолчанию компилируется во время выполнения, но установите Build Action = Compile, чтобы он был доступен во время разработки.
  • Добавьте обычный файл класса, который по умолчанию компилируется во время разработки, но установите его в Копировать в каталог вывода = Копировать всегда, поэтому мы можем оценить его и во время выполнения.

Проблема 2 - Self Imposed DLL Hell

Как минимум, это сложная задача, связанная с компилятором. Любой код, который использует класс, должен иметь гарантию, что он существует во время компиляции. Все, что динамически скомпилировано, будь то через App_Code или иначе, будет частью совершенно другой сборки. Таким образом, получение идентичного класса рассматривается как картина этого класса. Основной тип может быть одним и тем же, но ce n'est une pipe.

У нас есть два варианта: используйте интерфейс или перекресток между сборками:

  • Если мы используем интерфейс, мы можем скомпилировать его с помощью начальной сборки, и любые динамические типы могут реализовать тот же интерфейс. Таким образом, мы надежно полагаемся на то, что существует во время компиляции, и наш созданный класс можно безопасно поменять как свойство поддержки.

  • Если мы типы бросков в сборках, важно отметить, что любые существующие обычаи полагаются на тип, который был первоначально скомпилирован, Поэтому нам нужно захватить значения из динамического типа и применить эти значения свойств к исходному типу.

Класс Apple

Существующие ответы

Per evk Мне нравится идея опроса AppDomain.CurrentDomain.GetAssemblies() при запуске, чтобы проверить наличие новых сборок/классов. Я соглашусь, что использование интерфейса, вероятно, является целесообразным способом унифицировать предварительно скомпилированные/динамически скомпилированные классы, но в идеале я хотел бы иметь один файл/класс, который можно просто перечитать, если он изменится.

Per S.Deepika, мне нравится идея динамической компиляции из файла, но не нужно перемещать значения в отдельный проект.

Выпуск App_Code

App_Code разблокирует возможность создания двух версий одного и того же класса, но на самом деле трудно изменить один после публикации, как мы увидим. Любой файл .cs, расположенный в ~/App_Code/, будет динамически скомпилирован при запуске приложения. Поэтому в Visual Studio мы можем создать один и тот же класс дважды, добавив его в App_Code и установив Сборка Action в Скомпилировать.

Действие сборки и копирования:

Создать действие и копировать результат

При локальном отладке все файлы .cs будут встроены в сборку проекта, а также будет создан физический файл в ~/App_Code.

Мы можем идентифицировать оба типа:

// 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>();

Скомпилированные и динамические типы:

Скомпилированные и динамические типы

Это действительно близко к решению проблемы №1. Мы имеем доступ к обоим типам каждый раз, когда приложение работает. Мы можем использовать скомпилированную версию во время разработки, и любые изменения самого файла будут автоматически перекомпилированы IIS в версию, доступ к которой мы можем получить во время выполнения.

Проблема очевидна, однако, когда мы выходим из режима отладки и пытаемся опубликовать проект. Это решение основано на создании IIS сборки App_Code.xxxx динамически и зависит от файла .cs внутри корневой папки App_Code. Однако, когда файл .cs скомпилирован, он автоматически удаляется из опубликованного проекта, чтобы избежать точного сценария, который мы пытаемся создать (и деликатно управлять). Если файл остался в нем, он создаст два одинаковых класса, которые будут создавать конфликты имен при каждом использовании.

Мы можем попытаться заставить свою руку как скомпилировать файл в сборку проекта, так и скопировать файл в выходной каталог. Но App_Code не работает ни в одном из этого волшебства внутри ~/bin/App_Code/. Он будет работать только на корневом уровне ~/App_Code/

Источник компиляции App_Code:

Источник компиляции App_Code

С каждой публикацией мы можем вручную вырезать и вставлять созданную папку App_Code из корзины и размещать ее на корневом уровне, но в лучшем случае это нестабильно. Возможно, мы могли бы автоматизировать это в событиях сборки, но мы попробуем что-то еще...

Решение

Компилировать + (копировать на вывод и вручную компилировать файл)

Позвольте избежать использования папки App_Code, поскольку она добавит некоторые непредвиденные последствия.

Просто создайте новую папку с именем Config и добавьте класс, в котором будут храниться значения, которые мы хотим динамически изменять:

~/Config/AppleValues.cs

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

Опять же, мы захотим перейти к свойствам файла (F4) и установить компиляцию AND для вывода на печать. Это даст нам вторую версию файла, которую мы можем использовать позже.

Мы будем использовать этот класс, используя его в статическом классе, который выдает значения из любого места. Это помогает разделить проблемы, особенно между необходимостью динамической компиляции и статического доступа.

~/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; }
}

И мы можем использовать его следующим образом:

var x = Global.Apple.BodyColor;

То, что мы попытаемся сделать внутри статического конструктора, - это семя Apple со значениями из нашего динамического класса. Этот метод будет вызываться один раз при каждом перезапуске приложения, и любые изменения в папке bin автоматически инициируют повторное использование пула приложений.

Вкратце, вот что мы хотим сделать внутри конструктора:

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 - Путь к файлу может быть специфическим для того, где вы хотели бы развернуть это, но обратите внимание, что внутри статического метода вам нужно использовать HostingEnvironment.MapPath Server.MapPath

BuildFileIntoAssembly - Что касается загрузки сборки из файла, я адаптировал код из документов на CSharpCodeProvider и этот вопрос на Как загрузить класс из файла .cs. Кроме того, вместо того, чтобы бороться с зависимостями, я просто предоставил компилятор для каждой сборки, которая в настоящее время находится в домене приложений, так же, как и в исходной компиляции. Вероятно, есть способ сделать это с меньшими накладными расходами, но это одноразовая стоимость, поэтому кому это нужно.

CopyProperties - Чтобы отобразить новые свойства на старый объект, я адаптировал метод в этом вопросе о том, как Применить значения свойств от одного объекта к другому из того же тип автоматически?, который будет использовать отражение, чтобы сломать оба объекта и выполнить итерацию по каждому свойству.

Utilities.cs

Здесь полный исходный код для методов Utility сверху

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:
        // /questions/420221/compilerparametersreferencedassemblies-add-reference-to-systemwebuiwebcontrols/1868964#1868964
        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;
    }

}

Но почему Tho?

Хорошо, поэтому есть и другие более традиционные способы достижения одной и той же цели. В идеале мы снимаем для Конвенции > Конфигурация. Но это обеспечивает самый простой и гибкий, строго типизированный способ хранения значений конфигурации, которые я когда-либо видел.

Обычно значения конфигурации считываются через XML в одинаково нечетном процессе, который полагается на магические строки и слабую типизацию. Мы должны вызвать MapPath, чтобы перейти в хранилище значений, а затем сделать реляционное сопоставление объектов с XML на С#. Вместо этого у нас есть конечный тип от get go, и мы можем автоматизировать всю работу ORM между идентичными классами, которые просто собираются для разных сборок.

В любом случае выход сновидений этого процесса должен состоять в том, чтобы писать и потреблять С# напрямую. В этом случае, если я хочу добавить дополнительное, полностью настраиваемое свойство, это так же просто, как добавление свойства в класс. Готово!

Он будет доступен сразу и автоматически перекомпилируется, если это значение изменится без необходимости публикации новой сборки приложения.

Динамическое изменение класса Demo:

Динамическое изменение класса Demo

Здесь полный рабочий код для проекта:

Скомпилированный Config - Исходный код Github | Ссылка для скачивания

Ответ 2

Вы можете перемещать конфигурационную часть для отдельного проекта и создавать общий интерфейс, например (IApplicationConfiguration.ReadConfiguration) для доступа к нему.

Вы можете скомпилировать код динамически во время выполнения, как показано ниже, и вы можете получить доступ к деталям конфигурации, используя отражение.

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;
}

Ответ 3

Посмотрим, как работает динамическая компиляция файлов в App_Code. Когда будет получен первый запрос к вашему приложению, asp.net будет компилировать файлы кода в этой папке в сборку (если они не были скомпилированы ранее), а затем загрузить эту сборку в текущий домен приложения приложения asp.net. Вот почему вы видите, что ваше сообщение в сборке часов было скомпилировано и доступно в текущем домене приложения. Поскольку он был скомпилирован динамически, конечно, у вас есть ошибка времени компиляции при попытке ссылаться на него явно - этот код еще не скомпилирован, а когда он будет скомпилирован - он может иметь совершенно другую структуру и сообщение, на которое вы ссылаетесь, может просто не быть там вообще. Таким образом, вы не можете явно ссылаться на код из динамически генерируемой сборки.

Какие у вас есть варианты? Например, вы можете иметь интерфейс для своих сообщений:

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

Затем в вашем файле App_Code реализуем этот интерфейс:

// 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"; }
    }
}

И затем в основном приложении - укажите прокси, выполнив поиск текущего домена приложения для сборки с типом, который реализует интерфейс IMessage (только один раз, затем кеширует его) и проксирует все вызовы этого типа:

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;
}

Это приведет к вашей цели - теперь, когда вы меняете код в классе App_Code Messages, при следующем запросе asp.net срывает текущий домен приложения (сначала ожидает завершения всех ожидающих запросов запросов), затем создаст новый домен приложения, перекомпилируйте свой Messages и загрузите в новый домен приложения (обратите внимание, что это воссоздание домена приложения всегда происходит, когда вы что-то изменяете в App_Code, а не только в этой конкретной ситуации). Поэтому следующий запрос уже увидит новое значение вашего сообщения, если вы явно не перекомпилируете что-либо.

Обратите внимание, что вы, очевидно, не можете добавлять или удалять сообщения (или изменять их имена) без перекомпиляции основного приложения, поскольку для этого потребуются изменения в интерфейсе IMessages, который относится к основному коду приложения. Если вы попытаетесь - asp.net будет бросать ошибку сбоя компиляции на следующий (и все последующие) запросы.

Я бы лично избегал делать такие вещи, но если вы в порядке с этим - почему бы и нет.