web-dev-qa-db-ja.com

ASP.NET MVCアクションからHTTP 404応答を送信する適切な方法は何ですか?

ルートが与えられた場合:

{FeedName}/{ItemPermalink}

例:/ Blog/Hello-World

アイテムが存在しない場合、404を返します。ASP.NETMVCでこれを行う正しい方法は何ですか?

90
Daniel Schaffer

ヒップからの撮影(カウボーイコーディング;-))、次のようなものを提案します。

コントローラー:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return new HttpNotFoundResult("This doesn't exist");
    }
}

HttpNotFoundResult:

using System;
using System.Net;
using System.Web;
using System.Web.Mvc;

namespace YourNamespaceHere
{
    /// <summary>An implementation of <see cref="ActionResult" /> that throws an <see cref="HttpException" />.</summary>
    public class HttpNotFoundResult : ActionResult
    {
        /// <summary>Initializes a new instance of <see cref="HttpNotFoundResult" /> with the specified <paramref name="message"/>.</summary>
        /// <param name="message"></param>
        public HttpNotFoundResult(String message)
        {
            this.Message = message;
        }

        /// <summary>Initializes a new instance of <see cref="HttpNotFoundResult" /> with an empty message.</summary>
        public HttpNotFoundResult()
            : this(String.Empty) { }

        /// <summary>Gets or sets the message that will be passed to the thrown <see cref="HttpException" />.</summary>
        public String Message { get; set; }

        /// <summary>Overrides the base <see cref="ActionResult.ExecuteResult" /> functionality to throw an <see cref="HttpException" />.</summary>
        public override void ExecuteResult(ControllerContext context)
        {
            throw new HttpException((Int32)HttpStatusCode.NotFound, this.Message);
        }
    }
}
// By Erik van Brakel, with edits from Daniel Schaffer :)

このアプローチを使用して、フレームワーク標準に​​準拠します。そこにはすでにHttpUnauthorizedResultがあります。そのため、後でコードを保守する別の開発者の目でフレームワークを拡張するだけです(ご存知のように、どこに住んでいるかを知っているサイコ)。

リフレクターを使用してアセンブリを調べ、HttpUnauthorizedResultがどのように達成されるかを確認できます。このアプローチが何かを見逃しているかどうかはわかりません(ほとんど単純すぎるように思えます)。


リフレクターを使用して、今すぐHttpUnauthorizedResultを確認しました。応答のStatusCodeを0x191(401)に設定しているようです。これは401で機能しますが、404を新しい値として使用すると、Firefoxで空白のページが表示されるようです。ただし、Internet Explorerにはデフォルトの404が表示されます(ASP.NETバージョンではありません)。 webdeveloperツールバーを使用して、FFのヘッダーを検査しましたが、これは404 Not Found応答を示しています。単純にFFで誤って設定したものである可能性があります。


これは言われていますが、ジェフのアプローチはKISSの良い例だと思います。このサンプルで冗長性を実際に必要としない場合、彼の方法もうまく機能します。

68
Erik van Brakel

そのようにします。このコードはBaseControllerにあります

/// <summary>
/// returns our standard page not found view
/// </summary>
protected ViewResult PageNotFound()
{
    Response.StatusCode = 404;
    return View("PageNotFound");
}

そう呼ばれる

