Wie kann eine HTTP 404-Antwort von einer ASP.NET MVC-Aktion ordnungsgemäß gesendet werden?

92

Wenn die Route angegeben ist:

{FeedName} / {ItemPermalink}

Beispiel: / Blog / Hello-World

Wenn das Element nicht vorhanden ist, möchte ich eine 404 zurückgeben. Wie gehe ich in ASP.NET MVC richtig vor?

Daniel Schaffer
quelle
Vielen Dank, dass Sie diese Frage gestellt haben. Dies geht in meinen Standard-Projekterweiterungen: D
Erik van Brakel

Antworten:

69

Wenn ich aus der Hüfte schieße (Cowboy-Codierung ;-)), würde ich so etwas vorschlagen:

Regler:

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 :)

Mit diesem Ansatz halten Sie sich an die Framework-Standards. Es gibt bereits ein HttpUnauthorizedResult, sodass dies das Framework in den Augen eines anderen Entwicklers erweitern würde, der Ihren Code später verwaltet (Sie wissen, der Psycho, der weiß, wo Sie leben).

Sie könnten einen Reflektor verwenden, um einen Blick in die Baugruppe zu werfen und zu sehen, wie das HttpUnauthorizedResult erreicht wird, da ich nicht weiß, ob bei diesem Ansatz etwas fehlt (es scheint fast zu einfach).


Ich habe gerade einen Reflektor verwendet, um mir das HttpUnauthorizedResult anzusehen. Anscheinend setzen sie den StatusCode für die Antwort auf 0x191 (401). Obwohl dies für 401 funktioniert, scheint es, wenn ich 404 als neuen Wert verwende, in Firefox nur eine leere Seite zu bekommen. Internet Explorer zeigt jedoch einen Standardwert von 404 an (nicht die ASP.NET-Version). Mit der Webentwickler-Symbolleiste habe ich die Header in FF überprüft, die eine 404 Not Found-Antwort anzeigen. Könnte einfach etwas sein, das ich in FF falsch konfiguriert habe.


Trotzdem denke ich, dass Jeffs Ansatz ein gutes Beispiel für KISS ist. Wenn Sie die Ausführlichkeit in diesem Beispiel nicht wirklich benötigen, funktioniert seine Methode ebenfalls einwandfrei.

Erik van Brakel
quelle
Ja, ich habe auch die Aufzählung bemerkt. Wie gesagt, es ist nur ein grobes Beispiel. Sie können es gerne verbessern. Dies soll doch eine Wissensdatenbank sein ;-)
Erik van Brakel
Ich glaube, ich bin ein bisschen über Bord gegangen ... viel Spaß: D
Daniel Schaffer
FWIW, Jeffs Beispiel erfordert auch, dass Sie eine benutzerdefinierte 404-Seite haben.
Daniel Schaffer
2
Ein Problem beim Auslösen von HttpException, anstatt nur HttpContext.Response.StatusCode = 404 zu setzen, besteht darin, dass bei Verwendung des OnException Controller-Handlers (wie ich) auch die HttpExceptions abgefangen werden. Ich denke, nur das Einstellen des StatusCodes ist ein besserer Ansatz.
Igor Brejc
4
HttpException oder HttpNotFoundResult in MVC3 ist in vielerlei Hinsicht nützlich. Verwenden Sie im Fall von @Igor Brejc einfach die if- Anweisung in der OnException, um den nicht gefundenen Fehler herauszufiltern.
CallMeLaNN
46

Wir machen es so; Dieser Code befindet sich inBaseController

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

so genannt

public ActionResult ShowUserDetails(int? id)
{        
    // make sure we have a valid ID
    if (!id.HasValue) return PageNotFound();
Jeff Atwood
quelle
Ist diese Aktion dann mit einer Standardroute verbunden? Ich kann nicht sehen, wie es ausgeführt wird.
Christian Dalager
2
Könnte folgendermaßen ausgeführt werden: protected override void HandleUnknownAction (Zeichenfolge actionName) {PageNotFound (). ExecuteResult (this.ControllerContext); }
Tristan Warner-Smith
Früher habe ich das so gemacht, aber ich fand, dass das Aufteilen des Ergebnisses und der angezeigten Ansicht ein besserer Ansatz war. Schauen Sie sich meine Antwort unten an.
Brian Vallelunga
19
throw new HttpException(404, "Are you sure you're in the right place?");
Yfeldblum
quelle
Ich mag das, weil es den benutzerdefinierten Fehlerseiten folgt, die in eingerichtet wurden web.config.
Mike Cole
7

Das HttpNotFoundResult ist ein großartiger erster Schritt zu dem, was ich verwende. Die Rückgabe eines HttpNotFoundResult ist gut. Dann ist die Frage, was kommt als nächstes?

Ich habe einen Aktionsfilter namens HandleNotFoundAttribute erstellt, der dann eine 404-Fehlerseite anzeigt. Da eine Ansicht zurückgegeben wird, können Sie pro Controller eine spezielle 404-Ansicht erstellen oder eine gemeinsam genutzte Standard-404-Ansicht verwenden. Dies wird sogar aufgerufen, wenn auf einem Controller die angegebene Aktion nicht vorhanden ist, da das Framework eine HttpException mit dem Statuscode 404 auslöst.

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
                                        };
        }
    }
}
Brian Vallelunga
quelle
7

