これまでの章では、主に Web Form と MVC との比較について書いてきました。この章では、 Web MVC のプロジェクトを Core フレームワークを 使用するように書き換えてみましたので、そのことについて書いてみたいと思います。
ただし、書き換え作業を始めて直ちに問題に直面しました。それは、次のものです。
トランザクションに関しては、 こちら を参照してください。その章で実装したやり方をそのまま踏襲して確認したところ、
TransactionScope 未サポートの例外が発生しました。 TransactionScope が利用できないのでは、実用に耐えないと判断しました。
但し、Core の新しいバージョン Core 2.1 のプレビュー版が Microsoft 社から2018/2月に発表されました。そこには、TransactionScope クラスも
開発対象に含まれています。正式に Core 2.1 がリリースされた場合は、 TransactionScope 関連の問題は解決されると思います。そのため現状は、
次の方法で書き換えを行うこととしました。
結論から言えば、 .NET Framework 上の Core では TransactionScope を利用することが可能でした。 ASP.NET Core でプログラムを作成する際には、 当分の間は .Net Framework ライブラリを使用することになりそうです。
2018/11/30 追記
Core 2.1 を使用してみました。 2.0 用のソースなどをそのまま移行することで、ほぼ修正することなく動作を確認することができました。ごくわずかの手直しを行いましたが、どなたでも気が付く程度と思います。
結論としては、 Core 2.1 では TransactionScope クラスは正しく動作していますし、Core 2.0 と 2.1 との互換性は十分保たれていると思います。
以下では、書き換え作業をできるだけ時間順に書いていきたいと思います。ただし、以下のプログラムの書き換えは行いませんでした。
プロジェクト作成までの手順を書きます。
これ以降は、既存の Web MVC のプログラムを新しく作成したこのプロジェクトに追加し、適宜エラー個所を修正していく作業になります。
ASP.NET Core フレームワークでは Web ルートフォルダーが変更になっています。プロジェクト用のフォルダーの下に「wwwroot」というサブフォルダーができています。 このフォルダーがデフォルトの Web ルートフォルダーとなります。そのため、Web MVC プロジェクトで使用している Contents フォルダーや Scripts フォルダーを そのまま、 wwwroot フォルダーの下にコピーすることにしました。こうしたフォルダーの扱いは、そのプロジェクト毎に決めてください。そして、なるべくならばデフォルトの 使用方法に準拠した方が分かりやすいと思います。
プロジェクトで使用するライブラリの追加方法も変更になっています。
一時に全ソースを移行せずに、単純なソースから移行して、動作確認の手順を繰り返すことになりますから、ライブラリも必要になるたびに参照を追加すればよいです。
機械的に進めることができるのは、 namespace に係わる変更部分です。
Web MVC プログラムのトランザクションの実装に関しては、 こちら を参照してください。 全く同様に、次のように実装し、動作確認ができました。
1 public class TransactionController : Controller
2 {
3 public void Execute()
4 {
5 try
6 {
7 using (TransactionScope ts = new TransactionScope())
8 {
9 DoAction();
10 ts.Complete();
11 }
12 }
13 catch (System.Exception ex)
14 {
15 OccurredError(ex);
16 }
17 }
18 public virtual void DoAction() { }
19 public virtual void OccurredError(Exception ex)
20 {
21 ModelState.AddModelError("dbError", ex.Message);
22 }
23 static public string ErrorMessages(ModelStateDictionary modelState)
24 {
25 StringBuilder buf = new StringBuilder();
26 var errors = modelState.Values.SelectMany(v =>
27 v.Errors.Select(e => e.ErrorMessage)).ToList();
28 foreach (var str in errors)
29 {
30 buf.AppendFormat("<li>{0}</li>", str);
31 }
32 return buf.ToString();
33 }
34 }
トランザクションスコープの使い方の説明は、上記の参照先を見てください。違っているのは、エラーの扱い方です。Web MVC の場合、プログラムコードから 扱うには、次のようにしました。
ModelState.AddModelError("dbError", ex.Message);
また、ビューからエラー表示時を一か所で行うためには、Web MVC では次の特別なタグを使用しました。
@Html.ValidationSummary(false, "", new { id = "error-Message", @class = "text-danger bg-danger" })
Core では、ModelState の扱いが変わったためか、 @Html.ValidationSummary() は全く機能しません。そのため、エラーを表示する部分は自前で用意する必要がありました。 この件に関しては、 こちら も参照してください。
上記の23~34行で定義された、 static public メソッドは、 ModelState の内容を書式化するときに使用します。しかしビュークラスからはバインドされたモデルクラスしか操作できません。 そのため、コントローラクラスに static public メソッドとして用意し、いつでも利用できるようにしています。
Core フレームワークで大きく変わったのは、各種の設定を行うために Startup クラスを利用することになった点です。次に、今回利用している Startup クラスの内容を示します。ConfigureServices() メソッドと Configure() メソッドとが中心です。
1 public class Startup
2 {
3 public void ConfigureServices(IServiceCollection services)
4 {
5 services.AddDistributedMemoryCache();
6 services.AddSession();
7 services.AddMvc();
8 services.Configure<DbSettings>(this.Configuration.GetSection("DbSettings"));
9 services.Configure<AppSettings>(this.Configuration.GetSection("AppSettings"));
10 services.Configure<MapSettings>(this.Configuration.GetSection("MapSettings"));
11 }
12 public void Configure(IApplicationBuilder app, IHostingEnvironment env)
13 {
14 app.UseSession();
15 if (env.IsDevelopment())
16 {
17 app.UseBrowserLink();
18 app.UseDeveloperExceptionPage();
19 }
20 else
21 {
22 app.UseExceptionHandler("/Home/Error");
23 }
24 app.UseDefaultFiles();
25 app.UseStaticFiles();
26 app.UseMvc(routes =>
27 {
28 routes.MapRoute(
29 name: "default",
30 template: "{controller=Home}/{action=Index}/{id?}");
31 });
32 }
33 }
5行目:セッション変数の格納先として、メモリ内セッションプロバイダを利用します
6行目:セッションを利用します
7行目:MVC を利用します
8~10行目:設定ファイル用のオブジェクトの準備です。以降で説明します
14行目:MVC を開始します
15~23行目:デフォルトの処理内容のままです
24行目:静的な html ファイルを利用します。以降で説明します
25行目:デフォルトの Web ルートを使用します
26~32行目:デフォルトの処理内容のままです
デフォルトのままでは、静的な html ファイルは利用することができません。静的な html ファイルを利用するためには、以下のようにします。
public class Startup
{
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseDefaultFiles();
}
}
Configure() メソッドの中で、 app.UseDefaultFiles() メソッドを呼び出します。そして、 wwwroot サブフォルダーに次のいづれかのファイルを用意します。
最初に見つかった html ファイルから処理が開始します。
Core フレームワークでは、標準の設定ファイルは appsettings.json を使用するように変更されました。そこで、次のような json ファイルがあった場合に、 その利用方法を説明します。
{
"AppSettings": {
"TempDir": "C:\\temp\\"
},
"MapSettings": {
"MapKey": "XXXXXXXXXX",
"CenterLatitude": "38.018365",
"CenterLongitude": "138.368090",
"Zoom": "11"
}
}
ここでは、個々の項目の意味合いは説明しません。 AppSettings と MapSettings の項目があります。さらにその下に、それぞれ1項目と4項目がある場合を例にとります。
まづ、次のような単純なクラスを定義します。 Settings フォルダーを作成して、その中に定義しました。
1 public class AppSettings
2 {
3 public string TempDir { set; get; }
4 }
5 public class MapSettings
6 {
7 public string MapKey { set; get; }
8 public string CenterLatitude { set; get; }
9 public string CenterLongitude { set; get; }
10 public string Zoom { set; get; }
11 }
AppSettings クラスと MapSettings クラスです。どちらも単純で、設定ファイルの内容に対応したクラスですので理解は容易です。
次に、 Startup クラスにおいて設定ファイルの項目に対応したオブジェクトの準備をします。
1 public class Startup
2 {
3 public void ConfigureServices(IServiceCollection services)
4 {
5 services.Configure<AppSettings>(this.Configuration.GetSection("AppSettings"));
6 services.Configure<MapSettings>(this.Configuration.GetSection("MapSettings"));
7 }
8 }
5行目:AppSettings オブジェクトの準備をします
6行目:MapSettings オブジェクトの準備をします
このオブジェクトを利用したいコントローラクラス(以下の例では、 MyController クラス)では、次のようにします。
1 public class MyController : Controller
2 {
3 private readonly AppSettings appSettings = null;
4 public MyController(IOptions<AppSettings> settings)
5 {
6 appSettings = settings.Value;
7 }
適当なコントローラクラスのメソッドの中で
8 string dir = appSettings.TempDir;
9 }
3行目:設定項目用の変数です-ここでは、 AppSettings クラスのオブジェクトです
4行目:設定項目のオブジェクトを引数に取るコンストラクタを定義します
8行目:コントローラクラスの適当なメソッドの中で、設定項目のオブジェクトを操作します
セッションを利用するには、 Startup クラスに記述が必要です。その部分の説明は、 こちら を参照してください。以下では、セッションが 利用できる環境のもとで、コントロールクラスからどのようにしてセッションへの読み書きを行うのかを書いていきます。また、NuGet パッケージマネージャを使用して 「Microsoft.AspNetCore.Session」パッケージを参照に追加することも必要になります。
以下に、セッションへの書き込みと読出しのコード片を示します。コントローラクラスの適当なメソッドの中に記述してください。
1 HttpContext.Session.SetString("name", "Rick");
2 HttpContext.Session.SetInt32("number", 3);
3 var name = HttpContext.Session.GetString("name");
4 var yearsMember = HttpContext.Session.GetInt32("number");
1行目:文字列 "name" をキーにして、文字列 "Rick" をセッションに格納します
2行目:文字列 "number" をキーにして数字 3 をセッションに格納します
3行目:キー "name" で格納されている文字列を読み出します
4行目:キー "number" で格納されている数字を読み出します
Core で提供されているセッションには、文字列と数値という基本型の読み書きの方法しか提供されません。そこで、シリアル化が可能で
複雑なオブジェクトをセッションに読み書きする方法が Microsoft 社から紹介されています。この方法は「Newtonsoft.Json」を利用しているようです。
Visual Studio 2017 には標準で組み込まれていますが、最新のものを確認したい場合は、NuGet パッケージマネージャを使用してください。
複雑なオブジェクトをセッションに読み書きするための拡張メソッドを持つクラスを定義します。このクラスはライブラリ化するのがよいでしょう。
1 using Microsoft.AspNetCore.Http;
2 using Newtonsoft.Json;
3 public static class SessionExtensions
4 {
5 public static void Set<T>(this ISession session, string key, T value)
6 {
7 session.SetString(key, JsonConvert.SerializeObject(value));
8 }
9 public static T Get<T>(this ISession session,string key)
10 {
11 var value = session.GetString(key);
12 return value == null ? default(T) :
13 JsonConvert.DeserializeObject<T>(value);
14 }
15 }
3行目:クラス名を SessionExtensions とします
5~8行目:格納用メソッド定義です
9~14行:読出し用メソッド定義です
このクラスの使い方を以下のコード片で示します。コントローラクラスの適当なメソッドの中に記述してください。
1 HttpContext.Session.Set<DateTime>("date", DateTime.Now);
2 var date = HttpContext.Session.Get<DateTime>("date");
オブジェクトの型を渡すことで、セッションへの読み書きが可能になります。
Web MVC から ASP.NET Core MVC への書き換えは、多くの部分で機械的に行うことができました。大きな修正が必要だったのは、新規に導入された Startup クラス の部分と、ここで説明するコントローラに係わる変更でした。
コントローラの POST 用メソッドでは、引数の数と型が変更になりました。変更前と後を確認してください。
変更前
1 public ActionResult Index(string button, FormCollection collection)
変更後
2 public ActionResult Index([FromForm]IFormCollection collection)
変更前は、2引数で定義しました。第一引数は、画面にある複数のボタンのうち、サブミットを起こした原因となるボタンを知るために使用します。
第二引数は、ボタン以外の POST 形式のパラメータ値を知るために使用します。
一方 Core においては、 POST 形式のパラメータ一つで操作することになります。
Core において、引数から個々のパラメータ値を知るには次のようなプログラム片で可能です。
1 string button = collection["button"];
2 Microsoft.Extensions.Primitives.StringValues value;
3 string mid = collection.TryGetValue("majorId", out value) ? value.ToString() : string.Empty;
1行目:サブミットを起こしたボタンのラベルが返ります。ここで、サブミットを起こすボタンのタグには、 name="button" の記述が必要です。また、ボタン以外が原因でサブミットした場合は、 null が返ります
2行目: POST の各パラメータは StringValues 型で読み取ります
3行目: TryGetValue() メソッドによりパラメータの値を読み出します。この例では、 majorId は読み出したいタグの id 属性値になります
Query パラメータは次のように読み出します。
変更前
1 string val = Request.QueryString.Get("majorId");
変更後
2 string val = HttpContext.Request.Query["majorId"];
どちらも、コントローラクラスの適切なメソッド内の中で記述してください。
アップロードファイルの情報を受け取る POST メソッドの記述方法に変更が必要です。
変更前
1 public ActionResult Create(string button, FormCollection collection, HttpPostedFileBase[] fileUpload)
変更後
2 public ActionResult Create([FromForm]IFormCollection collection, List<IFormFile> fileUpload)
変更前は第三引数の型で受け取ります。 Core では第二引数の型で受け取ることになります。
さらに、Core では IFormFile 型のインターフェースを使用して、以下のコード片によりローカルファイルを作成します。
1 private void MakeFiles(List<IFormFile> files)
2 {
略
3 String fileName = "ローカルファイルの名前";
4 foreach (var file in files)
5 {
6 using (var stream = new FileStream(fileName, FileMode.Create))
7 {
8 file.CopyTo(stream);
9 stream.Flush();
10 }
11 }
12 }
試した限りでは、9行目の Flush() を行わないと正しくローカルファイルが作成されませんでした。
モデルクラスは今取り組んでいる問題領域に対応したクラスですから、Web MVC から Core に変更したとしても特段の変更は発生しません。
もし、このクラスに変更が必要になった場合は、そのクラスの設計自体に問題がある可能性が高いとおもいます。
今回の移行でも、次の理由による変更に限定されました。
ビュークラスに必要となった主な変更は、次の理由によるものでした。
namespace による変更は、これまでも何度も書いたものです。もう一つは、@Html.ValidationSummary() の動作が、Web MVC と Core で変わったようです。 そのままでは全くエラーメッセージが表示されなくなりました。この件に関しては、 こちら にも簡単に書きました。以下では、変更前と変更後を比べてみます。
変更前
1 @Html.ValidationSummary(false, "", new { id = "error-Message", @class = "text-danger bg-danger" })
変更後
2 @Html.Raw(FieldworkForCoreDotNet.Controllers.MajorsController.ErrorMessages(ViewContext.ModelState))
@Html.ValidationSummary() の部分を自前の処理に置き換えました。変更後の ErrorMessages() メソッドは TransactionController クラスに public static として
実装してあります。 MajorController クラスは TransactionController クラスから派生して作成したクラスですから、そのまま利用できます。
コントローラクラスが発見したエラーは ModelState に格納されています。その内容を、エラー項目ごとに <ul> タグで書式した文字列に変換しています。
Web MVC での汎用ハンドラに関しては、 こちら を参照してください。
Core では拡張子 ashx のハンドラはサポートされないようですので、書き換えを行いました。どのように書き換えたかといえば、通常のコントローラクラスにしただけです。
1 public class SessionsController : Controller
2 {
3 [HttpPost]
4 public IActionResult Index([FromForm]IFormCollection collection)
5 {
6 Microsoft.Extensions.Primitives.StringValues formValue;
7 string key, value, val;
8 for (int i = 1; ; ++i)
9 {
10 key = HttpContext.Request.Query["key" + i];
11 value = HttpContext.Request.Query["value" + i];
12 if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value))
13 break;
14 if (key.StartsWith("d_"))
15 {
16 key = key.Substring(2);
17 val = value;
18 }
19 else
20 {
21 val = collection.TryGetValue(value, out formValue) ? formValue.ToString() : string.Empty;
22 if (val == null)
23 break;
24 }
25 HttpContext.Session.SetString(key, val);
26 }
27 return this.Ok();
28 }
29 }
こちら にある Web MVC での汎用ハンドラのソースプログラムと比較すると、全く同じ構造になっています。
3行目にあるように、例えば Ajax から利用する場合は POST メソッドで通信する想定です。 GET でも利用したい場合は、 GET 用のメソッドも併せて2つ用意するのよいと思います。
Core ライブラリについて
Core の仕様に準拠して新しく作られたライブラリでは、 TransactionScope がサポートされていないことは本文に書きました。以下では、 .NET Framework 上の Core ライブラリに関して不思議な現象を書いてみます。
それは、 .NET Framework を使用した Core プロジェクトを新規に作成し、そのまま「デバッグの開始」ボタンをクリックすると、
デフォルトの初期画面すら起動しない(少なくとも、通常の時間以内では)という問題です。
この件は、 Microsoft 社のフォーラムにも質問として投稿しましたが、有効な回答が寄せられませんでした。そのことから、私が使用している
開発環境由来だろうとは思っていますが、 Windows 7 の上では起こらず、 Windows 10 で発生していることや、「デバッグなしで開始」を行うと通常に起動する
といった、奇妙な振る舞いをします。
もちろん、 Visual Studio 2017 のアンインストール・再インストールや修復では解消できませんでした。
いろいろのことを試した結果、この現象を回避する間接的な方法を見つけましたので、以下に紹介します。
この操作によって、初期画面が起動するようになります。変更内容から、 Visual Studio 2017 に組み込まれている IIS Express に関連した
何らかの障害が起こっているのだろうと思います。
本質的な解決法とは思えないのですが、もし同じ現象に遭遇した方がありましたら、参考にしてください。