スワッガーを書き、後悔しない方法

画像



かつて、私の同僚はバックログタスクに陥りました。「内部REST-apiとの対話を整理して、契約の変更がすぐにコンパイルエラーにつながるようにします」。 何がもっと簡単だろうか? -しかし、結果のサボテンを使って作業することで、.Net Coreに移行して中間アセンブラの手動コード生成と新しいコンパイラの学習を行う前に、ドキュメントを吸うことを余儀なくされました。 C# 個人的には、ランタイムとコンパイラー自体の構造の両方で、多くの興味深いことを発見しました。 ハブロフスクの住民がすでに知っていることもあれば、思考に役立つ食べ物もあると思います。



第1幕:コピー&ペースト



これは普通の典型的なタスクであり、私の友人はすでに明らかなことについて長い間考えたがらなかったため、結果は非常に早く現れました。 WCFではRESTサービスが私たちのものであったため、サービスインターフェースが移行される一般的なアセンブリMyProj.Abstracitons



が導入されました。 その中で、サービスインターフェースを実装し、リクエストをプロキシし、結果をデシリアライズするクラスに書き込む必要がありました。 アイデアは単純でした。同じインターフェイスを実装するクライアントでそれぞれのサービスを記述すると、サービスのメソッドを変更するとすぐに、コンパイルエラーがポップアップします。 そして、関数の引数を変更する人は、それが正しくシリアル化されていることを確認すると仮定します。 次のようになりました。







 public class FooClient : BaseClient<IFooService> { private static readonly Uri _baseSubUri public FooClient() : base(BaseUri, _baseSubUri, LogManager.GetCurrentClassLogger()) {} [MethodImpl(MethodImplOptions.NoInlining)] public Task<Foo> GetFoo(int a, DateTime b, double c) { return GetFoo<Foo>(new Dictionary<string, object>{ {“a”, a}, {“b”, b.ToString(SerializationConstant.DateTimeFormat)}}, new Dictionary<string, object>{ {“c”, c.ToString(SerializationConstant.FloatFormat)}}); } }
      
      





BaseClient<TService>



は、 HttpClient



ような薄いラッパーで、呼び出すメソッド(この場合はGetFoo



)を決定し、そのURLを計算し、リクエストを送信し、回答をGetFoo



し、結果をデシリアライズ(必要に応じて)して渡します。







それは:









原則として、難しくはなく、 NoInlining



たが、30年生の20番目のメソッドを書いた後、まったく同じタイプでしたが、人々は常にNoInlining



を書くのを忘れていNoInlining



