在预编译项目/库的其余部分时动态编译App_Code中的类

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在预编译项目/库的其余部分时动态编译App_Code中的类相关的知识,希望对你有一定的参考价值。

ASP.NET有像App_Code这样的特定应用程序文件夹:

包含要作为应用程序的一部分进行编译的共享类和业务对象(例如,.. cs和.vb文件)的源代码。在动态编译的Web站点项目中,ASP.NET会根据对应用程序的初始请求编译App_Code文件夹中的代码。检测到任何更改后,将重新编译此文件夹中的项目。

问题是,我正在构建一个Web应用程序,而不是一个动态编译的Web站点。但我希望能够直接在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;

目标是能够将任何文字存储在可以更新的可配置区域中,而无需重新编译整个应用程序。

我可以使用.cssetting its build action to compile文件,但这样做会从我的输出中删除App_Code/Globals.cs

问:有没有办法识别项目的某些部分应该dynamically compile,同时允许项目的其余部分进行预编译?


  • 如果我将构建操作设置为content - .cs文件将被复制到bin文件夹并在运行时编译。但是,在这种情况下,它在设计时不可用。
  • 如果我将构建操作设置为compile - 我可以在设计/运行时期间访问与任何其他编译类相同的对象,但在发布时它将被删除/ App_Code文件夹。我仍然可以通过Copy Always将它放在输出目录中,但已编译的类似乎优先,所以我不能在不重新部署整个应用程序的情况下推送配置更改。

App_Code Content vs Compile

答案

问题概述

我们需要克服两个不同的问题:

  1. 第一个是拥有一个文件,可以在构建时编译,也可以在运行时重新编译。
  2. 第二个是解决由解决第一个问题而创建的该类的两个不同版本,因此我们可以实际使用它们。

Problem 1 - Schrödinger's Compilation

第一个问题是尝试获得一个既编译又不编译的类。我们需要在设计时编译它,以便代码的其他部分知道它存在并且可以使用其强类型的属性。但通常情况下,已编译的代码会从输出中删除,因此同一类的多个版本不会导致命名冲突。

在任何情况下,我们都需要最初编译该类,但有两个选项可以保留可重新编译的副本:

  1. 将文件添加到App_Code,默认情况下在运行时编译,但设置为Build Action = Compile,以便它在设计时也可用。
  2. 添加一个常规类文件,默认情况下在设计时编译,但将其设置为Copy to Output Directory = Copy Always,这样我们就有机会在运行时对其进行评估。

Problem 2 - Self Imposed DLL Hell

至少,这对向编译器收费是一项棘手的任务。任何使用类的代码都必须保证它在编译时存在。无论是通过App_Code还是其他方式动态编译的任何内容都将成为完全不同的程序集的一部分。因此,生成一个相同的类更像是该类的图片。底层类型可能是相同的,但ce n'est une pipe

我们有两个选择:在程序集之间使用接口或crosswalk:

  1. 如果我们使用接口,我们可以使用初始构建编译它,并且任何动态类型都可以实现相同的接口。这样我们就可以安全地依赖编译时存在的东西,并且可以安全地将我们创建的类替换为支持属性。
  2. 如果我们cast types across assemblies,重要的是要注意任何现有的用法依赖于最初编译的类型。所以我们需要从动态类型和apply those property values to the original type中获取值。

Apple Class

Existing Answers

根据evk,我喜欢在启动时查询AppDomain.CurrentDomain.GetAssemblies()以检查任何新程序集/类的想法。我承认使用接口可能是统一预编译/动态编译类的可行方法,但我希望有一个文件/类,只要它发生变化就可以重新读取。

根据S.Deepika,我喜欢从文件中动态编译的想法,但不想将值移动到单独的项目中。

Ruling out App_Code

App_Code确实解锁了构建同一类的两个版本的能力,但实际上很难在发布之后修改任何一个版本,我们将会看到。位于〜/ App_Code /中的任何.cs文件将在应用程序运行时动态编译。因此,在Visual Studio中,我们可以通过将其添加到App_Code并将Build Action设置为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程序集,并依赖于.App文件位于根App_Code文件夹中。但是,当编译.cs文件时,它会自动从已发布的项目中删除,以避免我们尝试创建(并精心管理)的确切方案。如果文件被保留,它将产生两个相同的类,无论何时使用任何一个类都会产生命名冲突。

我们可以尝试通过将文件编译到项目的程序集中并将文件复制到输出目录来强制其手。但是App_Code在〜/ bin / App_Code /中没有任何神奇的功能。它只能在根级别工作〜/ App_Code /

App_Code编译源:

App_Code Compilation Source

每次发布时,我们都可以从bin中手动剪切并粘贴生成的App_Code文件夹,然后将其放回根级别,但这最多是不稳定的。也许我们可以将其自动化为构建事件,但我们会尝试别的......

Compile + (Copy to Output and Manually Compile File)

让我们避开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)并设置为编译并复制到输出。这将为我们提供我们稍后可以使用的文件的第二个版本。

我们将在静态类中使用它来使用此类,该类从任何地方公开值。这有助于区分问题,尤其是在动态编译和静态访问的需要之间。

~/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?的方法。

Utilities.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"

以上是关于在预编译项目/库的其余部分时动态编译App_Code中的类的主要内容,如果未能解决你的问题,请参考以下文章

如何在处理列的其余部分时将导致错误的数据插入到单独的文件中?

Boost 库未在 Netbeans 中使用 G++ 编译

Linux下静态库动态库的创建与调用

音视频交叉编译动态库静态库的学习

什么类型的数据可以在预编译的 C++ 和 Java for Android 之间传递?

VS2019 C++动态链接库的创建使用