54
71

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ASP.NET Coreを始める前に知っておきたい19のこと

Last updated at Posted at 2019-08-14

概要

ASP.NET Coreを使って、画像をアップロードし、表示するウェブアプリケーションを作りました。ASP部分はAzure App Service(Web Apps)でホストし、ファイルはAzure Storage Blobへアップロード、ファイルのメタ情報はAzure Websiteに無料で提供されているMySQL In Appに格納しました。

作るモノのイメージは結構はっきりしていたので、あまり時間もかからずできるだろうと思ったら、(やっぱり)意外といろんな所で 学び があり、せっかくなので残しておきたいと思いました。

もう1点、今回は全てMicrosoftの技術なのですが、全てmacOS上で開発を行いました。どんな罠があるのかと怯えながら開発を進めていたのですが、驚くことにmacOSを使っていることによる不利益は1回も起きませんでした。すごい。ま、Visual Studio for Macを使っているので、結局は全てMicrosoftの手のひらで踊らされているだけなのですけどね・・。

私自身、まだまだ勉強中で理解できていないところも多々あります。是非、コメントなど反応をいただけると嬉しいです。

ことはじめ

(1) 公式ドキュメントをよく読む

この辺りをよく読みます。チュートリアルも豊富なので、ソースコードを試しながら読んでもいいです。

どちらかといえば、流し読みでもいいので 全部読む ことをお勧めします。ソースコードを書くことより全部読むことの方が大切です。

実際に小さなアプリケーションを作り始めてみると、「これどうやって実現するんだろう?」ってことのほとんどは、ドキュメントに書かれています。(私だけかもしれませんが、如何にちゃんと読んでなかったかが分かります・・)

(2) 書籍をよく読む

リンクを貼るためにAmazonをみて気づいたのですが、この本、税込で5600円もするんですね・・。私には非常に有用で、後悔はしませんでした。基本的なMVC、Razor、DI、Entity Framework CoreなどのO/RM、ASP.NET Identity、Ajax連携など、一通り網羅されています。

まだまだ理解しきれていないところも多いのですが、この1冊で大体は分かった気になれます。サンプルコードも、程よく実用的なサイズで理解しやすいです。

(3) 先人に学ぶ

Qiitaの先人の知恵を学びます。ASP.NET Coreで検索して、今回の勉強に直接参考になったものを抜粋します。ASP.NET Coreというよりも、Visual Studio Codeに焦点を合わせていたり、gulpなどなどのツール類の話が中心のものは、あまり読んでいません。

(4) Visual Studio for Mac ではWeb Appsデプロイ後のリモートデバッグができない

私が知らないだけかもしれません。ホーム > App Service > 構成 > 全般設定でリモートの設定が変更できるのですが、Visual Studio for Macの項目は無いようです。また、Visual Studioも2015/2017/2019での機能となっています(さすがに2012でAzureの機能開発はできないと思いますが・・)。

回答は締め切られていないですが、同様に悩んでいる人もいるようです。

(5) Application Insights は必ず設定しておく

Visual Studio for Macでリモートデバッグができなかったので、各種本番環境での不具合はprintfデバッグに頼るしかありませんでした。しかしながら、例外をApplication Insightsが捕捉してくれたので、ステップ実行ほどの利便性は無いものの、デバッグに役立てることができました。

基本的には開発環境(ローカル)でデバッグしていましたので、本番環境と異なる部分だけデプロイ後のデバッグが必要でした。今回の開発環境と本番環境の差異は以下の通りでした。

開発環境 本番環境
データベース SQLite MySQL In App
ストレージ Azurite Azure Storage Blob

(6) デフォルトのデバッグ環境は Development となる

デバッグ環境では #if DEBUG なども使えますが、EnvironmentとしてはDevelopmentが設定されています。そのため、タグヘルパーを正しく設定していれば、以下のようなコードが有効になるわけです。

<environment include="Development">
    <div>&lt;environment include="Development"&gt;</div>
</environment>
<environment exclude="Development">
    <div>&lt;environment exclude="Development"&gt;</div>
</environment>
<environment include="Staging,Development,Staging_2">
    <div>
        &lt;environment include="Staging,Development,Staging_2"&gt;
    </div>
</environment>

この設定はプロジェクトのオプションから変更可能です。

Environmentの環境を切り替える

(7) 接続文字列はソースコードに書かない

例えば、Azure Storage Blobの接続文字列が漏れてしまった場合、他人にデータをアップロードされたり、自分のデータがダウンロードされてしまう危険性があります。ウェブで公開するデータがダウンロードされるだけなら問題ありませんが、非常に大きなデータをアップロードされてしまうとクラウド死を迎えてしまいます。しかしながら、ソースコードをGitHubなどで公開しておきたいとも思うはずです。

