Содержание
Для сопоставления пути запроса и делегата, который должен выполнится, в системе маршрутизации ASP.NET Core используются шаблоны маршрутов. До сих пор мы использовали простейшие шаблоны маршрутов типа /home, /index и так далее. Вместе с тем, мы можем задавать шаблоны маршрутов, содержащих параметры и различные ограничения эти параметров. Сегодня более подробно разберемся с шаблонами маршрутов в ASP.NET Core.
Параметры маршрута
При указании маршрута для конечной точки мы можем использоваться параметры. Каждый параметр имеет имя и заключается в фигурные скобки, например, {id}
. Рассмотрим следующий пример:
namespace ToDo { public class ToDoTask { public int Id { get; set; } //id задачи public string Name { get; set; } //имя задачи public DateTime Start { get; set; } = DateTime.Now; //дата создания задачи public DateTime End { get; set; } //дата окончания задачи } public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); List<ToDoTask> toDoList = new() { new ToDoTask { Id = 1, Name = "Разобраться с шаблонами маршрутов в ASP.NET Core", End = DateTime.Now.AddDays(1)}, new ToDoTask { Id = 2, Name = "Написать пример приложения", End = DateTime.Now.AddDays(2)}, new ToDoTask { Id = 3, Name = "Написать статью про ASP.NET Core", End = DateTime.Now.AddDays(3)}, }; app.MapGet("/", async (context) => { context.Response.ContentType = "text/html; charset=utf-8"; StringBuilder sb = new StringBuilder(); sb.AppendLine("<h1>Список задач</h1>"); sb.AppendLine("<table>"); sb.AppendLine("<tr><td>Id</td><td>Задача</td><td>Начало</td><td>Окончание</td></tr>"); foreach (ToDoTask task in toDoList) { sb.AppendLine($"<tr><td>{task.Id}</td><td>{task.Name}</td><td>{task.Start: dd:mm:yyyy}</td><td>{task.End: dd:mm:yyyy}</td></tr>"); } await context.Response.WriteAsync(sb.ToString()); }); app.Run(); } } }
Здесь определен класс ToDoTask
, описывающий одну задачу, список таких задач List<ToDoTask>
и одна конечная точка в которой все задачи выводятся в виде таблицы. То есть ничего особенного, всё это мы уже делали и не раз.
Теперь, допустим, нам необходимо сделать так, чтобы пользователь мог получить каждую задачу по отдельности, например, чтобы в дальнейшем её редактировать. Самый простой способ это сделать — сделать так, чтобы путь вида /id
, где id — это идентификатор задачи, приводил нас на страничку с задачей (или же наше приложение возвращало по этому пути, например JSON-объект). То есть, применительно к нашей задаче, это должны быть такие пути:
/1
/2
/3
- и т.д.
Сделать это можно, используя в шаблоне маршрута параметр. Например следующим образом:
app.MapGet("/{id}", (int id) => { ToDoTask task = toDoList.Where(t=>t.Id==id).FirstOrDefault(); StringBuilder sb = new StringBuilder(); if (task != null) { sb.AppendLine("<h1>Задача</h1>"); sb.AppendLine($"<p>ID: {task.Id}</p>"); sb.AppendLine($"<p>Название: {task.Name}</p>"); sb.AppendLine($"<p>Начало: {task.Start}</p>"); sb.AppendLine($"<p>Окончание: {task.End}</p>"); } else { sb.AppendLine($"Задача с ID {id} не найдена"); } return sb.ToString(); });
здесь мы, при создании конечной точки, определили параметр с именем id
и передали его в делегат:
"/{id}", (int id) => ...
Причем, мы также указали, что ожидаем получить в параметре именно значение int
. В самом делегате мы использовали значение параметра для поиска необходимой задачи и вывели её пользователю. Результат в приложении выглядит следующим образом:
Таким образом, мы добились следующей схемы работы нашего приложения:
- Если пользователь задает путь, содержащий id задачи, то сработает конечная точка, которая выводит конкретную задачу
- Если ID не задан, то выводится список задач в виде таблицы.
Указание нескольких параметров
При необходимости, мы можем указывать в шаблоне маршрута не один, а несколько параметров. Например, так:
app.MapGet("/{id}/{format}", (int id, string format) => { ToDoTask task = toDoList.Where(t=>t.Id==id).FirstOrDefault(); StringBuilder sb = new StringBuilder(); if (task != null) { if (format.ToLower() == "text") { sb.AppendLine($"ID: {task.Id}; Имя: {task.Name} Начало: {task.Start} Окончание: {task.End}"); } else { sb.AppendLine("<h1>Задача</h1>"); sb.AppendLine($"<p>ID: {task.Id}</p>"); sb.AppendLine($"<p>Название: {task.Name}</p>"); sb.AppendLine($"<p>Начало: {task.Start}</p>"); sb.AppendLine($"<p>Окончание: {task.End}</p>"); } } else { sb.AppendLine($"Задача с ID {id} не найдена"); } return sb.ToString(); });
В этом случае, если пользователь перейдет по пути /1/text
, то задача вернется в виде обычного текста
, а во всех остальных случаях — в виде html.
Параметры могут указываться в одном сегменте пути подряд, но, при условии, что между ними будет находиться како-либо разделитель, например, такой шаблон маршрута не сработает:
/{id}{format}
так как параметры не разделены. Зато сработает такой шаблон:
/{id}-{format}
и такой шаблон может соответствовать, например, такому пути /1-text
.
Необязательные параметры и параметры со значением по умолчанию
Параметры могут иметь значение по умолчанию или же быть необязательными. Вернемся к нашему примеру. На данный момент у нас определен следующий шаблон:
"/{id}/{format}"
это означает, что и параметр id
и параметр format
должны быть заданы. То, есть, например, путь /2
уже не соответствует ни одной из конечных точек приложения. Чтобы этот путь работал как уже определенная в приложении конечная точка, мы можем поступить следующими способами:
задать параметр format как необязательный — указав после имени параметра знак вопроса:
app.MapGet("/{id}/{format?}", (int id, string? format) =>
здесь необходимо также отметить, что в делегат мы передаем параметр как допускающий значение null
. Если этого не сделать, то при попытке опустить необязательный параметр в пути мы получим ошибку:
указать для параметра format значение по умолчанию. Значение параметра по умолчанию указывается после знака
=
, например
app.MapGet("/{id}/{format=text}", (int id, string format) =>
в этом случае, если параметр format будет опущен, то в делегат будет передано его значение по умолчанию, то есть строка »
Обработчик конечной точки как отдельный метод
Обработчик маршрута конечной точки, как и middleware может выноситься в отдельный метод. В этом методе мы также можем указывать все передаваемые параметры, например:
app.MapGet("/{id}/{format=text}", (int id, string format) => EndpointHandler);
сам метод EndpointHandler
должен принимать параметры маршрута:
public static string EndpointHandler(int id, string format)
Итого
Сегодня мы рассмотрели применение параметров маршрута для формирования шаблона. Параметры маршрута могут быть необязательными, а также иметь значения по умолчанию. Что касается нашего примера приложения, то, применительно к теме шаблонов маршрута и их параметров, в примере есть уязвимость — например, если в качестве параметра id указать не число, а строку, то это приведет к исключению. Решаются подобные проблемы, в том числе, с использованием ограничения маршрутов, о которых мы поговорим далее.