WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

ASP.NET Identity のロール管理 (MVC)

by WebSurfer 2017年11月2日 16:40

ASP.NET Identity ベースのフォーム認証を実装した ASP.NET MVC5 アプリケーションで、管理者がロール管理を行う機能を実装する例を書きます。先の記事「ASP.NET Identity のユーザー管理 (MVC)」の続きです。

ロール管理画面

CodeZine の記事「ASP.NET Identityでユーザーに役割(ロール)を持たせる 」に、ASP.NET Web Forms アプリ用のユーザー管理画面を作る手順が載っていますが、その MVC 版です。

ベースとなるのは、先の記事「ASP.NET Identity のユーザー管理 (MVC)」と同様、Visual Studio 2015 Community で MVC のテンプレートを利用し、認証に「個別のユーザーアカウント」を指定して自動生成させたプロジェクトです。

それにロール情報の追加、変更、削除および登録済みユーザーのロールのアサインを行う機能を実装するには以下のようにします:

(1) コードの追加

ロール情報の追加、変更、削除を行うのに用いるクラスは、IdentityRole クラスRoleManager<TRole, TKey> クラスです。

テンプレートで自動生成されたコードにはそれらのコードは含まれていませんので追記する必要があります。

具体的には、Models\IdentityModels.cs, App_Start\IdentityConfig.cs, App_Start\Startup.Auth.cs の 3 つの既存のファイルに、CodeZine の記事「ASP.NET Identityでユーザーに役割(ロール)を持たせる」を参考にコードを追加します。(記事のコードをそのままコピペすれば OK)

何を追加するかを簡単に書きますと、IdentityModels.cs, IdentityConfig.cs, Startup.Auth.cs の順に、以下の通りです。

  1. IdentityRole クラスを継承した ApplicationRole クラスを IdentityModels.cs に追加。
  2. RoleManager<TRole, TKey> クラスを継承した ApplicationRoleManager クラスを IdentityConfig.cs に追加。定義には ApplicationRoleManager クラスのインスタンスを生成して OwinContext に登録するための Create メソッドを含める。Create メソッドにはロールストアに何も定義されてない場合 Administrator という名前のロールを作成するコードも含まれています。
  3. ApplicationRoleManager のインスタンスを生成して OwinContext に登録するコードを Startup.Auth.cs の ConfigureAuth メソッドに追加。これにより、Controller で HttpContext.GetOwinContext() メソッドにより OwinContext を取得し、それから Get<ApplicationRoleManager>() メソッドで ApplicationRolerManager オブジェクトを取得できるようになります。

(2) Migrations を実施

上記 (1) の作業後アプリを実行すると、先にユーザー情報を登録して EF Code First の機能で DB を生成済みの場合は以下のようなエラーが出ると思います。ちなみに、下の画像でエラーとなっている行は、上記 (1) - 2 で追加した Create メソッドでロールストアにあるロール情報を取得するものです。

エラー画面

その場合は Migrations 実施してから再度実行してください。

具体的には、Visual Studio のパッケージマネージャーコンソールで、Enable-Migrations コマンドを実行して Code First Migrations を有効にし、次に Update-Database –Verbose コマンドを実行して保留中の移行をデータベースに適用します。

(3) AspNetRoles テーブルの確認

上記 (2) でエラーなく無事実行できれば、以下の画像のようにロール情報のストアとなる AspNetRoles テーブルに Discriminator フィールドが追加され、「結果」ウィンドウに示すように Name が Administrator というレコードができているはずです。

AspNetRoles テーブル

(4) ロール管理用 Controller / View の実装

ロール情報の追加、変更、削除および登録済みユーザーのロールのアサインを行うための Controller / View を実装します。Controller の実装例を以下に示します。説明はコード内のコメントに書きましたので、それを見てください。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

// 上は自動生成されたもの。それに以下の名前空間を追加
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using System.Net;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Mcv5App2.Models;

namespace Mcv5App2.Controllers
{
  // 以下の RoleModel, UserWithRoleInfo, RoleInfo の 3 クラス
  // はここで利用する Model。Controller 内に定義したのは単に
  // 分けるのが面倒だから

  // アクションメソッド Create, Edit 用の Model
  public class RoleModel
  {
    [Required]
    [StringLength(
      100, 
      ErrorMessage = "{0} は {2} 文字以上",
      MinimumLength = 3)]
    [Display(Name = "Role")]
    public string Role { get; set; }
  }

