web-dev-qa-db-ja.com

プロジェクト/ライブラリの残りの部分をプリコンパイルしながら、App_Codeでクラスを動的にコンパイルします

ASP.NETには、 App_Code のような特定のアプリケーションフォルダーがあります。

アプリケーションの一部としてコンパイルする共有クラスおよびビジネスオブジェクト(..csファイルや.vbファイルなど)のソースコードが含まれています。動的にコンパイルされたWebサイトプロジェクトでは、ASP.NETは、アプリケーションへの最初の要求時にApp_Codeフォルダー内のコードをコンパイルします。このフォルダ内のアイテムは、変更が検出されると再コンパイルされます。

問題は、動的にコンパイルされたWebサイトではなく、Webアプリケーションを構築していることです。ただし、XMLを介して提供するのではなく、構成値をC#に直接格納できるようにし、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;

目標は、アプリケーション全体を再コンパイルせずに更新できる構成可能な領域にリテラルを格納できるようにすることです。

ビルドアクションをコンパイルするように設定.csファイルを使用できますが、そうすると、出力からApp_Code/Globals.csが削除されます。

[〜#〜] q [〜#〜]識別する方法はありますかプロジェクトの一部は、プロジェクトの残りの部分をプリコンパイルできるようにしながら、 動的にコンパイル する必要がありますか?


  • build actioncontentに設定すると、.csファイルはbinフォルダーにコピーされ、コンパイルされます。実行時。ただし、その場合、設計時には利用できません。
  • build actioncompileに設定すると、設計/実行時に他のコンパイル済みクラスと同じようにオブジェクトにアクセスできますが、公開されると、/ App_Codeフォルダーから削除されます。 Copy Alwaysを介して出力ディレクトリに配置することはできますが、コンパイル済みのクラスが優先されるようであるため、アプリケーション全体を再デプロイせずに構成の変更をプッシュすることはできません。

App_Code Content vs Compile

15
KyleMit

問題の概要

ここでは、2つの異なる問題を克服する必要があります。

  1. 1つは、ビルド時にコンパイルでき、実行時に再コンパイルできる単一のファイルを用意することです。
  2. 2つ目は、最初の問題を解決することによって作成されたそのクラスの2つの異なるバージョンを解決して、実際にそれらを利用できるようにすることです。

問題1-シュレディンガーの編纂

最初の問題は、コンパイルされたクラスとコンパイルされていないクラスの両方を取得しようとすることです。コードの他のセクションがそれが存在することを認識し、強い型付けでそのプロパティを使用できるように、設計時にコンパイルする必要があります。ただし、通常、コンパイルされたコードは出力から削除されるため、同じクラスの複数のバージョンが名前の競合を引き起こすことはありません。

いずれにせよ、最初にクラスをコンパイルする必要がありますが、再コンパイル可能なコピーを永続化するための2つのオプションがあります。

  1. ファイルをApp_Codeに追加します。これはデフォルトで実行時にコンパイルされますが、 Build Action = Compile に設定して、で利用できるようにします。設計時間も。
  2. デフォルトで設計時にコンパイルされる通常のクラスファイルを追加しますが、出力ディレクトリにコピー=常にコピーに設定するため、実行時にも評価できます。

問題2-自主DLL地獄

最低限、これはコンパイラーに請求するのは難しい作業です。クラスを消費するコードはすべて、コンパイル時に存在することが保証されている必要があります。 App_Code経由かどうかに関係なく、動的にコンパイルされるものはすべて、まったく異なるアセンブリの一部になります。したがって、同一のクラスを作成することは、そのクラスの図のように扱われます。基になるタイプは同じかもしれませんが、 ce n'est une pipe

2つのオプションがあります。アセンブリ間でインターフェイスまたは横断歩道を使用します。

  1. interfaceを使用する場合、初期ビルドでコンパイルでき、どの動的タイプでも同じインターフェースを実装できます。このようにして、コンパイル時に存在するものに安全に依存し、作成したクラスをバッキングプロパティとして安全にスワップアウトできます。

  2. アセンブリ間で型をキャストする場合 、既存の使用法は最初にコンパイルされた型に依存することに注意することが重要です。したがって、動的タイプから値を取得し、 これらのプロパティ値を元のタイプに適用する にする必要があります。

Apple Class

既存の回答

evk ごとに、起動時にAppDomain.CurrentDomain.GetAssemblies()にクエリを実行して、新しいアセンブリ/クラスをチェックするというアイデアが好きです。インターフェースを使用することは、プリコンパイルされた/動的にコンパイルされたクラスを統合するためのおそらく賢明な方法であることを認めますが、理想的には、変更された場合に簡単に再読み取りできる単一のファイル/クラスが必要です。

S.Deepika ごとに、ファイルから動的にコンパイルするというアイデアは好きですが、値を別のプロジェクトに移動する必要はありません。

App_Codeを除外する

App_Codeは、同じクラスの2つのバージョンをビルドする機能のロックを解除しますが、公開後にどちらかを変更することは実際には困難ですこれから見ていきます。 〜/ App_Code /にある.csファイルは、アプリケーションの実行時に動的にコンパイルされます。したがって、Visual Studioでは、App_Codeに追加し、Build Actionに設定することで、同じクラスを2回ビルドできます。コンパイル

ビルドアクションとコピー出力

Build Action and Copy Output

ローカルでデバッグすると、すべての.csファイルがプロジェクトAssemblyに組み込まれ、〜/ 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_Codeフォルダー内にある.csファイルに依存します。ただし、。csファイルがコンパイルされ、公開されたプロジェクトから自動的に削除されて、作成しようとしている(そして慎重に管理している)正確なシナリオを回避します。ファイルが残っていると、2つの同一のクラスが生成されます。どちらかが使用されるたびに、名前の競合が発生します。

ファイルをプロジェクトのアセンブリにコンパイルすることと、ファイルを出力ディレクトリにコピーすることの両方によって、強制的に手を加えることができます。しかし、App_Codeは、〜/ bin/App_Code /内の魔法のいずれも機能しません。ルートレベルでのみ機能します〜/ App_Code /

App_Codeコンパイルソース

App_Code Compilation Source

公開するたびに、生成された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)およびコンパイル[〜#〜]および[〜#〜]コピーして出力するように設定します。これにより、後で使用できるファイルの2番目のバージョンが得られます。

このクラスは、どこからでも値を公開する静的クラス内で使用することで使用します。これは、特に動的にコンパイルする必要性と静的にアクセスする必要性の間の関心の分離に役立ちます。

~/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です。このメソッドは、アプリケーションが再起動されるたびに1回呼び出され、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 のドキュメントのコードと How to .csファイルからクラスをロードします 。また、依存関係と戦うのではなく、元のコンパイルで取得したのと同じように、 現在Appドメインにあるすべてのアセンブリへのコンパイラアクセス を指定しました。おそらくより少ないオーバーヘッドでそれを行う方法がありますが、それは一度のコストなので誰が気にします。

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:
        // 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;
    }

}

