【问题标题】:Dynamically compile a class in App_Code while pre-compiling the rest of the project/library在预编译项目/库的其余部分时动态编译 App_Code 中的类
【发布时间】:2018-06-04 07:59:36
【问题描述】:

ASP.NET 具有特殊的应用程序文件夹,例如 App_Code,其中:

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

问题是,我正在构建一个 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;

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

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

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


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

【问题讨论】:

  • 只是想知道,为什么不使用 web.config 的 appSettings 部分并使用 WebConfigurationManager.AppSettings
  • @CamiloTerevinto,当然这适用于我上面发布的消息示例。但我还希望能够填充一些稍微复杂的查找对象,这些查找对象正在存储具有属性的对象数组。没什么大不了的,但我通常会通过 XML 检索一些东西,但最终还是必须加载到 CS 中。
  • 我不认为这样的东西是开箱即用的,虽然你可以自己实现它(但我最好不要这样做)。如果采用这种方式,最好还是将消息存储在 xml 文件(或任何其他格式)中,并检测对该文件的更改以使缓存版本无效。

标签: c# asp.net asp.net-mvc asp.net-mvc-5 csproj


【解决方案1】:

问题概述

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

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

问题 1 - 薛定谔的编译

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

在任何情况下,我们需要最初编译类,但是有两个选项可以持久化一个可重新编译的副本:

  1. 将文件添加到App_Code,默认情况下在运行时编译,但将其设置为Build Action = Compile,以便在设计时也可用。
  2. 添加一个常规类文件,默认情况下在设计时编译,但将其设置为复制到输出目录=始终复制,因此我们也有机会在运行时对其进行评估。

问题 2 - 自我强加的 DLL 地狱

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

我们有两种选择:在程序集之间使用接口或人行横道:

  1. 如果我们使用接口,我们可以在初始构建中对其进行编译,并且任何动态类型都可以实现相同的接口。这样我们就可以安全地依赖于编译时存在的东西,并且我们创建的类可以安全地换出作为支持属性。

  2. 如果我们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

【讨论】:

  • 我真的因为某种原因笑了你的“苹果图片”图片:)
【解决方案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 将在下一个(以及所有后续)请求中引发编译失败错误。

    我个人会避免做这样的事情,但如果你能接受 - 为什么不呢。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-01-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-10-29
      • 1970-01-01
      相关资源
      最近更新 更多