。そのため、すべてが壊れました(小さなクイズ#1:なぜだと思いますか?)私は自分自身に質問をしました:「どうにかして人間にこれに近づくことは可能ですか?」。 しかし、タスクは既にマスターにマージされており、上から「ゴミを出さずに機能を飲んだ」と言われました。 しかし、私はあらゆる種類のラッパーを書くのに1日3時間を費やすという考えが好きではありませんでした。 一連の属性は言うまでもありませんが、人々は定期的にシリアル化を自分の変更やそのような痛みと同期させることを忘れていました。 したがって、次の週末に到達し、状況を何らかの形で改善するために着手し、数日間、彼は代替ソリューションをスケッチしました。







第2幕:反射



ここでのアイデアはさらにシンプルでした。同じことをすることを私たちが妨げているのは、手ではなく動的に生成しているのでしょうか? 完全に同一のタスクがあります。入力引数を取得し、それらをqueryString引数用の2つの辞書に変換し、残りを要求本体への引数として変換し、これらのパラメーターで標準のHttpClient



を呼び出すだけです。 その結果、同じSerializationConstant



すべての問題は、このハンドラーで1回しか記述されなかったという事実によって解決され、1回正しく実装でき、常に正しい結果が得られました。 それほど長くない喫煙文書とスタックオーバーフローの後、MVPは準備ができていました。







ここで、サービスを使用するには、次のようにします。







  1. インターフェースを作成する



     public interface ISampleClient : ISampleService, IDisposable { }
          
          



  2. 私たちは小さなラッパーを書いています(さらなる使用の便宜上)。



     public static ISampleClient New(Uri baseUri, TimeSpan? timeout = null) { return BaseUriClient<ISampleClient>.New(baseUri, Constant.ServiceSampleUri, timeout); }
          
          



  3. 私たちは使用します:







     [Fact] public async Task TestHelloAsync() { var manager = new ServiceManager(); manager.RunAll(BaseAddress); using (var client = SampleClient.New(BaseAddress)) { var hello = await client.GetHello(); Assert.Equal(hello, "Hello"); } manager.CloseAll(); }
          
          







免責事項

もちろん、このテストでは、実際の要求を行う実際のWCFサービスが発生するため、厳密に言えばこれは単体テストではありません。 しかし、私たちは皆、私たちの過ちから学びます。今、私は中毒を閉じ込めて、すべてを異なるやり方でしますが、その時、私はまだ方法を知りませんでした。







すべてが非常に単純で、理解できるように、特別なクラスの継承や属性の付加などの特別な魔法は必要ありません。 変数とメソッド名は自動的に表示されます。 一般的に、美しさ。 さらに、毎回サービスの名前で定数文字列を指定するのが面倒ではない場合、項目2​​は省略できます。







どのように機能しますか? 実際、十分な黒魔術で。 プロキシメソッドを生成する主な部分は次のとおりです。







 private static void ImplementMethod(TypeBuilder tb, MethodInfo interfaceMethod) { var wcfOperationDescriptor = ReflectionHelper.GetUriTemplate(interfaceMethod); var parameters = GetLamdaParameters(interfaceMethod); var newDict = Expression.New(typeof(Dictionary<string, object>)); var uriDict = Expression.Variable(newDict.Type); //    queryString var bodyDict = Expression.Variable(newDict.Type); //       var wcfRequest = Expression.Variable(typeof(IWcfRequest)); var dictionaryAdd = newDict.Type.GetMethod("Add"); var body = new List<Expression>(parameters.Length) //      var dict = new Dictionary<...> { Expression.Assign(uriDict, newDict), Expression.Assign(bodyDict, newDict) }; for (int i = 1; i < parameters.Length; i++) { var dictToAdd = wcfOperationDescriptor.UriTemplate.Contains("{" + parameters[i].Name + "}") ? uriDict : bodyDict; //    ,    uri ,        body.Add(Expression.Call(dictToAdd, dictionaryAdd, Expression.Constant(parameters[i].Name, typeof(string)), Expression.Convert(parameters[i], typeof(object)))); //      } var wcfRequestType = ReflectionHelper.GetPropertyInterfaceImplementation<IWcfRequest>(); //    ,     T,      var wcfProps = wcfRequestType.GetProperties(); var memberInit = Expression.MemberInit(Expression.New(wcfRequestType), Expression.Bind(Array.Find(wcfProps, info => info.Name == "Descriptor"), GetCreateDesriptorExpression(wcfOperationDescriptor)), Expression.Bind(Array.Find(wcfProps, info => info.Name == "QueryStringParameters"), Expression.Convert(uriDict, typeof(IReadOnlyDictionary<string, object>))), Expression.Bind(Array.Find(wcfProps, info => info.Name == "BodyPrameters"), Expression.Convert(bodyDict, typeof(IReadOnlyDictionary<string, object>)))); body.Add(Expression.Assign(wcfRequest, Expression.Convert(memberInit, wcfRequest.Type))); var requestMethod = GetRequestMethod(interfaceMethod); //   (GetResult  Execute),      body.Add(Expression.Call(Expression.Field(parameters[0], "Processor"), requestMethod, wcfRequest)); var bodyExpression = Expression.Lambda ( Expression.Block(new[] { uriDict, bodyDict, wcfRequest }, body.ToArray()), parameters ); var implementation = bodyExpression.CompileToInstanceMethod(tb, interfaceMethod.Name, MethodAttributes.Public | MethodAttributes.Virtual); //      tb.DefineMethodOverride(implementation, interfaceMethod); }
      
      





小さなクイズ#2

行c ReflectionHelper.GetPropertyInterfaceImplementation<IWcfRequest>()



注意してください。 なぜそれが必要だと思いますか? リフレクションのリフレクションは、単に書くのではなく、自分が望むものを生成するコードを書く方が面白いですか?







ここでの主なポイントは、式を使用してメソッドの本体を生成し、すべての引数を本体またはqueryStringのいずれかに配置し、CompileToInstanceMethod拡張機能を使用してデリゲートにではなく、すぐにクラスメソッドにコンパイルすることです。 これはそれほど難しいことではありませんが、作業バージョンが得られるまで数十回の反復が行われ、正しいバージョンが結晶化されました。







 internal static class XLambdaExpression { public static MethodInfo CompileToInstanceMethod(this LambdaExpression expression, TypeBuilder tb, string methodName) { var paramTypes = expression.Parameters.Select(x => x.Type).ToArray(); var proxyParamTypes = new Type[paramTypes.Length - 1]; Array.Copy(paramTypes, 1, proxyParamTypes, 0, proxyParamTypes.Length); var proxy = tb.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.Virtual, expression.ReturnType, proxyParamTypes); var method = tb.DefineMethod($"<{proxy.Name}>__Implementation", MethodAttributes.Private | MethodAttributes.Static, proxy.ReturnType, paramTypes); expression.CompileToMethod(method); proxy.GetILGenerator().EmitCallWithParams(method, paramTypes.Length); return proxy; } }
      
      





最も悲しいことは、これが比較的読みやすいオプションであり、CompileToMethodアプレットがそこから削除されたため、Coreに移動した後は放棄しなければならなかったことです。 その結果、匿名デリゲートを生成できますが、クラスメソッドは生成できません。 そして、これが必要なものです。 したがって、牛のバージョンでは、これはすべて古いものに置き換えられます いいね ILGenerator。 この場合の典型的なトリックは、C#コードを記述し、ildasmで解析し、それがどのように機能するかを確認することです。ここで、一般的なケースをカバーするために微調整する必要があります。 ILを自分で記述しようとすると、99%のケースで、 共通言語ランタイムが無効なプログラムを検出することができます:)エラー。 しかし、この場合、最終的なコードは、比較的読みやすい式よりも理解するのがはるかに困難です。







このCookieを樹皮から取り出す問題については、 ここで説明します (リストの最初の項目に興味があります)が、要求はかなり死んでいるように見えます。 しかし、すべてがそれほど悪いわけではありません。さらに良い解決策が見つかったからです!







第3幕:コンパイラーのカバー下



画像



この全体を100回書き直し、デバッグした後、コンパイルの段階でこれをすべてできないのはなぜかと思いました。 はい、生成されたタイプのオーバーヘッドをキャッシュすることで、クライアントの使用はごくわずかです。Activator.CreateInstanceを呼び出すだけで済みます。これは、特にシングルトンとして使用できるため、HTTPリクエスト全体を作成するコンテキストでは些細なことです。 サービスURL以外の状態はありません。 それでも、ここには適切な制限があります。







  1. 生成されたコードを見て把握することはできません。 原則として、これは必要ありません。 それは原始的ですが、最終的な作業コードを作成するまで、なぜ意図したとおりに機能しないのかについて多くを推測する必要がありました。 結論:まだ楽しみながら動的ビルドをデバッグする
  2. クライアントは常にクライアントと同じインターフェースを持っている必要があります。 いつ不快ですか? たとえば、サーバーに同期アプリケーションがあるが、クライアントではHTTP要求であるため、非同期である必要があります。 したがって、ストリームをブロックして応答を待つか、すべてのサーバーメソッドを非同期にする必要があります。必要のない場合でも、サービスに強制的にTask.FromResultを配置させます。
  3. 実行時にリフレクションを取り除くことは常に素晴らしいことです


ちょうどその時、Roslynについて興味深いことをたくさん聞きました。Roslyn-Microsoftの新しいモジュラーコンパイラで、プロセスをより深く掘り下げることができます。 当初、LLVMのように、目的の変換用のミドルウェアを簡単に作成できることを本当に期待していましたが、ドキュメントを読んだ後、Roslynで不要なジェスチャーなしで本格的なコード生成を行うことはできないという印象を受けました:これはLINQをloopに置き換えるプロジェクトで行われますが、明らかな理由であまり便利ではありません)、または「ここでコンマを忘れてしまったので、挿入してください」というスタイルのアナライザーです。 そして、私はこのトピックの言語リポジトリをgithubにする興味深い機能リクエストに遭遇しました( tyts )が、その後、2つの問題がすぐに現れました:最初に、この機能の非常に長いリリース前に、そして次に、彼らはそれが作業フォームは、私には何の助けにもなりません。 すべてがそれほど悪くはありませんでしたが、コメントで私が必要なことをしているように見える興味深いプロジェクトへのリンクを私に与えてくれたからです。







