WebSurfer's Home

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

SimpleInjector を ASP.NET MVC & Web API で利用

by WebSurfer 2019年10月19日 15:40

以下の画像の通り、Visual Studio Community 2015 の .NET 4.6.1 テンプレートでプロジェクトを作って、ASP.NET MVC と Web API の両方が実装された Web アプリケーションに SimpleInjector を実装してみました。

プロジェクト作成

その話を備忘録として書いておきます。元の話は Teratail のスレッド「SimpleInjectorを使って、ASP.net MVC | WebAPI の両方のコントローラーを持つWebアプリケーションを動作させたい。」です。

上にも書きましたが、プロジェクトは MVC と Web API の両方を実装しており、それらの両方の Controller に Dependency Injection (DI) を行う必要があります。さらに、ユーザー認証は「個別のユーザーアカウント」すなわち ASP.NET Identity を利用しており、自動生成される AccountController.cs の Controller にも DI を行うように設定する必要があります。

以下に、Web API, MVC, AccountControlle の順に実装方法を書きます。

(1) Web API の Controller

ネットの記事「【連載】ASP.NET Web API を使おう:第4回 Simple Injector(DIコンテナ)を適用する」を参考に Web API 用の SimpleInjector の NuGet パッケージ「SimpleInjector.Integration.WebApi.WebHost.QuickStart」を追加します。

SimpleInjector の DI コンテナは Controller のコンストラクタが 1 つだけでないと Controller を初期化できないようで、NuGet の適用で自動生成される SimpleInjectorWebApiInitializer クラスの container.Verify() メソッドでそれをチェックしています。

実際、上のテンプレートで自動生成される AccountController には 2 つコンストラクタが定義されているので、そのままでは container.Verify() でエラーになります。

とりあえずは認証は使わなくても Web API の Controller への DI の実装はできますので、AccountController の引数のある方のコンストラクタをコメントアウトすると container.Verify() は通りますのでそうしておきます。

上に紹介したネットの記事のように実装すると、Web API の Controller の初期化の際、DI コンテナが EmployeeRepository クラスを初期化して、Controller のコンストラクタに設定した IEmployeeRepository 型の引数を通じて渡します。すなわち、Inject する Dependency というのは EmployeeRepository クラスのインスタンスということになります。(DI の Dependency を「依存性」と訳すのは誤訳かも)

以上の設定で、ブラウザから jQuery ajax などで API を呼び出せば、期待した応答が返ってきます。

ここまでは、Web API に SimpleInjector を追加しただけです。MVC に追加するにはどうするかというのを次に書きます。

(2) MVC の Controller

MVC の場合も、上記の Web API と同様、Controller の初期化の際、DI コンテナが EmployeeRepository クラスを初期化して、Controller のコンストラクタに設定した IEmployeeRepository 型の引数を通じて渡せるようにしてみます。

まず、SimpleInjector の設定ですが、それについては stackoverflow の記事「Simple Injector initialize for both MVC and Web API controllers」に以下の記述がありました。

"we should use both .RegisterMvcControllers() and RegisterWebApiControlers() as well as both System.Web.Mvc.DependencyResolver(new SimpleInjectorDependencyResolver(container)) and GlobalConfiguration.Configuration.DependencyResolver(new SimpleInjectorWebApiDependencyResolver(container))"

具体的にどのようにしたかと言うと:

  1. まず、NuGet パッケージの SimpleInjector.Integration.Web.Mvc を追加インストール。
  2. 上の stacloverflow の記事を参考に、Web API 用に自動生成済みの SimpleInjectorWebApiInitializer に MVC 用の container.RegisterMvcControllers(...) と DependencyResolver.SetResolver(...) を追加。
  3. container.Options の DefaultScopedLifestyle の設定を AsyncScopedLifestyle() から WebRequestLifestyle() に変更。

    (SimpleInjector サイトの記事「Async Scoped lifestyle vs. Web Request lifestyle」に書いてある通り、MVC と Web API 混合プロジェクトでは WebRequestLifestyle を使うのが正解らしい)
  4. MVC に Controller を追加し、Web API の Controller と同様に、IEmployeeRepository 型の引数を持つコンストラクタを一つだけ実装。