  // アクションメソッド UserWithRoles, EditRoleAssignment
  //  (各ユーザーのロール一覧表示、ロールのアサイン・削除) 
  // 用の Model
  public class UserWithRoleInfo
  {
    public UserWithRoleInfo()
    {
      UserRoles = new List<RoleInfo>();
    }

    public string UserId { set; get; }
    public string UserName { set; get; }
    public string UserEmail { set; get; }

    public IList<RoleInfo> UserRoles { set; get; }
  }

  public class RoleInfo
  {
    public string RoleName { set; get; }
    public bool IsInThisRole { set; get; }
  }


  // ここから Controller のコード
  public class RolesController : Controller
  {
    private ApplicationUserManager _userManager;
    private ApplicationRoleManager _roleManager;

    public ApplicationUserManager UserManager
    {
      get
      {
        return _userManager ?? 
            HttpContext.GetOwinContext().
            GetUserManager<ApplicationUserManager>();
      }
      private set
      {
        _userManager = value;
      }
    }

    public ApplicationRoleManager RoleManager
    {
      get
      {
        return _roleManager ?? 
            HttpContext.GetOwinContext().
            Get<ApplicationRoleManager>();
      }
      private set
      {
        _roleManager = value;
      }
    }

    // GET: Roles(登録済みロールの一覧)
    // Model は ApplicationRole
    public ActionResult Index()
    {
      var roles = 
        RoleManager.Roles.OrderBy(role => role.Name);
      return View(roles);
    }

    // GET: Roles/Details/Id(指定 Id のロール詳細)
    // 上の一覧以上の情報は含まれないので不要かも・・・
    // Model は ApplicationRole
    public async Task<ActionResult> Details(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                        HttpStatusCode.BadRequest);
      }

