web_cs_01.md 63 KB

Введение в ASP.NET Core MVC. Создание веб-приложения, структура проекта, контроллеры

Эта и следующие лекции взяты отсюда

Фреймворк ASP.NET Core MVC является частью платформы ASP.NET Core, его отличительная особенность - применение паттерна MVC. Преимуществом использования фрейморка ASP.NET Core MVC по сравнению с "чистым" ASP.NET Core является то, что он упрощает в ряде ситуаций и сценариев организацию и создание приложений, особенно это относится к большим приложениям.

Стоит отметить, чам паттерн MVC не является исключительной особенностью ASP.NET Core MVC, данный паттерн появился еще в конце 1970-х годов в компании Xerox как способ организации компонентов в графическом приложение на языке Smalltalk и в настоящее время применяется во многих платформах и для различных языках программирования. Особенно популярен паттерн MVC в веб-приложениях.

Концепция паттерна MVC предполагает разделение приложения на три компонента:

  • Модель (model): описывает используемые в приложении данные, а также логику, которая связана непосредственно с данными, например, логику валидации данных. Как правило, объекты моделей хранятся в базе данных.

    В MVC модели представлены двумя основными типами: модели представлений, которые используются представлениями для отображения и передачи данных, и модели домена, которые описывают логику управления данными.

    Модель может содержать данные, хранить логику управления этими данными. В то же время модель не должна содержать логику взаимодействия с пользователем и не должна определять механизм обработки запроса. Кроме того, модель не должна содержать логику отображения данных в представлении.

  • Представление (view): отвечают за визуальную часть или пользовательский интерфейс, нередко html-страница, через который пользователь взаимодействует с приложением. Также представление может содержать логику, связанную с отображением данных. В то же время представление не должно содержать логику обработки запроса пользователя или управления данными.

  • Контроллер (controller): представляет центральный компонент MVC, который обеспечивает связь между пользователем и приложением, представлением и хранилищем данных. Он содержит логику обработки запроса пользователя. Контроллер получает вводимые пользователем данные и обрабатывает их. И в зависимости от результатов обработки отправляет пользователю определенный вывод, например, в виде представления, наполненного данными моделей.

Отношения между компонентами паттерна можно описать следующей схемой:

В этой схеме модель является независимым компонентом - любые изменения контроллера или представления никак не влияют на модель. Контроллер и представление являются относительно независимыми компонентами. Так, из представления можно обращаться к определенному контроллеру, а из контроллера генерировать представления, но при этом нередко их можно изменять независимо друг от друга.

Такое разграничение компонентов приложения позволяет реализовать концепцию разделение ответственности, при которой каждый компонент отвечает за свою строго очерченную сферу. В связи с чем легче построить работу над отдельными компонентами. И благодаря этому приложение легче разрабатывать, поддерживать и тестировать отдельные компоненты. Допустим, если нам важна визуальная часть или фронтэнд, то мы можем тестировать представление независимо от контроллера. Либо мы можем сосредоточиться на бэкэнде и тестировать контроллер.

Первый проект на ASP.NET Core MVC

Для создания проекта на ASP.NET Core MVC мы можем выбрать любой тип проекта на ASP.NET Core и в нем уже добавлять необходимые компоненты. Однако для упрощения Visual Studio уже по умолчанию предоставляет для этого шаблон ASP.NET Core Web App (Model-View-Controller):

Выберем данный шаблон для создания проекта. Дальше нам откроется окно для установки имени проекта. Допустим, проект будет называться asp_net_mvc:

Можно создать проект используя командную строку:

dotnet new mvc

Структура проекта

  • wwwroot: этот узел (на жестком диске ему соответствует одноименная папка) предназначен для хранения статических файлов - изображений, скриптов javascript, файлов css и т.д., которые используются приложением.
  • Controllers: папка для хранения контроллеров, используемых приложением. По умолчанию здесь уже есть один контроллер - Homecontroller
  • Models: каталог для хранения моделей. По умолчанию здесь создается модель ErrorviewModel
  • Views: каталог для хранения представлений. Здесь также по умолчанию добавляются ряд файлов - представлений
  • appsettings.json: хранит конфигурацию приложения
  • Program.cs: файл, который определяет входную точку в приложение ASP.NET Core

Можно запустить созданный проект (я делаю через консоль, но можно и в GUI)

dotnet run
Используются параметры запуска из /Users/kei/Projects/asp_net_mvc/Properties/launchSettings.json...
Сборка…
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5208
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/kei/Projects/asp_net_mvc