以上で MVC でも Web API でも SimpleInjector は動くようになりました。次は AccountController です。

(3) AccountController.cs の Controller

これの対応が問題で、一筋縄ではいきませんでした。(笑)

まず、MVC 5 template (ASP.NET Identity 2.0) combined with Simple InjectorSimple Injector with ASP.NET MVC 5 Identity 2.0 を参考に手を加えてみました。

後者の記事によると前者の記事のコードは "it’s based on an older version of Simple Injector, so some syntax changes were required." とのこと故、SimpleInjectorInitializer クラスは主に後者の記事のコードを参考にして実装しました。

しかし、上記の対応だけでは動かなくて、AccountController のコンストラクタにある ISecureDataFormat<AuthenticationTicket> 型の引数が登録されてないというエラーが出ます。

それについては、stackoverflow の記事 Simple Injector and default AccountContoller dependency issue の回答を参考に 4 行追加して対処しました。

それで 100% OK かは分かりませんが、登録・ログイン・ログアウトは問題なくできることだけは確認しました。それ以外の External Login などがどうかは未検証・未確認です。

登録・ログイン画面

上の画像の確認用の画面は、Microsoft のドキュメント「個々のアカウントと ASP.NET Web API 2.2 でのローカル ログインを使用して Web API をセキュリティで保護します」のサンプルコードから借用したものです。AcountController の API に対して jQuery ajax を利用して要求を送信し、登録・ログイン・ログアウトの操作を行います。

最後に、上記の対応済みの SimpleInjectorWebApiInitializer のコードを下に貼っておきます。

手を加える必要があるのは SimpleInjectorWebApiInitializer のコードだけではなく、MVC 5 template (ASP.NET Identity 2.0) combined with Simple Injector に書いてある通り他に何か所もありますので注意してください。

// Startup.cs で初期化するので以下の属性はコメントアウト
//[assembly: WebActivator.PostApplicationStartMethod(
//  typeof(WebApi2.App_Start.SimpleInjectorWebApiInitializer), 
//  "Initialize")]

namespace WebApi2.App_Start
{
  using System.Web.Http;
  using SimpleInjector;
  using SimpleInjector.Integration.WebApi;
  using SimpleInjector.Lifestyles;
  using WebApi2.Models;

  // MVC 用に追加
  using System.Web.Mvc;
  using SimpleInjector.Integration.Web;
  using SimpleInjector.Integration.Web.Mvc;
  using System.Reflection;

  // AccountController に Inject できるように追加
  using Microsoft.AspNet.Identity;
  using Microsoft.AspNet.Identity.EntityFramework;
  using Microsoft.AspNet.Identity.Owin;
  using Microsoft.Owin.Security.DataProtection;
  using Owin;
  using Microsoft.Owin;
  using System.Collections.Generic;
  using System.Web;
  using Microsoft.Owin.Security;
  using Microsoft.Owin.Security.DataHandler;
  using Microsoft.Owin.Security.DataHandler.Serializer;
  using Microsoft.Owin.Security.DataHandler.Encoder;

  public static class SimpleInjectorWebApiInitializer
  {
    // 自動生成された public static void Initialize() { ... }
    // は以���に差し替え。
    public static Container Initialize(IAppBuilder app)
    {
      var container = GetInitializeContainer(app);

      container.Verify();

      // Web API 用
      GlobalConfiguration.Configuration.DependencyResolver = 
        new SimpleInjectorWebApiDependencyResolver(container);

      // MVC 用
      DependencyResolver.SetResolver(
          new SimpleInjectorDependencyResolver(container));

      return container;
    }