数日掘り進めて基本的なプロジェクトをマスターした後、私はそれがうまくいくことに気付きました! そして、それは本来どおりに機能します。 魔法のようなものです。 通常のコンパイラーの上に独自のコンパイラーを作成するのとは異なり、ここでは、ソリューションに簡単にプラグインできる通常のnugetパッケージを作成します。ビルド中に、汚い作業を行います。この場合、サービスのクライアントコードを生成します。 スタジオと完全に統合されているため、何もする必要はありません-ナンセンス。 確かに、ソリューションの最初のインストール後のバックライトは機能しませんが、ソリューションの再構築と再発見後は、バックライトとIntelliSenseの両方が機能します! 確かに、すべてが機能するわけではありません。たとえば、<inheritdoc />を使用してインターフェイスから拡張ドキュメントを表示する方法はまだわかりません。何らかの理由で、スタジオはこれを実行したくないだけです。 さて、大丈夫、主なことは完了です-クラスが生成され、それらが動作し、生成の結果は常にナゲットを介してワンクリックで設定され、スパイされ修正されます。 望みどおりのすべて。







ユーザーの場合、使用方法は次のようになります。







画像



インターフェースを記述し、いくつかの属性をハングアップし、コンパイルするだけで、生成されたクラスを使用できます。 PostSharpは必要ありません! (冗談)。