      var target = await RoleManager.FindByIdAsync(id);
      if (target == null)
      {
        return HttpNotFound();
      }
      return View(target);
    }

    // GET: Roles/Create(新規ロール作成・登録)
    // Model は上に定義した RoleModel クラス
    public ActionResult Create()
    {
      return View();
    }

    // POST: Roles/Create(新規ロール作成・登録)
    // Model は上に定義した RoleModel クラス
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Create(RoleModel model)
    {
      if (ModelState.IsValid)
      {
        // ユーザーが入力したロール名を model.Role から取得し
        // て ApplicationRole を生成
        var role = new ApplicationRole { Name = model.Role };

        // 上の ApplicationRole から新規ロールを作成・登録
        var result = await RoleManager.CreateAsync(role);

        if (result.Succeeded)
        {
          // 登録に成功したら Roles/Index にリダイレクト
          return RedirectToAction("Index", "Roles");
        }

        // result.Succeeded が false の場合 ModelSate にエ
        // ラー情報を追加しないとエラーメッセージが出ない。
        // AccountController と同様に AddErrors メソッドを
        // 定義して利用(一番下に定義あり)
        AddErrors(result);
      }

      // ロールの登録に失敗した場合、登録画面を再描画
      return View(model);
    }

    // GET: Roles/Edit/Ed(指定 Id のロール情報の更新)
    // ここで更新できるのはロール名のみ
    // Model は上に定義した RoleModel クラス
    public async Task<ActionResult> Edit(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      var target = await RoleManager.FindByIdAsync(id);

      if (target == null)
      {
        return HttpNotFound();
      }

      RoleModel model = new RoleModel() { Role = target.Name };

      return View(model);
    }

    // POST: Roles/Edit/Id(指定 Id のロール情報の更新)
    // ここで更新できるのはロール名のみ
    // Model は上に定義した RoleModel クラス
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> 
                        Edit(string id, RoleModel model)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      if (ModelState.IsValid)
      {
        var target = await RoleManager.FindByIdAsync(id);

        // ユーザーが入力したロール名を model.Role から取得し
        // て ApplicationRole の Name を書き換え
        target.Name = model.Role;

        // Name を書き換えた ApplicationRole で更新をかける
        var result = await RoleManager.UpdateAsync(target);

        if (result.Succeeded)
        {
          // 更新に成功したら Roles/Index にリダイレクト
          return RedirectToAction("Index", "Roles");
        }

        // result.Succeeded が false の場合 ModelSate にエ
        // ラー情報を追加しないとエラーメッセージが出ない。
        AddErrors(result);
      }

      // 更新に失敗した場合、編集画面を再描画
      return View(model);
    }

    // GET: Roles/Delete/Id(指定 Id のロールを削除)
    // 階層更新が行われているようで、ユーザーがアサインされて
    // いるロールも削除可能。
    // Model は ApplicationRole
    public async Task<ActionResult> Delete(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      var target = await RoleManager.FindByIdAsync(id);

      if (target == null)
      {
        return HttpNotFound();
      }

      return View(target);
    }

    // POST: Roles/Delete/Id(指定 Id のロールを削除)
    // Model は ApplicationRole
    // 上の Delete(string id) と同シグネチャのメソッド
    // は定義できないので、メソッド名を変えて、下のよう
    // に ActionName("Delete") を設定する
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> 
                        DeleteConfirmed(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      var target = await RoleManager.FindByIdAsync(id);

      if (target == null)
      {
        return HttpNotFound();
      }

      // ユーザーがアサインされているロールも以下の一行で
      // 削除可能。内部で階層更新が行われているらしい。
      var result = await RoleManager.DeleteAsync(target);

      if (result.Succeeded)
      {
        // 削除に成功したら Roles/Index にリダイレクト
        return RedirectToAction("Index", "Roles");
      }

      // result.Succeeded が false の場合 ModelSate にエ
      // ラー情報を追加しないとエラーメッセージが出ない。
      AddErrors(result);

      // 削除に失敗した場合、削除画面を再描画
      return View(target);
    }


    // 以下は:
    // (1) UserWithRoles で、上の画像のように、登録済みユーザー
    //     の一覧と各ユーザーへのロールのアサイン状況を表示し、
    // (2) Edit ボタンクリックで EditRoleAssignment に遷移し、
    //     当該ユーザーへのロールのアサインを編集して保存する
    // ・・・ためのもの。

    // GET: Roles/UserWithRoles
    // ユーザー一覧と各ユーザーにアサインされているロールを表示
    // Model は上に定義した UserWithRoleInfo クラス
    public async Task<ActionResult> UserWithRoles()
    {
      var model = new List<UserWithRoleInfo>();

      // ToList() を付与してここで DB からデータを取得して
      // DataReader を閉じておかないと、下の IsInRole メソッド
      // でエラーになるので注意
      var users = UserManager.Users.
                  OrderBy(user => user.UserName).ToList();
      var roles = RoleManager.Roles.
                  OrderBy(role => role.Name).ToList();
            
      foreach (ApplicationUser user in users)
      {
        UserWithRoleInfo info = new UserWithRoleInfo();
        info.UserId = user.Id;
        info.UserName = user.UserName;
        info.UserEmail = user.Email;

        foreach (ApplicationRole role in roles)
        {
          RoleInfo roleInfo = new RoleInfo();
          roleInfo.RoleName = role.Name;
          roleInfo.IsInThisRole = await
              UserManager.IsInRoleAsync(user.Id, role.Name);
          info.UserRoles.Add(roleInfo);
        }
        model.Add(info);
      }

      return View(model);
    }

    // GET: Roles/EditRoleAssignment/Id
    // 指定 Id のユーザーのロールへのアサインの編集
    // Model は上に定義した UserWithRoleInfo クラス
    public async Task<ActionResult> 
                        EditRoleAssignment(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      var user = await UserManager.FindByIdAsync(id);

      if (user == null)
      {
        return HttpNotFound();
      }

      UserWithRoleInfo model = new UserWithRoleInfo();
      model.UserId = user.Id;
      model.UserName = user.UserName;
      model.UserEmail = user.Email;

      // ToList() を付与しておかないと下の IsInRole メソッド
      // で DataReader が閉じてないというエラーになる
      var roles = RoleManager.Roles.
                  OrderBy(role => role.Name).ToList();

      foreach (ApplicationRole role in roles)
      {
        RoleInfo roleInfo = new RoleInfo();
        roleInfo.RoleName = role.Name;
        roleInfo.IsInThisRole = await
            UserManager.IsInRoleAsync(user.Id, role.Name);
        model.UserRoles.Add(roleInfo);
      }

      return View(model);
    }

    // GET: Roles/EditRoleAssignment/Id
    // 指定 Id のユーザーのロールへのアサインの編集
    // Model は上に定義した UserWithRoleInfo クラス
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> 
      EditRoleAssignment(string id, UserWithRoleInfo model)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                            HttpStatusCode.BadRequest);
      }

      if (ModelState.IsValid)
      {
        IdentityResult result;

        foreach (RoleInfo roleInfo in model.UserRoles)
        {
          // id のユーザーが roleInfo.RoleName のロールに属して
          // いるか否か。以下でその情報が必要。
          bool isInRole = await 
            UserManager.IsInRoleAsync(id, roleInfo.RoleName);

          // roleInfo.IsInThisRole には編集画面でロールのチェッ
          // クボックスのチェック結果が格納されている
          if (roleInfo.IsInThisRole)
          {
            // チェックが入っていた場合

            // 既にロールにアサイン済みのユーザーを AddToRole
            // するとエラーになるので以下の判定が必要
            if (isInRole == false)
            {
              result = await UserManager.
                       AddToRoleAsync(id, roleInfo.RoleName);
              if (!result.Succeeded)
              {
                AddErrors(result);
                return View(model);
              }
            }
          }
          else
          {
            // チェックが入っていなかった場合

            // ロールにアサインされてないユーザーを
            // RemoveFromRole するとエラーになるので以下の
            // 判定が必要
            if (isInRole == true)
            {
              result = await UserManager.
                       RemoveFromRoleAsync(id, roleInfo.RoleName);
              if (!result.Succeeded)
              {
                AddErrors(result);
                return View(model);
              }
            }
          }                    
        }

        // 編集に成功したら Roles/UserWithRoles にリダイレクト
        return RedirectToAction("UserWithRoles", "Roles");
      }

      // 編集に失敗した場合、編集画面を再描画
      return View(model);
    }

    protected override void Dispose(bool disposing)
    {
      if (disposing)
      {
        if (_userManager != null)
        {
          _userManager.Dispose();
          _userManager = null;
        }

        if (_roleManager != null)
        {
          _roleManager.Dispose();
          _roleManager = null;
        }
      }

      base.Dispose(disposing);
    }

    // ModelSate にエラー情報を追加するためのヘルパメソッド
    private void AddErrors(IdentityResult result)
    {
      foreach (var error in result.Errors)
      {
        ModelState.AddModelError("", error);
      }
    }
  }
}