    public static Container GetInitializeContainer(
                                             IAppBuilder app)
    {
      var container = new Container();

      container.Options.DefaultScopedLifestyle = 
          new WebRequestLifestyle();

      container.RegisterInstance(app);

      container.Register<ApplicationUserManager>(
                                            Lifestyle.Scoped);

      // 接続文字列名は "DefaultConnection" で固定したので不要
      container.Register(
          () => new ApplicationDbContext(), Lifestyle.Scoped);

      container.Register<IUserStore<ApplicationUser>>(
          () => new UserStore<ApplicationUser>(
              container.GetInstance<ApplicationDbContext>()), 
          Lifestyle.Scoped);

      container.RegisterInitializer<ApplicationUserManager>(
          manager => InitializeUserManager(manager, app));

      // SignInManager は使ってないのでコメントアウト
      //container.Register<SignInManager<ApplicationUser,string>, 
      //    ApplicationSignInManager>(Lifestyle.Scoped);

      container.Register(
          () => container.IsVerifying ? 
              new OwinContext(new Dictionary<string, object>()).
                        Authentication
              : 
              HttpContext.Current.GetOwinContext().
                  Authentication, 
          Lifestyle.Scoped);

      InitializeContainer(container);

      // Web API 用
      container.RegisterWebApiControllers(
                GlobalConfiguration.Configuration);

      // MVC 用
      container.RegisterMvcControllers(
                Assembly.GetExecutingAssembly());

      return container;
    }

    private static void InitializeUserManager(
            ApplicationUserManager manager, IAppBuilder app)
    {
      manager.UserValidator = 
          new UserValidator<ApplicationUser>(manager)
      {
        AllowOnlyAlphanumericUserNames = false,
        RequireUniqueEmail = true
      };

      // Configure validation logic for passwords
      manager.PasswordValidator = new PasswordValidator
      {
        RequiredLength = 6,
        RequireNonLetterOrDigit = true,
        RequireDigit = true,
        RequireLowercase = true,
        RequireUppercase = true,
      };

      var dataProtectionProvider = 
                app.GetDataProtectionProvider();

      if (dataProtectionProvider != null)
      {
        manager.UserTokenProvider = 
            new DataProtectorTokenProvider<ApplicationUser>(
                dataProtectionProvider.Create(
                    "ASP.NET Identity"));
      }
    }

    private static void InitializeContainer(Container container)
    {
      // AccountController のコンストラクタの引数の
      // ISecureDataFormat<AuthenticationTicket> 型の登録

      container.Register<ISecureDataFormat<AuthenticationTicket>, 
        SecureDataFormat<AuthenticationTicket>>(Lifestyle.Scoped);

      container.Register<IDataSerializer<AuthenticationTicket>,
          TicketSerializer>(Lifestyle.Scoped);

      container.Register<ITextEncoder, Base64UrlTextEncoder>(
                                               Lifestyle.Scoped);

      container.Register<IDataProtector>(() =>
          new DpapiDataProtectionProvider().
              Create("ASP.NET Identity"), Lifestyle.Scoped);

      // 以下は自分でコーディングした MVC と Web API の
      // Controller に注入するためのもの
      container.Register<IEmployeeRepository, EmployeeRepository>
          (Lifestyle.Scoped);            
    }
  }
}

Tags: , , ,

Web API

MVC でファイルのアップロード

by WebSurfer 2019年8月3日 15:58

.NET Framework 版の MVC5 アプリでファイルをアップロードする方法について書きます。(Core 3.1 版は別の記事「ASP.NET Core MVC でファイルアップロード」に書きましたのでそちらを見てください。)

MVC でファイルのアップロード

普通に form を submit して POST 送信する場合と、jQuery Ajax を利用して非同期で送信する場合の両方の例を紹介します。ちなみに、上の画像は jQuery Ajax を使ってアップロードした結果です。

本題に入る前に、Microsoft の記事「ASP.NET Core でファイルをアップロードする」の「セキュリティの考慮事項」のセクションを一読することをお勧めします。

