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

问题描述 投票:14回答:3

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

c# asp.net asp.net-mvc asp.net-mvc-5 csproj
3个回答
12
投票

问题概述

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

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

}

But Why Tho?

好的,所以还有其他更传统的方法来实现相同的目标。理想情况下,我们会拍摄会议>配置。但这提供了绝对最简单,最灵活,强类型的方式来存储我见过的配置值。

通常,配置值是通过XML在同样奇怪的过程中读取的,该过程依赖于魔术字符串和弱类型。我们必须调用MapPath来获取存储的值,然后执行从XML到C#的对象关系映射。相反,在这里,我们从get go开始具有最终类型,并且我们可以自动化恰好恰好针对不同程序集编译的相同类之间的所有ORM工作。

在任何一种情况下,该过程的梦想输出都是能够直接编写和使用C#。在这种情况下,如果我想添加一个额外的,完全可配置的属性,就像向类中添加属性一样简单。完成!

如果该值发生变化,它将立即可用并自动重新编译,而无需发布应用程序的新版本。

动态更改类演示:

Dynamically Changing Class Demo

Here's the full, working source code for the project:

编译配置 - Github Source Code | Download Link


4
投票

您可以将配置部件移动到单独的项目,并创建通用接口(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将在下一个(以及所有后续)请求中抛出编译失败错误。

我个人会避免做这些事情,但如果你对此没事的话 - 为什么不呢。

© www.soinside.com 2019 - 2024. All rights reserved.