20200628のC#に関する記事は9件です。

ステップ関数

private void button_Click(object sender, EventArgs e)
{
    var x = np_arange(-5.0, 5.0, 0.1);
    var y = step_function(x);

    var plt = new Chart();
    plt_plot(plt, x, y);
    plt_xlim(plt, -6.0, 6.0);
    plt_ylim(plt, -0.1, 1.1);
    plt_show(plt);
}

private U[] np_arange<T, U>(T start, T end, U step) where T : struct where U : struct
{
    var start_ = (dynamic)start;
    var end_ = (dynamic)end;
    var step_ = (dynamic)step;

    return Enumerable.Range((int)(start_ / step_), (int)((end_ - start_) / step_)).Select(x => (U)(x * step_)).ToArray();
}

private int[] step_function<T>(T[] x) where T : struct
{
    return x.Select(n => ((dynamic)n > 0) ? 1 : 0).ToArray();
}

private void plt_plot<T, U>(Chart plt, T[] x, U[] y) where T : struct where U : struct
{
    plt.Size = new System.Drawing.Size(500, 350);

    plt.ChartAreas.Add("step");
    plt.Legends.Add("step");
    plt.Series.Add("step");

    plt.ChartAreas["step"].AxisX.MajorGrid.Enabled = false;
    plt.ChartAreas["step"].AxisX.LabelStyle.Format = "0.0";
    plt.ChartAreas["step"].AxisX.Minimum = (dynamic)x.First();
    plt.ChartAreas["step"].AxisX.Maximum = (dynamic)x.Last();
    plt.ChartAreas["step"].AxisY.MajorGrid.Enabled = false;
    plt.ChartAreas["step"].AxisY.LabelStyle.Format = "0.0";
    plt.ChartAreas["step"].AxisY.Minimum = (dynamic)y.First();
    plt.ChartAreas["step"].AxisY.Maximum = (dynamic)y.Last();

    plt.Series["step"].ChartType = SeriesChartType.Line;

    foreach (var n in x.Zip(y, Tuple.Create))
    {
        plt.Series["step"].Points.AddXY(n.Item1, n.Item2);
    }
}

private void plt_xlim(Chart plt, double minimum, double maximum)
{
    plt.ChartAreas["step"].AxisX.IntervalOffset = 0.0;
    plt.ChartAreas["step"].AxisX.Minimum = minimum;
    plt.ChartAreas["step"].AxisX.Maximum = maximum;
    plt.ChartAreas["step"].AxisX.Interval = 2;
}

private void plt_ylim(Chart plt, double minimum, double maximum)
{
    plt.ChartAreas["step"].AxisY.IntervalOffset = 0.1;
    plt.ChartAreas["step"].AxisY.Minimum = minimum;
    plt.ChartAreas["step"].AxisY.Maximum = maximum;
    plt.ChartAreas["step"].AxisY.Interval = 0.2;
}

private void plt_show(Chart plt)
{
    Bitmap bitmap = new Bitmap(plt.Width, plt.Height);
    plt.DrawToBitmap(bitmap, new Rectangle(System.Drawing.Point.Empty, plt.Size));
    Cv2.ImShow("plot", BitmapConverter.ToMat(bitmap));
}

000.png

参考文献:
斎藤康毅(2016)『ゼロから作るDeep Learning』オライリー・ジャパン

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

[Microsoft] 3. 機能コンポーネントを作成する - Angularチュートリアル Tour of Heroes を Blazor で再実装する

機能コンポーネントを作成する

作成したHeroesコンポーネントを、一覧部分と詳細部分に分けます。


HeroDetailコンポーネントを作成する

dotnet cliで作成します。

dotnet new razorcomponent --output BlazorTourOfHeroes/Shared --name HeroDetail

テンプレート部分を作成する

HeroDetailコンポーネントのテンプレート部分を作成します。

Shared/HeroDetail.razor
@if (Hero != null)
{
    <h2>@Hero.Name.ToUpper() Details</h2>
    <div><span>Id: </span>@Hero.Id</div>
    <div>
    <label>Name:
        <input @bind="Hero.Name" placeholder="name"/>
    </label>
    </div>
}

[Parameter]アノテーションをつける

外部からHeroオブジェクトを受け取るために、[Parameter]アノテーションをつけたHeroプロパティを作成します。

Shared/HeroDetail.razor
    [Parameter]
    public Hero Hero { get; set; }

HeroDetailコンポーネントを表示する

Heroesコンポーネントにある詳細表示部分をHeroDetailコンポーネントに置き換えます。

Heroコンポーネントを変更する

詳細表示部分をHeroDetailコンポーネントに置き換えます。

Shared/Heroes.razor
<HeroDetail Hero="selectedHero"></HeroDetail>

テンプレート全体はこのようになります。

Shared/Heroes.razor
<h2>My Heroes</h2>
<ul class="heroes">
    @foreach (var hero in heroes)
    {
        <li @onclick="@(_ => OnSelect(hero))"
            class="@(selectedHero == hero ? "selected" : "")"
            ><span class="badge">@hero.Id</span> @hero.Name
        </li>    
    }
</ul>

<HeroDetail Hero="selectedHero"></HeroDetail>

なにが変わった?

一覧表示をHeroesコンポーネント、詳細表示をHeroDetailコンポーネントとして分けたことで、メンテナンス性が良くなりました。

こんなんできました

Shared/Heroes.razor
@using BlazorTourOfHeroes.Model

<h2>My Heroes</h2>
<ul class="heroes">
    @foreach (var hero in heroes)
    {
        <li @onclick="@(_ => OnSelect(hero))"
            class="@(selectedHero == hero ? "selected" : "")"
            ><span class="badge">@hero.Id</span> @hero.Name
        </li>    
    }
</ul>

<HeroDetail Hero="selectedHero"></HeroDetail>

@code {
    private List<Hero> heroes = MockHeroes.Create();
    private Hero selectedHero = null;

    private void OnSelect(Hero hero)
    {
        selectedHero = hero;
    }
}
Shared/HeroDetail.razor
@using BlazorTourOfHeroes.Model

@if (Hero != null)
{
    <h2>@Hero.Name.ToUpper() Details</h2>
    <div><span>Id: </span>@Hero.Id</div>
    <div>
    <label>Name:
        <input @bind="Hero.Name" placeholder="name"/>
    </label>
    </div>
}

