WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

ASP.NET MVC 用の CompareValidator

by WebSurfer 31. December 2018 12:06

ASP.NET Web Forms には CompareValidator という検証コントロールがあって、2 つの TextBox に入力された値の大小比較をして検証できる機能があります。

それと同様な検証機能を ASP.NET MVC で実装する方法を書きます。(元の話は Teratail のスレッド ASP.NET MVC4 TimeSpan型入力値の検証です)

MVC の CompareValidator

ASP.NET MVC には CompareAttribute という検証用のデータアノテーション属性がありますが、それには大小を比較する機能はなく、同じか否かを検証するのみです。

なので、例えば、ユーザーが開始時間と終了時間をテキストボックスに入力した際に、終了時間が開始時間より後になっていることを検証するような場合は、カスタム検証機能を実装することになります。

複数のプロパティにまたがった検証をする場合、Teratail のスレッド「MVC モデルのバリデーションについて」の回答に書いたように、CustomValidationAttribute を使うか、モデルに IValidatableObject インターフェイスを実装するという手段を使うのが普通のようです。

しかし、2 つのテキストボックス入力の大小比較程度なら、CustomValidationAttribute を使わなくても、リフレクションを使って比較対象のプロパティの値を取得し、それと自身のプロパティの値を比較して検証結果を返すということができます。

参考にしたのは Compare Validator in MVC という記事の Limitations of the Compare DataAnnotation 以下のセクションです。

上の画像を表示するのに使ったサンプルコードは以下の通りです。TimeSpan 型のプロパティを検証の対象としていますが、DataTime 型その他でもキャストを変更して同様に検証可能です。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using System.Globalization;

namespace Mvc5App.Models
{
  public class KintaiModel
  {
    public IList<Record> Records { get; set; }
  }

  public class Record
  {
    public int Id { set; get; }
    public string Week { set; get; }
    public TimeSpan Open { set; get; }

    [TimeGreaterThan("Open", 
                   "{0} は Open より後に設定してください")]
    public TimeSpan Close { set; get; }

    // ・・・中略・・・
  }

  [AttributeUsage(AttributeTargets.Property)]
  public class TimeGreaterThanAttribute : ValidationAttribute
  {
    private string _startTimePropertyName;

    // コンストラクタ
    public TimeGreaterThanAttribute(
            string startTimePropertyName, string errorMsg)
    {
      this._startTimePropertyName = startTimePropertyName;
      this.ErrorMessage = errorMsg;
    }

    public override string FormatErrorMessage(string name)
    {
      return string.Format(CultureInfo.CurrentCulture,
                           ErrorMessageString, name);
    }

    protected override ValidationResult IsValid (
        object value, ValidationContext validationContent)
    {
      // ObjectType はこの属性を付与したプロパティが属する
      // クラス、即ち上のコードの Record クラスとなる
      System.Reflection.PropertyInfo propertyInfo = 
          validationContent.ObjectType.
          GetProperty(this._startTimePropertyName);
      object propertyValue = 
          propertyInfo.
          GetValue(validationContent.ObjectInstance, null);

      if ((TimeSpan)value > (TimeSpan)propertyValue)
      {
        return ValidationResult.Success;
      }
      else
      {
        // ValidationResult の引数に null または "" を設定する
        // と FormatErrorMessage メソッドの戻り値が設定される
        return new ValidationResult(null);
      }
    }
  }
}

上の画像・コードではクラスのコレクションのモデルバインディングを行っていますが、そのような場合でもリフレクションを使って比較対象のクラスのプロパティを特定して検証可能です。ただ、リフレクションを使うということで、性能的にどうかという懸念は消せてませんが。

また、上のサンプルコードはサーバー側だけの検証で、クライアント側での JavaScript / jQuery による検証はかかりません。

単一のプロパティのカスタム検証であれば、@IT の記事の「第4回 検証属性の自作とクラス・レベルのモデル検証」のセクションに書かれているようにして、サーバー側とクライアント側両方の検証機能を実装できます。

しかし、今回のケースのように 2 つのプロパティにまたがった検証をする場合、スクリプトを ASP.NET MVC の検証システムの中に無理なく不整合なく取り込む具体例は少なくとも自分は見たことがないです。(自分が知らないだけだという可能性は否定しきれませんが)