この記事では上に紹介した Microsoft の記事に書かれたセキュリティ関する配慮がされていませんので注意してください。例えば「アプリと同じディレクトリツリーに、アップロードしたファイルを保持しないでください」とありますが、この記事ではアプリケーションルート直下の UploadedFiles というフォルダに、アップロードされたファイルをチェックせず、ユーザーによって指定されたファイル名でそのまま保存するコードになっています。

この記事ではセキュリティの話はちょっと置いといて、単純に技術的にどうするかということを書きます。気をつけるべき点は以下の通りです。

  1. View では form 要素の enctype 属性に "multipart/form-data" が設定されるようにする。
  2. Controller のアクションメソッドでは、アップロードされたファイルがバインドされるパラメータまたはクラスのプロパティは HttpPostedFileBase 型であること。  
  3. 上で述べたバインドされるパラメータまたはクラスのプロパティの名前は、html ソースの <input type="file" ... /> の name 属性と一致させる。
  4. Internet Explorer (IE) でファイルをアップロードすると、クライアント PC でのフルパスがファイル名として送信されることがある(先の記事「IE でアップロードする際のファイル名」を参照)。その場合、HttpPostedFileBase.FieName でファイル名を取得するとクライアント PC でのフルパスになるので、必ず Path.GetFileName を使うこと。  
  5. ワーカープロセスがアップロードするホルダに対する「書き込み」権限を持っていること。
  6. ASP.NET では、デフォルト設定では 4MB を超えるリクエストは送信できないので注意。4MB を超える場合は、web.config の <httpRuntime> セクションの maxLengthRequest の設定で調整できる。

jQuery Ajax を使ってファイルをアップロードする場合は、上記に加えて以下の点に要注目です。

  1. XMLHttpRequest を使用して送信するためのキーと値のペアのセットを取得するために FormData オブジェクトを利用する。詳しくは MDN の記事「FormData オブジェクトの利用」にありますのでそちらを参照してください。
  2. ASP.NET MVC 組み込みの CSRF 防止機能は Ajax でもそのまま使えますので、View での @Html.AntiForgeryToken() と Controller のアクションメソッドへの [ValidateAntiForgeryToken] を忘れずに設定する。

上の画像を表示するのに使ったコードを以下に書いておきます。

Model

public class UploadModels
{
    public string CustomField { get; set; }
    public HttpPostedFileBase PostedFile { get; set; }
}

View

@model Mvc5App.Controllers.UploadModels

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

<h2>Upload</h2>

@using (Html.BeginForm("Upload", "Home", FormMethod.Post,
                new { enctype = "multipart/form-data" }))
{
    // form 内の隠しフィールドは Ajax でも送信される。
    // なので以下に設定したトークンは送信される。もちろん
    // クッキーのトークンも送信されるので、アクションメソ
    // ッドに [ValidateAntiForgeryToken] を付与すれば
    // CSRF の検証はできる
    @Html.AntiForgeryToken()

    // name 属性はモデルのクラスのプロパティ名と同じにしない
    // とサーバー側でモデルバインディングされないので注意。
    // 大文字小文字は区別しない。
    <input type="file" name="postedfile" />
    <button type="submit">Upload by Submit</button>
    <br />
    @ViewBag.Result
}
<br />
<input type="button" id="ajaxUpload" value="Ajax Upload" />
<br />

<div id="result"></div>


@section Scripts {
  <script type="text/javascript">
    //<![CDATA[
    $(function () {
      $('#ajaxUpload').on('click', function (e) {
        // FormData オブジェクトの利用
        var fd = new FormData(document.querySelector("form"));

        // 追加データを以下のようにして送信できる。フォーム
        // データの一番最後に追加されて送信される
        fd.append("CustomField", "This is some extra data");

        $.ajax({
          url: '/home/upload',
          method: 'post',
          data: fd,
          processData: false, // jQuery にデータを処理させない
          contentType: false  // contentType を設定させない
          }).done(function(response) {
            $("#result").empty;
            $("#result").text(response);
          }).fail(function( jqXHR, textStatus, errorThrown ) {
            $("#result").empty;
            $("#result").text('textStatus: ' + textStatus +
                            ', errorThrown: ' + errorThrown);
          });
      });
    });
    //]]>
  </script>
}