MSDN ライブラリの UserManager<TUser> クラスRoleManager<TRole, TKey> クラスの説明には非同期版メソッドしか書いてないのでそれらを使っていますが、同期版も拡張メソッドとして定義されているようです。ケースバイケースでどちらが適切かを考えて使い分けた方が良いかもしれません。

View はコントローラをベースにスキャフォールディング機能を利用して自動生成できます。Visual Studio でアクションメソッドのコードを右クリックして[ビューを追加(D)...]を選ぶと、そのためのダイアログが表示されます。

アクションメソッド EditRoleAssignment に対応する View は自動生成されるコードだけでは不十分でいろいろ追加・修正が必要です。以下にその完成版のコードを載せておきます。

@model Mcv5App2.Controllers.UserWithRoleInfo

@{
  ViewBag.Title = "EditRoleAssignment";
  Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>EditRoleAssignment</h2>


@using (Html.BeginForm())
{
  @Html.AntiForgeryToken()
    
  <h4>UserWithRoleInfo</h4>
  <hr />
  @Html.ValidationSummary("",new { @class="text-danger" })
  <table class="table">
    <tr>
      <th>
        @Html.DisplayNameFor(model => model.UserName)
      </th>
    @foreach (var role in Model.UserRoles)
    {
      <th>
        @Html.DisplayFor(modelItem => role.RoleName)
      </th>
    }
    </tr>
            
    <tr>
      <td>
        @Html.DisplayFor(model => model.UserName)
        @Html.HiddenFor(model => model.UserName)
        @Html.HiddenFor(model => model.UserEmail)
        @Html.HiddenFor(model => model.UserId)
      </td>
    @for (int i = 0; i < Model.UserRoles.Count; i++)
    {
      <td>
        @Html.EditorFor(model =>
                     model.UserRoles[i].IsInThisRole)
        @Html.HiddenFor(model => 
                     model.UserRoles[i].RoleName)
      </td>
    }
                    
    </tr>
  </table>
  <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>
  @Html.ActionLink("Back to List", "UserWithRoles")
</div>

@section Scripts {
  @Scripts.Render("~/bundles/jqueryval")
}

上の View のコードで注意すべき点は、コレクション(上の例では Model.UserRoles で取得できる RoleInfo クラスのコレクション) についてはレンダリングされる html 要素の name 属性が連番のインデックスを含むようにすることです。具体的には、name="prefix[index].Property" というパターンにします。

そうしないとモデルバインディングがうまくいきません。その理由など詳しくは先の記事「コレクションのデータアノテーション検証」を見てください。

Tags: ,

MVC

ASP.NET Identity のユーザー管理 (MVC)

by WebSurfer 2017年10月29日 18:10

ASP.NET Identity ベースのフォーム認証を実装した ASP.NET MVC5 アプリケーションで、管理者がユーザー情報の追加、変更、削除を行う機能を実装する例を書きます。

ユーザー管理画面

CodeZine の記事「ASP.NET Identity でユーザーを管理する」に、ASP.NET Web Forms アプリ用のユーザー管理画面を作る手順が載っていますが、その MVC 版です。

ベースとなるのは、Visual Studio 2015 Community で MVC のテンプレートを利用し、認証に「個別のユーザーアカウント」を指定して自動生成させたプロジェクトです。設定は以下の画像を見てください。

MVC のテンプレート

上の設定で自動生成されたプロジェクトは ASP.NET Identity ベースのフォーム認証を使用する ASP.NET MVC5 アプリケーションの基本的機能を持ちます。

プロジェクトの作成後、Visual Studio で[デバッグの開始(S)]または[デバッグなしで開始(H)]で実行すると、Web サーバーとして IIS Express が立ち上がって web アプリが実行され、テンプレートに含まれている Home/Index がブラウザ(デフォルトで IE)に表示されます。

そこから[Register]をクリックしてユーザー登録を行うと、初回に EF Code First と LocalDB の機能を利用してユーザー情報のストア(.mdf ファイル)が App_Data フォルダに生成され、その後は登録した Email とパスワードでログイン可能になります。

その状態から、Web アプリケーションに管理者がユーザー情報の追加、変更、削除を行う機能を実装します。

ユーザー情報の追加、変更、削除を行うのに用いるクラスは以下の通りです。いずれもテンプレートから自動生成されたコードに完全な形で含まれているので、それらをそのまま利用します。

(1) Models/IdentityModels.cs

(2) App_Start/IdentityConfig.cs

また、App_Start/Startup.Auth.cs の Startup クラスで、ApplicationUserManager のインスタンスを OwinContext へ登録するコードも自動生成されます。

なので、Controller では HttpContext.GetOwinContext() で OwinContext を取得し、それから GetUserManager<ApplicationUserManager>() メソッドで ApplicationUserManager オブジェクトを取得できるようになっています。

それらを利用してユーザー情報の追加、変更、削除を行う Controller の実装例を以下に示します。説明はコード内のコメントに書きましたので、それを見てください。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

// 上は自動生成されたもの。それに以下の名前空間を追加
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Mcv5App2.Models;
using System.Threading.Tasks;
using System.Net;
using System.ComponentModel.DataAnnotations;

namespace Mcv5App2.Controllers
{
  // Edit 用に AccountViewModels.cs の既存の RegisterViewModel
  // を流用しようとしたが、パスワードを入力しない場合は検証に
  // 引っかかっるので、以下の EditViewModel を定義して利用。
  // (Controller 内に定義したのは単に分けるのが面倒だから)
  public class EditViewModel
  {
    [Required]
    [EmailAddress]
    [Display(Name = "Email")]
    public string Email { get; set; }

    // PasswordValidator を使えばここでの検証は不要
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [System.ComponentModel.DataAnnotations.Compare(
     "Password", 
     ErrorMessage = 
     "Password と Confirm Password が一致しません")]
    public string ConfirmPassword { get; set; }
  }


  // ここから Controller のコード
  public class UsersController : Controller
  {
    private ApplicationUserManager _userManager;

    public ApplicationUserManager UserManager
    {
      get
      {
        return _userManager ?? 
            HttpContext.GetOwinContext().
            GetUserManager<ApplicationUserManager>();
      }
      private set
      {
        _userManager = value;
      }
    }

    // GET: Users(登録済みユーザーの一覧)
    // Model は ApplicationUser
    public ActionResult Index()
    {
      var users = UserManager.Users.
                OrderBy(user => user.UserName);
      return View(users);
    }

    // GET: Users/Details/Id(指定 Id のユーザー詳細)
    // Model は ApplicationUser
    public async Task<ActionResult> Details(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                        HttpStatusCode.BadRequest);
      }

      var target = await UserManager.FindByIdAsync(id);

      if (target == null)
      {
        return HttpNotFound();
      }

      return View(target);
    }

    // GET: Users/Create(新規ユーザー作成・登録)
    // Model は AccountViewModels.cs の RegisterViewModel
    public ActionResult Create()
    {
      return View();
    }

    // POST: Users/Create(新規ユーザー作成・登録)
    // Model は AccountViewModels.cs の RegisterViewModel
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> 
                        Create(RegisterViewModel model)
    {
      // PasswordValidator による判定の結果は ModelState
      // には反映されないので注意(下記 result で判定)
      if (ModelState.IsValid)
      {
        // ユーザー入力のメールアドレスを UserName, Email
        // プロパティに設定して ApplicationUser を生成
        var user = new ApplicationUser {
                            UserName = model.Email,
                            Email = model.Email };

        // 上の ApplicationUser とユーザー入力のパスワ
        // ードで新規ユーザーを作成・登録
        var result = await UserManager.
                     CreateAsync(user, model.Password);

        // ユーザー作成・登録の成否を result.Succeeded で
        // 判定。PasswordValidator の判定結果も result に
        // 反映される
        if (result.Succeeded)
        {
          // 登録に成功したら Users/Index にリダイレクト
          return RedirectToAction("Index", "Users");
        }
        // result.Succeeded が false の場合 ModelSate にエ
        // ラー情報を追加しないとエラーメッセージが出ない。
        // AccountController と同様に AddErrors メソッドを
        // 定義して利用(一番下に定義あり)
        AddErrors(result);
      }

      // ユーザー登録に失敗した場合、登録画面を再描画
      return View(model);
    }

    // GET: Users/Edit/Id(指定 Id のユーザー情報の更新)
    // ここで更新できるのは UserName, Email, パスワード
    // のみ。
    // Model は上に定義した EditViewModel
    public async Task<ActionResult> Edit(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                        HttpStatusCode.BadRequest);
      }

      var target = await UserManager.FindByIdAsync(id);

      if (target == null)
      {
        return HttpNotFound();
      }

      EditViewModel model = 
        new EditViewModel() { Email = target.Email };

      return View(model);
    }

    // POST: Users/Edit/Id(指定 Id のユーザー情報の更新)
    // ここで更新できるのは UserName, Email, パスワード
    // のみ。
    // UserName をソルトに使っていてパスワードだけもしくは
    // UserName だけを更新するのは NG かと思っていたが問題
    // なかった。(実際どのように対処しているかは不明)
    // Model は上に定義した EditViewModel
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> 
                  Edit(string id, EditViewModel model)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                        HttpStatusCode.BadRequest);
      }

      if (ModelState.IsValid)
      {
        var target = await UserManager.FindByIdAsync(id);
        target.Email = model.Email;
        target.UserName = model.Email;

        // ユーザーが新パスワードを入力した場合はパス
        // ワードも更新する
        if (!string.IsNullOrEmpty(model.Password))
        {
          // PasswordValidator による検証
          var resultPassword = 
              await UserManager.PasswordValidator.
                    ValidateAsync(model.Password);

          if (resultPassword.Succeeded)
          {
            // 検証 OK の場合、入力パスワードを hash
            var hashedPassword = 
                UserManager.PasswordHasher.
                HashPassword(model.Password);
            target.PasswordHash = hashedPassword;
          }
          else
          {
            // 検証 NG の場合 ModelSate にエラー情報を
            // 追加して編集画面を再描画
            AddErrors(resultPassword);
            return View(model);
          }
        }

        var resultUpdate = 
          await UserManager.UpdateAsync(target);

        if (resultUpdate.Succeeded)
        {
          // 更新に成功したら Users/Index にリダイレクト
          return RedirectToAction("Index", "Users");
        }
        AddErrors(resultUpdate);
      }

      // 更新に失敗した場合、編集画面を再描画
      return View(model);
    }

    // GET: Users/Delete/Id(指定 Id のユーザーを削除)
    // 階層更新が行われているようで、ロールがアサインされて
    // いるユーザーも削除可能。
    // Model は ApplicationUser
    public async Task<ActionResult> Delete(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                        HttpStatusCode.BadRequest);
      }

      var target = await UserManager.FindByIdAsync(id);

      if (target == null)
      {
        return HttpNotFound();
      }

      return View(target);
    }

    // POST: Users/Delete/Id(指定 Id のユーザーを削除)
    // Model は ApplicationUser
    // 上の Delete(string id) と同シグネチャのメソッド
    // は定義できないので、メソッド名を変えて、下のよう
    // に ActionName("Delete") を設定する
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> 
                        DeleteConfirmed(string id)
    {
      if (id == null)
      {
        return new HttpStatusCodeResult(
                        HttpStatusCode.BadRequest);
      }

      var target = await UserManager.FindByIdAsync(id);

      if (target == null)
      {
        return HttpNotFound();
      }

      // ロールがアサインされているユーザーも以下の一行で
      // 削除可能。内部で階層更新が行われているらしい。
      var result = await UserManager.DeleteAsync(target);

      if (result.Succeeded)
      {
        // 削除に成功したら Users/Index にリダイレクト
        return RedirectToAction("Index", "Users");
      }
      AddErrors(result);

      return View(target);
    }

    protected override void Dispose(bool disposing)
    {
      if (disposing)
      {
        if (_userManager != null)
        {
          _userManager.Dispose();
          _userManager = null;
        }
      }

      base.Dispose(disposing);
    }

    // ModelSate にエラー情報を追加するためのヘルパメソッド
    private void AddErrors(IdentityResult result)
    {
      foreach (var error in result.Errors)
      {
        ModelState.AddModelError("", error);
      }
    }
  }
}