Tags: ,

Validation

ASP.NET MVC に Web API 追加

by WebSurfer 22. December 2018 18:04

既存の ASP.MET MVC5 アプリに Web API 2.2 のコントローラを追加してみました。その方法を備忘録として書いておきます。需要はないかもしれませんが。(笑)

MVC + Web API プロジェクト

既存の MVC5 アプリは VS2015 のテンプレートで生成した .NET Framework v4.6.1 ベースの単独 MVC プロジェクトで、ASP.NET Identity を利用してクッキーベースの認証を行っています。

その既存の MVC5 アプリに Web API の機能を追加するのですが、Web API の認証は既存のクッキーベースとするのではなく、Web API で推奨されているトークンベースとします。その際、既存の MVC5 アプリが持つ ASP.NET Identity からユーザー情報を得てトークン認証を行うようにします。

基本的には、下の画像のように VS2015 の Web API テンプレートを使ってプロジェクトを新規に作り、それから必要な部分を既存の MVC プロジェクトに追加していくという感じです。

Web API プロジェクトの作成

Web API プロジェクトを作った後、以下の手順で、既存の MVC プロジェクトに必要なパッケージ、コードを追加します。

(1) SSL 有効化

開発環境でも SSL 通信下で検証ができるように、先の記事「IIS Express で SSL 通信」に従って IIS Express で SSL 通信を利用できるように設定します。

(2) NuGet パッケージのインストール

既存の MVC プロジェクトに Web API 関係の NuGet パッケージをインストールします。必要なパッケージは、Web API テンプレートで自動生成されたプロジェクトの NuGet パッケージの管理画面で「インストール済み」の WebApi 関係のパッケージを表示すると分かります。自分の環境では以下の 6 つでした。

  • Microsoft.AspNet.WebApi
  • Microsoft.AspNet.WebApi.Client
  • Microsoft.AspNet.WebApi.Core
  • Microsoft.AspNet.WebApi.WebHost
  • Microsoft.AspNet.WebApi.Owin
  • Microsoft.AspNet.WebApi.HelpPage

Microsoft.AspNet.WebApi を選んでインストールすると自動的に Client, Core, WebHost もインストールされます。Owin, HelpPage はその後で追加インストールしました。

(3) RequireHttpsAttribute の追加

SSL 通信を強制するためのフィルター RequireHttpsAttribute を追加します。(基本的な動作には影響ないのです��、実装しておいた方がよさそうですので)

まず、MVC 側ですが、MVC 5.2 以降であれば System.Web.Mvc 名前空間に RequireHttpsAttribute Class が用意されていますので、それを FilterConfig.cs で以下のように追加します。

public static void RegisterGlobalFilters(
                GlobalFilterCollection filters)
{
    filters.Add(new HandleErrorAttribute());

    // これを追加
    filters.Add(new RequireHttpsAttribute());
}

Web API 側では上記のフィルターは使えないので、カスタムフィルターを作ってそれを使うことになります。(MVC 用と Web API 用とではフィルターは違うので注意)

カスタムフィルターは、Microsoft の文書 Secure a Web API with Individual Accounts and Local Login in ASP.NET Web API 2.2 からリンクが張ってある GitHub のページ からダウンロードできるプロジェクトの Filters フォルダにサンプルがありましたので、それを借用しました。

そのカスタムフィルター RequireHttpsAttribute.cs のコードをそのまま載せておきます。(名前空間は自分のプロジェクトに合わせて変更しました)

using System;
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace Mvc5.Filters
{
  public class RequireHttpsAttribute : 
                            AuthorizationFilterAttribute
  {
    public int Port { get; set; }

    public RequireHttpsAttribute()
    {
      Port = 443;
    }

    public override void OnAuthorization(
                           HttpActionContext actionContext)
    {
      var request = actionContext.Request;

      if (request.RequestUri.Scheme != Uri.UriSchemeHttps)
      {
        var response = new HttpResponseMessage();

        if (request.Method == HttpMethod.Get || 
            request.Method == HttpMethod.Head)
        {
          var uri = new UriBuilder(request.RequestUri);
          uri.Scheme = Uri.UriSchemeHttps;
          uri.Port = this.Port;

          response.StatusCode = HttpStatusCode.Found;
          response.Headers.Location = uri.Uri;
        }
        else
        {
          response.StatusCode = HttpStatusCode.Forbidden;
        }

        actionContext.Response = response;
      }
      else
      {
        base.OnAuthorization(actionContext);
      }
    }
  }
}