そこで、ホーム > App Service > 構成 > アプリケーション設定 > 接続文字列に全て保存し、ソースコードには記述しないよう気をつけます。なお、開発環境(特にローカルPCのエミュレーション環境)については、漏洩してもアクセスできないと思われるので、(私は)そのまま書いてしまっています。

接続文字列の設定

こんな感じでアクセスして、接続文字列を取得します。

namespace Test.Controllers
{
    public class StorageController : Controller
    {
        private readonly IConfiguration config;
        public StorageController(IConfiguration _config)
        {
            config = _config;
        }
        public async Task Upload()
        {
            var connectionString = config.GetConnectionString("MYSQL_ProductionServer");
        }
    }
}

(8) macOS上でAzure Storage Blobをエミュレートする場合はAzuriteを使う

Azuriteを参考にさせていただきました。インストールは非常に簡単でした。

npm install -g azurite

/var/tmp/azuriteディレクトリを作成し、起動用のディレクトリとしました。
ただ、ここは起動のたびに削除されるので、永続的なストレージが必要なら別の場所がいいと思います。(参考先だと~/azuriteになっていますね)

azurite -l /var/tmp/azurite

(9) macOS上でデータベースを使うならSQLiteが楽

macOSには標準でSQLite3が入っているはずなので、開発環境でのデータベースはSQLite3を使うのが手っ取り早いはずです(たぶん)。

設定を以下のようにしておきます。/var/tmp/JobDatabase.dbファイルに保存されますが、上記の通り、/var/tmpが永続的なファイル保存場所では無いのでご注意ください。

appsettings.Development.json
{
  "ConnectionStrings": {
    "JobDatabase": "Data Source=/var/tmp/JobDatabase.db"
  }
}

SQLiteもMySQLと同じようにプロバイダとして Microsoft.EntityFrameworkCore.Sqlite をNuGetでインストールし、以下のようなコードを書くだけで、アクセスできるようになります。

namespace Test
{
    public class Startup
    {
        private readonly IConfiguration config;
        public Startup(IHostingEnvironment _env, IConfiguration _config)
        {
            config = _config;
        }
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<JobDatabase>(options =>
            {
                var connectionString = config.GetConnectionString("JobDatabase");
                options.UseSqlite(connectionString);
            }
        }
    }
}

(10) プロジェクトは空の状態から作る

サンプルで用意されているプロジェクトは、今後大きくスケールする可能性があるか、スケールする可能性のない小さいものが多いようです。必死に理解しながら修正していると、勉強をしているのか、システムの改修案件に精をだしているのか分からなくなります。

空のプロジェクトから始め、MVCならView(Razor ビューエンジン)→Controller(クリックして発火)→Model(データ更新)→.. のように順を追って、少しずつ作り上げていく方が理解が早いです。

(11) Startupである必要はない

プログラムの開始はProgram.Mainです。

public static void Main(string[] args)
{
    var host = CreateWebHostBuilder(args).Build();
    host.Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>();

ここで、UseStartup<Startup>() の部分を書き換えればよいのです。なんで Startup なんだろう・・?ではなく、なんでもよいがデフォルトは Startup になっている、ということでした。

(12) _ViewImports.cshtml に共通で使用するディレクティブを書く

タグヘルパーを使うには以下のようなコードが必要になります。各View(.cshtml)の先頭に書いてもいいのですが、同じものであればまとめることができます。_ViewImports.cshtmlはファイル名そのものが意味を持っており、左記の機能を実現するために使われます。

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

なお、ちゃんとドキュメントを読んでいれば、書いてあります。

(13) _ViewStart.cshtml に共通で使用するレイアウトを書く

同じレイアウト(テンプレート)を使いまわしたいときは、先頭に以下のような設定をします。これにより、レイアウトとして、_Layout.cshtmlを使用し、@RenderBodyにView本体の記述が入ることになります。

@{
    Layout = "_Layout";
}

しかし、これを全部のViewに書くのはナンセンスです。_ViewStart.cshtmlに書いておけば事足ります。ちなみにこれもドキュメントにきちんと書かれています。

(14) EnsureCreated()はMainで呼んでしまう

EnsureCreated(データベースが存在しない場合、スキーマを流し込んでくれます)を呼びたいです。IServiceCollection.AddDbContextの前に、です。そんな時にもサンプルがあるんです、公式ならね。

using ContosoUniversity.Models;                   // SchoolContext
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;   // CreateScope
using Microsoft.Extensions.Logging;
using System;

namespace ContosoUniversity
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateWebHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;

                try
                {
                    var context = services.GetRequiredService<SchoolContext>();
                    context.Database.EnsureCreated();
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred creating the DB.");
                }
            }

            host.Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();
    }
}