При запуске консоль отобразит адрес, по которому доступен проект. В моем случае это "http://localhost:5208". И если мы откроем в браузере этот адрес, то сработает запрос к контроллеру по умолчанию - классу HomeController, который выберет для генерации ответа нужное представление. И в итоге из представления будет создана html-страница:

Посмотрим что происходит в Program.cs:

var builder = WebApplication.CreateBuilder(args);

/**
в приложение добавляются сервисы MVC 
(поддержка контроллеров и представлений)
*/
builder.Services.AddControllersWithViews();

var app = builder.Build();

/**
HSTS (HTTP Strict Transport Security) — это стандарт безопасности, который принуждает браузеры подключаться к веб-сайту только по защищенному протоколу HTTPS, даже если пользователь вводит ссылку с HTTP. 
*/
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

// middleware 
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();

// подключает статические ресурсы
app.MapStaticAssets();

/**
Вместо конечных точек (как в минималистичном АПИ)
подключается маршрутизация через контроллер
 */
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}")
    .WithStaticAssets();

app.Run();

Прежде всего надо отметить, что функциональность MVC, в частности, поддержка контроллеров и представлений, подключается в приложение в виде сервиса - в данном случае с помощью вызова services.AddControllersWithViews(). После этого мы можем использовать функциональность фреймворка MVC.

Кроме того, чтобы связать приходящие от пользователей запросы с контроллерами применяется метод MapControllerRoute(). Через первый параметр - name в метод передается название маршрута - в данном случае "default". Через второй параметр - параметр pattern передается шаблон маршрута, которому должен соответствовать запрос. В качестве шаблона маршрута применяется шаблон "{controller=Home}/{action=Index}/{id?}", который представляет трехсегментный запрос. В нем первый сегмент представляет контроллер, второй сегмент - метод контроллера, а третий - необязательный параметр. При этом если в запросе не указаны сегменты (например, обращение идет к корню веб-приложения), то в качестве контроллера по умолчанию применяется HomeController, а в качестве его метода - метод Index.

Посмотрим что находится в HomeController.cs

namespace asp_net_mvc.Controllers;

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;

    // конструктор
    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
    }

    // точка входа по-умолчанию
    // реальный путь /home/index
    public IActionResult Index()
    {
        return View();
    }

    // точка входа /home/privacy
    public IActionResult Privacy()
    {
        return View();
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    }
}

Контроллер имеет конструктор, через который посредством механизма dependency injection передается сервис ILogger, используемый для логгирования. Также контроллер определяет три метода - Index, Privacy и Error.

Как видим все методы в итоге возвращают представление (return View()). Представления лежат в каталоге /Views/<Название контроллера> - в нашем случае /Views/Home, называются так же как методы и имеют расширение *.cshtml

Рассмотрим файл /Views/Home/Index.cshtml:

<!-- тут задается название страницы -->
@{ViewData["Title"] = "Home Page";}

<div class="text-center">
    <h1 class="display-4">
        Welcome
    </h1>
    <p>Learn about 
        <a href="https://learn.microsoft.com/aspnet/core">
            building Web apps with ASP.NET Core
        </a>.
    </p>
</div>

В принципе это обычный HTML файл, кроме первой строки. Немного поправим его:

<!-- тут задается название страницы -->
@{ViewData["Title"] = "Заголовок нашего MVC приложения";}
<div class="text-center">
    <h1 class="display-4">
        Привет ЙОТК!
    </h1>
</div>

Сервисы MVC

Функциональность MVC и ее работа в приложении зависит от добавляемых сервисов. В примере выше мы использовали метод AddControllersWithViews() для добавления сервисов MVC, благодаря чему система маршрутизации смогла связать запрос с контроллером.

Есть и другие сервисы:

  • AddMvc(): добавляет все сервисы фреймворка MVC (в том числе сервисы для работы с аутентификацией и авторизацией, валидацией и т.д.)

  • AddMvcCore(): добавляет только основные сервисы фреймворка MVC, а всю дополнительную функциональность, типа аутентификацией и авторизацией, валидацией и т.д., необходимо добавлять самостоятельно

  • AddControllersWithViews(): добавляет только те сервисы фреймворка MVC, которые позволяют использовать контроллеры и представления и связанную функциональность. При создании проекта по типу ASP.NET Core Web App (Model-View-Controller) используется именно этот метод

  • AddControllers(): позволяет использовать контроллеры, но без представлений.

И в зависимости от того, насколько широко нам надо использовать возможности фреймворка MVC, выбирается соответствующий метод. Например, мы могли бы использовать вместо вызова AddControllersWithViews() метод AddMvc().

Контроллеры

Контроллеры и их действия

Основным элементом в архитектуре ASP.NET Core MVC является контроллер. При получении запроса система маршрутизации выбирает для обработки запроса нужный контроллер и передает ему данные запроса. Контроллер обрабатывает эти данные и посылает обратно результат обработки.

