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

HttpClient で ASP.NET Web API にアクセス

by WebSurfer 2019年9月29日 12:44

Web API に HttpClient を使ってアクセスし、認証トークンを取得して、JSON 形式のデータを POST 送信するサンプルを書きます。

HttpClient で ASP.NET Web API にアクセス

アクセス先の Web API は先の記事「ASP.NET MVC に Web API 追加」に書いたもので、具体的には、Visual Studio 2015 のテンプレートで生成した既存の MVC5 プロジェクトに、別途 Web API プロジェクトを作って必要なパッケージ、コードを追加したものです。

既存の MVC5 プロジェクトは ASP.NET Identity を利用してクッキーベースのユーザー認証を行っています。

追加した Web API のユーザー認証はクッキーベースとするのではなく、Web API で推奨されているトークンベースとしています。

MVC5 側はクッキーベースで、Web API 側はトークンベースで独立して認証が働きます。なお、ユーザ情報はどちらも ASP.NET Identity から得ています。

その他、運用上は SSL の実装は必須ということで、SSL 通信を強制するためのフィルターを追加しています。

HttpClient を使ったアプリから Web API にアクセスするには Microsoft のドキュメント Call a Web API From a .NET Client (C#) で紹介されている Microsoft.AspNet.WebApi.Client を使うのが便利だそうですが、ここではそれは使わないで実装してみました。

基本的には先の記事「HttpClient で WCF サービスを呼出」の実装例とほぼ同じです。

ただし、認証トークンを取得するには、ユーザー情報を application/x-www-form-urlencoded 形式で POST しなければなりませんが、そこのところが上の記事とは異なります。

認証トークンを得るためには、先の記事「ASP.NET Web API の認証」で書きましたように、grant_type, username, password 情報をトークンエンドポイント /Token に POST します。

認証に成功すると access_token, token_type, expires_in, userName, .issued, .expires という情報が JSON 文字列として返ってきます。その中の access_token が認証トークンです。

Web API にアクセスする際は要求ヘッダに Authorization: Bearer に続けて空白一文字+有効な認証トークンを設定してやれば認証が通ります。

認証トークンの取得と JSON 形式のデータを POST 送信する Windows Forms アプリのサンプルコードは以下の通りです。説明はコードの中にコメントで入れましたので、それを見てください。

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Net.Http;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;

namespace WindowsFormsApplication2
{
    public partial class Form5 : Form
    {
        // socket 浪費防止のため static として使い回す
        private static HttpClient client;

        private string loginUrl = "トークン要求先 URL";
        private string apiUrl = "Web API の URL";
        private string email = "ユーザー ID";
        private string passsword = "パスワード";
        private int id = 6;
        private string name = "ガッチャマンの息子";

        // 認証トークン
        private string token = "";

        public Form5()
        {
            InitializeComponent();

            if (client == null)
            {
                client = new HttpClient();
            }

            this.textBox1.Text = email;
            this.textBox2.Text = passsword;
            this.textBox3.Text = id.ToString();
            this.textBox4.Text = name;
        }

        // 認証トークンの取得
        private async void button1_Click(object sender, 
                                                EventArgs e)
        {
            if (!string.IsNullOrEmpty(this.token)) return;

            // 認証に必要なユーザー情報は
            // application/x-www-form-urlencoded 形式で POST
            // 送信する。
            // そのために送信するユーザー情報から以下のように
            // Dictionary<string, string> オブジェクトを生成し、
            var param = new Dictionary<string, string>
            {
                { "grant_type", "password"},
                { "username", this.textBox1.Text },
                { "password", this.textBox2.Text }
            };

            // それを引数に FormUrlEncodedContent オブジェクト
            // を生成・初期化して PostAsync で送信する。
            // Content-Type: application/x-www-form-urlencoded
            // は自動的に設定されるとのこと
            var content = new FormUrlEncodedContent(param);
            var response = 
                 await client.PostAsync(this.loginUrl, content);

            // 応答コンテンツを Stream として取得
            using (Stream responseStream = 
                await response.Content.ReadAsStreamAsync())
            {
                // JSON シリアライザの初期化
                var ser = 
                  new DataContractJsonSerializer(typeof(Token));

                // 応答コンテンツを逆シリアル化して C# のオブジ
                // ェクトを取得
                Token auth = 
                         (Token)ser.ReadObject(responseStream);

                // 認証トークンを token フィールドに設定
                this.token = auth.access_token;
            }
        }

        // JSON 形式のデータを POST 送信
        private async void button2_Click(object sender, 
                                                EventArgs e)
        {
            if (string.IsNullOrEmpty(this.token)) return;

            // POST 送信を指定
            var request = 
                new HttpRequestMessage(HttpMethod.Post, apiUrl);

            // POST 送信する JSON 文字列
            string postData = "";

            // Hero オブジェクトを生成しそれを JSON 文字列に
            // シリアライズする
            Hero postHero = new Hero
            {
                Id = int.Parse(this.textBox3.Text),
                Name = this.textBox4.Text
            };

            // シリアライズは DataContractJsonSerializer を使う
            using (MemoryStream stream = new MemoryStream())
            {
                var ser = 
                  new DataContractJsonSerializer(typeof(Hero));
                ser.WriteObject(stream, postHero);
                stream.Position = 0;
                using (var reader = new StreamReader(stream))
                {
                    postData = reader.ReadToEnd();
                }
            }

            // Content-Type: application/json; charset=utf-8 が
            // 要求ヘッダに必要。それを POST 送信する JSON 文字
            // 列と共にここで設定
            request.Content = new StringContent(postData, 
                                            Encoding.UTF8, 
                                            "application/json");

            // 認証トークンを要求ヘッダに設定
            request.Headers.Add("Authorization", 
                                "Bearer " + this.token);

            // JSON 文字列を SendAsync で POST 送信する
            var response = await client.SendAsync(request);

            // 応答コンテンツを Stream として取得
            using (Stream responseStream = 
                await response.Content.ReadAsStreamAsync())
            {
                // JSON シリアライザの初期化
                var ser = new DataContractJsonSerializer(
                                           typeof(List<Hero>));

                // 応答コンテンツを逆シリアル化して C# のオブジ
                // ェクトを取得
                List<Hero> heros = 
                    (List<Hero>)ser.ReadObject(responseStream);

                string result = "";
                foreach (Hero hero in heros)
                {
                    result += string.Format("{0}: {1}\r\n", 
                                            hero.Id, hero.Name);
                }
                this.textBox5.Text = result;
            }
        }
    }

    // 以下のクラス定義を public partial class Form5 : Form の
    // 上に持ってくるとデザイン画面が開かないので注意

    // トークン要求に対し応答として返ってくるデータ
    // access_token, token_type, expires_in は OAuth2 で定めら
    // れているもの。userName は informational。他に .issued,
    // .expires というデータも返ってくるが . がプロパティの識
    // 別子として使えないので以下には設定しない
    [DataContract]
    public class Token
    {
        [DataMember]
        public string access_token { get; set; }

        [DataMember]
        public string token_type { get; set; }

        [DataMember]
        public int expires_in { get; set; }

        [DataMember]
        public string userName { get; set; }
    }

    // Web API に POST 送信するデータ
    [DataContract]
    public class Hero
    {
        [DataMember]
        public int Id { get; set; }

        [DataMember]
        public string Name { get; set; }
    }
}

上記のコードの実行結果がこの記事の上にある画像です。認証トークンを取得した後、Id と Name から作成した JSON 文字列を Web API に POST 送信し、返ってきた JSON 文字列を一番下のテキストボックスに表示したところです。

Tags: ,

Web API

ASP.NET Web API のバインディング (2)

by WebSurfer 2019年3月6日 14:47

ASP.NET Web API に、フォームデータ name=value の name に C# の識別子に使えない文字が含まれて POST されてくる場合、どのように value を取得するかを書きます。実際にそんなケースはなさそうなので、役に立たない情報かもしれませんが。(笑)

Web API のバインディング

一般的なバインディングの話は先の記事「ASP.NET Web API のバインディング」に書きました。その記事のコードの Post メソッドに "AAA:Id=6&AAA:Name=ガッチャマンの息子" という文字列がフォームデータとして送信されてくる場合を考えます。(注:当然、送信前に文字列は URL エンコードされます)

フォームデータが "AAA:Id=6" というように一つだけなら、例えば先の記事の Put の場合のように送信側で data:"=AAA:Id=6" として受信側で Put(int id, [FromBody] string name) の name に文字列として取得できますが、"AAA:Id=6&AAA:Name=ガッチャマンの息子" のように 2 つ以上あると組み込みのフォーマッター・モデルバインダーではむりそうです(何故か 1 つ目の "AAA:Id=6" しか取得できません)。

と言って、先の記事の例の Post(Hero postedHero) ようにアクションメソッドの引数をコンプレックス型にはできません。何故なら、コロン ':' は C# の識別子の文字としては使えませんので、AAA:Id や AAA:Name という名前のプロパティが定義できませんから。

ではどうするかと言うと、先の記事でも紹介した MSDN Blog「How WebAPI does Parameter Binding」の記事の最初のコードにあるように HttpRequestMessage を引数に持つメソッドで取得するのがよさそうです。

具体的には以下のコードのようにします。

public async Task<List<Hero>> Post(HttpRequestMessage request)
{
    HttpContent content = request.Content;
    string str = await content.ReadAsStringAsync();
    NameValueCollection formData = 
        await content.ReadAsFormDataAsync();
    string str1 = formData["AAA:Id"];
    string str2 = formData["AAA:Name"];
    Hero postedHero = 
        new Hero { Id = int.Parse(str1), Name = str2 };
    heroes.Add(postedHero);
    return heroes;
}

そうすれば、上の画像に示したように HttpContent.ReadAsFormDataAsync メソッドでフォームデータを取得できます。

クライアント側はボディに設定する文字列を URL エンコードすること、要求ヘッダに Content-Type: application/x-www-form-urlencoded; charset=UTF-8 を含めることを忘れないようにしてください。

Tags: , ,

Web API

About this blog

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

Calendar

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

View posts in large calendar