- 投稿日:2020-11-16T23:26:17+09:00
WebView2 C#とJavaScriptの連携
昨日の今日ではあるんですが、WebView2を使ってJavaScriptとC#を連携させる方法がわかったので、記事に残します。
(筆者の個人的な事情なんですが、Qiitaの記事作成が今日で三日連続です(笑)
今日は仕事終わりなので、サクッと記事を書いて終わりたい…(笑))コーディング
とりあえず、サンプルのコードを下記に貼り付けます。
Form1.cs と JavaScriptを仕込んだ sample.htmlだけなので、察しの良い人ならサンプルのコードを見ただけで十分かもしれません。
(.htmlに関しては適当に書いたので、あまり深く突っ込まないでください(笑))Form1.csusing Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.WinForms; using System; using System.Runtime.InteropServices; using System.Windows.Forms; namespace SampleWebView2Form { public partial class Form1 : Form { /// <summary>webviewのコントロール(わかりやすい様に、デザイナーを使わずにコード側で実装します。)</summary> private WebView2 WebView = new WebView2 { //個人の環境に合わせて下さい Source = new Uri("file:///C:/Users/name/Desktop/sample.html"), }; /// <summary>JavaScriptで呼ぶ関数を保持するオブジェクト</summary> private JsToCs CsClass = new JsToCs(); public Form1() { this.Controls.Add(WebView); InitializeComponent(); //WebView2のサイズをフォームのサイズに合わせる WebView.Size = this.Size; this.SizeChanged += Form1_SizeChanged; //WebView2のロード完了時のイベント WebView.NavigationCompleted += WebView_NavigationCompleted; } /// <summary>WebView2のロード完了時</summary> private void WebView_NavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e) { try { if (WebView.CoreWebView2 != null) { //JavaScriptからC#のメソッドが実行できる様に仕込む WebView.CoreWebView2.AddHostObjectToScript("class", CsClass); //JavaScriptの関数を実行 CsToJs(); } else MessageBox.Show("CoreWebView2==null"); } catch(Exception ex) { MessageBox.Show(ex.ToString()); } } /// <summary>Jsのメソッドを実行</summary> private async void CsToJs() { //WebView.ExecuteScriptAsync("func1()").ResultをするとWebView2がフリーズする string str1 = await WebView.ExecuteScriptAsync("func1(\"C#からの呼び出し\")"); MessageBox.Show("Jsからの戻り値>" + str1); } /// <summary>サイズ変更時のイベントでWebView2のサイズをフォームに合わせる</summary> private void Form1_SizeChanged(object sender, EventArgs e) { WebView.Size = this.Size; } } //↓属性設定が無いとエラーになります /// <summary>WebView2に読み込ませるためのJsで実行する関数を保持させたクラス</summary> [ClassInterface(ClassInterfaceType.AutoDual)] [ComVisible(true)] public class JsToCs { public void MessageShow(string strText) { MessageBox.Show("Jsからの呼び出し>" + strText); } } }sample.html<!DOCTYPE html> <html lang="ja"> <head> <meta content="text/html;charset=utf-8" http-equiv="Content-Type" /> <script language="javascript" type="text/javascript"> //C#から呼び出すための関数 function func1(str1) { alert("C# called>" + str1); return "success" } function ButtonClick() { //C#の関数の実行 chrome.webview.hostObjects.class.MessageShow("Js send text"); } </script> </head> <body> <h1>wasm_sample</h1> <input type="button" value='send Message' onclick="ButtonClick();"/> <script></script> </body> </html>解説
C#->JavaScript
C#からJavaScript内の関数を呼び出すには、ExecuteScriptAsync()
を使います。この関数は、先日書いた記事でExecuteScriptAsync("alert(\"message\")");
的な使い方をしたのですが、引数の中身がJavaScriptとして処理できるのであれば、JavaScriptのコード内の独自で作成した関数でも実行できます。
注意点として、ExecuteScriptAsync()
は Task なので、戻り値を取得する場合は.Result
かawait
を使用することになるのですが、.Result
を使用するとフリーズして処理が進まなくなるため、戻り値を取得するならawait
を使用する必要があります。
サンプルコードでは、下記の部分が該当の箇所です。Form1.cs/// <summary>Jsのメソッドを実行</summary> private async void CsToJs() { //WebView.ExecuteScriptAsync("func1()").ResultをするとWebView2がフリーズする string str1 = await WebView.ExecuteScriptAsync("func1(\"C#からの呼び出し\")"); MessageBox.Show("Jsからの戻り値>" + str1); }sample.html<script language="javascript" type="text/javascript"> //C#から呼び出すための関数 function func1(str1) { alert("C# called>" + str1); return "success" } </script>挙動としては下記画像の様になります。
・ロード直後にJavaScriptのアラート出力
・アラートを閉じると、メッセージボックス出力
JavaScript->C#
JavaScriptからC#の呼び出しには、AddHostObjectToScript()
を使ってC#内で作成した関数をJavaScriptに読み込ませます。
(WebMessageReceived
を使う方法もあるらしいので、気になる方は調べてみてください)
サンプルコードでは、下記の部分が該当の箇所です。Form1.cs/// <summary>JavaScriptで呼ぶ関数を保持するオブジェクト</summary> JsToCs CsClass = new JsToCs(); //JavaScriptからC#のメソッドが実行できる様に仕込む WebView.CoreWebView2.AddHostObjectToScript("class", CsClass); //~~~一部省略~~~ /// <summary>WebView2に読み込ませるためのJsで実行する関数を保持させたクラス</summary> [ClassInterface(ClassInterfaceType.AutoDual)] [ComVisible(true)] public class JsToCs { public void MessageShow(string strText) { MessageBox.Show("Jsからの呼び出し>" + strText); } }sample.html<script language="javascript" type="text/javascript"> function ButtonClick() { //C#の関数の実行 chrome.webview.hostObjects.class.MessageShow("Js send text"); } </script> <body> <input type="button" value='send Message' onclick="ButtonClick();"/> </body>挙動としては、ブラウザー内の「send Message」を押下してもらうと、C#のメッセージが出力される様になっています。
まとめ
今回はこんなところです。
WebView2に関しての記事は昨日書いたのですが、JavaScriptからC#を実行する処理に関しては直近一週間くらい、ずっと放置だったので、その問題が解消して良かったです。今日は特に書くことないですね(別に無くても良いんですが(笑))
昨日書いた記事のリンクを一応貼り付けときます。
https://qiita.com/NagaJun/items/4925a63ce7b93b80639e
最後まで読んで頂き、ありがとうございました。
- 投稿日:2020-11-16T19:56:23+09:00
WPFプロジェクトでリソースを正しく参照する
はじめに
WPFアプリをタスクトレイに常駐させたくなり、便利そうなライブラリがあったので使うことにしました。
Hardcodet NotifyIcon for WPFただチュートリアルに沿って試してもicoファイルが参照できず、少し手こずったのでメモ
準備
- VisualStudioからWPF/.NetCoreのプロジェクトを作成
- Nugetマネージャから上記ライブラリをインストール
アイコン追加
コンソールマネージャからWPFプロジェクト上で右クリックし、[プロパティ]を選択
左のタブの[リソース]を選択し、[このプロジェクトには..]を選択してリソースファイルを作成
左上のリソースの種類から[アイコン]を選択し、[リソースの作成]をクリック
アイコンのファイル名が求められるので、今回は適当に"sample"と入力
するとWPFのプロジェクトに"Resources"というディレクトリが作成され、"sample.ico"というファイルが作成されます。XAML
チュートリアルには以下のように記載があります
MainView.xaml<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:tb="http://www.hardcodet.net/taskbar" > <Grid> <tb:TaskbarIcon IconSource="/Icons/Error.ico" ToolTipText="hello world" /> </Grid> </Window>今回、icoファイルはRecourcesディレクトリに存在し、ファイル名はsample.icoなので
IconSourceの値をIconSource="/Recources/sample.ico"に変更すれば行けそうだ。
と思ったんですが、コンパイラに「見つかりません」と怒られてしまいました解決策
ソリューションエクスプローラからsample.icoをクリック
すると下にファイルのプロパティが表示されるまたRecourcesにicoファイルを置いた場合、パスの指定が間違っていたので
IconSource="../Recources/sample.ico"に修正。
こうすることで、正しくicoファイルを参照し、タスクトレイへ表示することができました。
- 投稿日:2020-11-16T14:21:36+09:00
SpanId、TraceId、ParentIdってなに?W3C Trace Context を利用した分散トレースの利用
はじめに
複数のサービスを組み合わせて提供するサービスの場合、利用者から発生したリクエストがどのサービスまで到達して、どのような結果になったのかのログを一貫したIDで確認できると追跡しやすくなります。
オレオレな仕組みとして実装するのであれば、利用者に一番近いところで生成した処理IDをHTTPヘッダーなどで引き回しつつログに出力すれば対応は可能ですが、これとほぼ同じような役割のHTTPヘッダーがW3CでTrace Contextとして定義されています。Trace Contextに対応したフレームワークであれば、前述のオレオレヘッダーを自前で引き回したりしなくとも分散トレースの仕組みを利用することができます。
この記事では、ASP.NET Coreのログとして出力される、SpanId、TraceId、ParentIdといった各種分散トレース用のIdと、W3Cで定義されているTrace Contextの関係を説明します。
ASP.NET Core 3.xにおける分散トレーシング
ASP.NET CoreでSerilogなどで構造化ログを出力した場合、下記のようにConnectionIdやRequestIdと共にSpanIdやTraceId、ParentIdといった項目が自動的に出力されていることに気がつくと思います。
{ "Timestamp": "2020-11-16T11:36:07.4064259+09:00", "Level": "Information", "MessageTemplate": "Executed endpoint '{EndpointName}'", "Properties": { "EndpointName": "TraceContextSample.Backend2.Controllers.ApiUsersController.GetContract (TraceContextSample.Backend2)", "EventId": { "Id": 1, "Name": "ExecutedEndpoint" }, "SourceContext": "Microsoft.AspNetCore.Routing.EndpointMiddleware", "RequestId": "0HM49R1FBA9QR:00000001", "RequestPath": "/api/ApiUsers/1", "SpanId": "|80590f9-4cbb08ce5d8bad25.4.e163ea97_7.90994b52_", "TraceId": "80590f9-4cbb08ce5d8bad25", "ParentId": "|80590f9-4cbb08ce5d8bad25.4.e163ea97_7.", "ConnectionId": "0HM49R1FBA9QR" } }これらの詳しい説明は、Improvements in .NET Core 3.0 for troubleshooting and monitoring distributed appsに記載されていますが、大まかには次のような意味合いになります。
# 属性名 意味 1 ConnectionId そのエンドポイントをリクエストしたセッションのIDを表します。 2 RequestId 今回そのエンドポイントに対してリクエストしたIDを表します。{ConnectionId}:{連番}の形式になります。 3 TraceId クライアントから並列、直列にエンドポイントを呼びだした際に、一連の呼び出しを識別するIDを表します。 4 ParentId そのエンドポイントを直接呼び出したクライアントのIDを表します。デフォルトでは |{呼び出し元のTraceId}{リクエスト連番}.の形式になります。 5 SpanId そのエンドポイントの処理IDを表します。デフォルトでは {ParentId}{自身の処理Id}の形式になります。 例えば、下記のように、コンソールアプリケーションからBFF経由で複数のWebApiをHTTP経由で呼びだすようなアプリケーションがあった場合を想定します(ここでは説明を単純にするため、フロントはWebやモバイルではなくコンソールにしています)。
このようなアプリケーションの場合、ASP.NET Coreの分散トレース機構はConsoleAppがBFFへHTTPリクエストを行う際に自動的にTraceIdを発行し、それ以降のリクエストではTraceIdを引き回しつつ、どのような呼び出し階層になっているかがわかるようなSpanIdやParentIdを自動的にログに付与するようになります。
実現している仕組み
どのように実現しているかといえば、単純にリクエストを出す際にHTTPヘッダー内にRequest-Idが存在すればそれを後続のリクエストに引き回し、存在しなければ生成してHTTPヘッダーに乗せるというようなことをしているようです。実際にConsoleAppがBFFのWebApiをリクエストした際のリクエスト本文はこのようになっています。
GET https://localhost:5021/api/ServiceUsers HTTP/1.1 Host: localhost:5021 Request-Id: |ee1e2648-42bba8b19667f176.4.実はこの動作を自動的に行ってくれるのはASP.NET Coreのアプリケーションだけで、WinFormsやWPF、コンソールアプリケーションのHttpClientでは自動的にRequest-Idを付与してくれません。これらのアプリケーション形態の場合は、下記のようにActivityクラスを利用して明示的にここからここまでがリクエストの範囲なんだよ、ということをHttpClientに教えてあげる必要があります。
using var activity = new Activity(nameof(RunAsync)).Start(); var response = await _tokenClient.RequestTokenAsync( new TokenSettings {AuthorityBaseUri = Constants.Authority.BaseUri}, Constants.ConsoleApp.ClientId, Constants.ConsoleApp.ClientSecret, new[] {Constants.Bff.ResourceName}); var data = await _bffClient.GetServiceUsersAsync(response.AccessToken); activity.Stop();W3C 形式のtraceparentヘッダー
Request-IdヘッダーはASP.NET Core 2.0のタイミングで追加されたヘッダーです。W3CのTrace Contextに準拠したtraceparentヘッダーに変更するには、プログラム起動時にActivity.DefaultIdFormat静的プロパティーにW3Cを設定します。
static async Task Main(string[] args) { Activity.DefaultIdFormat = ActivityIdFormat.W3C; await CreateHostBuilder(args) .Build() .RunAsync(); }HTTPリクエストは次のようになります。Request-Idからtraceparentに変更されていることがわかります。
GET https://localhost:5021/api/ServiceUsers HTTP/1.1 Host: localhost:5021 traceparent: 00-cd18b4710d68394a9bdfa33be609d9ab-65268765e26f6141-00出力するログは次のようになります。
BFFのログConnectionId:0HM49TJEPBT84 SpanId:c86545271cd34d4c TraceId:cd18b4710d68394a9bdfa33be609d9ab RequestId:0HM49TJEPBT84:00000001 ParentId:65268765e26f6141Backend1のログConnectionId:0HM49TJEVVKGE SpanId:d99be493ec58af4e TraceId:cd18b4710d68394a9bdfa33be609d9ab RequestId:0HM49TJEVVKGE:00000001 ParentId:ff6404a601698347まとめ
ASP.NET Croeの分散トレーシング機能を利用すると、複数のWebApi呼び出しからなる分散アプリケーションを串刺してログを収集することができます。これだけでもAzureのLog AnalyticsやAWSのCloudWatch Logs Insightといった各クラウド標準のログ解析基盤で複数のログを串刺しで集計することもできますし、W3C形式のIdに変更すればサードパーティー製の分散ログ基盤に適用することができます。
参考
- 投稿日:2020-11-16T12:28:41+09:00
Docker 上でお手軽にC#9(.NET 5)を試す
.NET 5 をお手軽に試したい
先日めでたく .NET 5がリリースされました。1.NET 5 では C# 9 がサポートされています。
早速試したいところですが手元のPCにインストールせず、少しだけ触りたいので Docker を使って使い捨ての .NET 5 開発環境を用意したいと思います。Docker のインストール不要なブラウザで使えるクラウド上の使い捨て Docker 実行環境の Play with Docker2 上で試します。
環境
実行した際の Play with Docker の Linux 環境は
Alpine Linux 3.12
、Docker のバージョンは19.03.11
です。
$ more /etc/issue
Welcome to Alpine Linux 3.12 Kernel \r on an \m (\l)
$ docker --version
Docker version 19.03.11, build 42e35e61f3.NET 5 の開発環境を用意する
.NET 5 SDK の Docker image を実行します。
-it
オプションで標準入出力をシェルにつなげてそのまま .NET 5 SDK のコンテナに乗り込みます。
※--rm
オプションでコンテナ終了時にコンテナが自動的に削除されます3。
手元の PC で試す場合は不要なコンテナが残りません。
$ docker run -it --rm mcr.microsoft.com/dotnet/sdk:5.0
Unable to find image 'mcr.microsoft.com/dotnet/sdk:5.0' locally 5.0: Pulling from dotnet/sdk bb79b6b2107f: Pull complete 97805e17b1a2: Pull complete 48d36279ea43: Pull complete 5d23a35fbf12: Pull complete 982bc1066a1e: Pull complete 6cc6e848c1f3: Pull complete df97eda6f03d: Pull complete 7520ee234b82: Pull complete Digest: sha256:ac49854ff6dcc1a2916ffc0981503f571698458187f925da0c2f2b6a0bec8dee Status: Downloaded newer image for mcr.microsoft.com/dotnet/sdk:5.0 root@e68a47087ad8:/#.NET 5 がインストールされていることが確認できます。
root@e68a47087ad8:/# dotnet --version
5.0.100Linux のディストリビューションは Debian なようです。
root@e68a47087ad8:/# more /etc/issue
Debian GNU/Linux 10 \n \lC# 9(.NET 5)の新機能を試す
コンソールアプリを作成して C# 9 の新機能を試していきたいと思います。
root@e68a47087ad8:/# dotnet new console -o MyConsoleApp && cd MyConsoleApp
Getting ready... The template "Console Application" was created successfully. Processing post-creation actions... Running 'dotnet restore' on MyConsoleApp/MyConsoleApp.csproj... Determining projects to restore... Restored /MyConsoleApp/MyConsoleApp.csproj (in 89 ms). Restore succeeded.プロジェクトが作成されました。
root@e68a47087ad8:/MyConsoleApp# ls -R
.: MyConsoleApp.csproj Program.cs obj ./obj: MyConsoleApp.csproj.nuget.dgspec.json MyConsoleApp.csproj.nuget.g.targets project.nuget.cache MyConsoleApp.csproj.nuget.g.props project.assets.jsonファイル編集したいところですが vim が入っていませんのでインストールしたいと思います。
まず、パッケージをアップデートします。
root@e68a47087ad8:/MyConsoleApp# apt update && apt upgrade
Get:1 http://security.debian.org/debian-security buster/updates InRelease [65.4 kB] Get:2 http://deb.debian.org/debian buster InRelease [121 kB] Get:3 http://deb.debian.org/debian buster-updates InRelease [51.9 kB] Get:4 http://security.debian.org/debian-security buster/updates/main amd64 Packages [248 kB] Get:5 http://deb.debian.org/debian buster/main amd64 Packages [7906 kB] Get:6 http://deb.debian.org/debian buster-updates/main amd64 Packages [7856 B] Fetched 8401 kB in 1s (5711 kB/s) Reading package lists... Done Building dependency tree Reading state information... Done 1 package can be upgraded. Run 'apt list --upgradable' to see it. Reading package lists... Done Building dependency tree Reading state information... Done Calculating upgrade... Done The following packages will be upgraded: tzdata 1 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. Need to get 264 kB of archives. After this operation, 3072 B of additional disk space will be used. Do you want to continue? [Y/n] Y Get:1 http://deb.debian.org/debian buster-updates/main amd64 tzdata all 2020d-0+deb10u1 [264 kB] Fetched 264 kB in 0s (24.0 MB/s) debconf: delaying package configuration, since apt-utils is not installed (Reading database ... 9877 files and directories currently installed.) Preparing to unpack .../tzdata_2020d-0+deb10u1_all.deb ... Unpacking tzdata (2020d-0+deb10u1) over (2020a-0+deb10u1) ... Setting up tzdata (2020d-0+deb10u1) ... debconf: unable to initialize frontend: Dialog debconf: (No usable dialog-like program is installed, so the dialog based frontend cannot be used. at /usr/share/perl5/Debconf/FrontEnd/Dialog.pm line 76.) debconf: falling back to frontend: Readline Current default time zone: 'Etc/UTC' Local time is now: Sat Nov 14 16:16:39 UTC 2020. Universal Time is now: Sat Nov 14 16:16:39 UTC 2020. Run 'dpkg-reconfigure tzdata' if you wish to change it.vim をインストールします。
root@e68a47087ad8:/MyConsoleApp# apt install vim
Reading package lists... Done Building dependency tree Reading state information... Done The following additional packages will be installed: libgpm2 vim-common vim-runtime xxd Suggested packages: gpm ctags vim-doc vim-scripts The following NEW packages will be installed: libgpm2 vim vim-common vim-runtime xxd 0 upgraded, 5 newly installed, 0 to remove and 0 not upgraded. Need to get 7425 kB of archives. After this operation, 33.8 MB of additional disk space will be used. Do you want to continue? [Y/n] Y Get:1 http://deb.debian.org/debian buster/main amd64 xxd amd64 2:8.1.0875-5 [140 kB] Get:2 http://deb.debian.org/debian buster/main amd64 vim-common all 2:8.1.0875-5 [195 kB] Get:3 http://deb.debian.org/debian buster/main amd64 libgpm2 amd64 1.20.7-5 [35.1 kB] Get:4 http://deb.debian.org/debian buster/main amd64 vim-runtime all 2:8.1.0875-5 [5775 kB] Get:5 http://deb.debian.org/debian buster/main amd64 vim amd64 2:8.1.0875-5 [1280 kB] Fetched 7425 kB in 0s (59.9 MB/s) debconf: delaying package configuration, since apt-utils is not installed Selecting previously unselected package xxd. (Reading database ... 9877 files and directories currently installed.) Preparing to unpack .../xxd_2%3a8.1.0875-5_amd64.deb ... Unpacking xxd (2:8.1.0875-5) ... Selecting previously unselected package vim-common. Preparing to unpack .../vim-common_2%3a8.1.0875-5_all.deb ... Unpacking vim-common (2:8.1.0875-5) ... Selecting previously unselected package libgpm2:amd64. Preparing to unpack .../libgpm2_1.20.7-5_amd64.deb ... Unpacking libgpm2:amd64 (1.20.7-5) ... Selecting previously unselected package vim-runtime. Preparing to unpack .../vim-runtime_2%3a8.1.0875-5_all.deb ... Adding 'diversion of /usr/share/vim/vim81/doc/help.txt to /usr/share/vim/vim81/doc/help.txt.vim-tiny by vim-runtime' Adding 'diversion of /usr/share/vim/vim81/doc/tags to /usr/share/vim/vim81/doc/tags.vim-tiny by vim-runtime' Unpacking vim-runtime (2:8.1.0875-5) ... Selecting previously unselected package vim. Preparing to unpack .../vim_2%3a8.1.0875-5_amd64.deb ... Unpacking vim (2:8.1.0875-5) ... Setting up libgpm2:amd64 (1.20.7-5) ... Setting up xxd (2:8.1.0875-5) ... Setting up vim-common (2:8.1.0875-5) ... Setting up vim-runtime (2:8.1.0875-5) ... Setting up vim (2:8.1.0875-5) ... update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/vim (vim) in auto mode update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/vimdiff (vimdiff) in auto mode update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/rvim (rvim) in auto mode update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/rview (rview) in auto mode update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/vi (vi) in auto mode update-alternatives: warning: skip creation of /usr/share/man/da/man1/vi.1.gz because associated file /usr/share/man/da/man1/vim.1.gz (of link group vi) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/de/man1/vi.1.gz because associated file /usr/share/man/de/man1/vim.1.gz (of link group vi) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/fr/man1/vi.1.gz because associated file /usr/share/man/fr/man1/vim.1.gz (of link group vi) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/it/man1/vi.1.gz because associated file /usr/share/man/it/man1/vim.1.gz (of link group vi) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/ja/man1/vi.1.gz because associated file /usr/share/man/ja/man1/vim.1.gz (of link group vi) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/pl/man1/vi.1.gz because associated file /usr/share/man/pl/man1/vim.1.gz (of link group vi) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/ru/man1/vi.1.gz because associated file /usr/share/man/ru/man1/vim.1.gz (of link group vi) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/man1/vi.1.gz because associated file /usr/share/man/man1/vim.1.gz (of link group vi) doesn't exist update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/view (view) in auto mode update-alternatives: warning: skip creation of /usr/share/man/da/man1/view.1.gz because associated file /usr/share/man/da/man1/vim.1.gz (of link group view) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/de/man1/view.1.gz because associated file /usr/share/man/de/man1/vim.1.gz (of link group view) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/fr/man1/view.1.gz because associated file /usr/share/man/fr/man1/vim.1.gz (of link group view) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/it/man1/view.1.gz because associated file /usr/share/man/it/man1/vim.1.gz (of link group view) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/ja/man1/view.1.gz because associated file /usr/share/man/ja/man1/vim.1.gz (of link group view) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/pl/man1/view.1.gz because associated file /usr/share/man/pl/man1/vim.1.gz (of link group view) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/ru/man1/view.1.gz because associated file /usr/share/man/ru/man1/vim.1.gz (of link group view) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/man1/view.1.gz because associated file /usr/share/man/man1/vim.1.gz (of link group view) doesn't exist update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/ex (ex) in auto mode update-alternatives: warning: skip creation of /usr/share/man/da/man1/ex.1.gz because associated file /usr/share/man/da/man1/vim.1.gz (of link group ex) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/de/man1/ex.1.gz because associated file /usr/share/man/de/man1/vim.1.gz (of link group ex) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/fr/man1/ex.1.gz because associated file /usr/share/man/fr/man1/vim.1.gz (of link group ex) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/it/man1/ex.1.gz because associated file /usr/share/man/it/man1/vim.1.gz (of link group ex) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/ja/man1/ex.1.gz because associated file /usr/share/man/ja/man1/vim.1.gz (of link group ex) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/pl/man1/ex.1.gz because associated file /usr/share/man/pl/man1/vim.1.gz (of link group ex) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/ru/man1/ex.1.gz because associated file /usr/share/man/ru/man1/vim.1.gz (of link group ex) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/man1/ex.1.gz because associated file /usr/share/man/man1/vim.1.gz (of link group ex) doesn't exist update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/editor (editor) in auto mode update-alternatives: warning: skip creation of /usr/share/man/da/man1/editor.1.gz because associated file /usr/share/man/da/man1/vim.1.gz (of link group editor) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/de/man1/editor.1.gz because associated file /usr/share/man/de/man1/vim.1.gz (of link group editor) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/fr/man1/editor.1.gz because associated file /usr/share/man/fr/man1/vim.1.gz (of link group editor) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/it/man1/editor.1.gz because associated file /usr/share/man/it/man1/vim.1.gz (of link group editor) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/ja/man1/editor.1.gz because associated file /usr/share/man/ja/man1/vim.1.gz (of link group editor) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/pl/man1/editor.1.gz because associated file /usr/share/man/pl/man1/vim.1.gz (of link group editor) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/ru/man1/editor.1.gz because associated file /usr/share/man/ru/man1/vim.1.gz (of link group editor) doesn't exist update-alternatives: warning: skip creation of /usr/share/man/man1/editor.1.gz because associated file /usr/share/man/man1/vim.1.gz (of link group editor) doesn't exist Processing triggers for libc-bin (2.28-10) ...準備ができましたので C# 9 のコードを試してみます。
詳細は参考にさせていただいた下記の記事をご覧ください。C# 小ネタ:.NET 5.0 で使える C# 9.0 で気に入ってる機能紹介
トップレベルステートメント4
エントリポイントならクラス無しで直接コードを書けます。簡単なバッチなどで便利そうです。
root@e68a47087ad8:/MyConsoleApp# vim Program.cs
System.Console.WriteLine("Hello from top.");ビルドします。(以降のサンプルでは省略します。)
root@e68a47087ad8:/MyConsoleApp# dotnet build
Microsoft (R) Build Engine version 16.8.0+126527ff1 for .NET Copyright (C) Microsoft Corporation. All rights reserved. Determining projects to restore... All projects are up-to-date for restore. MyConsoleApp -> /MyConsoleApp/bin/Debug/net5.0/MyConsoleApp.dll Build succeeded. 0 Warning(s) 0 Error(s) Time Elapsed 00:00:02.44
root@e68a47087ad8:/MyConsoleApp# dotnet run
Hello from top.レコード型5
イミュータブルなオブジェクトを手軽に作成することができます。
またプロパティの値が一致していれば違う参照のレコード型でも==
がtrue
になります。
またこの記事では試していませんがwith
式を使って一部のプロパティだけ書き換えた新しいレコードを作成することができます。値オブジェクトづくりがはかどりそうです。
root@e68a47087ad8:/MyConsoleApp# vim Program.cs
using static System.Console; record Money(decimal amount); class Program { static void Main() { var money1 = new Money(10); WriteLine($"money1: {Money}"); var money2 = new Money(10); WriteLine($"money2: {money2}"); WriteLine($"money1 == money2: {money1 == money2}"); } }
root@e68a47087ad8:/MyConsoleApp# dotnet run
money1: Money { amount = 10 } money2: Money { amount = 10 } money1 == money2: TrueTarget typed new6
型推論して明らかなときは
new
の型名を省略できます。
今まで右辺から推論するvar
はありましたがメンバー変数で使えませんでした。
また、引数や戻り値で型が明らかな場合でも型名を省略できるようです。using System.Collections.Generic; using System.Linq; class Program { static readonly Dictionary<int, List<string>> _cache = new(); static void Main() { _cache.Add(1, new() { "aaa" }); System.Console.WriteLine(_cache[1].First()); } }
root@e68a47087ad8:/MyConsoleApp# dotnet run
aaa条件式の新機能7
詳しくは参考にさせていただいた下記記事を参照ください
条件式に
or
,and
,not
が追加されています。
特徴として評価が1回なので副作用のある式でも使いやすいようです。
root@e68a47087ad8:/MyConsoleApp# vim Program.cs
using static System.Console; int Add2AndPrint(int i) { var res = i + 2; WriteLine(res); return res; } WriteLine(Add2AndPrint(2) is >= 3 and <= 5); object o = 10; WriteLine(o is int i && ((i % 3, i % 5) is ((0, int _) or (int _, 0)) and not (0, 0)));
root@e68a47087ad8:/MyConsoleApp# dotnet run
4 True Trueおわり
後から気づいたのですが、ソースコードを変更するたびに
dotnet build
しなくてもdotnet run
だけで勝手にビルドされました。便利です。
読んでいただきありがとうございました。
https://www.publickey1.jp/blog/20/net_51windowsmaclinuxweb.html ↩
https://qiita.com/okazuki/items/8043ea6f0095b3019cdf#%E3%83%88%E3%83%83%E3%83%97%E3%83%AC%E3%83%99%E3%83%AB%E3%82%B9%E3%83%86%E3%83%BC%E3%83%88%E3%83%A1%E3%83%B3%E3%83%88 ↩
https://qiita.com/okazuki/items/8043ea6f0095b3019cdf#%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E5%9E%8B ↩
https://qiita.com/okazuki/items/8043ea6f0095b3019cdf#target-typed-new-%E5%BC%8F ↩
https://qiita.com/okazuki/items/8043ea6f0095b3019cdf#%E5%A4%A7%E3%81%AA%E3%82%8A%E5%B0%8F%E3%81%AA%E3%82%8A%E3%81%AE%E6%AF%94%E8%BC%83%E3%82%82%E7%B0%A1%E5%8D%98%E3%81%AB ↩
- 投稿日:2020-11-16T00:47:46+09:00
【C#】csprojのバージョンを元にGitHubのリリース・nugetへの送信を行う
バイナリデータをUTF-16文字列に変換して保存するライブラリ base32768 のC#移植を作成しました。
https://github.com/naminodarie/Base32768
せっかくなので、GitHub Actionsを使っていろいろとやってみたので記事にしたいと思います。
workflows
GitHubのプロジェクトで
.github/workflows/*.yml
がGitHub Actionsの元になります。今回は
https://github.com/naminodarie/Base32768/blob/master/.github/workflows/build-release-publish.yml
を例に説明します。
環境変数
env: DOTNET_CLI_TELEMETRY_OPTOUT: true # 製品利用統計情報機能を無効化 DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true # パッケージキャッシュを展開しないCI用の設定 DOTNET_NOLOGO: true # ロゴを表示しない(これはなくても良い) NUGET_XMLDOC_MODE: skip # xmlドキュメントを抽出しない NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages # キャッシュをworkspace以下に展開。後述のキャッシュ用C#ではこのあたりの環境変数を設定しておくと良いかと思います。
jobs
バージョンを取得
- name: Get version id: get-version shell: pwsh run: | $doc = [XML](Get-Content 'Base32768/Base32768.csproj') $ver = ([string]$doc.Project.PropertyGroup.Version).Trim() echo "::set-output name=version::$ver" $vx = [version]::new() echo "::set-output name=valid::$([version]::TryParse($ver, [ref]$vx).ToString().ToLower())"PowerShell 7が使えるのでPowerShellでXMLをパースできます。
また、
[version]::TryParse
が1.0.0-beta.1
のようなプレリリースではfalseとなることを利用してプレリリースかどうかも取得しておきます。バージョン名のタグが存在するかチェック
- name: Check tag uses: mukunku/tag-exists-action@v1.0.0 id: check-tag with: tag: ${{ steps.get-version.outputs.version }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}mukunku/tag-exists-actionではタグが存在するかが
exists
に出力されます。出力をまとめる
outputs: exists: ${{ steps.check-tag.outputs.exists }} valid: ${{ steps.get-version.outputs.valid }} version: ${{ steps.get-version.outputs.version }}上記の出力をjobの出力としてまとめておきます。
更新時のみ実行する
new-version-only: runs-on: ubuntu-latest needs: [get-version] if: ${{ needs.get-version.outputs.exists == 'false' && needs.get-version.outputs.valid == 'true' }} steps: - run: echo "new version-> ${{ needs.get-version.outputs.version }}"バージョンのタグが存在せず、プレリリースではないときのみ実行するjobを設定しておきます。
ビルド・テスト
実行するかどうか
build: runs-on: ubuntu-latest needs: [new-version-only]needsに更新時のみ実行するためのjob
new-version-only
を設定することで、new-version-only
のifがtrueのときのみ後続の処理を実行するようにできます。キャッシュ
- uses: actions/cache@v2 with: path: ${{ github.workspace }}/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} restore-keys: | ${{ runner.os }}-nuget-nugetで取得する外部ライブラリをキャッシュする設定を入れておきます。
ビルド・テスト
- name: Install dependencies run: dotnet restore - name: Build run: dotnet build --no-restore -c Release - name: Test run: dotnet test --no-restore --verbosity normal -c Releasedotnetコマンドで普通に実行します。
nupkgを保存
<GeneratePackageOnBuild Condition="'$(Configuration)' == 'Release'">true</GeneratePackageOnBuild> <PackageOutputPath>$(MSBuildThisFileDirectory)..\bin\Packages\$(Configuration)\</PackageOutputPath>xcsprojで
- Releaseビルド時にnupkgを作成する
- nupkgの出力パス
を設定しておきます。- uses: actions/upload-artifact@v1 with: name: dist path: bin/Packages/Release設定した出力パスにあるファイルを
upload-artifact
で保存します。GitHubのリリース
- name: Download artifact uses: actions/download-artifact@v2 with: name: dist path: dist
download-artifact
でneedsのjobのupload-artifact
のものを取得できます。タグ・リリースを作成
- name: Push tag id: tag-version uses: mathieudutour/github-tag-action@v5 with: github_token: ${{ secrets.GITHUB_TOKEN }} custom_tag: ${{ needs.get-version.outputs.version }} tag_prefix: "" - name: Create release uses: ncipollo/release-action@v1 id: create-release with: token: ${{ secrets.GITHUB_TOKEN }} prerelease: true tag: ${{ steps.tag-version.outputs.new_tag }} name: Version ${{ needs.get-version.outputs.version }} body: ${{ steps.tag_version.outputs.changelog }} artifacts: "./dist/*.nupkg"これまでの処理からバージョン情報とnupkgをもとにリリースを作成します。
nugetへ送信
- name: Upload nuget run: dotnet nuget push **/*.nupkg --skip-duplicate --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_KEY }} - name: Setup GitHub nuget run: dotnet nuget add source https://nuget.pkg.github.com/${{ github.actor }}/index.json -n github -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} - name: Upload GitHub nuget run: dotnet nuget push **/*.nupkg --skip-duplicate --source "github"
dotnet
コマンドでnupkgを送信します。
${{ github.actor }}
で自身のユーザー名も取得できるのでGitHubのPackagesにも登録できます。runs-on: windows-latest
dotnet nuget add source
で--store-password-in-clear-text
を使わずに済むので、windows-latestで動かすことにしておきます。