Действия контроллера в ASP.NET Core

Методы действий является только открытымы методами в классе контроллера. Чтобы быть точным, это открытые методы, не отмеченные атрибутами NonAction. Любой запрашивающий URL, который проходит через систему маршрутизации ASP.NET - будь то маршрутизация на основе шаблонов или маршрутизация атрибутов - идентифицирует пару, составленную из псевдонима контроллера и имени действия. Эти две части информации передаются малоизвестному внутреннему компоненту модели приложения ASP.NET MVC - так же, как в классических ASP.NET MVC и ASP.NET Core. Это компонент Action Invoker, и, как понятно из его названия, он отвечает за вызов запрошенного действия.

Вызов действия

В ASP.NET Core типичный метод контроллера выглядит следующим образом:

public IActionResult Index()
{
    // return the action result object
}

В классическом ASP.NET MVC это выглядит точно так же, за исключением того, что возвращаемый тип не абстрагируется от интерфейса, а обращается к базовому классу ActionResult. Нет ничего особенного, просто добавлен еще один уровень абстракции. Во-первых, вызывающий действие вызывает метод действия контроллера. Метод запускается и возвращает объект IActionResult. Может показаться, что объект результата действия - это фактический результат, возвращаемый вызывающей стороне, но это не совсем так. Вместо этого объект результата действия возвращается инициатору действия, который затем обрабатывает его на втором шаге, чтобы сгенерировать содержимое, которое будет записано в поток вывода ответа для приема вызывающей стороной.

В ASP.NET Core IActionResult определяет контракт, представляющий результат метода действия. Контракт определен в коде ниже.

В классическом ASP.NET MVC это выглядит точно так же, за исключением того, что возвращаемый тип не абстрагируется от интерфейса, а обращается к базовому классу ActionResult. Нет ничего особенного, просто добавлен еще один уровень абстракции. Во-первых, вызывающий действие вызывает метод действия контроллера. Метод запускается и возвращает объект IActionResult. Может показаться, что объект результата действия - это фактический результат, возвращаемый вызывающей стороне, но это не совсем так. Вместо этого объект результата действия возвращается инициатору действия, который затем обрабатывает его на втором шаге, чтобы сгенерировать содержимое, которое будет записано в поток вывода ответа для приема вызывающей стороной.

В ASP.NET Core IActionResult определяет контракт, представляющий результат метода действия. Контракт определен в коде ниже.

public interface IActionResult
{
   Task ExecuteResultAsync(ActionContext context);
}

Многие типы в ASP.NET Core реализуют интерфейс IActionResult, часто через наследование от базового класса ActionResult. Во всяком случае, все типы результатов действий должны реализовывать метод - метод ExecuteResultAsync - для обработки данных, полученных контроллером. Результат выполнения действия - запись в поток выходных ответов. Давайте рассмотрим, например, метод RedirectToAction, доступный в базовом классе Controller. Вы вызываете метод для перенаправления потока на указанное действие. Метод возвращает тип RedirectToActionResult. В реализации этого типа метод ExecuteResultAsync вызывает метод Redirect для объекта Response. Аналогичным образом, когда метод контроллера завершается вызовом метода View, то возвращаемым типом действия является ViewResult. Когда вызывающий действие выполняет объект результата представления, эффект извлекает соответствующее представление Razor, превращая его содержимое в строку HTML и записывая строку HTML в выходной поток ответа.

Тип ActionResult

Метод действия может отдавать различные результаты. Например, метод действия может просто действовать как веб-служба и возвращать обычную строку или строку JSON в ответ на запрос. Аналогично, метод действия может определить, что нет содержимого для возврата или что требуется перенаправление на другой URL. ASP.NET Core предоставляет множество конкретных типов результатов действий, включая FileStreamResult, JsonResult, ContentResult и ViewResult. Каждый тип результата действия отвечает за конкретное действие. Пользовательские типы результатов действий могут быть определены для создания ответа, который возвращается любым из предопределенных типов.

В контексте ASP.NET MVC частичный рендеринг - это неофициальный теримин, который относится к практике возврата фрагмента HTML из метода контроллера. Таким образом, выполняя Ajax-вызов через JavaScript, клиентская страница может загрузить фрагмент HTML и присоединить его к текущей DOM и обновить представление, не обновляя всю страницу. В ASP.NET Core, а также в классическом ASP.NET MVC 5.x вы получаете метод контроллера, как показано ниже:

public ActionResult DeletePost(int id)
{
    // Executes the command
    _repository.Delete(id);

    // Return HTML to update the view
    return PartialView(Common.Views.ListOfCustomers);
}

Данное решение отлично работает, за исключением того, что иногда действие на клиенте (например нажатие, чтобы удалить элемент из списка) требует обновления двух или более фрагментов HTML. Чтобы избежать множественных HTTP-вызовов, вам необходим метод контроллера, который может возвращать массив фрагментов HTML. К сожалению, подобный тип для возврата массива фрагментов HTML не существует в ASP.NET MVC и не существует в ASP.NET Core. Решение, реальзованное выше на ASP.NET MVC 5.x будет немного отличаться в .NET Core, см. ниже.

Частичный рендеринг в ASP.NET Core

В следующем примере показан полный исходный код класса MultiplePartialViewResult. Реализация в .NET Core немного отличается, нежели как в классическом ASP.NET MVC.

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.DependencyInjection;

namespace Demos
{
    public class MultiplePartialViewResult : ActionResult
    {
        public const string ChunkSeparator = "---|||---";

        public IList<PartialViewResult> PartialViewResults { get; }

        public MultiplePartialViewResult(params PartialViewResult[] results)
        {
            if (PartialViewResults == null)
                PartialViewResults = new List<PartialViewResult>();

            foreach (var r in results)
                PartialViewResults.Add(r);
        }