このカスタムフィルターを有効にする方法は下の「(4) WebConfig.cs の追加」のセクションを見てください。

(4) WebConfig.cs の追加

Web API プロジェクトから WebConfig.cs をコピーして MVC プロジェクトの App_Start フォルダにコピーします。名前空間は自分のプロジェクトに合わせて変更してください。

Register メソッドに、上の「(3) RequireHttpsAttribute の追加」のセクションで用意した Web API 用のカスタムフィルターを有効化するため、以下のコードを追加します。

public static void Register(HttpConfiguration config)
{
  // ・・・中略・・・

  // カスタム RequireHttpsAttribute フィルターを追加
  config.Filters.Add(new Mvc5.Filters.RequireHttpsAttribute());
}

その後、WebConfig を Global.asax の Application_Start メソッドに登録します。

protected void Application_Start()
{
    // ・・・中略・・・

    // これを追加
    GlobalConfiguration.Configure(WebApiConfig.Register);
}

(5) UseOAuthBearerTokens 追加

トークン認証を有効にするため、Startup.Auth.cs の ConfigureAuth メソッドに以下のコードを追加します。コードは Web API テンプレートで作成したプロジェクトにありますので、それをコピーして修正すればいいです。

public partial class Startup
{
  // 追加
  public static OAuthAuthorizationServerOptions 
                          OAuthOptions { get; private set; }

  // 追加
  public static string PublicClientId { get; private set; }

  public void ConfigureAuth(IAppBuilder app)
  {

    // ・・・中略・・・

    // 追加
    PublicClientId = "self";
    OAuthOptions = new OAuthAuthorizationServerOptions
    {
      TokenEndpointPath = new PathString("/Token"),
      Provider = new ApplicationOAuthProvider(PublicClientId),

      // 不要と思われるのでコメントアウト(説明下記)
      //AuthorizeEndpointPath = 
      //        new PathString("/api/Account/ExternalLogin"),

      // デフォルトで 20 分。MVC 側に合わせて 14 日に設定
      AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),

      // SSL 強制のため false に設定
      AllowInsecureHttp = false
    };
    app.UseOAuthBearerTokens(OAuthOptions);

    // ・・・後略・・・

AllowInsecureHttp プロパティは SSL 通信強制のため false に設定してください。

ApplicationOAuthProvider は Web API テンプレートで作成したプロジェクトの Providers フォルダにあるものをコピーして使います。詳しくは下の「(6) ApplicationOAuthProvider 追加」セクションを見てください。

AuthorizeEndpointPath プロパティについては、stackoverflow の記事 What is AuthorizeEndpointPath? に説明があります。

ユーザーがクレデンシャルを入力してトークンを得るという条件に限定すれば、AuthorizeEndpointPath プロパティの設定はコメントアウトしても問題なさそうです。(それで 100% 問題ないと言い切れる自信はないですが)

(6) ApplicationOAuthProvider 追加

Web API テンプレートで作成したプロジェクトの Providers フォルダにある ApplicationOAuthProvider.cs をフォルダごと MVC プロジェクトにコピーします。名前空間は自分のプロジェクトに合わせて変更してください。

GrantResourceOwnerCredentials メソッドの中で使用されている第 2 引数に string を持つ GenerateUserIdentityAsync メソッドは、Web API プロジェクトの IdentityModel.cs に定義されているものをコピーして、MVC プロジェクトの IdentityConfig.cs にペーストしてください。

このプロバイダは、OWIN ミドルウェアのプラグインとして、OWIN ミドルウェアで発生するイベントを処理するためのものだそうです。詳しくは Microsoft の文書 Secure a Web API with Individual Accounts and Local Login in ASP.NET Web API 2.2 の Configuring the Authorization Server のセクションを見てください。

(7) トークン取得・削除のスクリプト

トークン取得・削除のスクリプトは以下のコードのようにします。

var tokenKey = 'accessToken';

function getToken() {
    var email = document.getElementById("email").value;
    var password = document.getElementById("password").value;

    var loginData = {
        grant_type: 'password',
        username: email,
        password: password
    };

    $.ajax({
        type: "POST",
        url: "/Token",
        data: loginData,
        success: function (data) {
            sessionStorage.setItem(tokenKey, data.access_token);
        },
        error: function (jqXHR, textStatus, errorThrown) {
            //・・・中略・・・
        }
    });
}

function removeToken() {
    sessionStorage.removeItem(tokenKey);
}

以上で MVC 側はクッキーベースで、Web API 側はトークンベースで独立して認証が働きます。それを可能にしているのは WebConfig.cs に含まれている以下の 2 行です。

config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(
                    OAuthDefaults.AuthenticationType));