@code {
    [Parameter]
    public Hero Hero { get; set; }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Azure Functions】Qiita APIで取得したデータをSQL Databaseに保存する

はじめに

この記事では、Qiita APIから自分が投稿した記事のView数を取得し、DBに保存するということをやっていきます。実現方法としては、Azure FunctionsのTimmer Trigger関数を使って、定期的に情報を取得し、Azure SQL Databaseに保存します。また、DBへの接続文字列などの情報はAzure KeyVaultに保存し、プログラムから参照するような構成とします。

開発環境

ローカルマシン(Mac:MacOS Catalina v10.15.4)で開発したものをAzureへデプロイする形で開発を行います。VS Codeの拡張機能であるAzure Functions for Visual Studio Codeを使用します。

スクリーンショット 2020-06-27 17.06.39.png

Azure SQL Databaseの作成

はじめにデータを保存するためのSQL Databaseを作成しておきます。詳細な手順は省略しますがMicrosoft公式ドキュメントを参考に作成してください。できるだけお金がかからないように最小スペックで作成します。

  • SQL Databaseのスペック(一部抜粋)
項目
価格レベル Basic
ストレージ容量 2GB

Azure Functionsにデプロイするプログラムの作成

Microsoft公式ドキュメント(クイック スタート:Visual Studio Code を使用して Azure で関数を作成する)の通りにローカル環境にプロジェクトを作成します。テンプレート選択のところはTimer Triggerを選択してください。今回はC#を使用して開発していきます。また、この記事では順を追ってプログラムを作成していきますが最終的なプログラムはGitHubで公開しています。

Qiita APIで記事のView数を取得する

テンプレートを生成できたところで、まずはQiita APIで記事のView数を取得する処理を書いていきます。テンプレートのファイルに指定したURIにAPIリクエストを送る関数GetJsonを定義します。指定するURIはQiitaの公式ドキュメントを参考に決定します。今回は自分が投稿した記事の一覧を取得するAPIを使用します。

  • 自分が投稿した記事の一覧を取得するAPI

https://qiita.com/api/v2/users/[Qiitaのユーザー名]/items

しかし、このAPIをただ使用するだけでは情報は取得できません。一般に公開されていない情報(ユーザーに関する情報や記事のview数など)はアクセストークを付与したリクエストを送る必要があります。なのでQiita APIで情報を取得するために必要なアクセストークンを格納するクラスも別ファイルとして作成しておきます。アクセストークンを発行していない場合はユーザー設定から発行しておきます。


Qiita APIで記事の情報を取得するプログラム
get_qiita_views.cs
using System;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace kanazawa.Function
{
    public static class get_qiita_views
    {
        [FunctionName("get_qiita_views")]
        public static async void Run([TimerTrigger("0 0 * * * *")]TimerInfo myTimer, ILogger log)
        {
            TimeZoneInfo jstTimeZone = TZConvert.GetTimeZoneInfo("Tokyo Standard Time");
            DateTime utcTime = DateTime.UtcNow;
            DateTime jstTime = TimeZoneInfo.ConvertTimeFromUtc(utcTime, jstTimeZone);
            log.LogInformation($"C# Timer trigger function executed at: {jstTime}");

            // Qiita APIのURL
            string url = "https://qiita.com/api/v2/users/" + Parameter.getQiitaUserName() + "/items";
            // 投稿記事情報取得
            string json = await GetJson(url);
        }

        private static async Task<string> GetJson(string url)
        {
            var httpClient = new System.Net.Http.HttpClient();
            // OAuth 2.0 Authorization Headerの設定
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Parameter.getQiitaAccessToken());

            var request = new HttpRequestMessage(HttpMethod.Get, url);
            HttpResponseMessage response = await httpClient.SendAsync(request);
            string result = await response.Content.ReadAsStringAsync();

            return result;
        }
    }
}
Parameter.cs
using System;

namespace kanazawa.Function
{
    public class Parameter
    {
        public static string getQiitaAccessToken(){
            return "******";
        }

        public static string getQiitaUserName(){
            return "******";
        }
    }
}

取得結果はJson形式なので、これをデシリアライズ(C#のオブジェクトに変換)する必要があります。デシリアライズするためにはデータを格納するモデルクラスが必要となりますが、手動で作成するのはかなり面倒です。そのため以下のサイトで自動でモデルクラスを作成してもらいます。

quicktype

curlコマンド等で別途Jsonを取得し、上記サイトでモデルクラスを作成しましょう。ここで1点注意点があります。上記サイトで生成されたモデルクラスには一部問題があり、私の場合は余計なフィールドがenum型で宣言されていました。この後実際にデシリアライズする際にエラーが出るので、その時でも良いですが
自分で確認して修正しましょう。


Jsonをデシリアライズする際に使用するモデルクラス
QiitaInformationModel.cs
// <auto-generated />
//
// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do:
//
//    using kanazawa.Function;
//
//    var qiitaInformation = QiitaInformation.FromJson(jsonString);
using System;
using System.Collections.Generic;

using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace kanazawa.Function
{
    public partial class QiitaInformationModel
    {
        [JsonProperty("rendered_body")]
        public string RenderedBody { get; set; }

        [JsonProperty("body")]
        public string Body { get; set; }

        [JsonProperty("coediting")]
        public bool Coediting { get; set; }

        [JsonProperty("comments_count")]
        public long CommentsCount { get; set; }

        [JsonProperty("created_at")]
        public DateTimeOffset CreatedAt { get; set; }

        [JsonProperty("group")]
        public object Group { get; set; }

        [JsonProperty("id")]
        public string Id { get; set; }

        [JsonProperty("likes_count")]
        public long LikesCount { get; set; }

        [JsonProperty("private")]
        public bool Private { get; set; }

        [JsonProperty("reactions_count")]
        public long ReactionsCount { get; set; }

        [JsonProperty("tags")]
        public Tag[] Tags { get; set; }

        [JsonProperty("title")]
        public string Title { get; set; }

        [JsonProperty("updated_at")]
        public DateTimeOffset UpdatedAt { get; set; }

        [JsonProperty("url")]
        public Uri Url { get; set; }

        [JsonProperty("user")]
        public User User { get; set; }

        [JsonProperty("page_views_count")]
        public int PageViewsCount { get; set; }
    }

    public partial class Tag
    {
        [JsonProperty("name")]
        public string Name { get; set; }

        [JsonProperty("versions")]
        public object[] Versions { get; set; }
    }

    public partial class User
    {
        [JsonProperty("description")]
        public string Description { get; set; }

        [JsonProperty("facebook_id")]
        public string FacebookId { get; set; }

        [JsonProperty("followees_count")]
        public long FolloweesCount { get; set; }

        [JsonProperty("followers_count")]
        public long FollowersCount { get; set; }

        [JsonProperty("github_login_name")]
        public string GithubLoginName { get; set; }

        [JsonProperty("id")]
        public string Id { get; set; }

        [JsonProperty("items_count")]
        public long ItemsCount { get; set; }

        [JsonProperty("linkedin_id")]
        public string LinkedinId { get; set; }

        [JsonProperty("location")]
        public string Location { get; set; }

        [JsonProperty("name")]
        public string Name { get; set; }

        [JsonProperty("organization")]
        public string Organization { get; set; }

        [JsonProperty("permanent_id")]
        public long PermanentId { get; set; }

        [JsonProperty("profile_image_url")]
        public Uri ProfileImageUrl { get; set; }

        [JsonProperty("team_only")]
        public bool TeamOnly { get; set; }

        [JsonProperty("twitter_screen_name")]
        public object TwitterScreenName { get; set; }

        [JsonProperty("website_url")]
        public string WebsiteUrl { get; set; }
    }

    public partial class QiitaInformation
    {
        public static QiitaInformation[] FromJson(string json) => JsonConvert.DeserializeObject<QiitaInformation[]>(json, kanazawa.Function.Converter.Settings);
    }

    public static class Serialize
    {
        public static string ToJson(this QiitaInformation[] self) => JsonConvert.SerializeObject(self, kanazawa.Function.Converter.Settings);
    }

    internal static class Converter
    {
        public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
        {
            MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
            DateParseHandling = DateParseHandling.None,
            Converters =
            {
                new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
            },
        };
    }
}

モデルクラスが完成したらデシリアライズの処理を追記していきます。また、記事のview数は記事の一覧取得のAPIからは取得できないので、記事ごとの詳細を取得するAPIを発行する処理も追記します。

  • 自分が投稿した記事ごとの詳細を取得するAPI

https://qiita.com/api/v2/items/[記事のID]


デシリアライズとView数取得処理を追記
get_qiita_views.cs
using System;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace kanazawa.Function
{
    public static class get_qiita_views
    {
        [FunctionName("get_qiita_views")]
        public static async void Run([TimerTrigger("0 0 * * * *")]TimerInfo myTimer, ILogger log)
        {
            log.LogInformation($"C# Timer trigger function executed at: {jstTime}");
       // Qiita APIのURL
            string url = "https://qiita.com/api/v2/users/" + Parameter.getQiitaUserName() + "/items";
            // 投稿記事情報取得
            string json = await GetJson(url);

            // デシリアライズ時の設定
            var settings = new JsonSerializerSettings
            {
                NullValueHandling = NullValueHandling.Ignore,
                MissingMemberHandling = MissingMemberHandling.Ignore
            };

            // デシリアライズ
            List<QiitaInformationModel> models = JsonConvert.DeserializeObject<List<QiitaInformationModel>>(json, settings);

            // 各投稿記事のView数を取得
            string getViewsCountUrl;
            foreach (var model in models)
            {
                getViewsCountUrl = "https://qiita.com/api/v2/items/" + model.Id;
                model.PageViewsCount = JsonConvert.DeserializeObject<QiitaInformationModel>(await GetJson(getViewsCountUrl)).PageViewsCount;
                log.LogInformation($"title: {model.Title}");
                log.LogInformation($"views: {model.PageViewsCount}");
            }
        }

        private static async Task<string> GetJson(string url)
        {
            var httpClient = new System.Net.Http.HttpClient();
            // OAuth 2.0 Authorization Headerの設定
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Parameter.getQiitaAccessToken());

            var request = new HttpRequestMessage(HttpMethod.Get, url);
            HttpResponseMessage response = await httpClient.SendAsync(request);
            string result = await response.Content.ReadAsStringAsync();

            return result;
        }
    }
}

