Роутинг

Ryan Nowak, Steve Smith и Rick Anderson

Роутинг используется для для передачи запросов обработчикам. Роуты настраиваются при запуске приложения и могут получать значения из URL, которые используются для передачи запросов. Роутинг также отвечает за создание ссылок, когда используются определенные роуты.

В данном руководстве рассматривается роутинг низкого уровня ASP.NET Core. Информация по роутингу ASP.NET Core MVC: 🔧 Роутинг к действиям контроллера

Скачайте или просмотрите пример

Основы роутинга

В роутинге используются роуты (реализации :dn:iface:`~Microsoft.AspNetCore.Routing.IRouter`) для:

  • передачи входящих запросов обработчикам роутов
  • создания URL, используемых в ответах

Обычно в приложении есть одна коллекция роутов. Роуты обрабатываются по порядку. Запросы ищут соответствие в коллекции по Отправление URL по назначению. Ответы используют роуты для генерации URL.

Роутинг связан с потоком связующего ПО связующее ПО по классу :dn:class:`~Microsoft.AspNetCore.Builder.RouterMiddleware`. ASP.NET MVC добавляет роутинг в поток как часть конфигурации. См. using-routing-middleware.

Отправление URL по назначению

Роутинг отправляет входящий запрос по назначению обработчику. Этот процесс, как правило, основывается на данных из URL, но может учитывать и другие данные из запроса. Способность отправлять запросы по назначению отдельным обработчикам является ключевым моментов в определении размера и сложности приложения.

Входящие запросы попадают к :dn:cls:`~Microsoft.AspNetCore.Builder.RouterMiddleware`, который вызывает метод :dn:method:`~Microsoft.AspNetCore.Routing.IRouter.RouteAsync` для каждого роута в цепочке. Экземпляр :dn:iface:`~Microsoft.AspNetCore.Routing.IRouter` смотрит, обрабатывать ли запрос, установив :dn:cls:`~Microsoft.AspNetCore.Routing.RouteContext` :dn:prop:`~Microsoft.AspNetCore.Routing.RouteContext.Handler` на не-null :dn:delegate:`~Microsoft.AspNetCore.Http.RequestDelegate`. Если обработчик устанавливает роут, он будет вызван для обработки запроса и другие роуты не будут с ним работать. Если все роуты выполнены, а обработчик для запроса не найден, связующее ПО вызывает next, то есть, вызывается следующее связующее ПО в потоке.

Первичным вводом для RouteAsync является :dn:cls:`~Microsoft.AspNetCore.Routing.RouteContext` :dn:prop:`~Microsoft.AspNetCore.Routing.RouteContext.HttpContext`, связанный с текущим запросом. RouteContext.Handler и :dn:cls:`~Microsoft.AspNetCore.Routing.RouteContext` :dn:prop:`~Microsoft.AspNetCore.Routing.RouteContext.RouteData` являются результатами, которые будут установлены после успешного нахождения соответствия.

При успешном соответствии для RouteAsync свойствам RouteContext.RouteData будут установлены соответствующие значения, основанные на результате обработки запроса. RouteContext.RouteData содержит важную информацию о результате выполнения роута, если он подошел запросу.

:dn:cls:`~Microsoft.AspNetCore.Routing.RouteData` :dn:prop:`~Microsoft.AspNetCore.Routing.RouteData.Values` - это словарь роутовых значений. Эти значения обычно определены токенизацией URL, и их можно использовать для принятия пользовательского ввода или для дальнейшей отправки запроса по назначению.

:dn:cls:`~Microsoft.AspNetCore.Routing.RouteData` :dn:prop:`~Microsoft.AspNetCore.Routing.RouteData.DataTokens` - это хранилище свойств для дополнительных данных, связанных с подходящим роутом. DataTokens поддерживает связанные данные для каждого роута. Эти значения определяются разработчиками и они вообще не влияют на поведение роутинга. Кроме того, данные в токенах могут быть любого типа в отличие от роутовых значений, которые должны легко конвертироваться в строку и обратно.

