20201116のC#に関する記事は5件です。

WebView2 C#とJavaScriptの連携

昨日の今日ではあるんですが、WebView2を使ってJavaScriptとC#を連携させる方法がわかったので、記事に残します。
(筆者の個人的な事情なんですが、Qiitaの記事作成が今日で三日連続です(笑)
今日は仕事終わりなので、サクッと記事を書いて終わりたい…(笑))

コーディング

とりあえず、サンプルのコードを下記に貼り付けます。
Form1.cs と JavaScriptを仕込んだ sample.htmlだけなので、察しの良い人ならサンプルのコードを見ただけで十分かもしれません。
(.htmlに関しては適当に書いたので、あまり深く突っ込まないでください(笑))

Form1.cs
using 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 なので、戻り値を取得する場合は.Resultawaitを使用することになるのですが、.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のアラート出力
image.png
・アラートを閉じると、メッセージボックス出力
image.png
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#のメッセージが出力される様になっています。
image.png

まとめ

今回はこんなところです。
WebView2に関しての記事は昨日書いたのですが、JavaScriptからC#を実行する処理に関しては直近一週間くらい、ずっと放置だったので、その問題が解消して良かったです。

今日は特に書くことないですね(別に無くても良いんですが(笑))
昨日書いた記事のリンクを一応貼り付けときます。
https://qiita.com/NagaJun/items/4925a63ce7b93b80639e
最後まで読んで頂き、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WPFプロジェクトでリソースを正しく参照する

はじめに

WPFアプリをタスクトレイに常駐させたくなり、便利そうなライブラリがあったので使うことにしました。
Hardcodet NotifyIcon for WPF

ただチュートリアルに沿って試してもicoファイルが参照できず、少し手こずったのでメモ

準備

  1. VisualStudioからWPF/.NetCoreのプロジェクトを作成
  2. Nugetマネージャから上記ライブラリをインストール

アイコン追加

コンソールマネージャからWPFプロジェクト上で右クリックし、[プロパティ]を選択
左のタブの[リソース]を選択し、[このプロジェクトには..]を選択してリソースファイルを作成
image.png

左上のリソースの種類から[アイコン]を選択し、[リソースの作成]をクリック
アイコンのファイル名が求められるので、今回は適当に"sample"と入力
するとWPFのプロジェクトに"Resources"というディレクトリが作成され、"sample.ico"というファイルが作成されます。

image.png

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をクリック
すると下にファイルのプロパティが表示される

このプロパティのビルドアクションリソースへ変更
image.png

またRecourcesにicoファイルを置いた場合、パスの指定が間違っていたので

IconSource="../Recources/sample.ico"

に修正。

こうすることで、正しくicoファイルを参照し、タスクトレイへ表示することができました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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やモバイルではなくコンソールにしています)。

image.png

 このようなアプリケーションの場合、ASP.NET Coreの分散トレース機構はConsoleAppがBFFへHTTPリクエストを行う際に自動的にTraceIdを発行し、それ以降のリクエストではTraceIdを引き回しつつ、どのような呼び出し階層になっているかがわかるようなSpanIdやParentIdを自動的にログに付与するようになります。

image.png

実現している仕組み

 どのように実現しているかといえば、単純にリクエストを出す際に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:65268765e26f6141
Backend1のログ
ConnectionId:0HM49TJEVVKGE
SpanId:d99be493ec58af4e
TraceId:cd18b4710d68394a9bdfa33be609d9ab
RequestId:0HM49TJEVVKGE:00000001
ParentId:ff6404a601698347

まとめ

 ASP.NET Croeの分散トレーシング機能を利用すると、複数のWebApi呼び出しからなる分散アプリケーションを串刺してログを収集することができます。これだけでもAzureのLog AnalyticsやAWSのCloudWatch Logs Insightといった各クラウド標準のログ解析基盤で複数のログを串刺しで集計することもできますし、W3C形式のIdに変更すればサードパーティー製の分散ログ基盤に適用することができます。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.100

Linux のディストリビューションは Debian なようです。

  • root@e68a47087ad8:/# more /etc/issue
Debian GNU/Linux 10 \n \l

C# 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: True

Target 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

詳しくは参考にさせていただいた下記記事を参照ください

C# 9.0 で条件式が革命を起こす

条件式に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だけで勝手にビルドされました。便利です。
読んでいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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]::TryParse1.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 Release

dotnetコマンドで普通に実行します。

nupkgを保存

    <GeneratePackageOnBuild Condition="'$(Configuration)' == 'Release'">true</GeneratePackageOnBuild>
    <PackageOutputPath>$(MSBuildThisFileDirectory)..\bin\Packages\$(Configuration)\</PackageOutputPath>x

csprojで
- 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で動かすことにしておきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む