取得したデータをAzure SQL Databaseに保存する

最初に作成したAzure SQL Databaseにデータを保存します。今回は予め以下のテーブルをDBに作成しておきました。

  • qiita_items

記事の情報を格納するマスタテーブル

カラム名
id varchar(50)
title varchar(100)
created_at datetime
  • page_views_count

記事の時間ごとのview数を格納するトランザクションテーブル

カラム名
id varchar(50)
counted_at varchar(50)
page_views_count int(4)

アクセストークンを格納したクラスにDBへの接続文字列を格納します。また、メインのクラスにDBへの接続・保存処理も記述していきます。


DBへの接続・保存処理を追記
get_qiita_views.cs
using System;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace kanazawa.Function
{
    public static class get_qiita_views
    {
        [FunctionName("get_qiita_views")]
        public static async void Run([TimerTrigger("0 0 * * * *")]TimerInfo myTimer, ILogger log)
        {
            TimeZoneInfo jstTimeZone = TZConvert.GetTimeZoneInfo("Tokyo Standard Time");
            DateTime utcTime = DateTime.UtcNow;
            DateTime jstTime = TimeZoneInfo.ConvertTimeFromUtc(utcTime, jstTimeZone);
            log.LogInformation($"C# Timer trigger function executed at: {jstTime}");

       // Qiita APIのURL
            string url = "https://qiita.com/api/v2/users/" + Parameter.getQiitaUserName() + "/items";
            // 投稿記事情報取得
            string json = await GetJson(url);

            // デシリアライズ時の設定
            var settings = new JsonSerializerSettings
            {
                NullValueHandling = NullValueHandling.Ignore,
                MissingMemberHandling = MissingMemberHandling.Ignore
            };

            // デシリアライズ
            List<QiitaInformationModel> models = JsonConvert.DeserializeObject<List<QiitaInformationModel>>(json, settings);

            // 各投稿記事のView数を取得
            string getViewsCountUrl;
            foreach (var model in models)
            {
                getViewsCountUrl = "https://qiita.com/api/v2/items/" + model.Id;
                model.PageViewsCount = JsonConvert.DeserializeObject<QiitaInformationModel>(await GetJson(getViewsCountUrl)).PageViewsCount;
                log.LogInformation($"title: {model.Title}");
                log.LogInformation($"views: {model.PageViewsCount}");
            }

            // DB接続文字列の取得
            var connectionString = Parameter.getConnectionString();

            // データ保存
            using (var connection = new SqlConnection(connectionString))
            {
                // データベースの接続開始
                connection.Open();

                try
                {
                    // マスタテーブルの更新チェック
                    Database.checkMasterData(models, log, connection);

                    // データを保存
                    Database.saveData(models, jstTime, log, connection);
                }
                catch (Exception exception)
                {
                    log.LogInformation(exception.Message);
                    throw;
                }
                finally
                {
                    // データベースの接続終了
                    connection.Close();
                }
            }
        }

        private static async Task<string> GetJson(string url)
        {
            var httpClient = new System.Net.Http.HttpClient();
            // OAuth 2.0 Authorization Headerの設定
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Parameter.getQiitaAccessToken());

            var request = new HttpRequestMessage(HttpMethod.Get, url);
            HttpResponseMessage response = await httpClient.SendAsync(request);
            string result = await response.Content.ReadAsStringAsync();

            return result;
        }
    }
}
Database.cs
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Data;

namespace kanazawa.Function
{
    public class Database
    {
        // 新たに記事が投稿された場合はマスタテーブルを更新
        public static void checkMasterData(List<QiitaInformationModel> models, ILogger log, SqlConnection connection)
        {
            using (var transaction = connection.BeginTransaction())
            {
                try
                {
                    using (var selectCommand = new SqlCommand() { Connection = connection, Transaction = transaction })
                    {
                        // SQLの準備
                        selectCommand.CommandText = @"SELECT id FROM qiita_items";

                        // SQLの実行
                        var table = new DataTable();
                        var adapter = new SqlDataAdapter(selectCommand);
                        adapter.Fill(table);

                        // 存在フラグ
                        bool flg = false;

                        foreach (var model in models)
                        {
                            flg = false;
                            for (int i = 0; i < table.Rows.Count; i++)
                            {
                                if (table.Rows[i]["id"].ToString().Equals(model.Id))
                                {
                                    flg = true;
                                    break;
                                }
                            }

                            if (flg == false)
                            {
                                using (var insertCommand = new SqlCommand() { Connection = connection, Transaction = transaction })
                                {
                                    // SQLの準備
                                    insertCommand.CommandText = @"INSERT INTO qiita_items VALUES (@ID, @TITLE, @CREATED_AT)";
                                    insertCommand.Parameters.Add(new SqlParameter("@ID", model.Id));
                                    insertCommand.Parameters.Add(new SqlParameter("@TITLE", model.Title));
                                    insertCommand.Parameters.Add(new SqlParameter("@CREATED_AT", model.CreatedAt));

                                    // SQLの実行
                                    insertCommand.ExecuteNonQuery();

                                    log.LogInformation($"succeeded to insert master data: {model.Title}");
                                }
                            }
                        }
                    }

                    // コミット
                    transaction.Commit();
                    log.LogInformation("Committed");
                }
                catch
                {
                    // ロールバック
                    transaction.Rollback();
                    log.LogInformation("Rollbacked");
                    throw;
                }
            }
        }