:dn:cls:`~Microsoft.AspNetCore.Routing.RouteData` :dn:prop:`~Microsoft.AspNetCore.Routing.RouteData.Routers` - это список роутов, которые привели к успешному соответствию запроса. Роуты можно вкладывать один в один, и свойство Routers влияет на путь через логическое дерево роутов, that resulted in a match. Как правило, первый элемент Routers - это коллекция роутов, и он используется для генерации URL. Последний элемент Routers - это роут, который совпал.

Генерирование URL

Генерирование URL - это процесс, при котором роутинг может создавать URL путь, основываясь на наборе роутовых значений. Здесь проявляется логическое разделение между обработчиками и URL, которые получают к ним доступ.

URL генерирование начинается с вызова метода :dn:method:`~Microsoft.AspNetCore.Routing.IRouter.GetVirtualPath` коллекции роутов. Затем по порядку для каждого роута вызывается свой метод GetVirtualPath, пока не вернется не-null :dn:cls:`~Microsoft.AspNetCore.Routing.VirtualPathData`.

Первичные вводы для GetVirtualPath:

Роуты изначально используют значения, предоставляемые Values и AmbientValues, чтобы решить, можно ли сгенерировать URL и какие значения туда включить. AmbientValues - это набор роутовых значений, которые были созданы после нахождения соответствия для текущего запроса в системе роутинга. Values - это роутовые значения, которые указывают, как генерировать желаемый URL для текущей операции. Также предоставляется HttpContext, если роуту нужно получить сервисы или дополнительные данные, связанные с текущим контекстом.

Совет

Values можно считать набором переопределенных вариантов для AmbientValues. Система URL генерации пытается заново использовать роутовые значения из текущего запроса, чтобы упростить генерирование URL для ссылок, используя тот же роут или значения роута.

Результатом GetVirtualPath является :dn:cls:`~Microsoft.AspNetCore.Routing.VirtualPathData`. VirtualPathData - это параллель RouteData; он содержит VirtualPath для результирующего URL, а также некоторые дополнительные свойства, которые должны быть установлены роутом.

Свойство :dn:cls:`~Microsoft.AspNetCore.Routing.VirtualPathData` :dn:prop:`~Microsoft.AspNetCore.Routing.VirtualPathData.VirtualPath` содержит виртуальный путь, созданный роутом. В зависимости от ваших нужд вам может понадобиться дальнейшая обработка пути. Например, если вы хотите отобразить сгенерированный URL в HTML, то вам нужно поставить впереди базовый путь приложения.

:dn:cls:`~Microsoft.AspNetCore.Routing.VirtualPathData` :dn:prop:`~Microsoft.AspNetCore.Routing.VirtualPathData.Router` - это ссылка к роуту, который успешно сгенерировал URL.

Свойства :dn:cls:`~Microsoft.AspNetCore.Routing.VirtualPathData` :dn:prop:`~Microsoft.AspNetCore.Routing.VirtualPathData.DataTokens` - это словарь дополнительных данных, связанных с роутом, который сгенерировал URL. Это параллель RouteData.DataTokens.

Создание роутов

Роутинг предоставляет класс :dn:cls:`~Microsoft.AspNetCore.Routing.Route` в качестве стандартной реализации IRouter. Route использует синтаксис шаблона роутов для определения паттернов, которые будут соответствовать пути URL при вызове :dn:method:`~Microsoft.AspNetCore.Routing.IRouter.RouteAsync`. Route будет использовать тот же шаблон роутов для генерации URL при вызове :dn:method:`~Microsoft.AspNetCore.Routing.IRouter.GetVirtualPath`.

В большинстве приложений роуты создаются с помощью MapRoute или похожих методов расширения, определенных для :dn:iface:`~Microsoft.AspNetCore.Routing.IRouteBuilder`. Все эти методы создают экземпляр``Route`` и добавляют его в коллекцию роутов.

Примечание

:dn:method:`~Microsoft.AspNetCore.Builder.MapRouteRouteBuilderExtensions.MapRoute` не принимает параметр обработчика роутов - он просто добавляет роуты, которые обрабатываются :dn:prop:`~Microsoft.AspNetCore.Routing.IRouteBuilder.DefaultHandler`. Поскольку обработчик по умолчанию это :dn:iface:`~Microsoft.AspNetCore.Routing.IRouter`, он может решать, обрабатывать запрос или нет. Например, ASP.NET MVC обычно настраивается как обработчик по умолчанию, который обрабатывает только те запросы, что соответствуют доступному контроллеру или действию. См. 🔧 Роутинг к действиям контроллера.

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

routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Index}/{id?}");

Это шаблон будет соответствовать URL пути как /Products/Details/17 и раскрывать роутовые значения { controller = Products, action = Details, id = 17 }. Роутовые значения определяются разделением URL пути на сегменты и соответствием каждого сегмента имени параметра роута в шаблоне. Рараметры роутов имеют имена. Их заключают в фигурные скобки { }.

Шаблон сверху также мог бы соответствовать URL пути / и создал бы значения { controller = Home, action = Index }. Это происходит потому, что параметры роута {controller} и {action} имеют значения по умолчанию, а параметр id является дополнительным. Знак равно =, за которым следует значение после имени параметра роута, определяет значение по умолчанию. Знак вопроса после параметра роута ? определяет параметр как дополнительный. Параметры роута со значением по умолчанию всегда создают роутовое значение, если роут соответствует - дополнительные параметры не создают роутовое значение, если соответствующего сегмента URL пути не было предоставлено.

См. route-template-reference.

В этот пример включено роутовое ограничение:

routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Index}/{id:int}");

Этот шаблон будет соответствовать пути URL как /Products/Details/17, но не /Products/Details/Apples. Определение параметра роута {id:int} создает роутовое ограничение для параметра id. Роутовые ограничения реализуют IRouteConstraint и смотрят, чтобы роутовые значения им следовали. В этом примере роутовое значение id должно конвертироваться в целое число. См. route-constraint-reference.

Переопределенные варианты MapRoute принимают значения для constraints, dataTokens и defaults. Эти дополнительные параметры MapRoute определены как тип object. Эти параметры обычно используются для передачи анонимно типизированного объекта, где имена свойств анонимного типа соответствуют именам параметров роута.

В двух следующих примерах создаются эквивалентные роуты:

routes.MapRoute(
    name: "default_route",
    template: "{controller}/{action}/{id?}",
    defaults: new { controller = "Home", action = "Index" });

routes.MapRoute(
    name: "default_route",
    template: "{controller=Home}/{action=Index}/{id?}");

Совет

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

В этом примере представлено чуть больше возможностей:

routes.MapRoute(
  name: "blog",
  template: "Blog/{*article}",
  defaults: new { controller = "Blog", action = "ReadArticle" });

Этот шаблон будет соответствовать пути URL как /Blog/All-About-Routing/Introduction и раскрывать значения { controller = Blog, action = ReadArticle, article = All-About-Routing/Introduction }. Роутовые значения по умолчанию для controller и action создаются роутом, хотя в шаблоне нет соответствующих роутовых параметров. Значения по умолчанию можно указать в шаблоне. Параметр article определен как catch-all с помощью *. Роутовые параметры catch-all захватывают оставшуюся часть URL пути, а также могут соответствовать пустой строке.

В этом примере добавляются роутовые ограничения и токены данных:

routes.MapRoute(
    name: "us_english_products",
    template: "en-US/Products/{id}",
    defaults: new { controller = "Products", action = "Details" },
    constraints: new { id = new IntRouteConstraint() },
    dataTokens: new { locale = "en-US" });

Этот шаблон будет соответствовать URL пути как /en-US/Products/5 и раскрывать значения { controller = Products, action = Details, id = 5 } и токены данных { locale = en-US }.

../_images/tokens.png

Генерирование URL

Класс Route также может генерировать URL, комбинируя набор роутовых значений с шаблоном. Логически это процесс, обратный нахождению соответствия с URL путем.

Совет

Чтобы лучше понимать генерирование URL, представьте, какой URL вы хотите сгенерировать, а затем подумайте о том, какой шаблон будет соответствовать этому URL. Какие значения будут созданы? По факту по такому принципу работает URL генерирование в классе Route.

В этом примере используется базовый роутовый стиль ASP.NET MVC:

routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Index}/{id?}");

С роутовыми значениями { controller = Products, action = List } этот роут создаст URL /Products/List. Роутовые значения подставляются для соответствующих параметров роута, чтобы формировать URL путь. Поскольку id - это дополнительный параметр, он может и не иметь значения.

