🙌

ASP.NET MVC CRUD

2022/10/26に公開約13,100字

この記事について

ASP.Net MVCとEntity Frameworkを使って、CRUD機能認証機能の実装を、この場を借りて共有したいと思います💪

また、この記事の内容は基本的に以下の記事の内容を踏襲したものとなっています。そのため、内容やソースコードに引用などが含まれますので、予めご了承ください🙇‍♂️

それでは行きましょう🛴

前提知識

https://zenn.dev/mizuneko4345/articles/ec0da624dace7f

環境構築

  • Visual Studio 2019

  • 新しいプロジェクトの作成

  • NuGetパッケージの管理

    • EntityFramework(プロジェクトcrudにチェックする)
  • .gitignoreの作成

    • 最初のコミット時に.gitignoreを設定する必要がある。
dotnet new gitignore

CRUD機能を実装する

Modelの作成

まずは、Todoを管理するPOCOを作成します。

今回はコードファーストでの実装のため、このTodoクラスをもとにテーブルが作成される。
※クラス名は単数形、テーブル名は複数形

Models.Todo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace crud.Models
{
    public class Todo
    {
        public int Id { get; set; }
        public string Summary { get; set; }
        public string Detail { get; set; }
        public DateTime Limit { get; set; }
        public bool Done { get; set; }
    }
} 

次に、TodoクラスとDBを接続するコンテキストクラスを作成します。データの取得・更新等を行う。
作成したTodoesコンテキストクラスDbContextクラスを継承する必要がある。
ここでは、DBから取得したTodoを格納するDBSetを宣言し、このプロパティを介してデータの取得。更新を行う。

Models.TodoesContext.cs
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;

namespace crud.Models
{
    public class TodoesContext : DbContext
    {
        public DbSet<Todo> Todoes { get; set; }
    }
} 

Viewの作成

Razorを用いた共通レイアウトの作成を行う。

Views/_LayoutPage1.cshtml
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
</head>
<body>
    <div>
        @RenderBody()
    </div>
</body>
</html>

ビルドを完了させて、コントローラの作成を行う。

ビルドを開始しました...
========== ビルド: 0 正常終了、0 失敗、1 更新不要、0 スキップ ==========

Controllerの作成

スキャフォールディングしてCRUDのベースコードを生成する。

初期応答????

  • ActionMethodはActionResultを返却する
  • Indexメソッドの処理結果、Index.cshtmlにTodoのリストを加えてクライアントに返す
Controllers/TodoesController.cs
public class TodoesController : Controller
    {
        private TodoesContext db = new TodoesContext();

        // GET: Todoes
	//ブラウザからTodoesへのアクセスリクエストがあれば呼ばれる
        public ActionResult Index()
        {
	  //ViewメソッドはActionMethodに対応したViewResultを返却する
	    //ViewResultはViewを表示するためクラス(データ)
            return View(db.Todoes.ToList());
        }
  }

一覧画面の生成

