Динамически компилировать класс в коде приложения при предварительной компиляции остальной части проекта / библиотеки

ASP.NET есть папки приложения specicial как App_Code которых:

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

проблема в том, что я создаю веб-приложение, а не динамически скомпилированный веб-сайт. Но я хотел бы иметь возможность хранить значения конфигурации непосредственно в C#, а не служить через 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;

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

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

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


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

App_Code Content vs Compile

3 ответов


Обзор

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

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

Задача 1-компиляция Шредингера

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

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

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

Проблема 2 - добровольный ДЛЛ Черт!--33-->

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

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

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

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

Apple Class

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

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

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

исключив App_Code

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

построить действие и скопировать вывод:

Build Action and Copy Output

когда мы отладим все местные .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>();

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

Compiled and Dynamic Types

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

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

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

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

App_Code Compilation Source

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

решение

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

давайте избегать папки 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";
}

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

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

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

то, что мы попытаемся сделать внутри статического конструктора, - это seed 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 и этот вопрос о как загрузить класс из a .cs файл. Кроме того, вместо того, чтобы бороться с зависимостями, я просто дал доступ компилятора к каждой сборке, которая была в настоящее время в домене приложения, то же самое, что и в оригинальной компиляции. Возможно, есть способ сделать это с меньшими затратами, но это разовая цена, так что какая разница.

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

коммунальные услуги.cs

вот полный исходный код для методов утилиты из выше

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

}

Но Почему Tho?

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

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

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

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

Динамически Меняющийся Класс Demo:

Dynamically Changing Class Demo

вот полный, рабочий исходный код проекта:

Составленный Конфиг - Исходный Код Github | Ссылка Для Скачивания


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

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

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

давайте посмотрим, как динамическая компиляция файлов в 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 вызовет ошибку сбоя компиляции при следующих (и всех последующих) запросах.

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