この 2 行をコメントアウトすると Web API 側もクッキーベースの認証となります。なお、その際 Web API に匿名アクセスすると 401 応答ではなく、以下のように 200 応答となりますので注意してください。

クッキー認証の場合の 200 応答

Tags:

Web API

MVC でチャンク形式でダウンロード

by WebSurfer 20. December 2018 11:27

MVC のアクションメソッドを使ってファイルをチャンク形式でエンコーディングしてブラウザにダウンロードする方法を書きます。

チャンク形式でダウンロード

チャンク形式エンコーディングせず、普通に(Content-Length を設定して)ダウンロードする方法については先の記事「MVC でファイルのダウンロード」に書きましたのでそちらを見てください。

チャンク形式エンコーディングでダウンロードするには、その記事で紹介したようなヘルパーメソッド File は使えませんので別の手段を考えることになると思います。

結局は、(1) HttpResponse オブジェクトを取得、(2) それから OutputStream プロパティを使って出力ストリームを取得、(3) コンテンツをチャンクに分割して Write メソッドでストリームに書き込む、(4) Flush メソッドでクライアントに送信する、(5) 全チャンクを送信するまで (3) と (4) の操作を繰り返す・・・ということになると思います。

MVC のアクションメソッドでは Controller.Response プロパティで HttpResponse オブジェクトを取得できますので、それを使って上記 (1) ~ (5) の操作を行うことができます。

そのコードを以下に書いておきます。結局は Web Forms アプリの記事「チャンク形式でダウンロード」のコードとほぼ同じですが・・・

public void ChunkedDownload()
{
  string folder = "~/Files/";
  string filename = "Sig552T8.jpg";
  string path = Server.MapPath(folder + filename);
  FileInfo fileInfo = new FileInfo(path);

  if (fileInfo.Exists)
  {
    int chunkSize = 10000;
    Byte[] buffer = new Byte[chunkSize];
    Response.Clear();

    using (FileStream stream = System.IO.File.OpenRead(path))
    {
      long length = stream.Length;
      Response.ContentType = "image/jpeg";
      Response.AddHeader("Content-Disposition",
              "attachment; filename=" + fileInfo.Name);

      while (length > 0 && Response.IsClientConnected)
      {
        int lengthRead = stream.Read(buffer, 0, chunkSize);
        Response.OutputStream.Write(buffer, 0, lengthRead);

        Response.Flush();
        length -= lengthRead;
      }
    }
  }            
}

アクションメソッドの戻り値は ActionResult である必要はなく void にできることに注意してください。

また、上のコードで最後を示す長さ 0 のチャンクも送信されます。(Fiddler で最後のバイト列が ... 0D 0A 30 0D 0A 0D 0A となっているのを確認しました)

もう一つ、MVC アプリでも HTTP ジェネリックハンドラ(.ashx ファイル)は使えますので、アクションメソッドを使わなくても、HTTP ジェネリックハンドラに同様な機能を実装することは可能です。

Tags:

Upload Download

About this blog

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

Calendar

<<  October 2019  >>
MoTuWeThFrSaSu
30123456
78910111213
14151617181920
21222324252627
28293031123
45678910

View posts in large calendar