Controler / Action Method

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.ComponentModel.DataAnnotations;
using Mvc5App.Models;
using System.IO;

namespace Mvc5App.Controllers
{
  public class HomeController : Controller
  {
    public ActionResult Upload()
    {
      return View();
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Upload(UploadModels model)
    {
      string result = "";
      HttpPostedFileBase postedFile = model.PostedFile;
      if (postedFile != null && 
          postedFile.ContentLength > 0)
      {
        // アップロードされたファイル名を取得。ブラウザが IE 
        // の場合 postedFile.FileName はクライアント側でのフ
        // ルパスになることがあるので Path.GetFileName を使う
        string filename = 
                  Path.GetFileName(postedFile.FileName);

        //  保存先のホルダの物理パス\ファイル名
        string path = Server.MapPath("~/UploadedFiles") + 
                      "\\" + filename;

        // アップロードされたファイルを保存
        postedFile.SaveAs(path);

        result = filename + 
                 " (" + postedFile.ContentType + ") - " +
                 postedFile.ContentLength.ToString() + 
                 " bytes アップロード完了";
      }
      else
      {
        result = "ファイルアップロードに失敗しました";
      }

      if (Request.IsAjaxRequest())
      {
        return Content(result);
      }
      else
      {
        ViewBag.Result = result;
        return View();
      }
    }
  }
}

上の例は単一ファイルをアップロードする場合のものです。複数のファイルをアップロードする場合は、例えば html ソースで以下のように name 属性の値に連番で index を付与できれば、

<input type="file" name="postedfiles[0]" />
<input type="file" name="postedfiles[1]" />
 ・・・中略・・・
<input type="file" name="postedfiles[n]" />

または、ブラウザが multiple 属性をサポートしていれば以下のようにして、

<input type="file" name="postedfiles" multiple="multiple" />

Model のクラスを以下のようにすれば PostedFiles にモデルバインドできます。

public class UploadModels
{
    public string CustomField { get; set; }
    public IList<HttpPostedFileBase> PostedFiles { get; set; }
}

Tags: , , ,

Upload Download

ASP.NET MVC の勉強を始めました

by WebSurfer 2011年4月16日 21:51
ASP.NET MVC 実践プログラミング

ASP.NET MVC の勉強を始めました。

リリースされた当初は一時の流行だろうと思っていましたが、バージョン 3 がリリースされるに至って、どうもそうではなさそうな感じがします。

というわけで、時代に取り残されてはいけないと思って(笑)買ったのが左の写真の本です。

ASP.NET(MVC ではない)のいいところは、.NET Framework ベースの Windows アプリケーションの開発経験者には親しみやすいところです。

実際、自分の場合、HTML や JavaScript の知識が全くなかったのですが、Windows アプリケーションを作るときのように Visual Studio でサーバーコントロールをドラッグ&ドロップしてページに貼り付け、ウィザードを使ってプロパティを設定すれば、とりあえず動く Web アプリケーションは簡単に作成できました。

しかし、世の中の主流らしい PHP ベースの Web アプリケーションの開発者にとっては、サーバーコントロール、ポストバック、イベントドリブン、ビューステートなどの ASP.NET 固有の概念は馴染みにくいものだそうです。

それらが使えるから、ASP.NET(MVC ではない)には高い生産性があるのですが、その生産性を捨てても ASP.NET MVC を選ぶだけの理由があるかもしれないと期待しています。

ASP.NET(MVC ではない)の知識も十分でない自分が、余計なものに手を出すべきではないのかもしれませんけどね。

まぁ、Web アプリケーション開発でメシを食っているわけではない自分にとっては所詮趣味の問題に過ぎないので、自己満足でいいんです。(笑)

Tags:

MVC

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar