概要
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) 書籍をよく読む
- プログラミング ASP.NET Core - Microsoft Press(日本での出版日:2019年5月27日 5200円+税)
リンクを貼るために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などなどのツール類の話が中心のものは、あまり読んでいません。
-
はじめてのASP.NET Core
プロジェクト開始直後に何をしていいのか、なんとなく理解できるようになりました -
【C#】コードファーストで DB に 初期データ入れる
この記事でEntity Framework Coreの使い方についての実装イメージが湧くようになりました
(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><environment include="Development"></div>
</environment>
<environment exclude="Development">
<div><environment exclude="Development"></div>
</environment>
<environment include="Staging,Development,Staging_2">
<div>
<environment include="Staging,Development,Staging_2">
</div>
</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
が永続的なファイル保存場所では無いのでご注意ください。
{
"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 です。こちらで設定できます。接続文字列も環境変数で提供される親切仕様。
(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への正しい接続文字列が格納されていたりします。
(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による認証や、認可といったセキュリティ部分、ロギングや例外処理といったアプリケーションの運用のために必要不可欠な部分などです。
この記事が誰かのお役にたてば幸いです。
ここまでお読みいただき、ありがとうございました。