public ActionResult ShowUserDetails(int? id)
{        
    // make sure we have a valid ID
    if (!id.HasValue) return PageNotFound();
46
Jeff Atwood
throw new HttpException(404, "Are you sure you're in the right place?");
19
yfeldblum

HttpNotFoundResultは、私が使用しているものへの素晴らしい第一歩です。 HttpNotFoundResultを返すのは良いことです。質問は、次は何ですか?

HandleNotFoundAttributeというアクションフィルターを作成し、404エラーページを表示します。ビューを返すため、コントローラーごとに特別な404ビューを作成するか、デフォルトの共有404ビューを使用できます。これは、フレームワークが404のステータスコードでHttpExceptionをスローするため、コントローラーに指定されたアクションが存在しない場合でも呼び出されます。

public class HandleNotFoundAttribute : ActionFilterAttribute, IExceptionFilter
{
    public void OnException(ExceptionContext filterContext)
    {
        var httpException = filterContext.Exception.GetBaseException() as HttpException;
        if (httpException != null && httpException.GetHttpCode() == (int)HttpStatusCode.NotFound)
        {
            filterContext.HttpContext.Response.TrySkipIisCustomErrors = true; // Prevents IIS from intercepting the error and displaying its own content.
            filterContext.ExceptionHandled = true;
            filterContext.HttpContext.Response.StatusCode = (int) HttpStatusCode.NotFound;
            filterContext.Result = new ViewResult
                                        {
                                            ViewName = "404",
                                            ViewData = filterContext.Controller.ViewData,
                                            TempData = filterContext.Controller.TempData
                                        };
        }
    }
}
7

MVC3の時点では、HttpStatusCodeResultのみを使用できることに注意してください。

7
enashnash

ActionFilterの使用はの維持が困難です。エラーをスローするたびに、属性にフィルターを設定する必要があるためです。設定を忘れたらどうしますか? 1つの方法は、ベースコントローラーでOnExceptionを導出することです。 BaseControllerから派生したControllerを定義する必要があり、すべてのコントローラーはBaseControllerから派生する必要があります。ベースコントローラーを使用することをお勧めします。

Exceptionを使用する場合、応答ステータスコードは500なので、Not Foundの場合は404、Unauthorizedの場合は401に変更する必要があります。上記のように、OnExceptionBaseControllerオーバーライドを使用して、フィルター属性の使用を避けます。

また、新しいMVC 3は、空のビューをブラウザーに返すことで、さらに面倒になります。いくつかの研究の後の最良の解決策は、ここでの私の答えに基づいています ASP.Net MVC 3でHttpNotFound()のビューを返す方法?

より便利にするために、ここに貼り付けます。


いくつかの研究の後。 MVC 3の回避策は、すべてのHttpNotFoundResultHttpUnauthorizedResultHttpStatusCodeResultクラスとnew(オーバーライド)HttpNotFound()メソッドをBaseControllerに実装します。

ベースコントローラーを使用して、派生したすべてのコントローラーを「制御」できるようにすることをお勧めします。

新しいHttpStatusCodeResultクラスを作成し、ActionResultから派生するのではなく、ViewResultプロパティを指定してビューまたは任意のViewをレンダリングするViewNameから派生します。元のHttpStatusCodeResultに従って_HttpContext.Response.StatusCode_と_HttpContext.Response.StatusDescription_を設定しますが、ViewResultから派生しているため、base.ExecuteResult(context)は適切なビューをレンダリングします。簡単ですか?これがMVCコアに実装されることを願っています。

私のBaseController以下を参照してください:

_using System.Web;
using System.Web.Mvc;

namespace YourNamespace.Controllers
{
    public class BaseController : Controller
    {
        public BaseController()
        {
            ViewBag.MetaDescription = Settings.metaDescription;
            ViewBag.MetaKeywords = Settings.metaKeywords;
        }

        protected new HttpNotFoundResult HttpNotFound(string statusDescription = null)
        {
            return new HttpNotFoundResult(statusDescription);
        }

        protected HttpUnauthorizedResult HttpUnauthorized(string statusDescription = null)
        {
            return new HttpUnauthorizedResult(statusDescription);
        }

        protected class HttpNotFoundResult : HttpStatusCodeResult
        {
            public HttpNotFoundResult() : this(null) { }

            public HttpNotFoundResult(string statusDescription) : base(404, statusDescription) { }

        }

        protected class HttpUnauthorizedResult : HttpStatusCodeResult
        {
            public HttpUnauthorizedResult(string statusDescription) : base(401, statusDescription) { }
        }

        protected class HttpStatusCodeResult : ViewResult
        {
            public int StatusCode { get; private set; }
            public string StatusDescription { get; private set; }

            public HttpStatusCodeResult(int statusCode) : this(statusCode, null) { }

            public HttpStatusCodeResult(int statusCode, string statusDescription)
            {
                this.StatusCode = statusCode;
                this.StatusDescription = statusDescription;
            }

            public override void ExecuteResult(ControllerContext context)
            {
                if (context == null)
                {
                    throw new ArgumentNullException("context");
                }

                context.HttpContext.Response.StatusCode = this.StatusCode;
                if (this.StatusDescription != null)
                {
                    context.HttpContext.Response.StatusDescription = this.StatusDescription;
                }
                // 1. Uncomment this to use the existing Error.ascx / Error.cshtml to view as an error or
                // 2. Uncomment this and change to any custom view and set the name here or simply
                // 3. (Recommended) Let it commented and the ViewName will be the current controller view action and on your view (or layout view even better) show the @ViewBag.Message to produce an inline message that tell the Not Found or Unauthorized
                //this.ViewName = "Error";
                this.ViewBag.Message = context.HttpContext.Response.StatusDescription;
                base.ExecuteResult(context);
            }
        }
    }
}
_

このようなアクションで使用するには:

_public ActionResult Index()
{
    // Some processing
    if (...)
        return HttpNotFound();
    // Other processing
}
_

_Layout.cshtml(マスターページなど)

_<div class="content">
    @if (ViewBag.Message != null)
    {
        <div class="inlineMsg"><p>@ViewBag.Message</p></div>
    }
    @RenderBody()
</div>
_

さらに、コードでコメントしたように_Error.shtml_のようなカスタムビューを使用したり、新しい_NotFound.cshtml_を作成したり、ステータスの説明やその他の説明のビューモデルを定義したりできます。

5
CallMeLaNN