In ASP.NET MVC, we manipulate Views, among other things. Some of these Views represent full pages and some are just page parts. These parts or areas belonging to a view are called partial views, and they are also returned by controller actions. Since these partial views should only be used within a view, the ASP.NET MVC framework allows us to protect any call to these partial actions by decorating them with the ChildActionOnly attribute. This attribute makes sure that the action:
- cannot be used as an entire view and the application developers will always run it using the HtmHelper.Action or HtmlHelper.RenderAction methods.
- has a URL that will not be accessible via the address bar, if a user somehow becomes aware of the existence of this URL.

However, as with any dynamic site, we will have AJAX requests that can also make requests for HTML content without having to load the page completely. This content also represents a part, and when we receive the response from the web server we have to embed this piece of HTML somewhere in the page. The AJAX request sent to the server will certainly invoke a controller action. This action, like those marked with the ChildActionOnly attribute, must have these constraints:
- should only go through AJAX requests.
- inaccessible via the browser address bar.

But the ASP.NET MVC framework does not offer any attributes that allow us to apply these restrictions to an action, but it gives us the tools to create them. For this, we need to code a filter that will be executed just before the execution of the action in question. If the incoming request complies with the requirements of a request made in AJAX then we let the action continue its way. In case the conditions are not met, we return a 404 page (as if the URL didn't exist).

The code of the new filter that I will call AjaxOnlyAttribute (derived from the ActionFilterAttribute class) is as follows:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class AjaxOnlyAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAjaxRequest())
        {
            base.OnActionExecuting(filterContext);
        }
        else
        {
           filterContext.HttpContext.Response.StatusCode = 404;
           filterContext.Result = new HttpNotFoundResult();
        }
    }
}

The new class only overrides the method that we are interested in, that is, the OnActionExecuting method that is called just before the start of the action invoked by the incoming request. The attribute can be set on the controller to handle the set of actions where an AJAX request is mandatory.

When getting new developers onboard, they might not immediately understand why those requests keep returning 404 messages. To avoid wasting time in recurrent explanations, I think it would be worth having the DEBUG mode enabled by default for developers. The code of our class would then look like this:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class AjaxOnlyAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAjaxRequest())
        {
            base.OnActionExecuting(filterContext);
        }
        else
        {
            #if DEBUG
            filterContext.Result = new ViewResult { ViewName = "AjaxOnly" };
            #else
            filterContext.HttpContext.Response.StatusCode = 404;
            filterContext.Result = new HttpNotFoundResult();
            #endif
        }
    }
}

 

Since developers have to compile the application in DEBUG mode most of the time, that AjaxOnly View can provide them with some explanations and instructions. Note that this version of the class only works if an AjaxOnly.cshtml View has been added to the Shared Views directory of the application.

Alternatively, we do not have to return a partial view. We can simply replace the following code:

filterContext.Result = new ViewResult { ViewName = "AjaxOnly" };

With some simple text content:

filterContext.Result = new ContentResult
{
    Content = $"This action '{filterContext.HttpContext.Request.RawUrl}' was designed for AJAX requests only"
};

However, the version that uses an actual View has an advantage, it's the ability to systematically render the relevant page "_Layout", typically defined at the application level.

 

[Translated from a contribution by Holty Sow]