しかし、なぜあなたは?

さて、同じ目標を達成するための他のより一般的な方法があります。理想的には、設定より規約で撮影します。しかし、これは私が今まで見た構成値を格納するための絶対的に最も簡単で、最も柔軟で、強く型付けされた方法を提供します。

通常、構成値は、マジックストリングと弱い型付けに依存する同様に奇妙なプロセスでXMLを介して読み込まれます。値のストアに到達するためにMapPathを呼び出してから、XMLからC#へのオブジェクトリレーショナルマッピングを実行する必要があります。代わりに、ここでは、最初から最終的な型があり、異なるアセンブリに対してコンパイルされた同じクラス間のすべてのORM作業を自動化できます。

いずれの場合も、そのプロセスの夢の出力は、C#を直接記述して使用できるようにすることです。この場合、完全に構成可能なプロパティを追加したい場合は、クラスにプロパティを追加するのと同じくらい簡単です。完了!

アプリの新しいビルドを公開しなくても、その値が変更された場合はすぐに利用可能になり、自動的に再コンパイルされます。

動的に変化するクラスデモ

Dynamically Changing Class Demo

プロジェクトの完全な実用的なソースコードは次のとおりです。

コンパイルされた構成- Githubソースコード | ダウンロードリンク

12
KyleMit

構成パーツを別のプロジェクトに移動し、(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;
}
4

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インターフェイスを実装するタイプのアセンブリの現在のアプリドメインを検索してプロキシを提供し(1回だけ、次にキャッシュします)、そのタイプへのすべての呼び出しをプロキシします。

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_CodeMessagesクラスのコードを変更すると、次のリクエストでasp.netは現在のアプリケーションドメインを破棄し(最初に保留中のすべてのリクエストが終了するのを待ちます)、新しいアプリドメインを作成します。 Messagesを再コンパイルし、その新しいアプリドメインにロードします(この特定の状況だけでなく、App_Codeで何かを変更すると、アプリドメインのこの再作成が常に発生することに注意してください)。したがって、次のリクエストでは、明示的に何も再コンパイルしなくても、メッセージの新しい値がすでに表示されます。

メインアプリケーションを再コンパイルせずにメッセージを追加または削除(または名前を変更)することは明らかにできないことに注意してください。これを行うには、メインアプリケーションコードに属するIMessagesインターフェイスを変更する必要があるためです。試してみると、asp.netは次の(および後続のすべての)要求でコンパイル失敗エラーをスローします。

私は個人的にそのようなことをするのを避けたいと思います、しかしあなたがそれで大丈夫なら-なぜそうではありません。

3
Evk