Например, проект, создаваемый по шаблону ASP.NET Core Web App (Model-View-Controller), по умолчанию содержит как минимум один контроллер - класс HomeController (его содержимое мы уже посмотрели выше)

В ASP.NET Core MVC контроллер представляет обычный класс на языке C#, который обычно наследуется от абстрактного базового класса Microsoft.AspNetCore.Mvc.Controller и который, как и любой класс на языке C#, может иметь поля, свойства, методы. Согласно соглашениям об именовании названия контроллеров обычно оканчиваются на суффикс "Controller", остальная же часть до этого суффикса считается именем контроллера, например, HomeController. Но в принципе эти условности также необязательны.

Но есть также и обязательные условности, которые предъявляются к контроллерам. В частности, класс контроллера должен удовлетворять как минимум одному из следующих условий:

  • Класс контроллера имеет суффикс "Controller"

    public class HomeController
    {  
        //............  
    }
    
  • Класс контроллера наследуется от класса, который имеет суффикс "Controller"

    public class Home : Controller
    {
        //.............
    }
    
  • К классу контроллера применяется атрибут [Controller]

    [Controller]
    public class Home
    {
        //..................
    }
    

Действия контроллера

Ключевым элементом контроллера являются его действия. Действия контроллера - это публичные методы, которые могут сопоставляться с запросами. Например, контроллер HomeController и его метод Index.

В общем случае метод может возвращать просто текст и возможна такая релизация

public string Index()
{
    return "Привет ЙОТК!";
}

Естественно, в этом случае нет ни заголовка ни тела HTML и текст выводится "как есть":

Обращение к действиям контроллера

Сопоставление запроса с контроллером и его действием происходит благодаря системе маршрутизации.

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}")
    .WithStaticAssets();

Метод app.MapControllerRoute добавляет один маршрут с именем default и шаблоном "{controller=Home}/{action=Index}/{id?}". Данный шаблон устанавливает трехсегментную структуру строки запроса: controller/action/id. То есть в начале идет название контроллера, затем название действия, и далее может идти необязательный параметр id.

Собственно поэтому система может соотнести запрос типа localhost:xxxx/Home/Index с контроллером и его действием.

Чтобы обратиться к контроллеру из веб-браузера, нам надо в адресной строке набрать адрес_сайта/Имя_контроллера/Действие_контроллера. Так, по запросу адрес_сайта/Home/Index система маршрутизации по умолчанию вызовет метод Index контроллера HomeController для обработки входящего запроса.

Методы, которые не являются действиями

Однако не все методы контроллера являются действиями. Контроллер также может иметь непубличные методы - такие методы не рассматриваются как действия и соответственно не могут соотноситься с запросами. Например, определим в контроллере следующий метод:

public class HomeController : Controller
{
    protected internal string Hello() => "Hello ASP.NET";
}

Метод Hello не является действием, поскольку его модификатор отличается от public. Соответственно мы не сможем обратиться к этому методу с запросом localhost:xxxx/Home/Hello. Хотя такие не публичные методы также могут быть полезными - в них можно определять какие-нибудь промежуточные вычисления и затем использовать в действиях контроллера. При этом если мы изменим модификатор метода на public, то метод Hello станет полноценным действием.

Атрибуты NonController, ActionName и NonAction

Возможно, сопоставление по умолчанию бывает не всегда удобно. Например, у нас есть класс в папке Controllers, но мы не хотим, чтобы он мог обрабатывать запрос и использоваться как контроллер. Чтобы указать, что этот класс не является контроллером, нам надо использовать над ним атрибут [NonController]:

[NonController]
public class HomeController : Controller
{
    //...........
}

Аналогично, если мы хотим, чтобы какой-либо публичный метод контроллера не рассматривался как действие, то мы можем использовать над ним атрибут [NonAction]:

[NonAction]
public string Hello()
{
    return "Hello ASP.NET";
}

Атрибут [ActionName] позволяет для метода задать другое имя действия. Например:

[ActionName("Welcome")]
public string Hello()
{
    return "Hello ASP.NET";
}

В этом случае чтобы обратиться к этому методу, надо отправить запрос localhost:xxxx/Home/Welcome. А запрос localhost:xxxx/Home/Hello работать не будет.

Типы запросов

Кроме того, методы могут обслуживать разные типы запросов. Для указания типа запроса HTTP нам надо применить к методу один из атрибутов: [HttpGet], [HttpPost], [HttpPut], [HttpPatch], [HttpDelete] и [HttpHead]. Если атрибут явным образом не указан, то метод может обрабатывать все типы запросов: GET, POST, PUT, DELETE.

