アーカイブ

Archive for the ‘プログラミング’ Category

.NET6.0のAlpineイメージの3.13はまだプレビュー

2021年11月29日 コメントを残す

おや?とりあえず特別な何かが無いのであれば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]
カテゴリー:C#, Docker

.NET Framework → .NET Coreの移行可能難易度を評価してくれるAWSのPorting Assistant for .NET

AWSのブログでPorting Assistant for .NET といAWS製のツールが紹介されていました。

このツールは既存の.NET Frameworkのソリューションを読み取り、その中で利用されているNugetライブラリーの.NET Core対応状況や、ソースコードそのものがどれだけ移植しやすい状況かを確認するツールのようです。

ポートソリューションの機能を使うと、参照しているライブラリで移行可能なライブラリのバージョンを調整したり、プロジェクトファイルの構成を変更してくれるようです。

カテゴリー:ASP.NET, AWS, Visual Studio

ASP.NET Coreの標準イメージ(Debian buster)からMySQL 3.6以前のデータベースに接続する場合はTLSをオフにするか、TLS1.0を有効にする必要がある。

2020年6月29日 コメントを残す

タイトルの通りですが、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つの対処をする必要があります。

Amazon Aurora MySQL でのセキュリティ

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, C#, Docker, MySql

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といったタグを使うほうが望ましい気がします。

カテゴリー:ASP.NET, ASP.NET Core, Docker

AWSでASP.NET Coreのデータプロテクションキーの保存先を考える。

2020年3月23日 コメントを残す

ASP.NET Coreではフレームワーク内部で使う暗号化やハッシュ時のキーは、アプリケーション起動時に指定された方法で作成され保存されます。ローカル開発時に1台のWebサーバーで開発しているときには問題ないけれど、複台構成の実行環境で動かす場合は生成するキーがサーバーごとで異なってしまうので問題が発生します。このキーをどうやって取り扱うかは、下記のドキュメントに詳しく記載されています。

ASP.NET Core データ保護の構成

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/",
            });
    // ... 略 ...
}
カテゴリー:ASP.NET, ASP.NET Core, AWS, C#

PowerAutomateでBacklogAPIを使いやすくするためのBridgeAPIを作りました。

2020年3月16日 コメントを残す

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パラメーターを持つカスタムコネクタを定義する

2020年2月26日 1件のコメント

この頃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をインポートすることになるので、ある程度用途別に複数の操作を一つのコレクションにまとめておくとわかりやすいですね。

コメント 2020-02-26 111347

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で実行するにはこんな感じになりますね。
コメント 2020-02-26 123508

次に、課題のコメントを取得するAPIについて確認していきましょう。
このAPIはPathにパラメーターが入っているので、先ほどの検索結果をもとに、こんな感じでURLを組み立てます。

https://xxx.backlog.jp/api/v2/issues/10089694/comments
  ?apiKey=xxx

コメント 2020-02-26 123858

2つのAPIのリクエストを登録したら、Postmanコレクションをエクスポートします。
コメント 2020-02-26 124016

カスタムコネクタの定義

カスタムコネクタの新規作成は、Power Automateの左側のメニューにあるカスタムコネクタから始めます。
コメント 2020-02-26 124321

右上のカスタムコネクタの新規作成から、Postman おレクションをインポートしますを選択して
コメント 2020-02-26 124448

先ほどエクスポートしたPostmanコレクションの定義ファイルを選択します。
コメント 2020-02-26 124553

全般とセキュリティーは特に設定を変更する箇所がないので、そのまま次に進みます。
コメント 2020-02-26 124649

課題検索のリクエスト定義を確認すると、apiKeyやprojectId[]がパラメーターとして指定できるようになっているのを確認できます。
コメント 2020-02-26 124850

応答の部分は、Postman コレクションでは取り込んでくれないのでこのままだとFlowの中でJSONを解析するステップを追加することになってしまいます。PowerAutomateには、応答のスキーマ定義を実際のJSOnから生成する機能があるので、+既定の応答を追加するを選択し
コメント 2020-02-26 125133

Postmanでリクエストした結果のJSONを本文に貼り付け応答リクエストのスキーマを作成します。
コメント 2020-02-26 125337

コメント 2020-02-26 125449

コメントの取得APIはリクエストのパスの中に変数が入ってきているのでこれをどうにかします。
コメント 2020-02-26 125627

取り込み画面からパスを直に直すインターフェイスはなさそうなので、ナビゲーションメニューのSwaggerエディターを起動して、Swagger仕様書を直に修正します。
コメント 2020-02-26 125730

変更前はこうなっているので、パス部分を外から指定できるように変数化します。
コメント 2020-02-26 125945

パス部分が変数化されましたね
コメント 2020-02-26 130104

あとは課題の検索と同様に、応答スキーマを実際のJSONリクエストから生成すれば作業は終了です。
コメント 2020-02-26 130159

右上のコネクタの作成から作業を終了しましょう。
コメント 2020-02-26 130232

カスタムコネクタを見ると、今作成したカスタムコネクタが登録されているのが確認できます。
コメント 2020-02-26 130334

フローで利用する

カスタムタブに出てくるので、選択して利用します。
コメント 2020-02-26 130510

コメントの取得は、課題の取得で受け取ったレスポンスをもとにループして取得するのでこんな感じになりますね。
コメント 2020-02-26 130842

説明するとだいぶ長くなりましたが、やってみると意外とサクサクできるのでまずは手を動かしてみると良い気がします。

— 追記

Backlogの複数条件(array)への対応(途中)

Backlog APIではパラメーター名の末尾が[]になっている項目(statusId[]など)は複数回パラメータとして指定できます。Postmanコレクションから取り込んだ場合、単一項目になってしまうので配列項目として型を変更する必要があります。

これに対応するには、パラメーターの編集操作から
コメント 2020-02-27 110925

種類をarrayに変更する必要があります。
コメント 2020-02-27 111019

Swagger上記の操作を行うと、Swagger上では下記のようになります。
コメント 2020-02-27 111111

ただ、この配列定義をアクションで指定する方法が現在わかっていなくて(汗)、上記の定義の状態で下記のフロー定義は通るんだけれど(もちろん1つのステータスしか指定できないけれど)
コメント 2020-02-27 111322

配列として定義すると
コメント 2020-02-27 111343

こんなエラーが発生してしまいます。
コメント 2020-02-27 111502

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: []
カテゴリー:Power Automate

.NET CoreでPublishSingleFile=trueの時に設定ファイルを含めない方法

2020年1月16日 コメントを残す

少し前に記事にした通り、こんな感じのコマンドラインで単一ファイルとしてビルドした場合、コンテンツを含めすべてのファイルが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>

カテゴリー:ASP.NET Core, C#

dotnet-ef コマンドをDockerなどでインストールする場合は–version指定をお忘れなく

2020年1月16日 コメントを残す

データベースのマイグレーションを実行するコンテナを別に一つ作っているんですが、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に依存するんですね。

カテゴリー:ASP.NET, ASP.NET Core, C#, Docker

StackExchange.RedisのConnectionMultiplexer.Connectで作ったコネクションは使いまわそう

2020年1月14日 コメントを残す

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

カテゴリー:プログラミング