.NET6.0のAlpineイメージの3.13はまだプレビュー
おや?とりあえず特別な何かが無いのであれば3.14を使おう
❯ mcr.microsoft.com/dotnet/sdk:6.0-alpine3.13
# dotnet --list-runtimes
Microsoft.AspNetCore.App 6.0.0-preview.7.21378.6 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 6.0.0-preview.7.21377.19 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
❯ mcr.microsoft.com/dotnet/sdk:6.0-alpine3.14
# dotnet --list-runtimes
Microsoft.AspNetCore.App 6.0.0 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 6.0.0 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
.NET Framework → .NET Coreの移行可能難易度を評価してくれるAWSのPorting Assistant for .NET
AWSのブログでPorting Assistant for .NET といAWS製のツールが紹介されていました。
このツールは既存の.NET Frameworkのソリューションを読み取り、その中で利用されているNugetライブラリーの.NET Core対応状況や、ソースコードそのものがどれだけ移植しやすい状況かを確認するツールのようです。
ポートソリューションの機能を使うと、参照しているライブラリで移行可能なライブラリのバージョンを調整したり、プロジェクトファイルの構成を変更してくれるようです。
ASP.NET Coreの標準イメージ(Debian buster)からMySQL 3.6以前のデータベースに接続する場合はTLSをオフにするか、TLS1.0を有効にする必要がある。
タイトルの通りですが、ASP.NETを利用しているアプリケーションで、ベースイメージをDebian busterしているコンテナからAWS Aurora(MySQL 3.6互換)に接続しようとしたところ、下記のようなメッセージが表示されました。
System.InvalidOperationException
HResult=0x80131509
Message=An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure()' to the 'UseMySql' call.
Source=Pomelo.EntityFrameworkCore.MySql
スタック トレース:
場所 Pomelo.EntityFrameworkCore.MySql.Storage.Internal.MySqlExecutionStrategy.<ExecuteAsync>d__7`2.MoveNext()
場所 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
場所 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
場所 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
場所 Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.AsyncEnumerator.<MoveNextAsync>d__17.MoveNext()
場所 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
場所 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
場所 Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.<ToListAsync>d__64`1.MoveNext()
場所 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
場所 Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.<ToListAsync>d__64`1.MoveNext()
場所 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
場所 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
場所 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
場所 System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
場所 ConsoleApp3.Program.<Main>d__0.MoveNext() (C:\Users\karuakun\source\repos\ConsoleApp3\ConsoleApp3\Program.cs):行 14
内部例外 1:
MySqlException: SSL Authentication Error
内部例外 2:
AuthenticationException: Authentication failed, see inner exception.
内部例外 3:
SslException: SSL Handshake failed with OpenSSL error - SSL_ERROR_SSL.
内部例外 4:
OpenSslCryptographicException: error:1425F102:SSL routines:ssl_choose_client_version:unsupported protocol
SSL のハンドシェイクに失敗しているようですね。普段データアクセスには Pomelo.EntityFrameworkCore.MySql を利用しているのですが、多分MySQL.Dataを使っても同じエラーが発生すると思います。
Webで調べてみると、dotnet coreに下記のIssueが登録されていました。https://github.com/dotnet/runtime/issues/30730#issuecomment-527732852
MySQL に接続する際にTLSを有効にした場合、OSがサポートしているTLSの最低バージョンとMySQLで利用できる最大バージョンの違いの問題で接続を確立できなかったようです。
AWS AuroraでMySQLを利用する場合、5.6と5.7のどちらかの互換エンジンを利用するのですが、5.6を利用した場合TLS1.0のみを利用できます。逆にDebian 10(buster)はTLS1.2を標準で利用するようになっています。このため、Debian 10からTLSを利用してMySQLに接続する場合は下記2つの対処をする必要があります。
TLSの無効にして接続する。
Pomelo.EntityFrameworkCore.MySqlでMySQLに接続する場合、接続文字列で特に指定がない場合はSslModeプロパティーはPreferredが利用されます。この場合サーバー側でTSLが有効な場合はTSLを利用して接続を確立しようとしますが、前述のとおりOSとMySQLのTLSのバージョンの違いにより接続できません。
この場合は、SslModeにNoneを指定し、TLSを無効にして接続することで回避することができます。SslModeを指定する場合は接続文字列を利用するのが簡単です。
server=localhost;userid=xxx;pwd=xxx;port=3306;sslmode=none;
TLSの最小バージョンを1.0に変更して接続する。
いやいや、TLSは無効にしたくないという場合は、Docker Build時にopenssl.confを書き換えて最低バージョンをTLS1.0に設定します。
→参照 Docker環境(Debian)からMySQLに接続できない問題https://qiita.com/hideBBBtec/items/ac6e62b586a77fb76061
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/core/runtime:3.1-buster-slim AS base
RUN sed -i 's,^\(MinProtocol[ ]*=\).*,\1'TLSv1.0',g' /etc/ssl/openssl.cnf
WORKDIR /app
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
COPY ["ConsoleApp3/ConsoleApp3.csproj", "ConsoleApp3/"]
RUN dotnet restore "ConsoleApp3/ConsoleApp3.csproj"
COPY . .
WORKDIR "/src/ConsoleApp3"
RUN dotnet build "ConsoleApp3.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "ConsoleApp3.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ConsoleApp3.dll"]
ASP.NET Core をコンテナで実行するのであれば、マイナーバージョンまで指定しよう
ASP.NETのプロジェクトで、Dockerサポートを追加すると追加されるDockerfileを見るとこんな感じになっていると思います。
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base WORKDIR /app EXPOSE 80 FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build ... 略 ...
バージョン指定がaspnet:3.1-buster-slimとなっているので、ASP.NET Coreは3.1系の最新が利用されます。このため機能は3.1.2で動いていたけれど、今日デプロイしたコンテナは3.1.3で動いているということが起きます。
ここを見ると、最新の.NET Frameworkがリリースされたら12時間以内にベースイメージが更新されるとあるので、イメージの指定がaspnet:3.1のように書かれていると.NET Coreのマイナーバージョンは制御できません。
本番で動いているものは、できるだけバージョンを固定しテストを実施した時のバージョンで動いて欲しいので、マイナーバージョンまで指定しましょう。もちろんフレームワーク側だけでなくOS側もバージョンを指定できるのであれば指定したほうが良いと思います。
現在公開されているASP.NETコンテナのタグの一覧はここで確認できます。
https://mcr.microsoft.com/v2/dotnet/core/aspnet/tags/list
https://mcr.microsoft.com/v2/dotnet/core/sdk/tags/list
例えばASP.NET Coreの3.1.3を利用するのであれば、3.1.3-alpine3.10 や 3.1.3-buster-slim-armといったタグを使うほうが望ましい気がします。
AWSでASP.NET Coreのデータプロテクションキーの保存先を考える。
ASP.NET Coreではフレームワーク内部で使う暗号化やハッシュ時のキーは、アプリケーション起動時に指定された方法で作成され保存されます。ローカル開発時に1台のWebサーバーで開発しているときには問題ないけれど、複台構成の実行環境で動かす場合は生成するキーがサーバーごとで異なってしまうので問題が発生します。このキーをどうやって取り扱うかは、下記のドキュメントに詳しく記載されています。
AWSでASP.NET Coreを動かす場合、下記の3つの方法を検討します。
– Redisに保存する
– パラメーターストアに保存する
– S3に保存する
Redisに保存する
上記のドキュメントにも記載がありますが、Microsoft.AspNetCore.DataProtection.Redisパッケージを利用して、Redisにキーを保存する方法です。
public void ConfigureServices(IServiceCollection services) { // ... 略 ... var redis = StackExchange.Redis.ConnectionMultiplexer.Connect(connectionString); services.AddDataProtection() .SetApplicationName("WebApp") .PersistKeysToStackExchangeRedis(redis, "WebAppDataProtection"); // ... 略 ... }
パラメーターストアに保存する
Amazon.AspNetCore.DataProtection.SSMパッケージを利用して、パラメーターストアに保存する方法です。
public void ConfigureServices(IServiceCollection services) { services.AddDefaultAWSOptions(Configuration.GetAWSOptions()); // ... 略 ... services.AddDataProtection() .SetApplicationName("WebApp") .PersistKeysToAWSSystemsManager("WebAppDataProtection"); // ... 略 ... }
S3に保存する(未検証)
試していないのですが、コミュニティーのライブラリであるAspNetCore.DataProtection.Aws.S3パッケージとAspNetCore.DataProtection.Aws.Kmsパッケージを利用して、キーをS3に保存する方法もあるようです。→ https://codeopinion.com/asp-net-core-data-protection/
public void ConfigureServices(IServiceCollection services) { // ... 略 ... services.AddDataProtection() .PersistKeysToAwsS3(new AmazonS3Client(), new S3XmlRepositoryConfig("bucketName") { KeyPrefix = "WebAppDataProtection/", }); // ... 略 ... }
PowerAutomateでBacklogAPIを使いやすくするためのBridgeAPIを作りました。
BacklogAPIって、Swagger定義が無いのと、同一項目を複数回クエリ内で利用するようなAPI(ex. Issue API)がいくつかあるんです。
Swagger上はこのエントリーで紹介したSwagger定義で、type:array、collectionFormat: multiで指定されるような項目なんだけれど、どうやっても複数項目の指定がうまくいかない。
paths: /api/v2/issues: get: summary: FindIssues description: FindIssues operationId: FindIssues parameters: - {name: apiKey, in: query, type: string, required: true} - {name: 'projectId[]', in: query, type: string, required: true} - {name: 'assigneeId[]', in: query, type: string, required: true} - name: statusId[] in: query type: array collectionFormat: multi items: {type: integer}
というわけで、複数項目をcollectionFormat: multiではなくJSONのArrayで指定できるようにしたBridgeAPIを作成しました。現時点でこのAPIでフォローしているのは自分で必要なものだけなので、必要があったら追加してくれるとうれしいです。
https://github.com/karuakun/backlog-api-bridge
一応、AzureのWebSiteとAWSのLabmdaで動かすためにプロジェクトを分けているので、配置する場合はどちらか好きなほうから配置してください。単純にデバックとかで動かしたい場合は、BacklogApiBridge.WebApp側でデバック実行すればよいです。
自分のためのAPIなので、すべてのBacklogSpaceへのリクエストをBridgeするようにはしていません。環境変数AllowBacklogSpaces__0とかに、自分のBacklogSpace名をしていして実行してください。
Azureにデプロイする場合は、Azure App Serviceの設定を編集するから、AllowBacklogSpaces__0環境変数を追加してください。
AWS Labmdaで実行する場合は、serverless.templateファイルの26行目あたりでallow-space-nameを書き換えてあげてください。あっ、aws-lambda-tools-defaults.jsonも修正してくださいね。
PowerAutomateでPostmanCollectionからpathパラメーターを持つカスタムコネクタを定義する
この頃PowerAutomateを触っているんです。PowerAutomateで標準のコネクタ以外のWebAPIを利用する場合、HTTPコネクタを利用するか、カスタムコネクタを定義して利用するかのどちらかになります。
HTTPコネクタは汎用的すぎて定義が煩雑になるので、何度も使うような物はできるだけカスタムコネクタに寄せてあげたほうが再利用性が高まってうれしいですね。
カスタムコネクタの定義方法
カスタムコネクタの定義方法には3つの方法があるんだけれど、最終的にはOpenAPI(Swagger)の仕様書を書くことになります。素でSwagger仕様書を書くのはつらいので、とっかかりとしてはPostmanを使うのが楽でいいですね(もちろん、可能であればブリッジ用のASP.NETプログラムを作って、NSwagなどでASP.NETのコントローラー定義からSwagger仕様書を自動生成するとかいうのもありです)。
ここでは Postmanでリクエストの作成 → Postman コレクションのエクスポート → カスタムコネクタの新規作成時に取り込み → Swagger仕様書の微調整 という手順で進めようと思います。
PostmanのインストールとPostmanコレクションの作成
Postman は HTTP のリクエストを簡単に作成、保存するためのツールで下記のURLからダウンロードしてインストールすることができます。
→ https://www.postman.com/
Postmanでは複数のリクエストをまとめてPostman Collectionというかたまりで管理することができます。Power Automateではカスタムコネクタを作成さいにPostman Collectionをインポートすることになるので、ある程度用途別に複数の操作を一つのコレクションにまとめておくとわかりやすいですね。
Postmanでリクエストの作成
実用的なところで、Backlogの課題と、課題に寄せられたコメントの2つを取得するAPIのリクエストを考えてみます。
BacklogのAPI仕様はここにまとまっています。今回はAPIKeyで認証を行うので、あらかじめBacklog内の個人設定からAPIKeyを発行しておいてください。
まずBacklogの課題を検索するAPIから見ていきましょう。
課題一覧の取得APIの仕様を見ると、/api/v2/issues というエンドポイントに諸々のパラメーターを付けて呼び出せば良いようです。
たとえば、特定プロジェクトで自分に割り振られた未完了の課題を検索したいのであれば、curlのリクエストだとこんな感じになりますね。
curl -X GET https://xxx.backlog.jp/api/v2/issues ?apiKey=xxx &projectId[]=xxx &assigneeId[]=xxx &statusId[]=1 &statusId[]=2
xxxの部分は環境依存なので、それぞれ環境ごとに確認してください。
で、これをPostmanで実行するにはこんな感じになりますね。
次に、課題のコメントを取得するAPIについて確認していきましょう。
このAPIはPathにパラメーターが入っているので、先ほどの検索結果をもとに、こんな感じでURLを組み立てます。
https://xxx.backlog.jp/api/v2/issues/10089694/comments ?apiKey=xxx
2つのAPIのリクエストを登録したら、Postmanコレクションをエクスポートします。
カスタムコネクタの定義
カスタムコネクタの新規作成は、Power Automateの左側のメニューにあるカスタムコネクタから始めます。
右上のカスタムコネクタの新規作成から、Postman おレクションをインポートしますを選択して
先ほどエクスポートしたPostmanコレクションの定義ファイルを選択します。
全般とセキュリティーは特に設定を変更する箇所がないので、そのまま次に進みます。
課題検索のリクエスト定義を確認すると、apiKeyやprojectId[]がパラメーターとして指定できるようになっているのを確認できます。
応答の部分は、Postman コレクションでは取り込んでくれないのでこのままだとFlowの中でJSONを解析するステップを追加することになってしまいます。PowerAutomateには、応答のスキーマ定義を実際のJSOnから生成する機能があるので、+既定の応答を追加するを選択し
Postmanでリクエストした結果のJSONを本文に貼り付け応答リクエストのスキーマを作成します。
コメントの取得APIはリクエストのパスの中に変数が入ってきているのでこれをどうにかします。
取り込み画面からパスを直に直すインターフェイスはなさそうなので、ナビゲーションメニューのSwaggerエディターを起動して、Swagger仕様書を直に修正します。
変更前はこうなっているので、パス部分を外から指定できるように変数化します。
パス部分が変数化されましたね
あとは課題の検索と同様に、応答スキーマを実際のJSONリクエストから生成すれば作業は終了です。
右上のコネクタの作成から作業を終了しましょう。
カスタムコネクタを見ると、今作成したカスタムコネクタが登録されているのが確認できます。
フローで利用する
カスタムタブに出てくるので、選択して利用します。
コメントの取得は、課題の取得で受け取ったレスポンスをもとにループして取得するのでこんな感じになりますね。
説明するとだいぶ長くなりましたが、やってみると意外とサクサクできるのでまずは手を動かしてみると良い気がします。
— 追記
Backlogの複数条件(array)への対応(途中)
Backlog APIではパラメーター名の末尾が[]になっている項目(statusId[]など)は複数回パラメータとして指定できます。Postmanコレクションから取り込んだ場合、単一項目になってしまうので配列項目として型を変更する必要があります。
これに対応するには、パラメーターの編集操作から
種類をarrayに変更する必要があります。
Swagger上記の操作を行うと、Swagger上では下記のようになります。
ただ、この配列定義をアクションで指定する方法が現在わかっていなくて(汗)、上記の定義の状態で下記のフロー定義は通るんだけれど(もちろん1つのステータスしか指定できないけれど)
配列として定義すると
こんなエラーが発生してしまいます。
InvalidTemplate. Unable to process template language expressions in action ‘FindIssues’ inputs at line ‘1’ and column ‘2093’: ‘Error reading string. Unexpected token: StartArray. Path ‘queries[‘statusId[]’]’.’.
ここまでの作業の結果出来上がったカスタムコネクタのSwagger定義を載せておきます。
swagger: '2.0' info: {version: 1.0.0, title: BacklogIssueManagement, description: BacklogIssueManagement} host: xxx.backlog.jp basePath: / schemes: [https] consumes: [] produces: [application/json] paths: /api/v2/issues: get: summary: FindIssues description: FindIssues operationId: FindIssues parameters: - {name: apiKey, in: query, type: string, required: true} - {name: 'projectId[]', in: query, type: string, required: true} - {name: 'assigneeId[]', in: query, type: string, required: true} - name: statusId[] in: query type: array collectionFormat: multi items: {type: integer} responses: default: description: default schema: type: array items: type: object properties: id: {type: integer, format: int32, description: id} projectId: {type: integer, format: int32, description: projectId} issueKey: {type: string, description: issueKey} keyId: {type: integer, format: int32, description: keyId} issueType: type: object properties: id: {type: integer, format: int32, description: id} projectId: {type: integer, format: int32, description: projectId} name: {type: string, description: name} color: {type: string, description: color} displayOrder: {type: integer, format: int32, description: displayOrder} description: issueType summary: {type: string, description: summary} description: {type: string, description: description} resolution: {type: string, description: resolution} priority: type: object properties: id: {type: integer, format: int32, description: id} name: {type: string, description: name} description: priority status: type: object properties: id: {type: integer, format: int32, description: id} projectId: {type: integer, format: int32, description: projectId} name: {type: string, description: name} color: {type: string, description: color} displayOrder: {type: integer, format: int32, description: displayOrder} description: status assignee: type: object properties: id: {type: integer, format: int32, description: id} userId: {type: string, description: userId} name: {type: string, description: name} roleType: {type: integer, format: int32, description: roleType} lang: {type: string, description: lang} mailAddress: {type: string, description: mailAddress} nulabAccount: type: object properties: nulabId: {type: string, description: nulabId} name: {type: string, description: name} uniqueId: {type: string, description: uniqueId} description: nulabAccount keyword: {type: string, description: keyword} description: assignee category: type: array items: {} description: category versions: type: array items: {} description: versions milestone: type: array items: {} description: milestone startDate: {type: string, description: startDate} dueDate: {type: string, description: dueDate} estimatedHours: {type: string, description: estimatedHours} actualHours: {type: string, description: actualHours} parentIssueId: {type: string, description: parentIssueId} createdUser: type: object properties: id: {type: integer, format: int32, description: id} userId: {type: string, description: userId} name: {type: string, description: name} roleType: {type: integer, format: int32, description: roleType} lang: {type: string, description: lang} mailAddress: {type: string, description: mailAddress} nulabAccount: type: object properties: nulabId: {type: string, description: nulabId} name: {type: string, description: name} uniqueId: {type: string, description: uniqueId} description: nulabAccount keyword: {type: string, description: keyword} description: createdUser created: {type: string, description: created} updatedUser: type: object properties: id: {type: integer, format: int32, description: id} userId: {type: string, description: userId} name: {type: string, description: name} roleType: {type: integer, format: int32, description: roleType} lang: {type: string, description: lang} mailAddress: {type: string, description: mailAddress} nulabAccount: {type: string, description: nulabAccount} keyword: {type: string, description: keyword} description: updatedUser updated: {type: string, description: updated} customFields: type: array items: type: object properties: id: {type: integer, format: int32, description: id} fieldTypeId: {type: integer, format: int32, description: fieldTypeId} name: {type: string, description: name} value: type: array items: {} description: value otherValue: {type: string, description: otherValue} description: customFields attachments: type: array items: type: object properties: id: {type: integer, format: int32, description: id} name: {type: string, description: name} size: {type: integer, format: int32, description: size} createdUser: type: object properties: id: {type: integer, format: int32, description: id} userId: {type: string, description: userId} name: {type: string, description: name} roleType: {type: integer, format: int32, description: roleType} lang: {type: string, description: lang} mailAddress: {type: string, description: mailAddress} nulabAccount: type: object properties: nulabId: {type: string, description: nulabId} name: {type: string, description: name} uniqueId: {type: string, description: uniqueId} description: nulabAccount keyword: {type: string, description: keyword} description: createdUser created: {type: string, description: created} description: attachments sharedFiles: type: array items: {} description: sharedFiles stars: type: array items: {} description: stars /api/v2/projects/BLDE_V2/statuses: get: summary: GetIssueStatuses description: GetIssueStatuses operationId: GetIssueStatuses parameters: - {name: apiKey, in: query, type: string, required: true} responses: default: description: default schema: {} /api/v2/issues/{issueId}/comments: get: summary: GetIssueComments description: GetIssueComments operationId: GetIssueComments parameters: - {name: apiKey, in: query, type: string, required: true} - {name: issueId, in: path, type: string, required: true} responses: default: description: default schema: type: array items: type: object properties: id: {type: integer, format: int32, description: id} content: {type: string, description: content} changeLog: type: array items: type: object properties: field: {type: string, description: field} newValue: {type: string, description: newValue} originalValue: {type: string, description: originalValue} attachmentInfo: {type: string, description: attachmentInfo} attributeInfo: {type: string, description: attributeInfo} notificationInfo: type: object properties: type: {type: string, description: type} description: notificationInfo description: changeLog createdUser: type: object properties: id: {type: integer, format: int32, description: id} userId: {type: string, description: userId} name: {type: string, description: name} roleType: {type: integer, format: int32, description: roleType} lang: {type: string, description: lang} mailAddress: {type: string, description: mailAddress} nulabAccount: type: object properties: nulabId: {type: string, description: nulabId} name: {type: string, description: name} uniqueId: {type: string, description: uniqueId} description: nulabAccount keyword: {type: string, description: keyword} description: createdUser created: {type: string, description: created} updated: {type: string, description: updated} stars: type: array items: {} description: stars notifications: type: array items: type: object properties: id: {type: integer, format: int32, description: id} alreadyRead: {type: boolean, description: alreadyRead} reason: {type: integer, format: int32, description: reason} user: type: object properties: id: {type: integer, format: int32, description: id} userId: {type: string, description: userId} name: {type: string, description: name} roleType: {type: integer, format: int32, description: roleType} lang: {type: string, description: lang} mailAddress: {type: string, description: mailAddress} nulabAccount: type: object properties: nulabId: {type: string, description: nulabId} name: {type: string, description: name} uniqueId: {type: string, description: uniqueId} description: nulabAccount keyword: {type: string, description: keyword} description: user resourceAlreadyRead: {type: boolean, description: resourceAlreadyRead} description: notifications /api/v2/projects/BLDE_V2: get: summary: GetProject description: GetProject operationId: GetProject parameters: - {name: apiKey, in: query, type: string, required: true} responses: default: description: default schema: {} /api/v2/users/myself: get: summary: GetOwnUser description: GetOwnUser operationId: GetOwnUser parameters: - {name: apiKey, in: query, type: string, required: true} responses: default: description: default schema: {} definitions: {} parameters: {} responses: {} securityDefinitions: {} security: [] tags: []
.NET CoreでPublishSingleFile=trueの時に設定ファイルを含めない方法
少し前に記事にした通り、こんな感じのコマンドラインで単一ファイルとしてビルドした場合、コンテンツを含めすべてのファイルがexeファイルの中に含められ、実行時に「%LocalAppData%\Temp.net[exe名]\ハッシュのディレクトリ」みたいなディレクトリに展開して実行されます。
dotnet publish -r win-x86 -c Release -p:PublishSingleFile=true -p:TieredCompilationQuickJit=true -p:PublishReadyToRun=true
設定ファイルの値を後で変えるときに困るのでどうにかならないのかなぁと調べていたら、ここに書いてありました。→ https://github.com/dotnet/designs/blob/master/accepted/single-file/design.md#build-system-interface
コンテンツのビルド時動作と同じように、ファイルに対してCopyToPublishDirectoryとExcludeFromSingleFileを定義してあげればよいようです。
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>netcoreapp3.1</TargetFramework> <UseWPF>true</UseWPF> <AssemblyName>Sample</AssemblyName> </PropertyGroup> 略 <ItemGroup> <None Update="settings*.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> <ExcludeFromSingleFile>true</ExcludeFromSingleFile> </None> </ItemGroup> </Project>
dotnet-ef コマンドをDockerなどでインストールする場合は–version指定をお忘れなく
データベースのマイグレーションを実行するコンテナを別に一つ作っているんですが、EntityFramework Core 3.1にアップグレードしたコンテナをビルドして実行したらこんなメッセージが表示されました。
It was not possible to find any compatible framework version
The framework ‘Microsoft.NETCore.App’, version ‘3.1.1’ was not found.
– The following frameworks were found:
3.1.0 at [/usr/share/dotnet/shared/Microsoft.NETCore.App]You can resolve the problem by installing the specified framework and/or SDK.
The specified framework can be found at:
– https://aka.ms/dotnet-core-applaunch?framework=Microsoft.NETCore.App&framework_version=3.1.1&arch=x64&rid=alpine.3.10-x64
3.1.1 を探しに行っている??
Dockerfile では、mcr.microsoft.com/dotnet/core/sdk:3.1-alpine3.10 をベースイメージに指定しているので、3.1.1 を使われると困ってしまう。
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-alpine3.10 AS build 略 RUN dotnet tool install --global dotnet-ef ENV PATH="$PATH:/root/.dotnet/tools"
ベースイメージで指定した.NET Coreと同じバージョンを指定しましょう
RUN dotnet tool install --global dotnet-ef --version 3.1.0 ENV PATH="$PATH:/root/.dotnet/tools"
EntityFramework Core は .NET Core 3に依存しないけれど、周辺のツール類は.NET Core 3に依存するんですね。
StackExchange.RedisのConnectionMultiplexer.Connectで作ったコネクションは使いまわそう
Redisを使っている部分(非.NET Core)で高負荷時に下記のようなメッセージが出るというので調査した時のメモです。
“No connection is available to service this operation: SET test; IOCP: (Busy=0,Free=1000,Min=2,Max=1000), WORKER: (Busy=24,Free=32743,Min=2,Max=32767), Local-CPU: n/a
ググってみると、StackExchange.Redisのバージョンを下げると問題が解消したとか、タイムアウトを長くしたら解消したとかいうものが出てくるのですが、他のライブラリとのバージョンの競合でバージョンは下げられないし、タイムアウトは対症療法でしかないということで、コードを確認していきます。
そもそもRedisからデータを取得するだけなのにやけに遅いんですよね。
調べてみると、こんな共通のコードをいたるところから呼び出していました。
public class RedisClient { public async Task GetAsync(string key) { using (var redis = await ConnectionMultiplexer.ConnectAsync(this.ConnectionString)) { var db = _redis.GetDatabase(); var result = await db.StringGetAsync(key); return (result.IsNull) ? default(T) : JsonConvert.DeserializeObject(result); } } }
Redisからデータを取得する際に毎回コネクションを作っているので、接続を作るのが遅いし、おそらくクライアント側の資源を使いつくしているせいで冒頭のメッセージが出ているっぽい。このあたりの方針は、StackExchange.RedisのREADMEに記載があって、HttpClientなんかと同じように、StackExchange.RedisのConnectionMultiplexer.Connectメソッドで作られたコネクションは、複数個所から共有して呼び出されることを想定しているようです。
単純にやるなら、Connectionをstaticとして保持しちゃう、ちゃんとやるならFactoryを導入してConnectionの生存期間を制御できるようにする。ってところでしょうか。
public class RedisClient { private static ConnectionMultiplexer _redis; private static ConnectionMultiplexer GetClient() { return _redis ?? (_redis = ConnectionMultiplexer.Connect(configuration.ConnectionString)); } public async Task GetAsync(string key) { var db = GetClient().GetDatabase(); var result = await db.StringGetAsync(key); return (result.IsNull) ? default(T) : JsonConvert.DeserializeObject(result); } }
.NET Core からは、IDistributedCache インターフェイスを使うとこの辺りは面倒見てくれるようになりますよね。
https://docs.microsoft.com/ja-jp/aspnet/core/performance/caching/distributed?view=aspnetcore-3.1