Например:

public class HomeController : Controller
{
    [HttpGet]
    public string Index() => "Hello METANIT.COM";

    [HttpPost]
    public string Hello() => "Hello ASP.NET";
}

Так, в данном случае метод Index обрабатывает только запросы типа GET, а метод Hello - запросы типа POST.

Контекст контроллера

При обращении к контроллеру среда ASP.NET создает для этого контроллера контекст, который содержит различные связанные с контроллером данные. Для получения контекста в классе контроллера нам доступно свойство ControllerContext, которое представляет одноименный класс ControllerContext. Этот класс определяет ряд важный свойств:

  • HttpContext: содержит информацию о контексте запроса
  • ActionDescriptor: возвращает дескриптор действия - объект ActionDescriptor, который описывает вызываемое действие контроллера
  • ModelState: возвращает словарь ModelStateDictionary, который используется для валидации данных, отправленных пользователем
  • RouteData: возвращает данные маршрута

Для получения информации о запросе нас прежде всего будет интересовать свойство HttpContext, которое представляет объект Microsoft.AspNetCore.Http.HttpContext. В принципе это тот же самый объект, который нам доступен в любом компоненте middleware в ASP.NET Core. Этот объект также доступен через свойство HttpContext класса контроллера. То есть следующие вызовы будут обращаться к одному и тому же объекту:

var ctx1 = ControllerContext.HttpContext;
var ctx2 = HttpContext;

Объект HttpContext инкапсулирует всю информацию о запросе. В частности, он определяет следующие свойства:

  • Request: содержит собственно информацию о текущем запросе.
  • Response: управляет ответом
  • User: представляет текущего пользователя, который обращается к приложению
  • Session: объект для работы с сессиями

Response

Свойство HttpContext.Response представляет объект HttpResponse и позволяет управлять ответом на запрос, в частности, устанавливать заголовки ответа, куки, отправлять в выходной поток некоторый ответ. Этот же объект доступен через свойство Response класса Conroller. Среди свойств объекта Response можно выделить следующие:

  • Body: объект Stream, который применяется для отправки данных в ответ пользователю
  • Cookies: куки, отправляемые в ответе
  • ContentType: MIME-тип ответа
  • Headers: коллекция заголовков ответа
  • StatusCode: статусный код ответа

С помощью объекта Response мы можем настроить параметры ответа и отправить его клиенту. Например, отправим из действия контроллера данные клиенту:

public class HomeController : Controller
{
    public async Task Index() 
    {
        Response.ContentType = "text/html;charset=utf-8";
        await Response.WriteAsync("<h2>Hello METANIT.COM</h2>");
    }
}

Здесь в методе Index вначале устанавливается заголовок Content-Type, а затем с помощью метода WriteAsync отправляется некоторое простейшее содержимое в виде строки с кодом html. Поскольку метод WriteAsync - асинхронный, то к нему можно применить оператор await, а метод-действие Index определен как асинхронный.

Request

Свойство HttpContext.Request представляет объект HttpRequest и предоставляет разнообразную информацию о запросе. Этот же объект доступен через свойство Request класса контроллера. Среди свойств объекта Request можно выделить следующие:

  • Body: объект Stream, который используетя для чтения данных запроса
  • Cookies: куки, полученные в запросе
  • Form: коллекция значений отправленных форм
  • Headers: коллекция заголовков запроса
  • Path: возвращает запрошенный путь - строка запроса без домена и порта
  • Query: возвращает коллекцию переданных через строку запроса параметров
  • QueryString: возвращает ту часть запроса, которая содержит параметры. Например, в запросе http://localhost:52682/Home/Index?alt=4 это будет ?alt=4

Вся основная информация нам доступна из заголовков. Например, получим из запроса все заголовки и выведем их в браузере:

public class HomeController : Controller
{
    public async Task Index() 
    {
        Response.ContentType = "text/html;charset=utf-8";
        System.Text.StringBuilder tableBuilder = new("<h2>Request headers</h2><table>");
        foreach (var header in Request.Headers)
        {
            tableBuilder.Append(
                $"<tr><td>{header.Key}</td><td>{header.Value}</td></tr>");
        }
        tableBuilder.Append("</table>");
        await Response.WriteAsync(tableBuilder.ToString());
    }
}

Передача данных в контроллер через строку запроса

Получение данных через строку запроса

Вместе с запросом приложению могут приходить различные данные. И чтобы получить эти данные, мы можем использовать разные способы. Самым распространенным способом считается применение параметров.

Определение в методах контроллера параметров ничем не отличается от определения параметров в языке C#. Например, определим в контроллере следующий метод:

public string Index(string name) => $"Your name: {name}";

В данном случае метод Index из вне получает некоторую строку через параметр name.

Передавать значения для параметров можно различными способами. При отправке GET-запроса значения можно передать через строку запроса. Строка запроса представляет ту часть адреса, которая идет после знака вопроса ? и представляет набор параметров, где каждый параметр отделен от другого с помощью амперсанда:

название_ресурса?параметр1=значение1&параметр2=значение2

Например, в адресе:

https://localhost:7288/Home/Index?name=Tom&age=37

часть ?name=Tom&age=37 представляет строку запроса, которая содержит два параметра: name и age. Значение параметра name - "Tom", а значение параметра age - 37.

Например, передадим в выше определенный метод Index через строку запроса данные для параметра name:

То есть в данном случае при обращении к методу Index с запросом https://localhost:7288/Home/Index?name=Eugene параметру name будет передаваться значение "Eugene".

Система привязки MVC, которую мы позже рассмотрим, по умолчанию сопоставляет параметры запроса и параметры метода по имени. То есть, если в строке запроса идет параметр name, то его значение будет передаваться именно параметру метода, который также называется name. При этом должно быть также соответствие по типу, то есть если параметр метода принимает числовое значение, то и через строку запроса надо передавать для этого параметра число, а не строку.

Подобным образом можно передать значения для нескольких параметров. Например, изменим метод Index следующим образом:

public string Index(string name, int age)
{
    return $"Name: {name}  Age: {age}";
}

В этом случае мы можем обратиться к действию, набрав в адресной строке https://localhost:7288/Home/Index?name=Tom&age=37.

Если же мы не используем параметры в строке запроса, то для параметров можно задать значения по умолчанию. Например, при отправке запроса https://localhost:7288/Home/Index параметры name и age будут иметь значения по умолчанию.

public string Index(string name = "Bob", int age = 33)
{
    return $"Name: {name}  Age: {age}";
}

Объект Request.Query

Параметры представляют самый простой способ получения данных, но в действительности нам необязательно их использовать. В контроллере доступен объект Request, у которого можно получить как данные строки запроса через свойство Request.Query. Это свойство представляет объект IQueryCollection, где по ключу - названию параметра можно получить его значение. Например:

public string Index()
{
    string name = Request.Query["name"];
    string age = Request.Query["age"];
    return $"Name: {name}  Age: {age}";
}

В данном случае мы можем передать методу Index данные через запрос типа https://localhost:7288/Home/Index?name=Tom&age=37.

Передача данных в контроллер через формы

Передача данных через формы в запросе POST

Кроме GET-запросов также широко применяются POST-запросы. Как правило, такие запросы отправляются с помощью форм на веб-странице. Но основные принципы передачи данных будут теми же, что и в GET-запросах.

Для передачи POST-запросов определим два метода:

[HttpGet]
public async Task Index()
{
    string content = @"<form method='post'>
        <label>Name:</label><br />
        <input name='name' /><br />
        <label>Age:</label><br />
        <input type='number' name='age' /><br />
        <input type='submit' value='Send' />
    </form>";
    Response.ContentType = "text/html;charset=utf-8";
    await Response.WriteAsync(content);
}

[HttpPost]
public string Index(string name, int age) => $"{name}: {age}";

Первый метод Index имеет атрибут [HttpGet], поэтому данный метод будет обрабатывать только запросы GET. Для упрощения примера в ответ метод будет возвращать html-код с формой ввода (хотя естественно, для формы html можно было бы определить представление)

Эта форма содержит два поля ввода. Что важно, первое поле имеет имя "name", которое задается с помощью атрибута "name":

<input name='name' />

Второе поле имеет имя "age":

<input type='number' name='age' />

Таким образом, при обращении к методу пользователь увидит в браузере форму ввода. При нажатии на кнопку "Send" введенные данные будут отправляться на сервер методом POST (<form method='post'>).

Поскольку у элемента <form> не задан атрибут action, который устанавливает адрес, то введенные данные отправляются на тот же адрес (то есть по сути методу с тем же именем - методу Index). Но поскольку у формы установлен атрибут method='post', то данные будут отправлять в запросе типа POST. А запросы данного типа обрабатывает второй метод Index:

[HttpPost]
public string Index(string name, int age) => $"{name}: {age}";

Чтобы система могла связать параметры метода и данные формы, необходимо, чтобы атрибуты name у полей формы соответствовали названиям параметров. То есть в данном случае параметры метода Index называются так же, как и поля формы - name и age.

Получение данных из контекста запроса

Для получения данных отправленных форм можно использовать свойство Request.Form. Это свойство представляет коллекцию IFormsCollection, где каждый элемент имеет ключ и значение. В качестве ключа элемента выступает название поля формы, а в качестве значения - введенные в это поле данные:

public async Task Index()
{
    string content = @"<form method='post' action='/Home/PersonData'>
        <label>Name:</label><br />
        <input name='name' /><br />
        <label>Age:</label><br />
        <input type='number' name='age' /><br />
        <input type='submit' value='Send' />
    </form>";
    Response.ContentType = "text/html;charset=utf-8";
    await Response.WriteAsync(content);
}

[HttpPost]
public string PersonData()
{
    string name = Request.Form["name"];
    string age = Request.Form["age"];
    return $"{name}: {age}";
}

Обратите внимание, у формы определен атрибут action, который содержит урл. То есть при нажатии кнопки "Send" запрос будет послан не на метод Index, а на урл /Home/PersonData, которому соответствует одноименный метод контроллера

Результаты действий

При обращении к веб-приложению, как правило, пользователь ожидает получить некоторый ответ, например, в виде веб-страницы, которая наполнена данными. На стороне сервера метод контроллера, получая параметры и данные запроса, обрабатывает их и формирует ответ в виде результата действия. Результат действия - это тот объект, который возвращается методом после обработки запроса.

Результатом действия может быть практически что угодно. Например, в прошлых темах использовался объект string, например:

public string Index()
{
    return "Hello METANIT.COM";
}

Пользователь передает методу некоторые значения и в ответ на запрос видит в своем браузере строку ответа.

Результатом действия может быть какой-нибудь сложный объект:

public class HomeController : Controller
{
    public Message Index() => new Message("Hello METANIT.COM");
}
public record class Message(string Text);

Результатом может быть даже void, то есть по сути ничего:

public void GetVoid()
{
 
}

В данном случае метод GetVoid представляет вполне реальное действие контроллера, к которому можно обращаться из адресной строки браузера, передавать параметры, и который может содержать сложную логику обработки запроса. Только после обращения к этому методу пользователь увидит в своем веб-браузере одну пустоту, так как метод ничего не возвращает.

Но в большинстве случаев мы будем иметь дело не с void и даже не с типом string, а с объектами типа IActionResult, которые непосредственно предназначены для генерации результата действия. Интерфейс IActionResult находится в пространстве имен Microsoft.AspNetCore.Mvc и определяет один метод:

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

Метод ExecuteResultAsync принимает контекст действия и выполняет генерацию результата.

Этот интерфейс затем реализуется абстрактным базовым классом ActionResult:

public abstract class ActionResult : IActionResult
{
    public virtual Task ExecuteResultAsync(ActionContext context)
    {
            ExecuteResult(context);
            return Task.FromResult(true);
    }
 
    public virtual void ExecuteResult(ActionContext context)
    {
    }
}

ActionResult добавляет синхронный метод, который выполняется в асинхронном. И если мы вдруг захотим создать свой класс результата действий, то как раз можем либо унаследовать его от ActionResult, либо реализовать интерфейс IActionResult.

Итак, создадим свой класс результата действий. Для этого вначале добавим в проект новый класс, который назовем HtmlResult

Определим в нем следующий код:

public class HtmlResult : IActionResult
{
    string htmlCode;

    // конструктор
    public HtmlResult(string html) => htmlCode = html;

    public async Task ExecuteResultAsync(ActionContext context)
    {
        string fullHtmlCode = @$"<!DOCTYPE html>
        <html>
            <head>
                <title>METANIT.COM</title>
                <meta charset=utf-8 />
            </head>
            <body>{htmlCode}</body>
        </html>";
        await context.HttpContext.Response.WriteAsync(fullHtmlCode);
    }
}

Данный класс будет реализовать интерфейс IActionResult. В конструкторе он принимает html-код, который затем будет выводиться на веб-страницу. Для вывода используется асинхронный метод context.HttpContext.Response.WriteAsync()

Теперь используем этот класс в контроллере:

public class HomeController : Controller
{
    public IActionResult Index()
    {
        return new HtmlResult("<h2>Hello METANIT.COM!</h2>");
    }
}

Здесь определен метод Index(), который возвращает объект HtmlResult. При обращении к этому объекту будет срабатывать его метод ExecuteResultAsync(), в котором будет происходить генерация html-страницы