上のコードは MSDN ライブラリに書いてある非同期版のメソッドを使っていますが、同期版も拡張メソッドとして定義されているようです。ケースバイケースでどちらが適切かを考えて使い分けた方が良いかもしれません。

View はコントローラをベースにスキャフォールディング機能を利用して自動生成できます。Visual Studio でアクションメソッドのコードを右クリックして[ビューを追加(D)...]を選ぶと、以下の画像のダイアログが表示されます。

View の作成

ここで Template と Model class を設定して[Add]ボタンをクリックすれば View は自動生成されます。

Data context class は必ず空白にしてください。余計な設定をすると余計なコードが自動生成されて "Multiple object sets per type are not supported." というエラーになると思います。

Tags:

MVC

スキャフォールディング機能

by WebSurfer 2017年7月23日 18:41

既存のデータベースから Entity Data Model (EDM) を作り、スキャフォールディング機能を使って Create, Read, Update, Delete (CRUD) 操作を行うための ASP.NET MVC アプリを作る手順を備忘録として書いておきます。

完成した Crate 画面

上の画面は作成した ASP.NET MVC アプリの編集 (Create) 画面を IE11 から呼び出して表示したものです。

この記事では、既存のデータベースとして Microsoft のサンプルデータベース Northwind を使用し、その中の Products テーブルの CRUD 操作を行うことにします。

