问题概述
这里我们需要克服两个不同的问题:
- 首先是拥有一个可以在构建时编译并在运行时重新编译的文件。
- 第二个是解决通过解决第一个问题创建的该类的两个不同版本,以便我们可以实际使用它们。
问题 1 - 薛定谔的编译
第一个问题是试图获得一个既已编译又未编译的类。我们需要在设计时对其进行编译,以便其他代码部分知道它的存在,并可以通过强类型使用它的属性。但通常情况下,编译后的代码会从输出中剔除,因此不会有同一个类的多个版本导致命名冲突。
在任何情况下,我们需要最初编译类,但是有两个选项可以持久化一个可重新编译的副本:
- 将文件添加到
App_Code,默认情况下在运行时编译,但将其设置为Build Action = Compile,以便在设计时也可用。
- 添加一个常规类文件,默认情况下在设计时编译,但将其设置为复制到输出目录=始终复制,因此我们也有机会在运行时对其进行评估。
问题 2 - 自我强加的 DLL 地狱
至少,这对编译器来说是一项棘手的任务。任何使用类的代码都必须保证它在编译时存在。任何动态编译的东西,无论是通过 App_Code 还是其他方式,都将是完全不同的程序集的一部分。因此,产生一个相同的类更像是该类的图片。底层类型可能相同,但ce n'est une pipe。
我们有两种选择:在程序集之间使用接口或人行横道:
如果我们使用接口,我们可以在初始构建中对其进行编译,并且任何动态类型都可以实现相同的接口。这样我们就可以安全地依赖于编译时存在的东西,并且我们创建的类可以安全地换出作为支持属性。
如果我们cast types across assemblies,请务必注意任何现有用法都依赖于最初编译的类型。所以我们需要从动态类型和apply those property values to the original type中获取值。
现有答案
根据evk,我喜欢在启动时查询AppDomain.CurrentDomain.GetAssemblies() 以检查任何新程序集/类的想法。我承认使用接口可能是统一预编译/动态编译类的一种可取的方式,但我希望有一个文件/类,如果它发生变化,可以简单地重新读取。
根据S.Deepika,我喜欢从文件动态编译的想法,但不想将值移动到单独的项目中。
排除App_Code
App_Code 确实解锁了构建同一类的两个版本的能力,但我们将看到,发布后实际上很难修改任何一个版本。任何位于 ~/App_Code/ 中的.cs 文件都将在应用程序运行时动态编译。因此,在 Visual Studio 中,我们可以通过将其添加到 App_Code 并将 Build Action 设置为 Compile 来构建相同的类两次。
构建动作并复制输出:
当我们在本地调试时,所有的 .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 程序集,并且依赖于根 App_Code 文件夹中的 .cs 文件。但是,当编译 .cs 文件时,它会自动从已发布的项目中删除,以避免我们试图创建(并精心管理)的确切场景。如果保留该文件,它将产生两个相同的类,这将在使用任何一个时产生命名冲突。
我们可以尝试通过将文件编译到项目的程序集中以及将文件复制到输出目录来强制它的手。但是 App_Code 在 ~/bin/App_Code/ 中不起作用。它只能在根级别工作 ~/App_Code/
App_Code编译源:
每次发布时,我们都可以手动从 bin 中剪切并粘贴生成的 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 instead of Server.MapPath
BuildFileIntoAssembly - 在从文件加载程序集方面,我改编了CSharpCodeProvider 上的文档中的代码以及How to load a class from a .cs file 上的这个问题。此外,我没有与依赖关系抗争,而是提供了compiler access to every assembly that was currently in the App Domain,就像它在原始编译中得到的一样。可能有一种方法可以减少开销,但这是一次性成本,所以谁在乎呢。
CopyProperties - 要将新属性映射到旧对象上,我已经调整了这个问题中关于如何Apply properties values from one object to another of the same type automatically? 的方法,它将使用反射分解两个对象并迭代每个属性。
实用程序.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;
}
}
但是为什么?
好的,所以还有其他更传统的方法可以实现相同的目标。理想情况下,我们会为 Convention > Configuration 拍摄。但这提供了我见过的最简单、最灵活、强类型的存储配置值的方法。
通常,配置值是通过 XML 在一个同样奇怪的过程中读取的,该过程依赖于魔术字符串和弱类型。我们必须调用MapPath 来获取值存储,然后执行从 XML 到 C# 的对象关系映射。取而代之的是,我们从一开始就拥有了最终类型,并且我们可以自动化恰好针对不同程序集编译的相同类之间的所有 ORM 工作。
在任何一种情况下,该进程的理想输出是能够直接编写和使用 C#。在这种情况下,如果我想添加一个额外的、完全可配置的属性,就像在类中添加一个属性一样简单。完成!
如果该值发生更改,它将立即可用并自动重新编译,而无需发布应用程序的新版本。
动态变化的类演示:
这是该项目的完整、可运行的源代码:
编译配置 - Github Source Code | Download Link