Beachten Sie, dass Sie ab MVC3 nur noch verwenden können HttpStatusCodeResult.

Enashnash
quelle
8
Oder noch einfacherHttpNotFoundResult
Matt Enright
6

Die Verwendung von ActionFilter ist schwierig zu warten, da bei jedem Fehler der Filter im Attribut festgelegt werden muss. Was ist, wenn wir vergessen, es einzustellen? Eine Möglichkeit besteht darin, den OnExceptionBasis-Controller abzuleiten . Sie müssen eine BaseControllerAbleitung von definieren Controllerund alle Ihre Controller müssen von abgeleitet sein BaseController. Es wird empfohlen, einen Basis-Controller zu haben.

Beachten Sie, dass bei Verwendung Exceptiondes Antwortstatuscodes 500 für Nicht gefunden in 404 und für Nicht autorisiert in 401 geändert werden muss. Verwenden Sie, wie oben erwähnt, OnExceptionOverrides fürBaseController , um die zu vermeiden.

Die neue MVC 3 macht es auch schwieriger, wenn eine leere Ansicht an den Browser zurückgegeben wird. Die beste Lösung nach einigen Recherchen basiert auf meiner Antwort hier Wie kann eine Ansicht für HttpNotFound () in ASP.Net MVC 3 zurückgeben?

Um mehr Komfort zu schaffen, füge ich es hier ein:


Nach einigem Studium. Die Abhilfe für MVC 3 hier ist alles abzuleiten HttpNotFoundResult, HttpUnauthorizedResult, HttpStatusCodeResultKlassen und implementieren neuen (überschreibt es) HttpNotFound() -Methode inBaseController .

Es wird empfohlen, den Basis-Controller zu verwenden, damit Sie die Kontrolle über alle abgeleiteten Controller haben.

Ich erstelle eine neue HttpStatusCodeResultKlasse, nicht um sie abzuleiten, ActionResultsondern ViewResultum die Ansicht oder eine beliebige zu rendern, Viewindem Sie die ViewNameEigenschaft angeben . Ich folge dem Original HttpStatusCodeResult, um das einzustellen HttpContext.Response.StatusCodeund werde HttpContext.Response.StatusDescriptiondann base.ExecuteResult(context)aber die passende Ansicht rendern, da ich wieder davon ableite ViewResult. Einfach genug ist es? Hoffe, dass dies im MVC-Kern implementiert wird.

Siehe meinen BaseControllerBalg:

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);
            }
        }
    }
}

So verwenden Sie Ihre Aktion:

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

Und in _Layout.cshtml (wie Masterseite)

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

Zusätzlich können Sie eine benutzerdefinierte Ansicht wie verwenden Error.shtmloder eine neue erstellen, NotFound.cshtmlwie ich sie im Code kommentiert habe, und Sie können ein Ansichtsmodell für die Statusbeschreibung und andere Erklärungen definieren.

CallMeLaNN
quelle
Sie können jederzeit einen globalen Filter registrieren, der einen Basis-Controller schlägt, da Sie sich daran erinnern müssen, Ihren Basis-Controller zu verwenden!
John Culviner
:) Ich bin mir auch nicht sicher, ob dies immer noch ein Problem in MVC4 ist. Was ich zu diesem Zeitpunkt meine, ist der HandleNotFoundAttribute-Filter, der von jemand anderem beantwortet wird. Es ist nicht erforderlich, für jede Aktion angewendet zu werden. ZB ist es nur für Aktionen geeignet, die id param haben, aber nicht für die Aktion Index (). Ich habe mich auf einen globalen Filter geeinigt, nicht für HandleNotFoundAttribute, sondern für ein benutzerdefiniertes HandleErrorAttribute.
CallMeLaNN
Ich dachte, MVC3 hätte es auch, nicht sicher. Gute Diskussion, unabhängig von anderen, die auf die Antwort
stoßen