这篇文章演示如何迁移现有的 ASP.NET HTTP 模块和处理程序 system.webserver到 ASP.NET Core 中间件。
在继续之前到 ASP.NET Core 中间件,让我们首先会扼要重述 HTTP 模块和处理程序的工作原理:
处理程序是:
实现IHttpHandler的类
用于处理具有给定文件名或扩展名的请求,如 . report
在 web.config中配置
模块为:
实现IHttpModule的类
为每个请求调用
能够短路(停止进一步处理请求)
可以添加到 HTTP 响应,或创建自己的响应
在 web.config中配置
模块处理传入请求的顺序由确定:
应用程序生命周期,是 ASP.NET 激发的一系列事件:BeginRequest、 AuthenticateRequest等。每个模块都可以为一个或多个事件创建处理程序。
对于同一事件,为在 web.config 中配置它们的顺序。
除了模块外,还可以将生命周期事件的处理程序添加到Global.asax.cs文件。 这些处理程序在配置的模块中的处理程序之后运行。
中间件比 HTTP 模块和处理程序更简单:
"模块"、"处理程序"、" Global.asax.cs"、 "WEB.CONFIG" (IIS配置除外)和 "应用程序生命周期" 消失
中间件已使用模块和处理程序的角色
中间件使用代码而不是在 web.config 中进行配置
通过管道分支,你可以将请求发送到特定的中间件,不仅可以基于 URL,还可以发送到请求标头、查询字符串等。
中间件非常类似于模块:
为每个请求按原则调用
无法将请求传递给下一个中间件来对请求进行短线路
能够创建自己的 HTTP 响应
中间件和模块按不同的顺序进行处理:
中间件的顺序取决于它们插入请求管道的顺序,而模块的顺序主要基于应用程序生命周期事件
响应的中间件顺序与请求的顺序相反,而对于请求和响应,模块的顺序是相同的。
请注意,在上图中,身份验证中间件与请求短路。
现有 HTTP 模块如下所示:
// ASP.NET 4 module using System; using System.Web; namespace MyApp.Modules { public class MyModule : IHttpModule { public void Dispose() { } public void Init(HttpApplication application) { application.BeginRequest += (new EventHandler(this.Application_BeginRequest)); application.EndRequest += (new EventHandler(this.Application_EndRequest)); } private void Application_BeginRequest(Object source, EventArgs e) { HttpContext context = ((HttpApplication)source).Context; // Do something with context near the beginning of request processing. } private void Application_EndRequest(Object source, EventArgs e) { HttpContext context = ((HttpApplication)source).Context; // Do something with context near the end of request processing. } } }
中所示中间件页上,ASP.NET Core 中间件是公开的类Invoke
方法拍摄HttpContext
并返回Task
。 新的中间件将如下所示:
// ASP.NET Core middleware using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using System.Threading.Tasks; namespace MyApp.Middleware { public class MyMiddleware { private readonly RequestDelegate _next; public MyMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext context) { // Do something with context near the beginning of request processing. await _next.Invoke(context); // Clean up. } } public static class MyMiddlewareExtensions { public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware<MyMiddleware>(); } } }
前面的中间件模板取自编写中间件的部分。
MyMiddlewareExtensions helper 类使你可以更轻松地在Startup
类中配置中间件。 UseMyMiddleware
方法将中间件类添加到请求管道。 中间件的构造函数中插入了中间件所需的服务。
如果用户未获得授权,则模块可能会终止请求:
// ASP.NET 4 module that may terminate the request private void Application_BeginRequest(Object source, EventArgs e) { HttpContext context = ((HttpApplication)source).Context; // Do something with context near the beginning of request processing. if (TerminateRequest()) { context.Response.End(); return; } }
中间件通过不调用Invoke
管道中的下一个中间件来处理这种情况。 请记住,这并不完全终止请求,因为当响应通过管道返回以前的中间件时,仍然会调用以前的。
// ASP.NET Core middleware that may terminate the request public async Task Invoke(HttpContext context) { // Do something with context near the beginning of request processing. if (!TerminateRequest()) await _next.Invoke(context); // Clean up. }
当你迁移到新中间件模块的功能时,你可能会发现你的代码不会编译,因为HttpContext
类中 ASP.NET Core 已显著更改。 更高版本上,你将了解如何将迁移到新的 ASP.NET Core HttpContext。
HTTP 模块通常使用 web.config添加到请求管道:
<?xml version="1.0" encoding="utf-8"?> <!--ASP.NET 4 web.config--> <configuration> <system.webServer> <modules> <add name="MyModule" type="MyApp.Modules.MyModule"/> </modules> </system.webServer> </configuration>
通过在Startup
类中将新的中间件添加到请求管道来转换此项:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseMyMiddleware(); app.UseMyMiddlewareWithParams(); var myMiddlewareOptions = Configuration.GetSection("MyMiddlewareOptionsSection").Get<MyMiddlewareOptions>(); var myMiddlewareOptions2 = Configuration.GetSection("MyMiddlewareOptionsSection2").Get<MyMiddlewareOptions>(); app.UseMyMiddlewareWithParams(myMiddlewareOptions); app.UseMyMiddlewareWithParams(myMiddlewareOptions2); app.UseMyTerminatingMiddleware(); // Create branch to the MyHandlerMiddleware. // All requests ending in .report will follow this branch. app.MapWhen( context => context.Request.Path.ToString().EndsWith(".report"), appBranch => { // ... optionally add more middleware to this branch appBranch.UseMyHandler(); }); app.MapWhen( context => context.Request.Path.ToString().EndsWith(".context"), appBranch => { appBranch.UseHttpContextDemoMiddleware(); }); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
插入新中间件的管道中的确切位置取决于它在web.config 中的模块列表中处理为模块BeginRequest
( EndRequest
、等)的事件及其顺序。
如前面所述,没有任何应用程序生命周期中 ASP.NET Core,中间件处理响应的顺序不同于使用模块的顺序。 这可能会使你的排序决策更具挑战性。
如果排序会成为一个问题,则可以将模块拆分为多个中间件组件,这些组件可以独立排序。
HTTP 处理程序如下所示:
// ASP.NET 4 handler using System.Web; namespace MyApp.HttpHandlers { public class MyHandler : IHttpHandler { public bool IsReusable { get { return true; } } public void ProcessRequest(HttpContext context) { string response = GenerateResponse(context); context.Response.ContentType = GetContentType(); context.Response.Output.Write(response); } // ... private string GenerateResponse(HttpContext context) { string title = context.Request.QueryString["title"]; return string.Format("Title of the report: {0}", title); } private string GetContentType() { return "text/plain"; } } }
在 ASP.NET Core 项目中,你将翻译以下到中间件类似于以下内容:
// ASP.NET Core middleware migrated from a handler using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using System.Threading.Tasks; namespace MyApp.Middleware { public class MyHandlerMiddleware { // Must have constructor with this signature, otherwise exception at run time public MyHandlerMiddleware(RequestDelegate next) { // This is an HTTP Handler, so no need to store next } public async Task Invoke(HttpContext context) { string response = GenerateResponse(context); context.Response.ContentType = GetContentType(); await context.Response.WriteAsync(response); } // ... private string GenerateResponse(HttpContext context) { string title = context.Request.Query["title"]; return string.Format("Title of the report: {0}", title); } private string GetContentType() { return "text/plain"; } } public static class MyHandlerExtensions { public static IApplicationBuilder UseMyHandler(this IApplicationBuilder builder) { return builder.UseMiddleware<MyHandlerMiddleware>(); } } }
此中间件与与模块对应的中间件非常类似。 唯一的区别在于,这里不会调用_next.Invoke(context)
。 这样做很有意义,因为处理程序位于请求管道的末尾,因此没有要调用的下一个中间件。
配置 HTTP 处理程序是在 web.config 中完成的, 如下所示:
<?xml version="1.0" encoding="utf-8"?> <!--ASP.NET 4 web.config--> <configuration> <system.webServer> <handlers> <add name="MyHandler" verb="*" path="*.report" type="MyApp.HttpHandlers.MyHandler" resourceType="Unspecified" preCondition="integratedMode"/> </handlers> </system.webServer> </configuration>
可以通过将新的处理程序中间件添加到Startup
类中的请求管道来转换此转换,类似于从模块转换的中间件。 此方法的问题是,它会将所有请求发送到新的处理程序中间件。 但是,只需要具有给定扩展的请求来访问中间件。 这将为你提供与 HTTP 处理程序相同的功能。
一种解决方法是使用MapWhen
扩展方法分支具有给定扩展的请求的管道。 可在添加其他中间件Configure
的相同方法中执行此操作:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseMyMiddleware(); app.UseMyMiddlewareWithParams(); var myMiddlewareOptions = Configuration.GetSection("MyMiddlewareOptionsSection").Get<MyMiddlewareOptions>(); var myMiddlewareOptions2 = Configuration.GetSection("MyMiddlewareOptionsSection2").Get<MyMiddlewareOptions>(); app.UseMyMiddlewareWithParams(myMiddlewareOptions); app.UseMyMiddlewareWithParams(myMiddlewareOptions2); app.UseMyTerminatingMiddleware(); // Create branch to the MyHandlerMiddleware. // All requests ending in .report will follow this branch. app.MapWhen( context => context.Request.Path.ToString().EndsWith(".report"), appBranch => { // ... optionally add more middleware to this branch appBranch.UseMyHandler(); }); app.MapWhen( context => context.Request.Path.ToString().EndsWith(".context"), appBranch => { appBranch.UseHttpContextDemoMiddleware(); }); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
MapWhen
采用以下参数:
一个采用的HttpContext
lambda,如果请求true
应向下分支,则返回。 这意味着,不仅可以根据请求的扩展来分支请求,还可以处理请求标头、查询字符串参数等。
一个采用IApplicationBuilder
并添加分支的所有中间件的 lambda。 这意味着,可以将其他中间件添加到处理程序中间件前面的分支。
将在所有请求上调用分支之前添加到管道的中间件;该分支不会对它们产生任何影响。
某些模块和处理程序具有存储在 web.config 中的配置选项。但是,在 ASP.NET Core 中新的配置模型使用代替了Web.config。
新配置系统提供以下选项来解决此类情况:
将选项直接注入中间件,如下一节所示。
使用options 模式:
创建用于保存中间件选项的类,例如:
public class MyMiddlewareOptions { public string Param1 { get; set; } public string Param2 { get; set; } }
存储选项值
配置系统允许您将选项值存储在任何所需的位置。 但是,大多数站点都使用appsettings,因此我们将采取这种方法:
{ "MyMiddlewareOptionsSection": { "Param1": "Param1Value", "Param2": "Param2Value" } }
MyMiddlewareOptionsSection是部分名称。 它不必与 options 类的名称相同。
将选项值与 options 类相关联
选项模式使用 ASP.NET Core 依赖关系注入框架将选项类型相关联 (如MyMiddlewareOptions
) 与MyMiddlewareOptions
具有实际选项对象。
更新你Startup
的类:
如果使用的是appsettings,请将其添加到Startup
构造函数中的配置生成器:
public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) .AddEnvironmentVariables(); Configuration = builder.Build(); }
配置 options 服务:
public void ConfigureServices(IServiceCollection services) { // Setup options service services.AddOptions(); // Load options from section "MyMiddlewareOptionsSection" services.Configure<MyMiddlewareOptions>( Configuration.GetSection("MyMiddlewareOptionsSection")); // Add framework services. services.AddMvc(); }
将选项与 options 类相关联:
public void ConfigureServices(IServiceCollection services) { // Setup options service services.AddOptions(); // Load options from section "MyMiddlewareOptionsSection" services.Configure<MyMiddlewareOptions>( Configuration.GetSection("MyMiddlewareOptionsSection")); // Add framework services. services.AddMvc(); }
将选项注入中间件构造函数。 这类似于将选项注入控制器。
public class MyMiddlewareWithParams { private readonly RequestDelegate _next; private readonly MyMiddlewareOptions _myMiddlewareOptions; public MyMiddlewareWithParams(RequestDelegate next, IOptions<MyMiddlewareOptions> optionsAccessor) { _next = next; _myMiddlewareOptions = optionsAccessor.Value; } public async Task Invoke(HttpContext context) { // Do something with context near the beginning of request processing // using configuration in _myMiddlewareOptions await _next.Invoke(context); // Do something with context near the end of request processing // using configuration in _myMiddlewareOptions } }
将中间件添加到中的 UseMiddleware 扩展方法IApplicationBuilder
会处理依赖关系注入。
这并不限于IOptions
对象。 中间件所需的任何其他对象都可以通过这种方式注入。
Options 模式的优点在于,它在选项值与其使用者之间产生松散耦合。 将选项类与实际选项值相关联后,任何其他类都可以通过依赖关系注入框架访问这些选项。 无需围绕选项值进行传递。
如果要使用不同的选项两次使用同一中间件,则会出现这种情况。 例如,在不同的分支中使用的授权中间件允许不同角色。 不能将两个不同的选项对象与一个 options 类相关联。
解决方法是在Startup
类中获取选项对象,并将其直接传递给中间件的每个实例。
将第二个键添加到appsettings
若要将第二组选项添加到appsettings文件,请使用新密钥来唯一标识它:
{ "MyMiddlewareOptionsSection2": { "Param1": "Param1Value2", "Param2": "Param2Value2" }, "MyMiddlewareOptionsSection": { "Param1": "Param1Value", "Param2": "Param2Value" } }
检索选项值并将其传递给中间件。 Use...
扩展方法(将中间件添加到管道)是要传入选项值的逻辑位置:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseMyMiddleware(); app.UseMyMiddlewareWithParams(); var myMiddlewareOptions = Configuration.GetSection("MyMiddlewareOptionsSection").Get<MyMiddlewareOptions>(); var myMiddlewareOptions2 = Configuration.GetSection("MyMiddlewareOptionsSection2").Get<MyMiddlewareOptions>(); app.UseMyMiddlewareWithParams(myMiddlewareOptions); app.UseMyMiddlewareWithParams(myMiddlewareOptions2); app.UseMyTerminatingMiddleware(); // Create branch to the MyHandlerMiddleware. // All requests ending in .report will follow this branch. app.MapWhen( context => context.Request.Path.ToString().EndsWith(".report"), appBranch => { // ... optionally add more middleware to this branch appBranch.UseMyHandler(); }); app.MapWhen( context => context.Request.Path.ToString().EndsWith(".context"), appBranch => { appBranch.UseHttpContextDemoMiddleware(); }); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
启用中间件以采用 options 参数。 提供Use...
扩展方法(采用 options 参数并将其传递给UseMiddleware
)的重载。 当UseMiddleware
用参数调用时,它会在实例化中间件对象时将参数传递给中间件构造函数。
public static class MyMiddlewareWithParamsExtensions { public static IApplicationBuilder UseMyMiddlewareWithParams( this IApplicationBuilder builder) { return builder.UseMiddleware<MyMiddlewareWithParams>(); } public static IApplicationBuilder UseMyMiddlewareWithParams( this IApplicationBuilder builder, MyMiddlewareOptions myMiddlewareOptions) { return builder.UseMiddleware<MyMiddlewareWithParams>( new OptionsWrapper<MyMiddlewareOptions>(myMiddlewareOptions)); } }
请注意这如何包装OptionsWrapper
对象中的选项对象。 这实现IOptions
了中间件构造函数的预期。
你以前看到,中间Invoke
件中的方法采用类型HttpContext
为的参数:
public async Task Invoke(HttpContext context)
HttpContext
已显著更改 ASP.NET Core 中。 本部分演示如何将 system.servicemodel 最常用的属性转换为新 Microsoft.AspNetCore.Http.HttpContext
的。
HttpContext会转换为:
IDictionary<object, object> items = httpContext.Items;
唯一的请求 ID (不含 System.web)
提供每个请求的唯一 id。 在日志中包含非常有用。
string requestId = httpContext.TraceIdentifier;
HttpMethod转换为:
string httpMethod = httpContext.Request.Method;
HttpContext转换为:
IQueryCollection queryParameters = httpContext.Request.Query; // If no query parameter "key" used, values will have 0 items // If single value used for a key (...?key=v1), values will have 1 item ("v1") // If key has multiple values (...?key=v1&key=v2), values will have 2 items ("v1" and "v2") IList<string> values = queryParameters["key"]; // If no query parameter "key" used, value will be "" // If single value used for a key (...?key=v1), value will be "v1" // If key has multiple values (...?key=v1&key=v2), value will be "v1,v2" string value = queryParameters["key"].ToString();
Httpcontext.current转换为( RawUrl ):
// using Microsoft.AspNetCore.Http.Extensions; var url = httpContext.Request.GetDisplayUrl();
IsSecureConnection转换为:
var isSecureConnection = httpContext.Request.IsHttps;
UserHostAddress转换为:
var userHostAddress = httpContext.Connection.RemoteIpAddress?.ToString();
Httpcontext.current转换为:
IRequestCookieCollection cookies = httpContext.Request.Cookies; string unknownCookieValue = cookies["unknownCookie"]; // will be null (no exception) string knownCookieValue = cookies["cookie1name"]; // will be actual value
RequestContext RouteData转换为:
var routeValue = httpContext.GetRouteValue("key");
Httpcontext.current转换为:
// using Microsoft.AspNetCore.Http.Headers; // using Microsoft.Net.Http.Headers; IHeaderDictionary headersDictionary = httpContext.Request.Headers; // GetTypedHeaders extension method provides strongly typed access to many headers var requestHeaders = httpContext.Request.GetTypedHeaders(); CacheControlHeaderValue cacheControlHeaderValue = requestHeaders.CacheControl; // For unknown header, unknownheaderValues has zero items and unknownheaderValue is "" IList<string> unknownheaderValues = headersDictionary["unknownheader"]; string unknownheaderValue = headersDictionary["unknownheader"].ToString(); // For known header, knownheaderValues has 1 item and knownheaderValue is the value IList<string> knownheaderValues = headersDictionary[HeaderNames.AcceptLanguage]; string knownheaderValue = headersDictionary[HeaderNames.AcceptLanguage].ToString();
UserAgent转换为:
string userAgent = headersDictionary[HeaderNames.UserAgent].ToString();
UrlReferrer转换为:
string urlReferrer = headersDictionary[HeaderNames.Referer].ToString();
HttpContext转换为:
// using Microsoft.Net.Http.Headers; MediaTypeHeaderValue mediaHeaderValue = requestHeaders.ContentType; string contentType = mediaHeaderValue?.MediaType.ToString(); // ex. application/x-www-form-urlencoded string contentMainType = mediaHeaderValue?.Type.ToString(); // ex. application string contentSubType = mediaHeaderValue?.SubType.ToString(); // ex. x-www-form-urlencoded System.Text.Encoding requestEncoding = mediaHeaderValue?.Encoding;
Httpcontext.current转换为:
if (httpContext.Request.HasFormContentType) { IFormCollection form; form = httpContext.Request.Form; // sync // Or form = await httpContext.Request.ReadFormAsync(); // async string firstName = form["firstname"]; string lastName = form["lastname"]; }
警告
仅当 content 子类型为x-www-url 编码或窗体数据时才读取窗体值。
InputStream转换为:
string inputBody; using (var reader = new System.IO.StreamReader( httpContext.Request.Body, System.Text.Encoding.UTF8)) { inputBody = reader.ReadToEnd(); }
警告
仅在管道末尾的处理程序类型中间件中使用此代码。
对于每个请求,可以读取上面所示的原始主体。 第一次读取后尝试读取正文的中间件将读取空正文。
这并不适用于读取如上所示的窗体,因为这是从缓冲区中完成的。
Httpcontext.current转换为( StatusDescription ):
// using Microsoft.AspNetCore.Http; httpContext.Response.StatusCode = StatusCodes.Status200OK;
ContentEncoding和httpcontext转换为以下内容:
// using Microsoft.Net.Http.Headers; var mediaType = new MediaTypeHeaderValue("application/json"); mediaType.Encoding = System.Text.Encoding.UTF8; httpContext.Response.ContentType = mediaType.ToString();
Httpcontext.current还会转换为:
httpContext.Response.ContentType = "text/html";
Httpcontext.current转换为:
string responseContent = GetResponseContent(); await httpContext.Response.WriteAsync(responseContent);
HttpContext.Response.TransmitFile
此处讨论了如何提供文件。
HttpContext.Response.Headers
发送响应标头比较复杂,因为如果在将任何内容写入响应正文后设置这些标头,则不会发送这些标头。
解决方法是设置一个回调方法,该方法将在开始写入响应之前被调用。 最好在中间件中的Invoke
方法的开头完成此操作。 这是设置响应标头的此回调方法。
下面的代码设置一个名SetHeaders
为的回调方法:
public async Task Invoke(HttpContext httpContext) { // ... httpContext.Response.OnStarting(SetHeaders, state: httpContext);
SetHeaders
回调方法将如下所示:
// using Microsoft.AspNet.Http.Headers; // using Microsoft.Net.Http.Headers; private Task SetHeaders(object context) { var httpContext = (HttpContext)context; // Set header with single value httpContext.Response.Headers["ResponseHeaderName"] = "headerValue"; // Set header with multiple values string[] responseHeaderValues = new string[] { "headerValue1", "headerValue1" }; httpContext.Response.Headers["ResponseHeaderName"] = responseHeaderValues; // Translating ASP.NET 4's HttpContext.Response.RedirectLocation httpContext.Response.Headers[HeaderNames.Location] = "http://www.example.com"; // Or httpContext.Response.Redirect("http://www.example.com"); // GetTypedHeaders extension method provides strongly typed access to many headers var responseHeaders = httpContext.Response.GetTypedHeaders(); // Translating ASP.NET 4's HttpContext.Response.CacheControl responseHeaders.CacheControl = new CacheControlHeaderValue { MaxAge = new System.TimeSpan(365, 0, 0, 0) // Many more properties available }; // If you use .NET Framework 4.6+, Task.CompletedTask will be a bit faster return Task.FromResult(0); }
HttpContext.Response.Cookies
Cookie 在设置 cookie响应标头中传递到浏览器。 因此,发送 cookie 需要与用于发送响应标头的回调相同:
public async Task Invoke(HttpContext httpContext) { // ... httpContext.Response.OnStarting(SetCookies, state: httpContext); httpContext.Response.OnStarting(SetHeaders, state: httpContext);
SetCookies
回调方法将如下所示:
private Task SetCookies(object context) { var httpContext = (HttpContext)context; IResponseCookies responseCookies = httpContext.Response.Cookies; responseCookies.Append("cookie1name", "cookie1value"); responseCookies.Append("cookie2name", "cookie2value", new CookieOptions { Expires = System.DateTime.Now.AddDays(5), HttpOnly = true }); // If you use .NET Framework 4.6+, Task.CompletedTask will be a bit faster return Task.FromResult(0); }