        // 各記事のview数を保存
        public static void saveData(List<QiitaInformationModel> models, DateTime jstTime, ILogger log, SqlConnection connection)
        {
            using (var transaction = connection.BeginTransaction())
            {
                try
                {
                    foreach (var model in models)
                    {
                        using (var command = new SqlCommand() { Connection = connection, Transaction = transaction })
                        {
                            // SQLの準備
                            command.CommandText = @"INSERT INTO page_views_count VALUES (@ID, @COUNTED_AT, @PAGE_VIEWS_COUNT)";
                            command.Parameters.Add(new SqlParameter("@ID", model.Id));
                            command.Parameters.Add(new SqlParameter("@COUNTED_AT", jstTime.ToString("yyyy/MM/dd HH")));
                            command.Parameters.Add(new SqlParameter("@PAGE_VIEWS_COUNT", model.PageViewsCount));

                            // SQLの実行
                            command.ExecuteNonQuery();

                            log.LogInformation($"succeeded to insert data: {model.Title}");
                        }
                    }

                    // コミット
                    transaction.Commit();
                    log.LogInformation("Committed");
                }
                catch
                {
                    // ロールバック
                    transaction.Rollback();
                    log.LogInformation("Rollbacked");
                    throw;
                }
            }
        }
    }
}
Parameter.cs
using System;

namespace kanazawa.Function
{
    public class Parameter
    {
        public static string getQiitaAccessToken(){
            return "******";
        }

        public static string getQiitaUserName(){
            return "******";
        }

        public static string getConnectionString(){
            return "******";
        }
    }
}

ここで一度ローカルでテスト実行してみましょう。SQL Databaseの方で接続元IPアドレスを制限している場合は、ローカルPCからアクセスできるように設定した上でテスト実行します。うまくいったら一度Azureへデプロイしましょう。デプロイ時にAzure Functionsのリソースを作成できるので合わせて作成します。

  • テスト実行
    スクリーンショット 2020-06-27 18.31.40.png

  • Azureへのデプロイ
    スクリーンショット 2020-06-27 18.25.48.png

Azure KeyVaultの利用

ここまでの実装でQiitaからデータを取得して、DBに保存することができます。しかし、DBへの接続情報などをソースコードの中に記述してしまっているため、セキュリティー的によろしくありません。ここではAzure KeyVaultにシークレットとして保存し、Azure Functionsから参照できるようにソースコードの改善とAzureの設定を入れていきます。

Azure KeyVaultの作成

Microsoft公式ドキュメント(チュートリアル:Linux VM と Python アプリを使用してシークレットを Azure Key Vault に格納する)を参考にAzure KeyVaultの作成とシークレットの格納を行います。今回は以下の3つのシークレットを格納します。

  • Qiitaのアクセストークン
  • Qiitaユーザー名
  • SQL Databaseへの接続文字列

Azure KeyVaultの作成とシークレットの格納が完了したら、作成したAzure Functionsからシークレットを参照できるように権限を付与します。まずはAzure FunctionsのマネージドIDを有効化し、権限を付与する対象を作成します。作成後、Azure KeyVaultのアクセスポリシー設定画面からAzure FunctionsのマネージドIDに対してシークレットの取得権限を付与します。詳しいやり方はMicrosoft公式ドキュメント(App Service と Azure Functions の Key Vault 参照を使用する)を参照してください。

Azure Functionsの修正

参照先の設定が終わったので、Azure Functions側にシークレットを参照するように設定とソースコードの修正を入れていきます。Azure Functionsのアプリケーション設定を入れるとAzure Functionsの実行環境の環境変数にその値が反映されるのでソースコードから参照できるようになります。Microsoft公式ドキュメント(App Service と Azure Functions の Key Vault 参照を使用する)を参考にAzure Functionsに以下のアプリケーション設定を追加します。

  • Qiitaのアクセストークン
  • Qiitaユーザー名
  • SQL Databaseへの接続文字列

スクリーンショット 2020-06-28 17.23.06.png

上記ドキュメントにも記載されていますが、値にはAzure KeyVaultへの参照構文を入力します。参照構文は以下の形式です。

  • @Microsoft.KeyVault(SecretUri=[参照したいシークレットのシークレット識別子])

シークレット識別子はAzure KeyVaultの該当シークレットの設定変更画面から取得できます。

スクリーンショット 2020-06-28 17.23.30.png

ここまで準備ができたら後はソースコードを修正するだけです。各種秘匿情報を格納していたクラスを環境変数を参照するように修正します。


環境変数を参照するように修正
Parameter.cs
using System;

namespace kanazawa.Function
{
    public class Parameter
    {
        public static string getQiitaAccessToken(){
            return Environment.GetEnvironmentVariable("Qiita-Access-Token");
        }

        public static string getQiitaUserName(){
            return Environment.GetEnvironmentVariable("Qiita-User-Name");
        }

        public static string getConnectionString(){
            return Environment.GetEnvironmentVariable("ConnectionString");
        }
    }
}

これでソースコード内に秘匿すべき情報を記述せずにQiitaからのデータ取得とDBへの保存ができるようになりました。実際にデプロイしてみて試してみましょう!今回の例では一時間に一回プログラムが実行されるようにスケジューーリングしているので、DBには一時間ごとの各記事のview数が保存されるはずです。実行のスケジューリングを変更したい場合はソースコードの以下の部分を変更して再デプロイしてください。