大まかな手順は以下の通りです。

  1. SQL Server にサンプルデータベース Northwind をアタッチして接続できるようにしておく。(LocalDB も使えます。詳しくは先の記事「LocalDB で Northwind と Pubs を利用」を参照)
  2. Visual Studio Community 2015 のテンプレートを使用して ASP.NET MVC5 プロジェクトを生成。
  3. Visual Studio のデザイナを利用して、Northwind データベースから EDM を生成してプロジェクトに追加する。
  4. 追加した EDM をベースに、スキャフォールディング機能を使って CRUD 操作用の Controller と View を自動生成する。

以下に、Visual Studio で上の操作を行った際に表示された画像を貼って要点を書いておきます。自力では一行もコードを書くことなく CRUD 操作が可能な ASP.NET MVC アプリを自動生成できます。

(1) 新しい項目の追加

新しい項目の追加

プロジェクトに EDM を追加するため、ソリューションエクスプローラーでプロジェクトノードを右クリック⇒[追加(D)]⇒[新しい項目(W)...]をクリック。

(2) ADO.NET Entity Data Model を選択

ADO.NET Entity Data Model を選択

表示された「新しい項目の追加」ダイアログで[ADO.NET Entity Data Model]を選択し[追加(A)]をクリック。

(3) モデルのコンテンツの選択

