tech.guitarrapc.cóm

Technical updates

Azure Functions C#で外部アセンブリ読み込みとRoslynコンパイラに渡してみる

前回は、Azure FunctionsにC#コードを投げつけるとRoslyn Scriptingで評価して結果を返すところまでやりました。

https://tech.guitarrapc.com/entry/2016/05/04/150011

次にやりたくなるのが、独自クラス、メソッドもRoslynに評価させることですね。自分用ヘルパーをうまく読みこませられないかやってみましょう。

どうやって独自クラスなどを読みこませるか

Roslyn ScriptingのReferenceを見てみると、Add referencesつまり、参照の追加があります。これを使えば外部DLLの追加が可能そうです。

https://github.com/dotnet/roslyn/wiki/Scripting-API-Samples#addref

var result = await CSharpScript.EvaluateAsync("System.Net.Dns.GetHostName()",
     ScriptOptions.Default.WithReferences(typeof(System.Net.Dns).Assembly));

外部DLL を AzureFunctions で参照する

Azure Functionsでは、#rキーワードを使うことで外部DLLを読み込むことができます。

https://github.com/dotnet/roslyn/wiki/Interactive-Window#r

https://azure.microsoft.com/en-us/documentation/articles/functions-reference-csharp/#referencing-external-assemblies

外部DLL の配置は Function単位

1つ注意があります。#rで外部 DLLを読み込むときは Function単位でbinフォルダを作ってそこに配置する必要があります。

If you need to reference a private assembly, you can upload the assembly file into a bin folder relative to your function and reference it by using the file name (e.g. #r "MyAssembly.dll").

つまりこうですね。

これは、↓図のような#loadのようにFunctionの外において参照できないことを意味します。

相対参照できるか試しても読み込めないことがエラーで示されます。

https://gist.github.com/guitarrapc/03ffffa514962a6a9fcbfecb4e41ecfb

2016-05-08T17:35:37.376 run.csx(2,1): error CS0006: Metadata file '../MyExtesnsions.dll' could not be found
2016-05-08T17:35:37.690 run.csx(7,7): error CS0246: The type or namespace name 'MyExtesnsions' could not be found (are you missing a using directive or an assembly reference?)
2016-05-08T17:35:37.690 run.csx(22,24): error CS0246: The type or namespace name 'EnumerableExtensions' could not be found (are you missing a using directive or an assembly reference?)

絶対参照なら一見読み込んでコンパイルが通るようにみえますが、今度はUnable to find assemblyと「読み込んでいるはずのアセンブリが見つからないエラー」が出ています。

https://gist.github.com/guitarrapc/404bcc9ed30823328cf0e3cb75753acf

2016-05-08T18:17:23.599 Function started (Id=478b950c-d69c-454a-a8c9-12de5f8f2fb5)
2016-05-08T18:17:23.770 Unable to find assembly 'MyExtensions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Are you missing a private assembly file?
2016-05-08T18:17:23.833 Function completed (Failure, Id=478b950c-d69c-454a-a8c9-12de5f8f2fb5)
2016-05-08T18:17:23.942 Exception while executing function: Functions.Test. mscorlib: Exception has been thrown by the target of an invocation. ƒ-Test#ℛ*b2ec9bce-cf2c-4b67-bee1-e7188b743111#29-0: Could not load file or assembly 'MyExtensions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.

ふとシンボリックリンクで回避できるか試すも、特権がないのでmklinkも弾かれます。

グローバルに参照させる方法は打つ手ないのかな。。。。 ということで、諦めてFunction内にbinフォルダを作って配置しましょう。

外部アセンブリクラスを作成する

適当にクラスライブラリを作成します。

今回は、2つのFunction CSharpCompilerWebhookCSharpCSharpCompilerSlackOuthookCSharpでこの外部アセンブリを参照します。そこで、MyExtensions.csprojのビルド後処理でコピーするようにしてみました。

https://gist.github.com/guitarrapc/e68ee4f6d59d8a998c24055ca7660c55

ポイントは、SolutionDir変数の定義と、Targetによるdllコピー処理の記述、AfterBuildイベントのフックです。

特にVS2015以降では、SolutionDirが.csprojで初期状態を記述しなくなって面倒になりました。

  <PropertyGroup>
    <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
  </PropertyGroup>
// 省略
  <Target Name="CopyDll">
    <Copy SourceFiles="$(SolutionDir)MyExtensions\bin\$(ConfigurationName)\MyExtensions.dll" DestinationFolder="$(SolutionDir)CSharpCompilerSlackOuthookCSharp\bin" ContinueOnError="true" />
    <Copy SourceFiles="$(SolutionDir)MyExtensions\bin\$(ConfigurationName)\MyExtensions.dll" DestinationFolder="$(SolutionDir)CSharpCompilerWebhookCSharp\bin" ContinueOnError="true" />
  </Target>
  <Target Name="AfterBuild">
    <CallTarget Condition="'$(Configuration)' == 'Debug'" Targets="CopyDll" />
    <CallTarget Condition="'$(Configuration)' == 'Release'" Targets="CopyDll" />
  </Target>

このMyExtensionsクラスをビルドすることで、MyExtensions.dllCSharpCompilerWebhookCSharp\binCSharpCompilerSlackOuthookCSharp\binにコピーされればokです。

本当はCIを使うところですが、めんどうなのでGitHub連携のまま行きます。ということで、せっかくコピーしたdllがGitHubリポジトリに入らないことが無いように.gitignore[Bb]in/をコメントアウトしてMyExtensions/[Bb]in/を追加します。

https://gist.github.com/guitarrapc/a1d66f79221869e55985aa36b6e22ce5

GitHub上でdllが見えますね?

さぁ準備完了です。

.csx で外部アセンブリを参照してみる

さっそく参照してみましょう。まずは、CSharpCompilerWebhookCSharp Functionでやります。

まず、呼び出し元のrun.csxでdllを参照しusing <NameSpace>します。その上で、呼び出し先のCSharpScripting.csxでも、using <NameSpace>を行いします。

あとは、Roslyn Scripting APIの通り、.WithImports()にusing対象の名前空間、.WithReferences()でアセンブリかアセンブリパスを渡します。

https://gist.github.com/guitarrapc/66736cdec4654a65b048c9a9bad42545

これでコンパイルが無事に通れば、準備完了です。サンプルJSONを投げてみましょう。

https://gist.github.com/guitarrapc/faba8968229a6149a5d4fd4db4c20bc7

無事に結果が返りました。

SlackのOutgoting Webhook用のFunction、CSharpCompilerSlackOuthookCSharp でも試してみましょう。

https://gist.github.com/guitarrapc/744b5f41ff617ebbe296fe3e779e9de1

無事に実行されました。

まとめ

今回の内容もGitHubに上げました。

https://github.com/guitarrapc/AzureFunctionsIntroduction

これで任意の外部アセンブリを読み込ませていい感じでRoslyn Scripting APIでも読みこませられますね!

かなりやりたいことは網羅できるはずです。