それでは、どのように機能しますか?







第4幕:決勝



当初、私は深く行くつもりはありませんでした、なぜなら すでに私の要件を完全に満たす完成したライブラリがありましたが、アナライザを作成してパッケージを作成するだけでした。 しかし、提供されたAPIの不適切な使用、またはライブラリ自体のエラーまたは欠点のために、現実はより深刻で、エラーをキャッチすることが判明しましたが、避けられない報復は依然として私を追い越しました。 最終的にすべてが上の写真のように起動するように、私はそれを把握し、密輸しなければなりませんでした。







実際、ほとんどすべてのソルトは新しい.Net Coreツールチェーンにあります。







 <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <PackageType>DotnetCliTool</PackageType> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp1.0</TargetFramework> <AssemblyName>dotnet-codegen</AssemblyName> </PropertyGroup> </Project>
      
      





本質的に、これはプロジェクトを構築するときにミドルウェアを決定する方法です。 その後、コンパイラはdotnet-codegenが何であるかを理解し、呼び出すことができます。 プロジェクトをビルドすると、次のように表示されます。







画像



ビルドをクリックしたとき(または単にファイルを保存したときでも!):





  1. CodeGeneration.Roslyn.Tasks



    アセンブリからGenerateCodeFromAttributes



    があります。これは、 Microsoft.Build.Utilities.ToolTask



    を継承し、プロジェクトのアセンブリ中にこれらすべての開始を決定します。 実際、出力ウィンドウでこのタスクの作業が少し高くなっています。
  2. テキストファイルCodeGeneration.Roslyn.InputAssemblies.txt



    ます。 CodeGeneration.Roslyn.InputAssemblies.txt



    、収集するアセンブリへのフルパスが書き込まれます。
  3. CodeGeneration.Roslyn.Tool



    CodeGeneration.Roslyn.Tool



    、分析用のファイルのリスト、入力アセンブリなどを取得します。 一般的に、作業に必要なすべてのもの。
  4. それでは、すべてが簡単です。プロジェクト内のICodeGenerator



    インターフェイスのすべての子孫を見つけ、コードが生成する唯一のGenerateAsync



    メソッドを呼び出します。
  5. コンパイラは、objディレクトリから生成された新しいファイルを自動的に取得し、結果のアセンブリに追加します


その結果、このライブラリの現在のバージョンでは、一部のクラスに属性を掛けて、文字通り100行のコードを記述できます。これに基づいて、必要なすべてが生成されます。 別のアセンブリのクラスを生成できないという制限があります。つまり、生成されたクラスは常にコンパイルする同じアセンブリに追加されますが、原則としてそれと共存できます。







追加の行為:要約