        public override async Task ExecuteResultAsync(ActionContext context)
        {
            if (context == null)
                throw new ArgumentNullException(nameof(context));

            var services = context.HttpContext.RequestServices;
            var executor = services.GetRequiredService<PartialViewResultExecutor>();

            var total = PartialViewResults.Count;
            var writer = new StringWriter();
            for (var index = 0; index < total; index++)
            {
                var pv = PartialViewResults[index];
                var view = executor.FindView(context, pv).View;
                var viewContext = new ViewContext(context,
                    view,
                    pv.ViewData, 
                    pv.TempData,
                    writer, 
                    new HtmlHelperOptions());
                await view.RenderAsync(viewContext);

                if (index < total - 1)
                    await writer.WriteAsync(ChunkSeparator);
            }

            await context.HttpContext.Response.WriteAsync(writer.ToString());
        }
    }
}

Разница с классическим ASP.NET MVC заключается в способе получения объекта представления и в том, как представление отображается для создания разметки HTML. Ключевые моменты:

var services = context.HttpContext.RequestServices;
var executor = services.GetRequiredService<PartialViewResultExecutor>();

Первая строка сохраняет ссылку на коллекцию сервисов времени выполнения. Во второй строке используется ссылка для получения фактического сервиса, ответственного за выполнение типа результата действия PartialViewResult. Другими словами, в ASP.NET Core есть отдельный слой кода, который «выполняет» типы результатов действий. Или, можно сказать, что в ASP.NET Core фактический код исполнителя был абстрагирован от контракта.

Представление отображается в контексте представления, и контекст представления содержит буфер, в котором накапливается HTML, повторяя процесс для объединения всех частичных представлений. Затем, буфер сбрасывается в реальный поток вывода.

context.HttpContext.Response.WriteAsync(writer.ToString());

В следующем коде показано, как использовать тип результата действия MultiplePartialViewResult в классе контроллера.

public ActionResult DeleteCustomer(int id)
{
   _repository.Delete(id);
   var customers = _repository.FindAll();

   // Return 
   var model = new IndexViewModel(customers);
   var result = new MultiplePartialViewResult(
       PartialView(U.PartialViews.ListOfCustomers, model),
       PartialView(U.PartialViews.OnBehalfOfCustomers, model));
   return result;
}

Результат выполнения этого кода такой же, как и в классическом ASP.NET: клиент удаляется из базы данных и возвращается два фрагмента HTML, один с обновленным списком клиентов, а другой с другим элементом пользовательского интерфейса для обновления. На рисунке ниже вы видите два сегмента пользовательского интерфейса, которые будут обновляться при каждом удалении клиента.

Действия контроллера в ASP.NET Core

Фильтры действий

Фильтр действия - это фрагмент кода, который выполняется над методом действия и может использоваться для изменения и расширения поведения, жестко закодированного в самом методе. Фильтр действий полностью представлен следующим интерфейсом:

public interface IActionFilter
{
    void OnActionExecuting(ActionExecutingContext filterContext);
    void OnActionExecuted(ActionExecutedContext filterContext);
}

Он предлагает обработчики для запуска кода до и после выполнения действия. Из фильтра вы получаете доступ к запросу и контексту контроллера и можете читать и изменять параметры. Каждый контроллер, который наследуется от базового класса Controller, получает реализацию интерфейса IActionFilter по умолчанию и предоставляет два переопределяемых метода с именами OnActionExecuting и OnActionExecuted. Таким образом, каждый контроллер дает вам возможность решить, что делать до и / или после вызова определенного метода. Обратите внимание, что это не так для контроллеров POCO (обычный старый объект CLR). В ASP.NET Core контроллер POCO по сути является контроллером, который не наследуется от базового класса Controller.

Фильтры действий не ограничиваются выполнением некоторых действий до и/или после вызова метода контроллера. Что если вы хотите дать возможность некоторому коду решить, подходит ли выбранный метод для выполнения определенного действия или нет? Для этого типа настройки требуется другая категория фильтров: селекторы действий. Селекторы действий могут быть двух основных типов и использоваться для фильтрации выбора метода по имени и/или по глаголу. Наглядным примером использования этой функции является включение открытого метода только через HTTP-вызов Ajax.

Все, что вам нужно, это класс, который наследуется от ActionMethodSelectorAttribute и переопределяет метод IsValidForRequest:

public class AjaxOnlyAttribute : ActionMethodSelectorAttribute
{
    public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
    {
        return routeContext.HttpContext.Request.IsAjaxRequest();
    }
}

Обратите внимание, что метод IsAjaxRequest является методом расширения класса HttpRequest. В любом случае, все, что делает IsAjaxRequest, проверяет, приносит ли входящий запрос определенный заголовок HTTP, который браузеры обычно добавляют при реализации XMLHttpRequest - компонент браузера, отвечающий за низкоуровневые вызовы Ajax.

public static class HttpRequestExtensions
{
    public static bool IsAjaxRequest(this HttpRequest request)
    {
        if (request == null)
            throw new ArgumentNullException("request");
        if (request.Headers != null)
            return request.Headers["X-Requested-With"] == "XMLHttpRequest";
        return false;
    }
}

Таким образом любой метод контроллера, помеченный атрибутом AjaxOnly, может обслуживать только вызовы, сделанные через объект браузера XMLHttpRequest.

[AjaxOnly]
public ActionResult Details(int customerId)
{ 
    var model = ...;
    return PartialView(model);
}

Если вы попытаетесь вызвать URL, который, в соответствии с маршрутами, должен быть сопоставлен с методом Ajax-only, вы получите исключение Not-Found.