get_qiita_views.cs
...
    public static class get_qiita_views
    {
        [FunctionName("get_qiita_views")]
        public static async void Run([TimerTrigger("0 0 * * * *")]TimerInfo myTimer, ILogger log)
        {
...

TimerTrigger("0 0 * * * *")の引数で定義されているスケジューリング設定は左から秒、分、時間、日、月、曜日を表しています。*と記述することで毎秒、毎分といった意味となります。他にも表現方法はあるので興味のある方はMicrosoft公式ドキュメント(Azure Functions のタイマー トリガー)を参照してください。

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

[Microsoft] 2. リストを表示する - Angularチュートリアル Tour of Heroes を Blazor で再実装する

選択肢リストを表示する

前回からの続きです。
ヒーローの一覧を表示できるようにします。
一覧からヒーローを選択すると、詳細が表示されるようにします。


ヒーローたちのモックを作成する

ヒーローのリストを返すクラスを作成しました。

Model/MockHeroes.cs
using System.Collections.Generic;

namespace BlazorTourOfHeroes.Model
{
    public class MockHeroes
    {
        public static List<Hero> Create() => new List<Hero> {
            new Hero { Id= 11, Name= "Dr Nice", },
            new Hero { Id= 12, Name= "Narco" },
            new Hero { Id= 13, Name= "Bombasto" },
            new Hero { Id= 14, Name= "Celeritas" },
            new Hero { Id= 15, Name= "Magneta" },
            new Hero { Id= 16, Name= "RubberMan" },
            new Hero { Id= 17, Name= "Dynama" },
            new Hero { Id= 18, Name= "Dr IQ" },
            new Hero { Id= 19, Name= "Magma" },
            new Hero { Id= 20, Name= "Tornado" },
        };
    }
}

ヒーローたちを表示する

Heroesコンポーネントにheroes変数を追加します。

Shared/Heroes.razor
    private List<Hero> heroes = MockHeroes.Create();

ヒーローたちを一覧する

foreachでヒーローたちをイテレートします。

Shared/Heroes.razor
<h2>My Heroes</h2>
<ul class="heroes">
    @foreach (var hero in heroes)
    {
        <li><span class="badge">@hero.Id</span> @hero.Name </li>    
    }
</ul>

ヒーローたちにスタイルをあてる

全体共通のスタイルシートを編集します。

Anguarのチュートリアルからコピペしました。

こんなんできました

image.png

詳細表示を作成する

一覧をクリックしたら、詳細を表示するようにします。

クリックイベントをバインドする

<li> タグにクリックイベントを追加します。

引数を渡したいので、ラムダにします。

Shared/Heroes.razor
<li @onclick="@(_ => OnSelect(hero))">

クリックイベントハンドラを作成する

選択されたヒーローを格納する変数とイベントハンドラを作成します。

Shared/Heroes.razor
    private Hero selectedHero;

    private void OnSelect(Hero hero)
    {
        selectedHero = hero;
    }

詳細表示部分を追加する

選択されたヒーローを表示するようにします。

Shared/Heroes.razor
<h2>@selectedHero.Name.ToUpper() Details</h2>
<div><span>Id: </span>@selectedHero.Id</div>
<div>
  <label>Name:
    <input @bind="selectedHero.Name" placeholder="name"/>
  </label>
</div>

ここで実行すると、ブラウザのコンソールにぬるぽが出力されました。

crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
      Unhandled exception rendering component: Object reference not set to an instance of an object.
System.NullReferenceException: Object reference not set to an instance of an object.
  at BlazorTourOfHeroes.Shared.Heroes.BuildRenderTree (Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) <0x2c456d0 + 0x00246> in <filename unknown>:0 
  at Microsoft.AspNetCore.Components.ComponentBase.<.ctor>b__6_0 (Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) <0x2b30640 + 0x0001a> in <filename unknown>:0 
  at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch (Microsoft.AspNetCore.Components.Rendering.RenderBatchBuilder batchBuilder, Microsoft.AspNetCore.Components.RenderFragment renderFragment) <0x2b2ff78 + 0x00054> in <filename unknown>:0 
  at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch (Microsoft.AspNetCore.Components.Rendering.RenderQueueEntry renderQueueEntry) <0x2b2da88 + 0x0004c> in <filename unknown>:0 
  at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue () <0x2b2d1c8 + 0x00092> in <filename unknown>:0 blazor.webassembly.js:formatted:2093

ぬるぽを修正

selectedHeroがnullのときは、詳細部分を出力しないようにします。

Shared/Heroes.razor
@if (selectedHero != null)
{
    <h2>@selectedHero.Name.ToUpper() Details</h2>
    <div><span>Id: </span>@selectedHero.Id</div>
    <div>
    <label>Name:
        <input @bind="selectedHero.Name" placeholder="name"/>
    </label>
    </div>
}

選択中のヒーローにスタイルをあてる

class属性の中でRazor構文を使います。

Shared/Heroes.razor
        <li @onclick="@(_ => OnSelect(hero))"
            class="@(selectedHero == hero ? "selected" : "")"
            ><span class="badge">@hero.Id</span> @hero.Name
        </li>    

こんなんできました

image.png

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

[Microsoft] 1. ヒーローエディタ - Angularチュートリアル Tour of Heroes を Blazor で再実装する

ヒーローエディタ

雛形が出来上がりましたので、ヒーロー情報を表示するコンポーネントを作成し、アプリケーション内に配置してみます。


Heroesコンポーネントを作成する

dotnet cliを使ってrazorコンポーネントを作成します。

dotnet new razorcomponent --output BlazorTourOfHeroes/Shared --name Heroes

既存の BlazorTourOfHeroes/Shared ディレクトリの中に Heroes.razor ファイルが出来上がります。

Shared/Heroes.razor
<h3>Heroes</h3>

@code {

}

@Qiita さんrazorシンタックスハイライト待ってます)

hero変数を追加する

コンポーネントにhero変数を追加します。

    private string hero = "Windstorm";

heroを表示する

既存のHTMLタグを消去して、hero変数に置き換えます。

@hero

Heroesコンポーネントを表示する

Heroesコンポーネントをトップページに表示するため、index.razorを変更します。

Pages/Index.razor
<Heroes></Heroes>

こんなんできました

image.png

Heroクラスを作成する

名前以外の属性を格納するためHeroクラスを作成します。

Model/Hero.cs
namespace BlazorTourOfHeroes.Model
{
    public class Hero
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

Heroesコンポーネントに戻り、作成したHeroクラスを使うようにリファクタリングします。

Shared/Heroes.razor
@using BlazorTourOfHeroes.Model

@hero

@code {
    private Hero hero = new Hero
    {
        Id = 1,
        Name = "Windstorm",
    };
}

heroオブジェクトを表示する

テンプレートを変更してheroオブジェクトを表示するようにします。

Shared/Heroes.razor
<h2>@hero.Name</h2>
<div><span>id: </span>@hero.Id</div>
<div><span>name: </span>@hero.Name</div>

hero名を大文字にする

大文字にするのは、String.ToUpper()を呼ぶだけです。

Shared/Heroes.razor
<h2>@hero.Name.ToUpper()</h2>

こんなんできました。

image.png

heroを編集できるようにする

<input>テキストボックスを用意してheroを編集できるようにします。

バインディング

Heroesコンポーネントをリファクタリングして次のようにします。

Shared/Heroes.razor
<div><label>name: <input @bind="hero.Name" placeholder="name" /></label></div>

こんなんできました

image.png

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

[Microsoft] 新規プロジェクトの作成 - Angularチュートリアル Tour of Heroes を Blazor で再実装する

Angularのチュートリアル: Tour of Heroes をBlazorを使って再実装してみます。

新規プロジェクトを作成する

dotnet CLIを使用して初期アプリケーションを作成します。

  1. 開発環境をセットアップします。
  2. ソリューションとプロジェクトを作成します。
  3. サーバを起動してアプリケーションを動かしてみます。

開発環境をセットアップする

こちらの手順に従って開発環境をセットアップします。

Macの場合は、brewを使えば一発です。

brew cask install dotnet-sdk

ソリューションとプロジェクトを作成する

テスト等も含めたいので、プロジェクトを作成する前にソリューションを作成します。

dotnet new sln -o BlazorTourOfHeroes

ディレクトリとソリューションファイルが出来上がるので移動します。

cd BlazorTourOfHeroes

続いてBlazorプロジェクトとテストプロジェクトを作成します。

dotnet new blazorwasm -o BlazorTourOfHeroes
dotnet new xunit -o BlazorTourOfHeroes.Tests

ソリューションに作成したプロジェクトを追加します。

dotnet sln add BlazorTourOfHeroes
dotnet sln add BlazorTourOfHeroes.Tests

gitignoreファイルを作成します。

dotnet new gitignore

あとはいつものようにgitを初期化してコミットしておきます。

サーバを起動する

サーバを起動してみます。

dotnet run -p BlazorTourOfHeroes

ブラウザを起動して https://localhost:5001http://localhost:5000 へアクセスします。

こんなんできました

こんな感じの画面が見られます。

image.png

画面を構成するコンポーネント

以下のようなrazorファイルが生成されています。
razorファイルひとつがコンポーネントになります。
Angularのように、スタイルシートを個別に持つことはできません。

├── App.razor
├── Pages
│   ├── Counter.razor
│   ├── FetchData.razor
│   └── Index.razor
└── Shared
    ├── Heroes.razor
    ├── MainLayout.razor
    ├── NavMenu.razor
    └── SurveyPrompt.razor

App.razorがAngularでいうところのルーティングを担っています。

変更してみる

トップページのタイトルを変更する

Index.razorファイルを編集します。

Pages/Index.razor
<h1>Tour of Heroes</h1>

スタイルを変更する

スタイルシートはアプリケーション全体で一つです。

wwwroot/css/app.css
h1 {
    color: #369;
    font-family: Arial, Helvetica, sans-serif;
    font-size: 250%;
}

h2, h3 {
    color: #444;
    font-family: Arial, Helvetica, sans-serif;
    font-weight: lighter;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WinFormsアプリを.NET Coreと.NET Frameworkで並行開発可能にする

日記に近いメモです。

前置き

メンテナンスしている、とあるWinFormsアプリは.NET Frameworkで動作しているのですが、開発者としては.NET 5に備え.NET Core 3.1に移行したいところです。
しかし、手元のWinFormsアプリはVisual Studio 2017 Expressで開発しているので、そう簡単に.NET Core(の新しいcsproj形式)に移行できません。
WinFormsデザイナーが使用できないと、UIに手を入れるのが非常に面倒になります。

そこで「移行」するのではなく、Visual Studio 2017(.NET Framework)で開発可能なまま.NET Coreでもビルドできる状態を目指していきます。
(使用していないソースコードを削除する以外は、ソースコードは変えません。)
Visual Studio 2019が使えればこんな面倒なことはしません。

対象のソースコード

下記のようなディレクトリ構成だとします。細かいファイルは省略してます。
なお、このアプリはx86でしか動作しませんが、開発環境はx64です。

SolutionFolder/
├───MyApps.sln
├───MyFormsApp/
│   ├───MyForms.csproj
│   ├───Form1.cs
│   ├───...
│   └───Program.cs
└───MyLib/
    └───MyLib.csproj

準備

基本的にはWindows フォーム デスクトップ アプリを .NET Core に移植する方法を参考にします。

.NET Portability Analyzer

.NET Portability Analyzer で事前に検証すべきですが、最近はWindows 互換機能パックにより互換性が高めなので、明らかに.NET Coreで使用できない機能(WCFとか)を使用していないのであれば、自己責任でやらなくてもいいかもしれません。

.NET Core SDK

アプリはx86でしか動作しませんが、開発環境はx64なので.NET Core 3.1 SDKをインストールします。

dotnetコマンドが動作すること確認しておきます。

PS > dotnet --list-sdks
3.1.100 [C:\Program Files\dotnet\sdk]
PS > dotnet --list-runtimes
Microsoft.AspNetCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

csprojに含まれないソースコードを削除する

.NET Coreではワイルドカードでソースコードを指定するため、使用していないソースコード(ゴミ)が残っている場合は削除しておきます。

新しいcsprojを作成する

docs.microsoft.comの例に倣い、MyFormsAppCoreというディレクトリを作成して、その中に.NET Core用のプロジェクトを作ります。

MyFormsAppの中に作ると、dotnetコマンドで毎回プロジェクトファイルを指定しないと行けないので面倒になります。

SolutionFolder/
├───MyApps.sln
├───MyFormsApp/
│   ├───MyForms.csproj
│   ├───Form1.cs
│   ├───...
│   └───Program.cs
├───MyLib/
│   └───MyLib.csproj
└───MyFormsAppCore/      <--- New folder for core project
PS > cd SolutionFolder/MyFormsAppCore
PS SolutionFolder\MyFormsAppCore> New-Item -Type File MyFormsAppCore.csproj

dotnet new winformsコマンドで作成してもいいんですが、
どうせ*.csファイルを消して.csprojファイルを作成する必要があるので直接作ります。

csprojの編集

下記を貼り付けます。

MyFormsAppCore.csproj
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
    <AssemblyName>MyCoreApp</AssemblyName>
    <RootNamespace>WindowsFormsApp1</RootNamespace>
    <Deterministic>false</Deterministic>
    <LangVersion>7.3</LangVersion>
    <PlatformTarget>x86</PlatformTarget>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="..\MyFormsApp\**\*.cs" Exclude="..\MyFormsApp\obj\**\*.cs" />
    <EmbeddedResource Include="..\MyFormsApp\**\*.resx" />
  </ItemGroup>

</Project>
  • AssemblyName / RootNamespace は実際のプロジェクトに合わせて(古いcsprojファイルを参照して)変えます。
  • LangVersionは Visual Studio 2017で使用可能な7.3までにします。
  • AssemblyVersion.csで、[assembly: AssemblyVersion("1.0.*")]のようなワイルドカードを使用していたので、エラー CS8357が発生します。今回はソースコードは変えない前提なので、<Deterministic>false</Deterministic>を追加します。
  • Visual Studio 2017でビルドしていると、objディレクトリ内に自動生成された*.csファイルが作成されるので、Exclude属性で除外しています。

NuGet参照

使用するライブラリの精査が面倒なら、 Windows 互換機能パックへの参照を追加します。

PS SolutionFolder\MyFormsAppCore> dotnet add package Microsoft.Windows.Compatibility

手元のプロジェクトだとMEFを使用していたので、

PS SolutionFolder\MyFormsAppCore> dotnet add package System.Composition

だけで済みました。

プロジェクト参照

使用するライブラリへの参照を追加します。
元のMyFormsApp.csprojの中から、..\MyLib\MyLib.csprojを指定しているItemGroupタグをコピーして、MyFormsAppCore.csprojに貼り付けます。

...
  <ItemGroup>
    <ProjectReference Include="..\MyLib\MyLib.csproj">
    <Project>{8E29561D-E424-4AAB-A4FA-E966EF653A0F}</Project>
    <Name>MyLib</Name>
  </ItemGroup>
...

ライブラリ自体は.NET Frameworkのままですが、結果的に手元のプロジェクトは何もせずにビルドできました。
正しくは事前に.NET Coreプロジェクト化したcsprojを用意して、dotnet add referenceで追加する必要があるでしょう。

ビルド

上手く行っていれば、問題なくビルドできるはずです。

# Debugビルド
PS SolutionFolder\MyFormsAppCore> dotnet build -r win-x86
# Releaseビルド
PS SolutionFolder\MyFormsAppCore> dotnet build -c Reelase -r win-x86

実行

PS SolutionFolder\MyFormsAppCore> dotnet run -r win-x86

発行

発行する方法はいくつかありますが、batファイルを用意することにします。

dotnet publish -c Release -r win-x86 --output publish

実行後、SolutionFolder\MyFormsAppCore\publish\MyCoreApp.exeが作成されているので、それを実行します。

PublishTrimmedPublishSingleFileを使用したければ、dotnet publishコマンドのリファレンスに従って、オプションは調整しましょう。

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

Visual Studio 2017でビルドしているWinFormsアプリを.NET Coreでも並行開発可能にする

日記に近いメモです。

前置き

メンテナンスしている、とあるWinFormsアプリは.NET Frameworkで動作しているのですが、開発者としては.NET 5に備え.NET Core 3.1に移行したいところです。
しかし、手元のWinFormsアプリはVisual Studio 2017 Expressで開発しているので、そう簡単に.NET Core(の新しいcsproj形式)に移行できません。
WinFormsデザイナーが使用できないと、UIに手を入れるのが非常に面倒になります。

そこで「移行」するのではなく、Visual Studio 2017(.NET Framework)で開発可能なまま.NET Coreでもビルドできる状態を目指していきます。
(使用していないソースコードを削除する以外は、ソースコードは変えません。)
Visual Studio 2019が使えればこんな面倒なことはしません。

対象のソースコード

下記のようなディレクトリ構成だとします。細かいファイルは省略してます。
なお、このアプリはx86でしか動作しませんが、開発環境はx64です。

SolutionFolder/
├───MyApps.sln
├───MyFormsApp/
│   ├───MyForms.csproj
│   ├───Form1.cs
│   ├───...
│   └───Program.cs
└───MyLib/
    └───MyLib.csproj

準備

基本的にはWindows フォーム デスクトップ アプリを .NET Core に移植する方法を参考にします。

.NET Portability Analyzer

.NET Portability Analyzer で事前に検証すべきですが、最近はWindows 互換機能パックにより互換性が高めなので、明らかに.NET Coreで使用できない機能(WCFとか)を使用していないのであれば、自己責任でやらなくてもいいかもしれません。

.NET Core SDK

アプリはx86でしか動作しませんが、開発環境はx64なので.NET Core 3.1 SDKをインストールします。

dotnetコマンドが動作すること確認しておきます。

PS > dotnet --list-sdks
3.1.100 [C:\Program Files\dotnet\sdk]
PS > dotnet --list-runtimes
Microsoft.AspNetCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

csprojに含まれないソースコードを削除する

.NET Coreではワイルドカードでソースコードを指定するため、使用していないソースコード(ゴミ)が残っている場合は削除しておきます。

新しいcsprojを作成する

docs.microsoft.comの例に倣い、MyFormsAppCoreというディレクトリを作成して、その中に.NET Core用のプロジェクトを作ります。

MyFormsAppの中に作ると、dotnetコマンドで毎回プロジェクトファイルを指定しないと行けないので面倒になります。

SolutionFolder/
├───MyApps.sln
├───MyFormsApp/
│   ├───MyForms.csproj
│   ├───Form1.cs
│   ├───...
│   └───Program.cs
├───MyLib/
│   └───MyLib.csproj
└───MyFormsAppCore/      <--- New folder for core project
PS > cd SolutionFolder/MyFormsAppCore
PS SolutionFolder\MyFormsAppCore> New-Item -Type File MyFormsAppCore.csproj

dotnet new winformsコマンドで作成してもいいんですが、
どうせ*.csファイルを消して.csprojファイルを作成する必要があるので直接作ります。

csprojの編集

下記を貼り付けます。

MyFormsAppCore.csproj
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
    <AssemblyName>MyCoreApp</AssemblyName>
    <RootNamespace>WindowsFormsApp1</RootNamespace>
    <Deterministic>false</Deterministic>
    <LangVersion>7.3</LangVersion>
    <PlatformTarget>x86</PlatformTarget>
    <RuntimeIdentifier>win-x86</RuntimeIdentifier>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="..\MyFormsApp\**\*.cs" Exclude="..\MyFormsApp\obj\**\*.cs" />
    <EmbeddedResource Include="..\MyFormsApp\**\*.resx" />
  </ItemGroup>

</Project>
  • AssemblyName / RootNamespace は実際のプロジェクトに合わせて(古いcsprojファイルを参照して)変えます。
  • LangVersionは Visual Studio 2017で使用可能な7.3までにします。
  • AssemblyVersion.csで、[assembly: AssemblyVersion("1.0.*")]のようなワイルドカードを使用していたので、エラー CS8357が発生します。今回はソースコードは変えない前提なので、<Deterministic>false</Deterministic>を追加します。
  • Visual Studio 2017でビルドしていると、objディレクトリ内に自動生成された*.csファイルが作成されるので、Exclude属性で除外しています。
  • 前述の通り、x86でしか動作しないWinFormsアプリなのでRuntimeIdentifierPlatformTargetを指定しています。x64でも動作させるなら、指定は外しておくべきでしょう。

NuGet参照

使用するライブラリの精査が面倒なら、 Windows 互換機能パックへの参照を追加します。

PS SolutionFolder\MyFormsAppCore> dotnet add package Microsoft.Windows.Compatibility

手元のプロジェクトだとMEFを使用していたので、

PS SolutionFolder\MyFormsAppCore> dotnet add package System.Composition

だけで済みました。

プロジェクト参照

使用するライブラリへの参照を追加します。
元のMyFormsApp.csprojの中から、..\MyLib\MyLib.csprojを指定しているItemGroupタグをコピーして、MyFormsAppCore.csprojに貼り付けます。

...
  <ItemGroup>
    <ProjectReference Include="..\MyLib\MyLib.csproj">
    <Project>{8E29561D-E424-4AAB-A4FA-E966EF653A0F}</Project>
    <Name>MyLib</Name>
  </ItemGroup>
...

ライブラリ自体は.NET Frameworkのままですが、結果的に手元のプロジェクトは何もせずにビルドできました。
正しくは事前に.NET Coreプロジェクト化したcsprojを用意して、dotnet add referenceで追加する必要があるでしょう。

ビルド

上手く行っていれば、問題なくビルドできるはずです。

# Debugビルド
PS SolutionFolder\MyFormsAppCore> dotnet build
# Releaseビルド
PS SolutionFolder\MyFormsAppCore> dotnet build -c Reelase

実行

PS SolutionFolder\MyFormsAppCore> dotnet run

発行

発行する方法はいくつかありますが、batファイルを用意することにします。

dotnet publish -c Release --output publish

実行後、SolutionFolder\MyFormsAppCore\publish\MyCoreApp.exeが作成されているので、それを実行します。

PublishTrimmedPublishSingleFileを使用したければ、dotnet publishコマンドのリファレンスに従って、オプションは調整しましょう。

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

Visual Studio 2017でビルドしているWinFormsアプリを.NET Coreでも並行開発可能にする方法

日記に近いメモです。

前置き

メンテナンスしている、とあるWinFormsアプリは.NET Frameworkで動作しているのですが、開発者としては.NET 5に備え.NET Core 3.1に移行したいところです。
しかし、手元のWinFormsアプリはVisual Studio 2017 Expressで開発しているので、そう簡単に.NET Core(の新しいcsproj形式)に移行できません。
WinFormsデザイナーが使用できないと、UIに手を入れるのが非常に面倒になります。

そこで「移行」するのではなく、Visual Studio 2017(.NET Framework)で開発可能なまま.NET Coreでもビルドできる状態を目指していきます。
(使用していないソースコードを削除する以外は、ソースコードは変えません。)
Visual Studio 2019が使えればこんな面倒なことはしません。

対象のソースコード

下記のようなディレクトリ構成だとします。細かいファイルは省略してます。
なお、このアプリはx86でしか動作しませんが、開発環境はx64です。

SolutionFolder/
├───MyApps.sln
├───MyFormsApp/
│   ├───MyForms.csproj
│   ├───Form1.cs
│   ├───...
│   └───Program.cs
└───MyLib/
    └───MyLib.csproj

準備

基本的にはWindows フォーム デスクトップ アプリを .NET Core に移植する方法を参考にします。

.NET Portability Analyzer

.NET Portability Analyzer で事前に検証すべきですが、最近はWindows 互換機能パックにより互換性が高めなので、明らかに.NET Coreで使用できない機能(WCFとか)を使用していないのであれば、自己責任でやらなくてもいいかもしれません。

.NET Core SDK

アプリはx86でしか動作しませんが、開発環境に合わせて.NET Core 3.1 SDK x64をインストールします。

dotnetコマンドが動作すること確認しておきます。

PS > dotnet --list-sdks
3.1.100 [C:\Program Files\dotnet\sdk]
PS > dotnet --list-runtimes
Microsoft.AspNetCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

csprojに含まれないソースコードを削除する

.NET Coreではワイルドカードでソースコードを指定するため、使用していないソースコード(ゴミ)が残っている場合は削除しておきます。

新しいcsprojを作成する

docs.microsoft.comの例に倣い、MyFormsAppCoreというディレクトリを作成して、その中に.NET Core用のプロジェクトを作ります。

MyFormsAppの中に作ると、dotnetコマンドで毎回プロジェクトファイルを指定しないと行けないので面倒になります。

SolutionFolder/
├───MyApps.sln
├───MyFormsApp/
│   ├───MyForms.csproj
│   ├───Form1.cs
│   ├───...
│   └───Program.cs
├───MyLib/
│   └───MyLib.csproj
└───MyFormsAppCore/      <--- New folder for core project
PS > cd SolutionFolder/MyFormsAppCore
PS SolutionFolder\MyFormsAppCore> New-Item -Type File MyFormsAppCore.csproj

dotnet new winformsコマンドで作成してもいいんですが、
どうせ*.csファイルを消して.csprojファイルを作成する必要があるので直接作ります。

csprojの編集

下記を貼り付けます。

MyFormsAppCore.csproj
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
    <AssemblyName>MyCoreApp</AssemblyName>
    <RootNamespace>WindowsFormsApp1</RootNamespace>
    <Deterministic>false</Deterministic>
    <LangVersion>7.3</LangVersion>
    <PlatformTarget>x86</PlatformTarget>
    <RuntimeIdentifier>win-x86</RuntimeIdentifier>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="..\MyFormsApp\**\*.cs" Exclude="..\MyFormsApp\obj\**\*.cs" />
    <EmbeddedResource Include="..\MyFormsApp\**\*.resx" />
  </ItemGroup>

</Project>
  • AssemblyName / RootNamespace は実際のプロジェクトに合わせて(古いcsprojファイルを参照して)変えます。
  • LangVersionは Visual Studio 2017で使用可能な7.3までにします。
  • AssemblyVersion.csで、[assembly: AssemblyVersion("1.0.*")]のようなワイルドカードを使用していたので、エラー CS8357が発生します。今回はソースコードは変えない前提なので、<Deterministic>false</Deterministic>を追加します。
  • Visual Studio 2017でビルドしていると、objディレクトリ内に自動生成された*.csファイルが作成されるので、Exclude属性で除外しています。
  • 前述の通り、x86でしか動作しないのでPlatformTargetx86を指定しています。
  • x86でしか動作しないWinFormsアプリなので、コマンドラインでの指定を省略できるようにRuntimeIdentifierwin-x86を指定しています。x64でも動作させるなら、指定は外しておくべきでしょう。

追加の編集

デフォルトで、ある程度使える設定はされていますが、何も設定をしなくていいわけではありません。
下記の設定(タグ)を、旧csprojからコピペが必要でした。
パスも調整する必要があります。

  • ApplicationIcon
  • ApplicationManifest

NuGet参照

使用するライブラリの精査が面倒なら、 Windows 互換機能パックへの参照を追加します。

PS SolutionFolder\MyFormsAppCore> dotnet add package Microsoft.Windows.Compatibility

手元のプロジェクトだとMEFを使用していたので、

PS SolutionFolder\MyFormsAppCore> dotnet add package System.Composition

だけで済みました。

プロジェクト参照

使用するライブラリへの参照を追加します。
元のMyFormsApp.csprojの中から、..\MyLib\MyLib.csprojを指定しているItemGroupタグをコピーして、MyFormsAppCore.csprojに貼り付けます。

...
  <ItemGroup>
    <ProjectReference Include="..\MyLib\MyLib.csproj">
    <Project>{8E29561D-E424-4AAB-A4FA-E966EF653A0F}</Project>
    <Name>MyLib</Name>
  </ItemGroup>
...

ライブラリ自体は.NET Frameworkのままですが、結果的に手元のプロジェクトは何もせずにビルドできました。
正しくは事前に.NET Coreプロジェクト化したcsprojを用意して、dotnet add referenceで追加する必要があるでしょう。

ビルド

上手く行っていれば、問題なくビルドできるはずです。

# Debugビルド
PS SolutionFolder\MyFormsAppCore> dotnet build
# Releaseビルド
PS SolutionFolder\MyFormsAppCore> dotnet build -c Reelase

実行

PS SolutionFolder\MyFormsAppCore> dotnet run

発行

発行する方法はいくつかありますが、batファイルを用意することにします。

dotnet publish -c Release --output publish

実行後、SolutionFolder\MyFormsAppCore\publish\MyCoreApp.exeが作成されているので、それを実行します。

PublishTrimmedPublishSingleFileを使用したければ、dotnet publishコマンドのリファレンスに従って、オプションは調整しましょう。

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