Použití httpContextu v ASP.NET Core
Poznámka:
Toto není nejnovější verze tohoto článku. Aktuální verzi najdete ve verzi .NET 8 tohoto článku.
Upozorňující
Tato verze ASP.NET Core se už nepodporuje. Další informace najdete v tématu .NET a .NET Core Zásady podpory. Aktuální verzi najdete ve verzi .NET 8 tohoto článku.
Důležité
Tyto informace se týkají předběžného vydání produktu, který může být podstatně změněn před komerčním vydáním. Microsoft neposkytuje žádné záruky, výslovné ani předpokládané, týkající se zde uváděných informací.
Aktuální verzi najdete ve verzi .NET 8 tohoto článku.
HttpContext Zapouzdřuje všechny informace o jednotlivých požadavcích a odpovědích HTTP. Instance HttpContext
se inicializuje při přijetí požadavku HTTP. Instance HttpContext
je přístupná prostřednictvím middlewaru a aplikačních architektur, jako jsou kontrolery webového rozhraní API, Razor Stránky, SignalR, gRPC a další.
Další informace o přístupu k sadě HttpContext
, naleznete v tématu Access HttpContext v ASP.NET Core.
HttpRequest
HttpContext.Request poskytuje přístup k HttpRequest. HttpRequest
obsahuje informace o příchozím požadavku HTTP a inicializuje se při přijetí požadavku HTTP serverem. HttpRequest
není jen pro čtení a middleware může měnit hodnoty požadavků v kanálu middlewaru.
Mezi běžně používané členy HttpRequest
patří:
Vlastnost | Popis | Příklad |
---|---|---|
HttpRequest.Path | Cesta požadavku. | /en/article/getstarted |
HttpRequest.Method | Metoda požadavku. | GET |
HttpRequest.Headers | Kolekce hlaviček požadavků. | user-agent=Edge x-custom-header=MyValue |
HttpRequest.RouteValues | Kolekce hodnot tras. Kolekce se nastaví, když se požadavek shoduje s trasou. | language=en article=getstarted |
HttpRequest.Query | Kolekce hodnot dotazu parsovaných z QueryString. | filter=hello page=1 |
HttpRequest.ReadFormAsync() | Metoda, která čte tělo požadavku jako formulář a vrátí kolekci hodnot formuláře. Informace o tom, proč ReadFormAsync byste měli použít pro přístup k datům formuláře, naleznete v tématu Prefer ReadFormAsync přes Request.Form. |
email=user@contoso.com |
HttpRequest.Body | A Stream pro čtení textu požadavku. | Datová část UTF-8 JSON |
Získání hlaviček požadavků
HttpRequest.Headers poskytuje přístup k hlavičce požadavku odeslané pomocí požadavku HTTP. Pomocí této kolekce můžete získat přístup k hlavičce dvěma způsoby:
- Zadejte název záhlaví indexeru v kolekci hlaviček. V názvu záhlaví se nerozlišuje malá a velká písmena. Indexer má přístup k libovolné hodnotě záhlaví.
- Kolekce hlaviček má také vlastnosti pro získání a nastavení běžně používaných hlaviček HTTP. Vlastnosti poskytují rychlý a řízený způsob, jak získat přístup k hlavičce technologie IntelliSense.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", (HttpRequest request) =>
{
var userAgent = request.Headers.UserAgent;
var customHeader = request.Headers["x-custom-header"];
return Results.Ok(new { userAgent = userAgent, customHeader = customHeader });
});
app.Run();
Informace o efektivním zpracování hlaviček, které se zobrazují více než jednou, najdete v stručné části StringValues.
Text požadavku pro čtení
Požadavek HTTP může obsahovat text požadavku. Text požadavku je data přidružená k požadavku, jako je například obsah formuláře HTML, datová část JSON UTF-8 nebo soubor.
HttpRequest.Bodyumožňuje čtení textu požadavku:Stream
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/uploadstream", async (IConfiguration config, HttpContext context) =>
{
var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());
await using var writeStream = File.Create(filePath);
await context.Request.Body.CopyToAsync(writeStream);
});
app.Run();
HttpRequest.Body
lze číst přímo nebo používat s jinými rozhraními API, která přijímají stream.
Poznámka:
Minimální rozhraní API podporují vazbu HttpRequest.Body přímo s parametrem Stream .
Povolení ukládání textu požadavku do vyrovnávací paměti
Text požadavku lze číst pouze jednou od začátku do konce. Čtení textu požadavku jen pro předávání dál zabraňuje režii ukládání celého textu požadavku do vyrovnávací paměti a snižuje využití paměti. V některých scénářích je ale potřeba přečíst text požadavku několikrát. Middleware může například potřebovat přečíst text požadavku a potom ho převinout zpět, aby byl pro koncový bod dostupný.
Metoda EnableBuffering rozšíření umožňuje ukládání textu požadavku HTTP do vyrovnávací paměti a je doporučeným způsobem, jak povolit více čtení. Vzhledem k tomu, že požadavek může mít libovolnou velikost, EnableBuffering
podporuje možnosti ukládání velkých těl požadavků do vyrovnávací paměti na disk nebo je zcela odmítnout.
Middleware v následujícím příkladu:
- Povolí více čtení pomocí
EnableBuffering
funkce . Před přečtením textu požadavku se musí volat. - Přečte text požadavku.
- Převine text požadavku na začátek, aby ho mohl přečíst jiný middleware nebo koncový bod.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
context.Request.EnableBuffering();
await ReadRequestBody(context.Request.Body);
context.Request.Body.Position = 0;
await next.Invoke();
});
app.Run();
BodyReader
Alternativním způsobem čtení textu požadavku je použití HttpRequest.BodyReader vlastnosti. Vlastnost BodyReader
zveřejňuje text požadavku jako PipeReader. Toto rozhraní API je z vstupně-výstupních kanálů, což je pokročilý a vysoce výkonný způsob čtení textu požadavku.
Čtenář přímo přistupuje k textu požadavku a spravuje paměť jménem volajícího. Na rozdíl od HttpRequest.Body
toho čtenář data požadavku nekopíruje do vyrovnávací paměti. Čtenář je ale složitější než datový proud a měl by se používat s opatrností.
Informace o tom, jak číst obsah z BodyReader
, naleznete v části I/O pipelines PipeReader.
HttpResponse
HttpContext.Response poskytuje přístup k HttpResponse. HttpResponse
slouží k nastavení informací o odpovědi HTTP odeslané zpět klientovi.
Mezi běžně používané členy HttpResponse
patří:
Vlastnost | Popis | Příklad |
---|---|---|
HttpResponse.StatusCode | Kód odpovědi. Před zápisem do textu odpovědi je nutné nastavit nastavení. | 200 |
HttpResponse.ContentType | Hlavička odpovědi content-type . Před zápisem do textu odpovědi je nutné nastavit nastavení. |
application/json |
HttpResponse.Headers | Kolekce hlaviček odpovědí. Před zápisem do textu odpovědi je nutné nastavit nastavení. | server=Kestrel x-custom-header=MyValue |
HttpResponse.Body | A Stream pro napsání textu odpovědi. | Vygenerovaná webová stránka |
Nastavení hlaviček odpovědí
HttpResponse.Headers poskytuje přístup k hlavičce odpovědi odeslané pomocí odpovědi HTTP. Pomocí této kolekce můžete získat přístup k hlavičce dvěma způsoby:
- Zadejte název záhlaví indexeru v kolekci hlaviček. V názvu záhlaví se nerozlišuje malá a velká písmena. Indexer má přístup k libovolné hodnotě záhlaví.
- Kolekce hlaviček má také vlastnosti pro získání a nastavení běžně používaných hlaviček HTTP. Vlastnosti poskytují rychlý a řízený způsob, jak získat přístup k hlavičce technologie IntelliSense.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", (HttpResponse response) =>
{
response.Headers.CacheControl = "no-cache";
response.Headers["x-custom-header"] = "Custom value";
return Results.File(File.OpenRead("helloworld.txt"));
});
app.Run();
Aplikace nemůže upravit hlavičky po spuštění odpovědi. Jakmile se odpověď spustí, hlavičky se odešlou klientovi. Odpověď se spustí vyprázdněním textu odpovědi nebo voláním HttpResponse.StartAsync(CancellationToken). Vlastnost HttpResponse.HasStarted označuje, zda byla odpověď spuštěna. Při pokusu o úpravu hlaviček po spuštění odpovědi dojde k chybě:
System.InvalidOperationException: Hlavičky jsou jen pro čtení, odpověď se už spustila.
Poznámka:
Pokud není povolené ukládání odpovědí do vyrovnávací paměti, všechny operace zápisu (například WriteAsync) vyprázdní text odpovědi interně a označí odpověď jako spuštěnou. Ukládání odpovědí do vyrovnávací paměti je ve výchozím nastavení zakázané.
Text odpovědi napsat
Odpověď HTTP může obsahovat text odpovědi. Tělo odpovědi je data přidružená k odpovědi, jako je vygenerovaný obsah webové stránky, datová část JSON UTF-8 nebo soubor.
HttpResponse.Body umožňuje zápis textu odpovědi pomocí Stream:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/downloadfile", async (IConfiguration config, HttpContext context) =>
{
var filePath = Path.Combine(config["StoredFilesPath"], "helloworld.txt");
await using var fileStream = File.OpenRead(filePath);
await fileStream.CopyToAsync(context.Response.Body);
});
app.Run();
HttpResponse.Body
lze zapisovat přímo nebo používat s jinými rozhraními API, která zapisují do datového proudu.
BodyWriter
Alternativním způsobem zápisu textu odpovědi je použití HttpResponse.BodyWriter vlastnosti. Vlastnost BodyWriter
zveřejňuje tělo odpovědi jako PipeWriter. Toto rozhraní API pochází z vstupně-výstupních kanálů a jedná se o pokročilý vysoce výkonný způsob, jak napsat odpověď.
Zapisovač poskytuje přímý přístup k textu odpovědi a spravuje paměť jménem volajícího. Na rozdíl od HttpResponse.Body
zápisu data požadavku nekopíruje do vyrovnávací paměti. Zápis je ale složitější než datový proud a kód zapisovače by se měl důkladně testovat.
Informace o tom, jak psát obsah do BodyWriter
, naleznete v části I/O pipelines PipeWriter.
Nastavení přívěsů pro odezvu
Přívěsy pro odpovědi podporují protokoly HTTP/2 a HTTP/3. Přívěsy jsou hlavičky odeslané s odpovědí po dokončení textu odpovědi. Protože přívěsy se posílají po těle odpovědi, mohou být k odpovědi kdykoli přidány přívěsy.
Následující kód nastavuje přívěsy pomocí AppendTrailer:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", (HttpResponse response) =>
{
// Write body
response.WriteAsync("Hello world");
if (response.SupportsTrailers())
{
response.AppendTrailer("trailername", "TrailerValue");
}
});
app.Run();
RequestAborted
Token HttpContext.RequestAborted zrušení lze použít k upozornění, že požadavek HTTP byl přerušen klientem nebo serverem. Token zrušení by se měl předávat dlouhotrvajícím úkolům, aby se daly zrušit, pokud je požadavek přerušený. Například přerušením databázového dotazu nebo požadavku HTTP na získání dat, která se mají vrátit v odpovědi.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var httpClient = new HttpClient();
app.MapPost("/books/{bookId}", async (int bookId, HttpContext context) =>
{
var stream = await httpClient.GetStreamAsync(
$"http://contoso/books/{bookId}.json", context.RequestAborted);
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
Token RequestAborted
zrušení se nemusí používat pro operace čtení textu požadavku, protože čtení se vždy vyvolá okamžitě, když je požadavek přerušen. Token RequestAborted
je také obvykle nepotřebný při zápisu textu odpovědi, protože zápis okamžitě no-op při přerušení požadavku.
V některých případech může předání tokenu k operacím zápisu RequestAborted
být pohodlným způsobem, jak vynutit ukončení smyčky zápisu v rané fázi pomocí OperationCanceledException. Obvykle je ale lepší předat RequestAborted
token do všech asynchronních operací zodpovědných za načtení obsahu těla odpovědi.
Poznámka:
Minimální rozhraní API podporují vazbu HttpContext.RequestAborted přímo s parametrem CancellationToken .
Abort()
Metodu HttpContext.Abort() lze použít k přerušení požadavku HTTP ze serveru. Přerušení požadavku HTTP okamžitě aktivuje HttpContext.RequestAborted token zrušení a odešle klientovi oznámení, že server žádost přerušil.
Middleware v následujícím příkladu:
- Přidá vlastní kontrolu škodlivých požadavků.
- Přeruší požadavek HTTP, pokud je požadavek škodlivý.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
if (RequestAppearsMalicious(context.Request))
{
// Malicious requests don't even deserve an error response (e.g. 400).
context.Abort();
return;
}
await next.Invoke();
});
app.Run();
User
Vlastnost HttpContext.User slouží k získání nebo nastavení uživatele, reprezentované ClaimsPrincipal, pro požadavek. Obvykle se ClaimsPrincipal nastavuje ověřováním ASP.NET Core.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/user/current", [Authorize] async (HttpContext context) =>
{
var user = await GetUserAsync(context.User.Identity.Name);
return Results.Ok(user);
});
app.Run();
Poznámka:
Minimální rozhraní API podporují vazbu HttpContext.User přímo s parametrem ClaimsPrincipal .
Features
Vlastnost HttpContext.Features poskytuje přístup k kolekci rozhraní funkcí pro aktuální požadavek. Vzhledem k tomu, že kolekce funkcí je proměnlivá i v kontextu požadavku, můžete middleware použít k úpravě kolekce a přidání podpory dalších funkcí. Některé pokročilé funkce jsou k dispozici pouze přístupem k přidruženému rozhraní prostřednictvím kolekce funkcí.
Následující příklad:
- Získá IHttpMinRequestBodyDataRateFeature z kolekce funkcí.
- Nastaví MinDataRate na hodnotu null. Tím se odebere minimální rychlost dat, kterou musí tělo požadavku odeslat klient pro tento požadavek HTTP.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/long-running-stream", async (HttpContext context) =>
{
var feature = context.Features.Get<IHttpMinRequestBodyDataRateFeature>();
if (feature != null)
{
feature.MinDataRate = null;
}
// await and read long-running stream from request body.
await Task.Yield();
});
app.Run();
Další informace o používání funkcí žádostí a HttpContext
naleznete v tématu Funkce žádosti v ASP.NET Core.
HttpContext není bezpečný pro přístup z více vláken
Tento článek primárně popisuje použití HttpContext
v toku požadavků a odpovědí ze Razor stránek, kontrolerů, middlewaru atd. Při použití HttpContext
mimo požadavek a tok odpovědi zvažte následující skutečnosti:
- Není
HttpContext
bezpečné vlákno, přístup k němu z více vláken může vést k výjimkám, poškození dat a obecně nepředvídatelným výsledkům. - Rozhraní IHttpContextAccessor by mělo být použito s opatrností. Stejně jako vždy
HttpContext
nesmí být zachyceno mimo tok požadavku.IHttpContextAccessor
:- Spoléhá na to, který může mít negativní dopad na AsyncLocal<T> výkon asynchronních volání.
- Vytvoří závislost na okolním stavu, což může ztížit testování.
- IHttpContextAccessor.HttpContext může být
null
v případě přístupu mimo tok požadavku. - Pokud chcete získat přístup k informacím mimo
HttpContext
tok požadavku, zkopírujte informace uvnitř toku požadavku. Dávejte pozor, abyste zkopírovali skutečná data, a ne jenom odkazy. Například místo kopírování odkazu na ,IHeaderDictionary
zkopírujte příslušné hodnoty záhlaví nebo zkopírujte celý klíč slovníku podle klíče před opuštěním toku požadavku. - Nezachycujte
IHttpContextAccessor.HttpContext
je v konstruktoru.
Následující ukázkové protokoly protokolují větve GitHubu při vyžádání z koncového /branch
bodu:
using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();
builder.Services.AddHttpClient("GitHub", httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// The GitHub API requires two headers. The Use-Agent header is added
// dynamically through UserAgentHeaderHandler
httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
}).AddHttpMessageHandler<UserAgentHeaderHandler>();
builder.Services.AddTransient<UserAgentHeaderHandler>();
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapGet("/branches", async (IHttpClientFactory httpClientFactory,
HttpContext context, Logger<Program> logger) =>
{
var httpClient = httpClientFactory.CreateClient("GitHub");
var httpResponseMessage = await httpClient.GetAsync(
"repos/dotnet/AspNetCore.Docs/branches");
if (!httpResponseMessage.IsSuccessStatusCode)
return Results.BadRequest();
await using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();
var response = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubBranch>>(contentStream);
app.Logger.LogInformation($"/branches request: " +
$"{JsonSerializer.Serialize(response)}");
return Results.Ok(response);
});
app.Run();
Rozhraní API GitHubu vyžaduje dvě hlavičky. Záhlaví User-Agent
se přidává dynamicky pomocí UserAgentHeaderHandler
:
using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();
builder.Services.AddHttpClient("GitHub", httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// The GitHub API requires two headers. The Use-Agent header is added
// dynamically through UserAgentHeaderHandler
httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
}).AddHttpMessageHandler<UserAgentHeaderHandler>();
builder.Services.AddTransient<UserAgentHeaderHandler>();
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapGet("/branches", async (IHttpClientFactory httpClientFactory,
HttpContext context, Logger<Program> logger) =>
{
var httpClient = httpClientFactory.CreateClient("GitHub");
var httpResponseMessage = await httpClient.GetAsync(
"repos/dotnet/AspNetCore.Docs/branches");
if (!httpResponseMessage.IsSuccessStatusCode)
return Results.BadRequest();
await using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();
var response = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubBranch>>(contentStream);
app.Logger.LogInformation($"/branches request: " +
$"{JsonSerializer.Serialize(response)}");
return Results.Ok(response);
});
app.Run();
Pomocná rutina UserAgentHeaderHandler
:
using Microsoft.Net.Http.Headers;
namespace HttpContextInBackgroundThread;
public class UserAgentHeaderHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger _logger;
public UserAgentHeaderHandler(IHttpContextAccessor httpContextAccessor,
ILogger<UserAgentHeaderHandler> logger)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
protected override async Task<HttpResponseMessage>
SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var contextRequest = _httpContextAccessor.HttpContext?.Request;
string? userAgentString = contextRequest?.Headers["user-agent"].ToString();
if (string.IsNullOrEmpty(userAgentString))
{
userAgentString = "Unknown";
}
request.Headers.Add(HeaderNames.UserAgent, userAgentString);
_logger.LogInformation($"User-Agent: {userAgentString}");
return await base.SendAsync(request, cancellationToken);
}
}
V předchozím kódu, pokud HttpContext
je null
, userAgent
řetězec je nastaven na "Unknown"
. Pokud je to možné, HttpContext
mělo by se službě explicitně předat. Explicitní předávání HttpContext
dat:
- Zpřístupňuje rozhraní API služby lépe použitelné mimo tok požadavku.
- Je lepší pro výkon.
- Usnadňuje pochopení kódu a důvod, než aby se spoléhal na okolní stav.
Když musí služba přistupovat HttpContext
, měla by mít za to, HttpContext
null
že při nevolaném vlákně žádosti by měla být.
Aplikace také zahrnuje PeriodicBranchesLoggerService
protokoly otevřené větve GitHubu zadaného úložiště každých 30 sekund:
using System.Text.Json;
namespace HttpContextInBackgroundThread;
public class PeriodicBranchesLoggerService : BackgroundService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
private readonly PeriodicTimer _timer;
public PeriodicBranchesLoggerService(IHttpClientFactory httpClientFactory,
ILogger<PeriodicBranchesLoggerService> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
_timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (await _timer.WaitForNextTickAsync(stoppingToken))
{
try
{
// Cancel sending the request to sync branches if it takes too long
// rather than miss sending the next request scheduled 30 seconds from now.
// Having a single loop prevents this service from sending an unbounded
// number of requests simultaneously.
using var syncTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
syncTokenSource.CancelAfter(TimeSpan.FromSeconds(30));
var httpClient = _httpClientFactory.CreateClient("GitHub");
var httpResponseMessage = await httpClient.GetAsync("repos/dotnet/AspNetCore.Docs/branches",
stoppingToken);
if (httpResponseMessage.IsSuccessStatusCode)
{
await using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync(stoppingToken);
// Sync the response with preferred datastore.
var response = await JsonSerializer.DeserializeAsync<
IEnumerable<GitHubBranch>>(contentStream, cancellationToken: stoppingToken);
_logger.LogInformation(
$"Branch sync successful! Response: {JsonSerializer.Serialize(response)}");
}
else
{
_logger.LogError(1, $"Branch sync failed! HTTP status code: {httpResponseMessage.StatusCode}");
}
}
catch (Exception ex)
{
_logger.LogError(1, ex, "Branch sync failed!");
}
}
}
public override Task StopAsync(CancellationToken stoppingToken)
{
// This will cause any active call to WaitForNextTickAsync() to return false immediately.
_timer.Dispose();
// This will cancel the stoppingToken and await ExecuteAsync(stoppingToken).
return base.StopAsync(stoppingToken);
}
}
PeriodicBranchesLoggerService
je hostovaná služba, která běží mimo tok požadavku a odpovědi. Protokolování z objektu PeriodicBranchesLoggerService
má hodnotu null HttpContext
. Bylo PeriodicBranchesLoggerService
zapsáno, že nezávisí na HttpContext
.
using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();
builder.Services.AddHttpClient("GitHub", httpClient =>
{