Controllers/TodoesController.cs
public ActionResult Details(int? id) //ブラウザリクエストでidが未指定の場合に引数のidにnullをセット
{
    if (id == null)
    {
	return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    //idがnullでないときにDBから指定idと一致する内容をTodoesテーブルから抽出
    Todo todo = db.Todoes.Find(id);
    if (todo == null)
    {
      //一致するデータが存在しないなら"400"
	return HttpNotFound();
    }
    //一致するデータが存在すればそのtodoをViewにセットして返却
    return View(todo);
}

追加画面の生成

GETメソッドでアクセス時は画面を表示するだけ

Controllers/TodoesController.cs
// GET: Todoes/Create
public ActionResult Create()
{
    return View();
}

Create画面で保存ボタンをクリックすると、入力内容がPOSTされる。

Controllers/TodoesController.cs
[HttpPost] //POST時に呼ばれるActuionMethodを指定
[ValidateAntiForgeryToken] //アンチXSC

        //ASP>NET-MVCではクライアントからの入力値を自動的にPOCOに割りてるモデルBindという機能を持つ
        //クライアント(自分他ユーザ)からPOSTされてきたデータのキー名を見て引数のPOCOに自動敵に割り当てる
        //モデルBindの対象となる
	
//BindでPOSTされたデータをTodoモデルに紐づける
public ActionResult Create([Bind(Include = "ID,Summary,Detail,Limit")] Todo todo)
{
    if (ModelState.IsValid) //入力チェック
    {
	//ログインしているユーザのオブジェクトを取得
	var user = db.Users.Where(item => item.UserName == User.Identity.Name).FirstOrDefault();

	if (user != null)
	{
	    todo.User = user;

	    
	    db.Todoes.Add(todo); //Addメソッドでtodoをdbsetに登録する
	    db.SaveChanges(); //SaveChanges()でdbsetをdbに反映する
	    //RedirectToActionは指定されたアクションに処理を転送するヘルパーメソッド
	    //Indexが指定されているので、登録完了すると一覧画面に戻るフローになる
	    return RedirectToAction("Index");
	}

    }
    //入力内容がNGならCreate.cshtmlに返してもう一度入力させる
    return View(todo);
}

編集画面の生成

GETメソッドでアクセス時は画面を表示するだけ

Controllers/TodoesController.cs
public ActionResult Edit(int? id)
{
    if (id == null)
    {
	return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Todo todo = db.Todoes.Find(id);
    if (todo == null)
    {
	return HttpNotFound();
    }
    //指定されたidのtodoを取得して返す
    return View(todo);
}

編集画面で更新ボタンをクリックすると、入力内容がPOSTされる。

Controllers/TodoesController.cs
[HttpPost]
//POSTされてきたトークンを自動的に検証する
[ValidateAntiForgeryToken]
//入力されたデータが引数のtodoオブジェクトにBindされる
public ActionResult Edit([Bind(Include = "ID,Summary,Detail,Limit,Done")] Todo todo)
{
    if (ModelState.IsValid)
    {
	//Entryメソッドで引数から受けとったtodoを設定
	//StateプロパティにModeifiedを指定し、該当のtodoを更新できる
	db.Entry(todo).State = EntityState.Modified;
	db.SaveChanges(); //DBに処理が反映される
	return RedirectToAction("Index"); //更新完了後にindexに処理が転送される。
    }
    return View(todo);
}

削除画面の生成

GETメソッドでアクセス時は画面を表示するだけ

Controllers/TodoesController.cs
public ActionResult Delete(int? id)
{
    if (id == null)
    {
	return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Todo todo = db.Todoes.Find(id);
    if (todo == null)
    {
	return HttpNotFound();
    }
    return View(todo);
}

削除画面で指定したidのtodoを選択した状態で削除ボタンをクリックするとPOSTされる。

Controllers/TodoesController.cs
//ActionNameを指定し、ActionMethodの名前とURLのメソッド部分を別々にできる
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
//POSTのアクションメソッド名をDeleteにするとGETメソッドと同じ引数になりエラーになるので改名している
public ActionResult DeleteConfirmed(int id) //ActionMethodの名前をDeleteConfirmedとして、ActionName("Delete")とすることで、URLでDeleteがPOSTされてきた際にDeleteConfirmedが呼ばれる
{
    if (id == null)
    {
	return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
  //指定されたidのtodoを取得
    Todo todo = db.Todoes.Find(id);
    //そのtodoをTodoewsテーブルから削除
    db.Todoes.Remove(todo);
    //削除内容をdbに保存
    db.SaveChanges();
    //ホーム画面に処理を転送して表示
    return RedirectToAction("Index");
}

終了処理

Controllers/TodoesController.cs
protected override void Dispose(bool disposing) //Disposeメソッドで保持しているコンテキストを解放
{
    if (disposing)
    {
	db.Dispose();
    }
    base.Dispose(disposing);
}

スキャフォールディング後のView

一覧画面

Controllerから受け取るデータ型を定義する。
crud.Models.Todoなので、Todoクラスにあるリストを受け取る
@:Razorのコードナゲットといいいサーバー再度で実行される
ViewBag:Viewで使用するデータを保持する他、ViewとController間のやり取りで使用
Layout:各ページで使用する共通レイアウトを指定
@Html:HTMLヘルパーといい、HTMLを生成する様々なメソッドがある
ActionLink:リンクを生成するメソッド
DisplayNameFor:modelの定義に応じてプロパティ表示名を出力

Views/Todoes/Index.cshtml
@model IEnumerable<crud.Models.Todo>

@{
    ViewBag.Title = "Index";
    Layout = "~/Views/_LayoutPage1.cshtml";
}
<p>
    @Html.ActionLink("Create New", "Create")
</p>
<th>
    @Html.DisplayNameFor(model => model.Summary)
</th>
  • DisplayName未使用の場合

スキャフォールディング後のModel操作

Modelを操作して表示名を変更する

Models.Todo.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Web;

namespace crud.Models
{
    public class Todo
    {
        [DisplayName("概要")]
        public string Summary { get; set; }
        [DisplayName("詳細")]
        public string Detail { get; set; }
        [DisplayName("期限")]
        public DateTime Limit { get; set; }
        [DisplayName("完了")]
        public bool Done { get; set; }
    }
}

DisplayName使用の場合

ちなみに、Idを消してデバッグ実行すると以下のエラーが出る
これはテーブルを生成するのに必要な主キーがないというエラー

Models.Todo.cs
// GET: Todoes
public ActionResult Index()
{
    return View(db.Todoes.ToList());
}
System.Data.Entity.ModelConfiguration.ModelValidationException: 'One or more validation errors were detected during model generation:

crud.Models.Todo: : EntityType 'Todo' has no key defined. Define the key for this EntityType.
Todoes: EntityType: EntitySet 'Todoes' is based on type 'Todo' that has no keys defined.

編集画面

Indexとは違いリストではなく単一のTodoとして受け取る
BeginForm():ブロック内をフォームタグでラップし、Htmlヘルパーを用いてフォームを生成
AntiForgeryToken():CSRF対策のhiddentag
ValidationSummary:入力チェックのエラーメッセージが出力される

https://docs.microsoft.com/ja-jp/dotnet/api/system.web.ui.webcontrols.validationsummary?view=netframework-4.8
HiddenFor:Hiddenタグを生成する(TodoのIdを保持????)
LabelFor:Labrlタグを生成する
EditorFor:プロパティの型に応じたタグを生成する(ナンバー、日付とか)
ValidationMessageFor:各項目の入力チェックのエラーメッセージを出力する
Views/Todoes/Edit.cshtml
ud.Models.Todo

@{
    ViewBag.Title = "Edit";
    Layout = "~/Views/_LayoutPage1.cshtml";
}

<h2>Edit</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <h4>Todo</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        @Html.HiddenFor(model => model.Id)

        <div class="form-group">
            @Html.LabelFor(model => model.Summary, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Summary, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Summary, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
}
<div>
    @Html.ActionLink("Back to List", "Index")
</div>

ValidationMessageForなどの入力チェックでjQueryが使用されている。

Views/Todoes/Edit.cshtml
<script src="~/Scripts/jquery-3.4.1.min.js"></script>
<script src="~/Scripts/jquery.validate.min.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>

csharp:Views/Todoes/Edit.cshtmlではDoneを入力フォームから除外します。

Routeの初期設定

最初に表示するページの設定

App_Start/Route.config
defaults: new { controller = "Todoes", action = "Index", id = UrlParameter.Optional }

デバッグ実行

以下でエラー

Controllers/TodoesController
// GET: Todoes
public ActionResult Index()
{
    return View(db.Todoes.ToList());
}
  1. データベースの初期化中に例外が発生
  2. データベースとして使用できない
1. System.Data.DataException: 'An exception occurred while initializing the database. See the InnerException for details.'

2. SqlException: Cannot attach the file 'C:\Users\sekai\asp.net-crud\TodoApp\TodoApp\App_Data\TodoApp.Models.TodoesContext.mdf' as database 'TodoApp.Models.TodoesContext'.

ローカルに同じプロジェクト名がある
NuGetパッケージの管理で、EntityFrameworkにプロジェクトにチェックを入れていなかった

↑を直したらうまくいったし、gitignoreもちゃんとできた。

タスクを2件追加する

DBに格納されているかを確認する

cmdでSQL Serverがインストールされているサーバー名が出力される

>sqllocaldb i
MSSQLLocalDB
ProjectsV13

TodoesコンテキストSQL Server Express LocalDBに接続してテーブルを確認。

CSRF対策(クロスサイトリクエストフォージェリ)

偽サイトからのリクエストを受信して処理してしまう脆弱性のこと
対策としてViewにAntiForgeryTokenメソッドを使い、トークンを発行しHiddenタグにセットする

Views/Todoes/Edit.cshtml
@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    

Viewに対応するActionMethodに対して、ValidateAntiForgeryTokenアノテーションを記載するとPOSTされてきたトークンを自動検証する

Controllers/TodoesController.cs
[HttpPost]
[ValidateAntiForgeryToken]

過多ポスティング攻撃対策

これでも動く👈デバック実行済み

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Todo todo)
{...

悪意のあるデータをPOSTできるという脆弱性があるので、
Create ActionMethodにModelBindする必要がある(過多POST対策)
ASP.NET MVCではクライアントからの入力内容を自動的にPOCOに割り当てるModelBindがある。
クライアントが入力しPOSTしてきた内容のKEYを見て引数のPOCO????に自動割り振りする。

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "Id,Summary,Detail,Limit")] Todo todo)
{...

BootStrapの導入

参照/Content内にBootstarpがインストールされたことを確認する。

共通レイアウトの編集

Views/_LayoutPage1.cshtml
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <link href="~/Content/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
    <div class="container">
        @RenderBody()
    </div>
</body>
</html>




Discussion

ログインするとコメントできます