モデルのコンテンツの選択

既存の SQL Server データベースから EDM を生成するため、[データベースから EF Designer]を選択し[次へ(N)]をクリック。

(4) データ接続の選択

データ接続

接続先の SQL Server データベースの指定、接続文字列等の設定を行うため[新しい接続(C)...]をクリック。

(注:上の画像で DefaultConnection というのは Visual Studio のテンプレートでプロジェクトを生成した時に自動的に設定される ASP.NET Identity 用のユーザー情報のストアに使用されるデータベースです)

(5) 接続のプロパティ設定

接続のプロパティ設定

自分の環境では MySQL がデフォルトで選択されているので、これを SQL Server に変更するため[変更(C)...]をクリック。デフォルトで SQL Server が選択されていれば不要な操作です。

(6) データソースの変更

データソースの変更

今回は SQL Server の既定の(または名前付き)インスタンスに接続するため[Microsoft SQL Server]を選択します。

(7) 接続のプロパティ設定

接続のプロパティ設定

[サーバー名(E)]にサーバー名を入力すると[データベース名の選択または入力(D)]にアタッチ済みのデーターベース一覧が表示されるので、その中から Northwind を選択し[OK]ボタンをクリック。

(注:この例では開発マシンにインストールした SQL Server 2008 Express に接続しています。デフォルトでインストールしたので sqlexpress という名前の名前付きインスタンスになっています)