Со значениями { controller = Home, action = Index } этот роут сгенерирует URL /. Предоставленные значения соответствуют значениям по умолчанию, так что сегменты, соответствующие этим значениям, можно спокойно опустить. Обратите внимание, что оба сгенерированных URL создадут те же значения, которые использовались для генерирования URL.

Совет

Для генерирования URL ASP.NET MVC приложение должно использовать :dn:cls:`~Microsoft.AspNetCore.Mvc.Routing.UrlHelper`.

См. url-generation-reference.

Использование связующего ПО роутинга

Чтобы использовать связующее ПО роутинга, добавьте его в dependencies в project.json:

"Microsoft.AspNetCore.Routing": <current version>

Добавьте роутинг в контейнер сервисов Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddRouting();
}

Роуты нужно настраивать в методе Configure класса Startup. В примере ниже используются эти API:

public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
    var trackPackageRouteHandler = new RouteHandler(context =>
    {
        var routeValues = context.GetRouteData().Values;
        return context.Response.WriteAsync(
            $"Hello! Route values: {string.Join(", ", routeValues)}");
    });

    var routeBuilder = new RouteBuilder(app, trackPackageRouteHandler);

    routeBuilder.MapRoute(
        "Track Package Route",
        "package/{operation:regex(^track|create|detonate$)}/{id:int}");

    routeBuilder.MapGet("hello/{name}", context =>
    {
        var name = context.GetRouteValue("name");
        // This is the route handler when HTTP GET "hello/<anything>"  matches
        // To match HTTP GET "hello/<anything>/<anything>, 
        // use routeBuilder.MapGet("hello/{*name}"
        return context.Response.WriteAsync($"Hi, {name}!");
    });            

    var routes = routeBuilder.Build();
    app.UseRouter(routes);

В таблице ниже показаны ответы с заданными URI.

URI Ответ
/package/create/3 Hello! Route values: [operation, create], [id, 3]
/package/track/-3 Hello! Route values: [operation, track], [id, -3]
/package/track/-3/ Hello! Route values: [operation, track], [id, -3]
/package/track/ <Fall through, no match>
GET /hello/Joe Hi, Joe!
POST /hello/Joe <Fall through, matches HTTP GET only>
GET /hello/Joe/Smith <Fall through, no match>

Если вы настраиваете один роут, вызовите app.UseRouter передав ему экземпляр IRouter. Вам не нужно вызывать RouteBuilder.

Фреймворк предлагает набор методов расширения для создания роутов:

Некоторые из этих методов, например MapGet, требуют :dn:delegate:`~Microsoft.AspNetCore.Http.RequestDelegate`. RequestDelegate используется как обработчик роутов при соответствии роута. Другие методы этого плана настраивают поток связующего ПО, который используется как обработчик роутов. Если метод Map не принимает обработчик, например MapRoute, он будет использовать :dn:prop:`~Microsoft.AspNetCore.Routing.IRouteBuilder.DefaultHandler`.

Методы Map[Verb] ограничивают роут HTTP Verb в имени метода. Например, см. MapGet и MapVerb.

Роутовые шаблоны

Токены внутри фигурных скобок ({ }) определяют параметры роута, которые будут переданы, если роут совпадет. В роутовом сегменте вы можете определить больше одного параметра роута, но они должны быть разделены буквальным значением. Например, {controller=Home}{action=Index} не будет валидным роутом, поскольку между {controller} и {action} нет буквального значения. У этих роутовых параметров должно быть имя, а также здесь могут быть указаны дополнительные атрибуты.

Буквальное значение, кроме параметров роутов (например, {id}) и разделителя /, должно совпадать с текстом в URL. В данном случае текст чувствителен к регистру, а нахождение соответствия основано на декодированном представлении URL пути. Чтобы соответствовать разделителю параметров { or }, повторите символ ({{ or }}).

У URL паттернов, которые пытаются принять имя файла с дополнительными файловыми расширениями, есть дополнительные особенности. Например, при использовании шаблона files/{filename}.{ext?} - когда существует и filename, и ext, будут заполнены оба значения. Если в URL существует только filename, роут будет соответствовать, поскольку идущая после него точка . является дополнительным знаком. Следующие URL будут соответствовать этому роуту:

  • /files/myFile.txt
  • /files/myFile.
  • /files/myFile

Вы можете использовать * в качестве префикса для параметра роута, чтобы связаться с остальной частью URI - это называется параметром catch-all. Например, blog/{*slug} будет соответствовать любому URI, который начинается с /blog и за ним следуют любые значения (которые присваиваются slug). Параметры сatch-all могут также соответствовать пустой строке.

У роутовых параметров могут быть значения по умолчанию, если указать значение по умолчанию после имени параметра и разделить их =. Например, {controller=Home} определит Home в качестве значения по умолчанию для controller. Значение по умолчанию используется тогда, когда в URL для параметра нет значения. Кроме того, параметры роута могут быть дополнительными (указываются с помощью ? в конце имени параметра, как в id?). Разница между дополнительным параметром и параметром “со значением по умолчанию” состоит в том, что параметр со значением по умолчанию всегда создает значение, а у дополнительного параметра значение будет только в том случае, если оно ему передано.

У параметров роута также могут быть ограничения, которые должны соответствовать роутовому значению, взятому из URL. Двоеточие``:`` и имя ограничения после имени параметра обозначает строковое ограничение для параметра роута. Если ограничению нужны аргументы, они передаются в круглых скобках ( ) после имени ограничения. Несколько строковых ограничений можно указать, добавив еще : и имя ограничения. Имя ограничения передается сервису :dn:iface:`~Microsoft.AspNetCore.Routing.IInlineConstraintResolver` для создания экземпляра :dn:iface:`~Microsoft.AspNetCore.Routing.IRouteConstraint`, который используется в обработке URL. Например, роутовый шаблон blog/{article:minlength(10)} указывает ограничение minlength с аргументом 10. См. route-constraint-reference.

Вот некоторые шаблоны роутов и их поведение.

Шаблон | Пример соответствующего URL | Заметки |

+===================================+================================+================================================+ | hello | | /hello | | Соответствует только одному пути ‘/hello’ + +———————————–+——————————–+————————————————+ |{Page=Home} | | / | | Устанавливает Page на Home | +———————————–+——————————–+————————————————+ |{Page=Home} | | /Contact | | Устанавливает Page на Contact | +———————————–+——————————–+————————————————+ |{controller}/{action}/{id?} | | /Products/List | | Передает данные контроллеру Products и методу List | | | | | | | +———————————–+——————————–+————————————————+ | {controller}/{action}/{id?} | | /Products/Details/123 | | Передает данные контроллеру Products и | | | | | | методу действия Details. id установлен на 123 | +———————————–+——————————–+————————————————+ |{controller=Home}/{action=Index}/{id?} | | / | | Передает данные контроллеру Home и методу Index | | | | | |; id игнорируется. | +———————————–+——————————–+————————————————+

Использование шаблона - это самый простой способ работы с роутингом. Ограничения и значения по умолчанию также можно указать вне шаблона.

Совет

Включите Логирование, чтобы увидеть, как встроенные роутинговые реализации, как Route, находят соответствия с запросами.

Роутовые ограничения

Роутовые ограничения выполняются тогда, когда Route нашел соответствие с синтаксисом входящего URL и токенизировал URL путь в роутовые значения. Роутовые ограничения обычно изучают значения, связанные с шаблоном, и принимают простое решение да/нет относительно того, принимать ли значение. Некоторые значения используют данные вне роута, чтобы решить, будет ли обработан запрос. Например, HttpMethodRouteConstraint может принять или отклонить запрос, основываясь на HTTP verb.

Предупреждение

Избегайте использования ограничений для валидации ввода, поскольку тогда вернется ошибка 404 (Not Found), а не 400 с соответствующим сообщением. Роутовые ограничения должны использоваться для устранения неоднозначности между схожими роутами, а не для валидации.

В следующей таблице представлены некоторые роутовые ограничения и их ожидаемое поведение.

Роутовые ограничения
ограничение пример пример соответствия заметки
int {id:int} 123 соответствует любому целочисленному значению
bool {active:bool} true соответствует true или false
datetime {dob:datetime} 2016-01-01 соответствует валидному значению DateTime
decimal {price:decimal} 49.99 соответствует валидному значению decimal
double {weight:double} 4.234 соответствует валидному значению double
float {weight:float} 3.14 соответствует валидному значению float
guid {id:guid} 7342570B-<snip> соответствует валидному значению Guid
long {ticks:long} 123456789 соответствует валидному значению long
minlength(value) {username:minlength(5)} steve строка должна иметь длину минимум 5 символов
maxlength(value) {filename:maxlength(8)} somefile в строке на должно быть более 8 символов
length(min,max) {filename:length(4,16)} Somefile.txt строка должна иметь длину от 8 до 16 символов
min(value) {age:min(18)} 19 значение должно быть минимум 18
max(value) {age:max(120)} 91 значение не должно быть больше 120
range(min,max) {age:range(18,120)} 91 значение должно находиться в диапазоне от 18 до 120
alpha {name:alpha} Steve строка должна содержать символы алфавита
regex(expression) {ssn:regex(^d{3}-d{2}-d{4}$)} 123-45-6789 строка должна соответствовать указанному регулярному выражению
required {name:required} Steve Значение без параметров должно присутствовать при генерировании URL

Предупреждение

Роутовые ограничения, предлагаемые фреймворком, не меняют значения, хранящиеся в роутовых значениях. Все роутовые значения, взятые из URL, хранятся как строки.

Совет

Чтобы ограничить параметр известным набором возможных значений, можно использовать регулярное выражение (например, {action:regex(list|get|create)}. Роутовое значение action будет ограничено только list, get или create. Ограничения, которые передаются в словарь значений (не строковые внутри шаблона), которые не соответствуют ни одному известному ограничению, обрабатываются также как регулярные выражения.

Генерирование URL

В примере выше показано, как генерировать ссылку к роуту, если есть словарь роутовых значений и RouteCollection.

app.Run(async (context) =>
{
    var dictionary = new RouteValueDictionary
    {
        { "operation", "create" },
        { "id", 123}
    };

    var vpc = new VirtualPathContext(context, null, dictionary, "Track Package Route");
    var path = routes.GetVirtualPath(vpc).VirtualPath;

    context.Response.ContentType = "text/html";
    await context.Response.WriteAsync("Menu<hr/>");
    await context.Response.WriteAsync($"<a href='{path}'>Create Package 123</a><br/>");
});

VirtualPath, сгенерированный в конце примера, - /package/create/123.

Второй параметр конструктора :dn:cls:`~Microsoft.AspNetCore.Routing.VirtualPathContext` - это коллекция окружающих значений. Окружающие значения ограничивают число значений, которые разработчик должен указать внутри конкретного запроса. Роутовые значения текущего запроса считаются окружающими значениями для генерирования ссылки. Например, в ASP.NET MVC приложении, если вы находитесь в действии About в HomeController, вам не нужно указывать роутовое значение контроллера, чтобы связаться с методом действия Index (будет использовано окружающее значение Home).

Окружающие значения, которые не соответствуют параметру, игнорируются. Также окружающие значения игнорируются, если указанные напрямую значения их переопределяют, проходя в URL слева направо.

Значения, которые указаны явно, но ничему не соответствуют, они добавляются в строку запроса. В следующей таблице показан результат использования шаблона {controller}/{action}/{id?}.

Генерирование ссылок с помощью шаблона {controller}/{action}/{id?}
Окружающие значения Явно указанные значения Результат
controller=”Home” action=”About” /Home/About
controller=”Home” controller=”Order”,action=”About” /Order/About
controller=”Home”,color=”Red” action=”About” /Home/About
controller=”Home” action=”About”,color=”Red” /Home/About?color=Red

Если у роута есть значение по умолчанию, которое не соответствует параметру, и это значение указано явно, оно должно соответствовать значению по умолчанию. Например:

routes.MapRoute("blog_route", "blog/{*slug}",
  defaults: new { controller = "Blog", action = "ReadPost" });

Ссылка для этого роута будет сгенерирована только тогда, если для контроллера и действия будут предоставлены соответствующие значения.

Поделись хорошей новостью с друзьями!
Следи за новостями!