(15) 開発目的ならAzure Database for MySQLを契約しなくても良い

Azure Database for MySQL を東日本リージョンで使用する場合、1時間あたり5.824円かかります。1ヶ月(Azureでの1ヶ月=730時間)では、4251円かかります。実験が終わったら止めればいいのですが、万が一止め忘れてしまったら・・。それにAzure App ServiceがFree版なのに、MySQLだけお金を取られるのも微妙です。

そんな時に便利なのが、MySQL In App です。こちらで設定できます。接続文字列も環境変数で提供される親切仕様。

MySQL In App

(16) MySQL In App で用意される MYSQLCONNSTR_localdb は間違っている

接続文字列は正しく取得できていて、設定もされている(試しに違うサーバーの接続文字列を試すとうまくいく)のですけど、MySQL In Appだけは接続できない。諦めるか否か悩んでいたところ、一つのポストを見つけました。

I finally found the problem!!!
Connection string that Azure give to me in env variable MYSQLCONNSTR_localdb is WRONG!!!

嘘だよね・・。与えられた接続文字列ではアクセスできないよってことらしいのですが、バグを2年も放置するはずがないので、使い方が違うのかも。

NG: Database=localdb;Data Source=127.0.0.1:PPPPP;User Id=azure;Password=XXXXX
OK: server=127.0.0.1;userid=azure;password=XXXXX;database=localdb;Port=PPPPP

というわけで、ポート番号とパスワード以外は固定だと思われるので、その部分だけ動的に変更して接続文字列を作るようにしました。ここでしか使わないのでベタ書きです。

var connectionString = Environment.GetEnvironmentVariable("MYSQLCONNSTR_localdb");
var dictionary = connectionString.Split(';')
                                 .Select(x => x.Split('='))
                                 .ToDictionary(x => x[0], x => x[1]);
connectionString = $"server=127.0.0.1;userid=azure;password={dictionary["Password"]};database=localdb;Port={dictionary["Data Source"].Split(':')[1]}";
options.UseMySql(connectionString);

(17) MySql.Data.EntityFrameworkCore ではなく Pomelo.EntityFrameworkCore.Mysql を使う

System.InvalidCastException: Unable to cast object of type System.Boolean to type System.Int16

エラーが出ました。スキーマもデータも見直したし、ステップ実行して、不正なデータが混入していないことも確認したんです。たぶん。どうやっても解決できなかったのですが、これまた一つのポストを見つけました。

This is a bug in the MySQL EF.Core implementation: bug 93028.
On other sites, people have reported fixing the issue by switching to Pomelo.EntityFrameworkCore.MySql.

嘘だよね・・。というわけで、NuGetパッケージから MySql.Data.EntityFrameworkCore を削除し、Pomelo.EntityFrameworkCore.Mysql をインストールするだけでこの問題は解決しました。ちなみに、メソッド名が微妙に違っておりまして、UseMySQL から UseMySql になってました。プロバイダ違いを明確にするためなのか・・な?

(18) Azure App Service に設定されている環境変数などは Kudu で見る

上部メニューのEnvironmentから設定されている環境変数の一覧を見ることができます。また、Debug Console > CMDではVM内のファイルなども見ることができ、/home/data/mysql/MYSQLCONNSTR_localdb.iniにはMySQL In Appへの正しい接続文字列が格納されていたりします。

Kuduを起動する

環境変数を確認する

(19) MySQL In App のデータは phpMyAdmin で確認する

ホーム > App Service > MySQL In App > 管理から phpMyAdmin にアクセスできます。

An attempt was made to access a socket in a way forbidden by its access permissions.

などのエラーが発生する場合、App Service自体がスリープになってしまっている可能性があります。MySQL In AppはApp ServiceのVM上でホストされているため、これがスリープしているとアクセスできません。(ちなみにInternal Server Errorなどにより、App Serviceが落ちてしまったり、起動できなかったりしても同じです)

まとめ

新しいことを学ぶのはとても楽しかったです。同時に、「やればできるだろう」と『やってみた』の差がいかに大きいかということも改めて実感しました。

今回は、初めてのことが多くて手が出せなかった分野も多くあります。ASP.NET Identityによる認証や、認可といったセキュリティ部分、ロギングや例外処理といったアプリケーションの運用のために必要不可欠な部分などです。

この記事が誰かのお役にたてば幸いです。
ここまでお読みいただき、ありがとうございました。

54
71
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
54
71

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?