(8) 接続文字列の確認と web.config への保存

接続文字列の確認と web.config への保存

(4) の画面に戻るので、接続文字列とその名前を確認し[次へ(N)]ボタンをクリック。

(9) データベースオブジェクトと設定の選択

データベースオブジェクトと設定の選択

今回の例では Products テーブルの CRUD 操作を行うので[テーブル]にチェックを入れ[完了(F)]ボタンをクリック。

(10) 生成された EDM

生成された EDM

無事 EDM が生成されると上のような画面が表示されます。ソリューションエクスプローラーの .edmx ファイルの下には DbContext を継承したコンテキストクラス、データベースの各テーブルを表すモデルクラスも自動生成されます。

(11) コントローラーの追加

コントローラーの追加

スキャフォールディング機能を利用して Controller と View を MVC アプリに追加します。ソリューションエクスプローラーで Controller フォルダを右クリック⇒[追加(D)]⇒[コントローラー(T)...]をクリック。

(12) スキャフォールディングを追加

スキャフォールディングを追加

上の手順で生成した EDM をベースに Controller と View を一式生成するには[MVC 5 Controller with Views, using Entiry Framework]を選択し[追加」ボタンをクリックします。

(13) コンテキストクラス、モデルクラスの指定

コンテキストクラス、モデルクラスの指定

(10) で生成されたコンテキストクラス、モデルクラスを指定します。今回の例ではコンテキストクラスは NORTHWINDEntities クラス、モデルクラスは Products テーブルの CRUD 操作を行うので Products クラスになります。

[Add]ボタンをクリックすると Controller と View が自動生成されプロジェクトに追加されます。


以上の操作は Visual Studio 2010 で MVC4 アプリを作る場合もほぼ同じですが、生成される EDM, Controller, View はかなり進化しているようです。主な点は以下の通りです。

  1. 生成される EDM は EF6 の DbContext ベース。そのためか、Code First で作ったモデルと DB First で作ったモデルは共存可能になっている。(Visual Studio 2010 ではデフォルトで EF4 の ObjectContext ベース。Code First で作ったモデルとは同じプロジェクト内では共存不可)
  2. 一覧 (Index) 画面には CatrgoryID, SupplierID(数字)ではなく、ナビゲーションプロパティをたどって CategoryName, CompanyName を取得してそれが表示される。さらに、Linq 式に Include メソッドを使いプロパティを先読みするという配慮もされている。
  3. 新規作成 (Create)、編集 (Edit) 画面では CatrgoryID, SupplierID にドロップダウンリストを使用して CategoryName, CompanyName が表示され、ユーザーは数字ではなくそれに該当する名前を見て選択できる。
  4. View には @Html.AntiForgeryToken()、Controller のアクションメソッドには ValidateAntiForgeryToken 属性が自動的に付与される。
  5. オーバーポスティングアタック対策として引数にホワイトリスト(Bind 属性)が自動的に付与される。

Tags: ,

MVC

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  2017年12月  >>
262728293012
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar