Ответ 1
Обзор проблемы
Нам нужно преодолеть две различные проблемы:
- Первый имеет один файл, который может быть скомпилирован во время сборки, а также повторно скомпилирован во время выполнения.
- Во-вторых, это решение двух разных версий этого класса, созданных путем решения первой проблемы, поэтому мы можем фактически использовать их.
Проблема 1 - Компиляция Шредингера
Первая проблема заключается в попытке получить класс, который скомпилирован и не скомпилирован. Нам нужно скомпилировать его во время разработки, чтобы другие разделы кода знали, что он существует, и могут использовать его свойства с сильной типизацией. Но, как правило, скомпилированный код исключается из вывода, поэтому не существует нескольких версий одного и того же класса, вызывающих конфликты именования.
В любом случае нам нужно сначала скомпилировать класс, но есть две возможности для сохранения повторно скомпилируемой копии:
- Добавьте файл в
App_Code
, который по умолчанию компилируется во время выполнения, но установите Build Action = Compile, чтобы он был доступен во время разработки. - Добавьте обычный файл класса, который по умолчанию компилируется во время разработки, но установите его в Копировать в каталог вывода = Копировать всегда, поэтому мы можем оценить его и во время выполнения.
Проблема 2 - Self Imposed DLL Hell
Как минимум, это сложная задача, связанная с компилятором. Любой код, который использует класс, должен иметь гарантию, что он существует во время компиляции. Все, что динамически скомпилировано, будь то через App_Code или иначе, будет частью совершенно другой сборки. Таким образом, получение идентичного класса рассматривается как картина этого класса. Основной тип может быть одним и тем же, но ce n'est une pipe.
У нас есть два варианта: используйте интерфейс или перекресток между сборками:
-
Если мы используем интерфейс, мы можем скомпилировать его с помощью начальной сборки, и любые динамические типы могут реализовать тот же интерфейс. Таким образом, мы надежно полагаемся на то, что существует во время компиляции, и наш созданный класс можно безопасно поменять как свойство поддержки.
-
Если мы типы бросков в сборках, важно отметить, что любые существующие обычаи полагаются на тип, который был первоначально скомпилирован, Поэтому нам нужно захватить значения из динамического типа и применить эти значения свойств к исходному типу.
Существующие ответы
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, поскольку она добавит некоторые непредвиденные последствия.
Просто создайте новую папку с именем 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:
Здесь полный рабочий код для проекта:
Скомпилированный Config - Исходный код Github | Ссылка для скачивания