Однако в большинстве случаев нам не придется создавать свои классы результатов, потому что фреймворк ASP.NET Core MVC итак предоставляет довольно большую палитру классов результатов для самых различных ситуаций:

  • ContentResult: отправляет ответ в виде строки
  • EmptyResult: отправляет пустой ответ в виде статусного кода 200

    public IActionResult GetVoid()
    {
        return new EmptyResult();
    }
    

    Аналогичен следующему методу:

    public void GetVoid()
    {
    }
    
  • NoContentResult: во многом похож на EmptyResult, также отправляет пустой ответ, только в виде статусного кода 204

    public IActionResult GetVoid()
    {
        return new NoContentResult();
    }
    
  • FileResult: является базовым классом для всех объектов, которые пишут набор байтов в выходной поток. Предназначен для отправки файлов

  • FileContentResult: класс, производный от FileResult, пишет в ответ массив байтов

  • VirtualFileResult: также производный от FileResult класс, пишет в ответ файл, находящийся по заданному пути

  • PhysicalFileResult: также производный от FileResult класс, пишет в ответ файл, находящийся по заданному пути. Только в отличие от предыдущего класса использует физический путь, а не виртуальный.

  • FileStreamResult: класс, производный от FileResult, пишет бинарный поток в выходной ответ

  • ObjectResult: возвращает произвольный объект, как правило, применяется в качестве базового класса для других классов результатов. Но можно применять и самостоятельно:

    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return new ObjectResult(new Person("Tom", 37));
        }
    }
    record class Person(string Name, int Age);
    
  • StatusCodeResult: результат действия, который возвращает клиенту определенный статусный код HTTP

  • UnauthorizedResult: класс, производный от StatusCodeResult. Возвращает клиенту ответ в виде статусного кода HTTP 401, указывая, что пользователь не прошел авторизацию и не имеет прав доступа к запрошенному ресурсу.

  • NotFoundResult: производный от StatusCodeResult. Возвращает клиенту ответ в виде статусного кода HTTP 404, указывая, что запрошенный ресурс не найден

  • NotFoundObjectResult: производный от ObjectResult. Также возвращает клиенту ответ в виде статусного кода HTTP 404 с дополнительной информацией

  • BadRequestResult: производный от StatusCodeResult. Возвращает статусный код 400, тем самым указывая, что запрос некорректен

  • BadRequestObjectResult: производный от ObjectResult. Возвращает статусный код 400 с некоторой дополнительной информацией

  • OkResult: производный от StatusCodeResult. Возвращает статусный код 200, который уведомляет об успешном выполнении запроса

  • OkObjectResult: производный от ObjectResult. Возвращает статусный код 200 с некоторой дополнительной информацией

  • CreatedResult: возвращает статусный код 201, который уведомляет о создании нового ресурса. В качестве параметра принимает адрес нового ресурса

  • ChallengeResult: используется для проверки аутентификации пользователя

  • JsonResult: возвращает в качестве ответа объект или набор объектов в формате JSON

  • PartialViewResult: производит рендеринг частичного представления в выходной поток

  • RedirectResult: перенаправляет пользователя по другому адресу URL, возвращая статусный код 302 для временной переадресации или код 301 для постоянной переадресации зависимости от того, установлен ли флаг Permanent.

  • RedirectToRouteResult: класс работает подобно RedirectResult, но перенаправляет пользователя по определенному адресу URL, указанному через параметры маршрута

  • RedirectToActionResult: выполняет переадресацию на определенный метод контроллера

  • LocalRedirectResult: перенаправляет пользователя по определенному адресу URL в рамках веб-приложения

  • ViewComponentResult: возвращает в ответ сущность ViewComponent

  • ViewResult: производит рендеринг представления и отправляет результаты рендеринга в виде html-страницы клиенту

Рассмотрим некоторые из этих классов.

ContentResult и JsonResult

ContentResult

ContentResult отправляет клиенту ответ в виде строки. Так, следующий пример:

public string Index()
{
    return "Hello METANIT.COM";
}

Можно переписать с использованием ContentResult:

public IActionResult Index()
{
    return Content("Hello METANIT.COM");
}

Для отправки ContentResult не надо использовать конструктор, так как в контроллере уже определен специальный метод Content(), который принимает отправляемую строку и создает объект ContentResult.

JsonResult

Одним из наиболее популярных в наше время форматов хранения и передачи данных является формат JSON (JavaScript Object Notation). JSON не зависит от языка программирования, он более удобен и легче обрабатывается.

В JSON каждый отдельный объект заключается в фигурные скобки и представляет собой набор пар ключ-значение, разделенных запятыми, где ключом является название свойства объекта, а значением соответственно значение этого свойства. Например: {"name":"Tom"}. Здесь "name" является ключом, а "Tom" - значением.

Для отправки объекта в формате json в контроллере имеется метод Json(object obj), который в качестве параметра принимает отправляемый объект. Например:

public JsonResult GetName()
{
    return Json("Tom");
}

В данном случае на сторону клиента отправляется строка "Tom".