このライブラリを書いたとき、誰かに役立つと期待していましたが、それでややがっかりしました。 Swaggerは同じタスクを実行しますが、同時にクロスプラットフォームであり、便利なインターフェースを備えています。 しかし、それにもかかわらず、私の場合は、単にタイプを変更し、ファイルを保存してすぐにコンパイルエラーを取得できます。 すべてが開始されたもの:







画像







最後に大事なことを言い忘れましたが、私はこのすべてを実装することで多くの喜びを得ました。また、私には思えるように、言語とコンパイラについての十分な知識を得ました。 したがって、私は記事を書くことにしました:多分世界は新しいスワガーを必要としないかもしれませんが、コード生成が必要な場合、T4は軽spするかあなたに合わず、反射は私たちのオプションではありません、そしてここに素晴らしい仕事をする素晴らしいツールがあります、素晴らしい現在のパイプラインに統合され、その結果、nougatパッケージのように広がります。 はい、スタジオからのバックライトが含まれています! (ただし、ソリューションの第1世代と再発見後のみ)。







私は、このプロセスをコアフレームワークではない大人のフレームワークで試したことはないので、すぐに言う必要があります。 ただし、このパッケージのターゲットには、 portable-net45+win8+wpa81



portable-net4+win8+wpa81



、さらにはnet20



が含まれることを考慮すると、特別な問題はありません。 そこに何か、余分な依存関係、 NIHが気に入らない場合でも、独自の、よりコーシャのある実装をいつでも作成できます。コードには多くのメリットがあります。 別の落とし穴はデバッグです。私はこのすべてをデバッグする方法を理解していませんでした。コードは盲目的に書かれました。 しかし、ネイティブCodeGeneration.Roslyn



ライブラリのCodeGeneration.Roslyn



者は、プロジェクトの構造を見るだけで間違いなく必要な知識を持っていますが、最終的にはそれらを使用しませんでした。







そして今、私は明確な良心をもって言うことができます:私は私が別のsw歩を書いたことを全く後悔していません。







参照:









私のプロジェクトはすべてMITライセンス、fork-study-breakの下にありますが、私は苦情はありません:)







当初、これらはすべて完全に機能するプロジェクトとして計画されていましたが、実際の要件の結果として現れたため、最悪の場合、マイナーなドピルカの後、すべてを実稼働で使用できます。







もちろん、質問に対する答えは次のとおりです。







  1. MethodImplOptions.NoInliningは、呼び出す必要があるメソッドの名前を決定するために使用されます。 なぜなら ほとんどのメソッドは非常に単純であるため、多くは文字通り単一行であり、コンパイラーはそれらをインライン化することを好みます。 ご存知のように、コンパイラーは32バイト未満の本体でメソッドをインライン化します(まだ多くの条件がありますが、これに焦点を当てることはありません。それらはすべてここで満たされました)。したがって、多数の引数を持つメソッドが正常に呼び出されるという面白いバグを見ることができます-実行時にエラーをスローする 適切なメソッドが見つからず、コールスタックの最上部に到達します。



      MethodBase method = null; for (var i = 0; i < MAX_STACKFRAME_NESTING; i++) { var tempMethod = new StackFrame(i).GetMethod(); if (typeof(TService).IsAssignableFrom(tempMethod.DeclaringType)) { method = tempMethod; break; } }
          
          



  2. 実際のところ、reflexメソッドを作成したとき、現在のRemoteClient.Core



    アセンブリではなく、動的に作成されたクラスにクラスを追加しているとは本当に思っていませんでした。 これは非常に重要です。 その結果、すべての機能をテストし、これらすべてが機能するという自信を獲得した後、 WcfRequest



    クラスがパブリックであることがWcfRequest



    。 「障害」は、「実装はプライベートであり、インターフェイスのみが表示されるべきだ」と考えました。 そして、内部属性を設定します。 そして、それはすべて壊れました。 理由は簡単です。親アセンブリA.dll



    内部クラスをインスタンス化しようとするアセンブリA.Dynamicalygenerated.dll



    を生成し、アクセスエラーで自然にクラッシュします。 さて、これは、アセンブリ間に不愉快な循環依存を得るという事実を数えていません。 その結果、すべてのプロパティにセッターを単純に追加する「ダミークラス」の動的生成は、かなり単純なソリューションであることが判明し、同時に、生成されたアセンブリについてA.dll



    直接依存し、テールを持たないという意味で便利になりました。裏側。



All Articles