Допустим, у нас есть следующий класс Person:

record class Person(string Name, int Age);

И тогда для отправки клиенту объекта Person мы можем написать следующий метод:

public IActionResult Index()
{
    Person tom = new Person("Tom", 37);
    return Json(tom);
}

При обращении к методу веб-браузер выведет полное описание объекта в формате json

Дополнительная версия метода Json() в качестве второго параметра принимает объект, который задает настройки сериализации в формат json. В качестве такого объекта выступает объект типа JsonSerializerOptions:

public class HomeController : Controller
{
    public IActionResult Index()
    {
        var tom = new Person("Tom", 37);
        var jsonOptions = new System.Text.Json.JsonSerializerOptions 
        { 
            PropertyNameCaseInsensitive = true, // не учитываем регистр
            WriteIndented = true                // отступы для красоты
        };
        return Json(tom, jsonOptions);
    }
}
record class Person(string Name, int Age);

В данном случае объект JsonSerializerOptions задает с помощью свойств параметры сериализации в json. В частности, значение свойства PropertyNameCaseInsensitive = true говорит, что регистр названий свойств не учитывается. А свойство WriteIndented = true задает установку отступов перед свойствами для более человекопонятного вывода

Отправка статусных кодов

Нередко возникает необходимость отправить в ответ на запрос какой-либо статусный код. Например, если пользователь пытается получить доступ к ресурсу, который недоступен, или для которого у пользователя нету прав. Либо если просто нужно уведомить браузер пользователя с помощью статусного кода об успешном выполнении операции, как иногда применяется в ajax-запросах. Для этого в ASP.NET Core MVC определена богатая палитра классов, которые можно использовать для отправки статусного кода.

StatusCodeResult

StatusCodeResult позволяет отправить любой статусный код клиенту:

public IActionResult Index()
{
    return StatusCode(401);
}

Для создания этого результата используется метод StatusCode(), в который передается отправляемый код статуса.

Подобным образом мы можем послать браузеру любой другой статусный код. Но для отдельных кодов статуса предназначены свои отдельные классы.

HttpNotFoundResult и HttpNotFoundObjectResult

NotFoundResult и NotFoundObjectResult посылает код 404, уведомляя браузер о том, что ресурс не найден. Второй класс в дополнении к статусному коду позволяет отправить доплнительную информацию, которая потом отобразится в браузере.

Объекты обоих классов создаются методом NotFound. Для первого класса - это метод без параметров, для второго класса - метод, который в качестве параметра принимает отправляемую информацию. Например, используем NotFoundObjectResult:

public IActionResult Index()
{
    return NotFound("Resource not found");
}

UnauthorizedResult и UnauthorizedObjectResult

UnauthorizedResult посылает код 401, уведомляя пользователя, что он не автризован для доступа к ресурсу:

public IActionResult Index(int age)
{
    if (age < 18) return Unauthorized();
    return Content("Access is available");
}

Для создания ответа используется метод Unauthorized().

UnauthorizedObjectResult также посылает код 401, только позволяет добавить в ответ некоторый объект с информацией об ошибке:

public class HomeController : Controller
{
    public IActionResult Index(int age)
    {
        if (age < 18) return Unauthorized(new Error("Access is denied"));
        return Content("Access is available");
    }
}
record class Error(string Message);

BadResult и BadObjectResult

BadResult и BadObjectResult посылают код 400, что говорит о том, что запрос некорректный. Второй класс в дополнении к статусному коду позволяет отправить доплнительную информацию, которая потом отобразится в браузере.

Эти классы можно применять, например, если в запросе нет каких-то параметров или данные представляют совсем не те типы, которые мы ожидаем получить, и т.д.

Объекты обоих классов создаются методом BadRequest. Для первого класса - это метод без параметров, для второго класса - метод, который в качестве параметра принимает отправляемую информацию:

public IActionResult Index(string? name)
{
    if (string.IsNullOrEmpty(name)) return BadRequest("Name undefined");
    return Content($"Name: {name}");
}

OkResult и OkObjectResult

OkResult и OkObjectResult посылают код 200, уведомляя об успешном выполнении запроса. Второй класс в дополнении к статусному коду позволяет отправить дополнительную информацию, которая потом отправляется клиенту в формате json.

Объекты обоих классов создаются методом Ok(). Для первого класса - это метод без параметров, для второго класса - метод, который в качестве параметра принимает отправляемую информацию:

public IActionResult Index()
{
    return Ok("Don't worry. Be happy");
}

Задание на дом

Сделать репозиторий - конспект, то есть создать приложение и в конспекте привести блоки кода и скриншоты для всех примеров из лекции.