OwinStartup.cs
public class OwinStartup
{
internal static IDataProtectionProvider DataProtectionProvider { get; private set; }
public void Configuration(IAppBuilder app)
{
DataProtectionProvider = app.GetDataProtectionProvider();
var config = new HttpConfiguration();
SimpleInjectorConfig.Configure(app);
ConfigureOAuth(app);
WebApiConfig.Register(config);
app.UseCors(CorsOptions.AllowAll);
app.UseWebApi(config);
}
private static void ConfigureOAuth(IAppBuilder app)
{
app.CreatePerOwinContext(
() => (IDisposable)GlobalConfiguration.Configuration.DependencyResolver.GetService(
typeof(AppUserManager)));
var options = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new AppAuthProvider(),
AllowInsecureHttp = true,
};
app.UseOAuthAuthorizationServer(options);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
SimpleInjectorConfig.cs
public static class SimpleInjectorConfig
{
public static void Configure(IAppBuilder app)
{
var container = new Container();
container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();
//allows scoped instances to be resolved during OWIN request
app.Use(async (context, next) =>
{
using (AsyncScopedLifestyle.BeginScope(container))
{
await next();
}
});
container.Register<AppIdentityDbContext>(Lifestyle.Scoped);
container.Register<AppUserManager>(Lifestyle.Scoped);
container.Register(
() =>
container.IsVerifying
? new OwinContext().Authentication
: HttpContext.Current.GetOwinContext().Authentication, Lifestyle.Scoped);
container.Register<AppSignInManager>(Lifestyle.Scoped);
container.Verify();
GlobalConfiguration.Configuration.DependencyResolver =
new SimpleInjectorWebApiDependencyResolver(container);
}
}
So in my implemenation of OAuthAuthorizationServerProvider called AppAuthProvider Im trying to get instance of AppUserManager ( I need to find user ) using this code:
var manager = context.OwinContext.Get<AppUserManager>();
But dont know why I still get null. I really dont know what to do because everythings seems to be configured correctly. Any ideas ? Thanks !
I found a solution. Updated code below:
OwinStartup.cs
public class OwinStartup
{
internal static IDataProtectionProvider DataProtectionProvider { get; private set; }
public void Configuration(IAppBuilder app)
{
DataProtectionProvider = app.GetDataProtectionProvider();
var container = SimpleInjectorConfig.Configure();
//allows scoped instances to be resolved during OWIN request
app.Use(async (context, next) =>
{
using (AsyncScopedLifestyle.BeginScope(container))
{
await next();
}
});
var config = new HttpConfiguration
{
DependencyResolver = new SimpleInjectorWebApiDependencyResolver(container)
};
ConfigureOAuth(app, config);
WebApiConfig.Register(config);
app.UseCors(CorsOptions.AllowAll);
app.UseWebApi(config);
}
private static void ConfigureOAuth(IAppBuilder app, HttpConfiguration config)
{
app.CreatePerOwinContext(
() => (AppUserManager)config.DependencyResolver.GetService(
typeof(AppUserManager)));
var options = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new AppAuthProvider(),
//TODO: Change in production.
AllowInsecureHttp = true,
};
app.UseOAuthAuthorizationServer(options);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
SimpleInjectorConfig.cs
public static class SimpleInjectorConfig
{
public static Container Configure()
{
var container = new Container();
container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();
container.Register<AppIdentityDbContext>(Lifestyle.Scoped);
container.Register<AppUserManager>(Lifestyle.Scoped);
container.Register(
() =>
container.IsVerifying
? new OwinContext().Authentication
: HttpContext.Current.GetOwinContext().Authentication, Lifestyle.Scoped);
container.Register<AppSignInManager>(Lifestyle.Scoped);
container.Verify();
return container;
}
}
Maybe someone will use it.
Related
If I do not use Autofac, I can connect without issues. But when I use Autofac, I get an error of
java.util.concurrent.ExecutionException: java.net.ConnectException: Connection refused`.
Here is how I setup my SignalR Server:
public class Program
{
private static ApplicationSetting mApplicationSetting;
private static ILoggingOperation mLoggingOperation;
static void Main(string[] args)
{
StartWebHost();
Console.ReadLine();
}
static void StartWebHost()
{
using (WebApp.Start("http://10.16.32.52:8085"))
{
Console.WriteLine(#"Server running at 10.16.32.52:8085");
}
}
}
class Startup
{
public void Configuration(IAppBuilder app)
{
var builder = new ContainerBuilder();
builder.RegisterHubs(Assembly.GetExecutingAssembly()).PropertiesAutowired();
builder.RegisterType<Application>().As<IApplication>();
builder.RegisterType<CommonActions>().As<ICommonActions>();
builder.RegisterType<LoggingOperation>().As<ILoggingOperation>();
var container = builder.Build();
var resolver = new Autofac.Integration.SignalR.AutofacDependencyResolver(container);
app.UseCors(CorsOptions.AllowAll);
app.MapSignalR(new HubConfiguration
{
Resolver = resolver,
EnableJSONP = true,
EnableDetailedErrors = true,
EnableJavaScriptProxies = true
});
}
}
My Hub is called ApplicationHub and it is in another class. What can I try next?
start up cs file .net core: (This is also get called while creating test server)
public class Startup
{
private IHostingEnvironment env;
private Dictionary<string, string> secretlist = new Dictionary<string, string>();
public Startup(IConfiguration configuration, IHostingEnvironment env)
{
this.Configuration = configuration;
this.CurrentEnvironment = env;
}
public Startup(IHostingEnvironment env)
{
this.env = env;
}
public IConfiguration Configuration { get; }
private IHostingEnvironment CurrentEnvironment { get; set; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<FormOptions>(x =>
{
x.ValueLengthLimit = int.MaxValue;
x.MultipartBodyLengthLimit = int.MaxValue;
x.MultipartHeadersLengthLimit = int.MaxValue;
});
services.AddApplicationInsightsTelemetry(this.Configuration);
services.AddSingleton<ITelemetryInitializer, AppInsightsInitializer>();
// Adds services required for using options.
services.AddOptions();
services.Configure<AppSettingsConfig>(this.Configuration.GetSection("AppSettings"));
if (this.CurrentEnvironment.IsDevelopment())
{
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(Environment.ExpandEnvironmentVariables(this.Configuration.GetValue<string>("AppSettings:KeyStorage_UNCPath"))))
.ProtectKeysWithDpapiNG();
}
else
{
CloudStorageAccount storageAccount = new CloudStorageAccount(
new Microsoft.WindowsAzure.Storage.Auth.StorageCredentials(
this.Configuration.GetValue<string>("AppSettings:StorageAccountName"),
this.Configuration.GetValue<string>("AppSettings:StorageAccessValue")), true);
//Create blob client
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
// Get a reference to a container named "keycontainer."
CloudBlobContainer container = blobClient.GetContainerReference("keycontainer");
services.AddDataProtection().PersistKeysToAzureBlobStorage(container, "keys.xml");
}
services.Configure<AppSettingsConfig>(options =>
{
if (!this.CurrentEnvironment.IsDevelopment())
{
}
});
var azureAdConfig = new AzureAdConfig();
this.Configuration.GetSection("Authentication:AzureAd").Bind(azureAdConfig);
services.Configure<AzureAdConfig>(this.Configuration.GetSection("Authentication:AzureAd"));
var connectionStringsConfig = new ConnectionStringsConfig();
connectionStringsConfig.oneConnection = this.secretlist["ConnectionStrings"];
//this.Configuration.GetSection("ConnectionStrings").Bind(connectionStringsConfig);
//services.Configure<ConnectionStringsConfig>(this.Configuration.GetSection("ConnectionStrings"));
if (this.RequireAAD())
{
// Add framework services.
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new RequireHttpsAttribute());
});
}
else
{
services.Configure<MvcOptions>(options =>
{
});
}
// Add Authentication services.
if (this.RequireAAD())
{
// Configure the OWIN pipeline to use cookie auth.
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
// Configure the OWIN pipeline to use OpenID Connect auth.
// https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-openid-connect-code
.AddOpenIdConnect(options =>
{
options.ClientId = azureAdConfig.ClientId;
options.ClientSecret = azureAdConfig.ClientSecret;
options.Authority = string.Format(azureAdConfig.AADInstance, azureAdConfig.Tenant);
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.Resource = azureAdConfig.ResourceURI_Graph;
// PostLogoutRedirectUri = Configuration["AzureAd:PostLogoutRedirectUri"],
options.Events = new AuthEvents(azureAdConfig, connectionStringsConfig);
});
if (this.RequireAAD())
{
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter(policy));
config.Filters.Add(typeof(ExceptionFilter));
});
}
else
{
services.AddMvc();
}
if (this.Configuration.GetValue<bool>("API: SWITCH_ENABLE_API", true))
{
//services.AddScoped<IDBOperation, Operations>();
services.AddScoped<ILookupSearch, Operations>();
services.AddScoped<IFullSearch, Operations>();
}
services.AddSingleton<Common.Data.RepositoryFactories>(new Common.Data.RepositoryFactories(new Dictionary<Type, Func<DbContext, object>>
{
{ typeof(IQueryRepository), dbcontext => new QueryRepository(dbcontext) },
{ typeof(IDomainValuesRepository), dbcontext => new DomainValuesRepository(dbcontext) },
{ typeof(IRequestsRepository), dbcontext => new RequestsRepository(dbcontext) },
// { typeof(IoneDomainValuesRepository), dbcontext => new oneDomainValuesRepository(dbcontext) }
}));
services.AddTransient<Common.Contracts.IRepositoryProvider, Common.Data.RepositoryProvider>();
services.AddScoped<one.Data.Contracts.IoneUow, one.Data.oneUow>();
services.AddTransient<IUow,Uow>();
// For accessing appinsights for dependency injection?
services.AddApplicationInsightsTelemetry();
// For Storing Tokens in DB
services.AddDistributedSqlServerCache(o =>
{
o.ConnectionString = this.secretlist["ConnectionStrings"];
// o.ConnectionString = this.Configuration.GetConnectionString("oneConnection");
// o.ConnectionString = this.Configuration[this.Configuration.GetSection("KeyVaultSeetings")["oneConnectionString"]];
o.SchemaName = "dbo";
o.TableName = "CacheTable";
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IEntityExtractor, EntityExtractor>();
services.AddScoped<ITokenCacheService, DistributedTokenCacheService>();
services.AddScoped<ITokenService, TokenService>();
services.AddTransient<IAPIClient,APIClient>();
}
/// <summary>
/// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
/// </summary>
/// <param name="app"></param>
/// <param name="env"></param>
/// <param name="loggerFactory"></param>
/// <param name="tc"></param>
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, TelemetryClient tc)
{
var azureAdConfig = new AzureAdConfig();
this.Configuration.GetSection("Authentication:AzureAd").Bind(azureAdConfig);
loggerFactory.AddConsole(this.Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
loggerFactory.AddProvider(new MyFilteredLoggerProvider(tc));
loggerFactory.AddApplicationInsights(app.ApplicationServices, this.Configuration.GetValue<string>("Logging:LogLevel:Default") == "Information" ? Microsoft.Extensions.Logging.LogLevel.Information : Microsoft.Extensions.Logging.LogLevel.Warning);
this.SetupStore(app);
app.UseRewriter(new RewriteOptions().AddRedirectToHttps());
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
{
HotModuleReplacement = true
});
}
else
{
app.UseExceptionHandler("/Home/Error");
}
// TODO . Switch
app.UseStaticFiles();
if (this.RequireAAD())
{
app.UseAuthentication();
}
app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});
}
}
Controller is decorated as :
[Route("api/[controller]")]
public class SearchController : BaseController
Controller Action is decorated as :
[Route("TestMethod")]
[ActionName("TestMethod")]
[HttpGet]
public async Task<EmptyResult> Test()
Configuration of TestServer Test CS file :
public DemoTest()
{
// Set up server configuration
var configuration = new ConfigurationBuilder()
.AddJsonFile(#"appsettings.json")
.Build();
// Create builder
var builder = new WebHostBuilder()
.UseStartup<Startup>()
.UseConfiguration(configuration);
// Create test server
var server = new TestServer(builder);
// Create database context
this._context = server.Host.Services.GetService(typeof(DBContext)) as DBContext;
// Create client to query server endpoints
this._client = server.CreateClient();
_client.BaseAddress = new Uri("https://localhost:44316/");
}
Test as a Fact :
[Fact]
public async Task Test()
{
try
{
var response = await this._client.GetAsync("/api/Search/TestMethod");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
//Assert.False(result != null);
}
catch (Exception ex)
{
throw;
}
}
Getting Status as 302 and SearchController action is not getting
called. All the dependencies are resolved using start up configuration
file
Any idea ???
You could check the content for var responseString = await response.Content.ReadAsStringAsync(); to see what the content is.
I assume it is the login page which is due to that you required Authorize.
First, try to remove the code below for a try.
services.AddMvc(config =>
{
//var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
//config.Filters.Add(new Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter(policy));
config.Filters.Add(typeof(ExceptionFilter));
});
For Authentication, you will need to imitate the login process, here is a link for Identity, you could try implement your own login for AAD.
Razor Pages .NET Core 2.1 Integration Testing post authentication
Lets say I have a hub:
public class MyHub: Hub<IMyHub> {
public MyHub(){}
public Task DoWork(){
var principal = this.Context.User; // Currently WindowsIdentity as its not authenticated
var auth = new OwinContext(this.Context.Request.Environment).Authentication;
var types = auth.GetAuthenticationTypes(); // Empty list
// ....
}
}
If I execute same code inside an WebApi2 Controller the .GetAuthenticationTypes() would give me the correct result of pre'configured authProviders.
Any ideas why its not behaving like within an controller? Is that by design?
Update 1
Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
var httpConfig = new HttpConfiguration();
// .. Ioc Registering Hubs
WebApiConfig.Register(httpConfig);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
// Have also tried registering signalr here before auth
// app.MapSignalR("/signalR", new HubConfiguration() { .... });
app.UseOAuthIntrospection(options =>
{
//...
options.AuthenticationType = OAuthDefaults.AuthenticationType;
options.ClientId = clientId;
options.ClientSecret = clientSecret;
options.RequireHttpsMetadata = false;
options.AuthenticationMode = AuthenticationMode.Passive;
options.Events = new OAuthIntrospectionEvents()
{
OnRetrieveToken = context =>
{
// Getting token from QueryString passed from js app.
var token = context.Request.Query["Authorization"];
if (!string.IsNullOrWhiteSpace(token))
{
context.Token = token;
}
return Task.CompletedTask;
}
}
//...
};
app.UseCors(CorsOptions.AllowAll);
app.MapSignalR("/signalR", new HubConfiguration() { .... });
app.UseWebApi(httpConfig); // Have tried swapping these 0 effect
}
In your Owin startup part, you should configure app.MapSignalR(); before registering authentication.
public void Configuration(IAppBuilder app)
{
app.MapSignalR();//Configure it first
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Home/Index")
});
}
This guide does not appear to work for SimpleInjector.
My OWIN startup looks like this:
container = new Container();
container.Options.DefaultScopedLifestyle = new ExecutionContextScopeLifestyle();
container.RegisterSingleton(() => new SimpleInjectorSignalRDependencyResolver(_container));
container.RegisterSingleton(() =>
new HubConfiguration()
{
EnableDetailedErrors = true,
Resolver = _container.GetInstance<SimpleInjectorSignalRDependencyResolver>()
});
container.RegisterSingleton<IHubActivator, SimpleInjectorHubActivator>();
container.RegisterSingleton<IStockTicker,StockTicker>();
container.RegisterSingleton<HubContextAdapter<StockTickerHub, IStockTickerHubClient>>();
container.RegisterSingleton(() => GlobalHost.ConnectionManager);
container.Verify();
GlobalHost.DependencyResolver = container.GetInstance<SimpleInjectorSignalRDependencyResolver>();
app.Use(async (context, next) =>
{
using (container.BeginExecutionContextScope())
{
await next();
}
});
app.MapSignalR(container.GetInstance<HubConfiguration>());
And The HubContextAdapter looks like this:
public class HubContextAdapter<THub, TClient>
where THub : Hub
where TClient : class
{
private readonly IConnectionManager _manager;
public HubContextAdapter(IConnectionManager manager)
{
_manager = manager;
}
public IHubContext<TClient> GetHubContext()
{
return _manager.GetHubContext<THub, TClient>();
}
}
And SimpleInjectorSignalRDependencyResolver looks like:
public class SimpleInjectorSignalRDependencyResolver : DefaultDependencyResolver
{
public SimpleInjectorSignalRDependencyResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public override object GetService(Type serviceType)
{
return _serviceProvider.GetService(serviceType) ?? base.GetService(serviceType);
}
public override IEnumerable<object> GetServices(Type serviceType)
{
var #this = (IEnumerable<object>)_serviceProvider.GetService(
typeof(IEnumerable<>).MakeGenericType(serviceType));
var #base = base.GetServices(serviceType);
return #this == null ? #base : #base == null ? #this : #this.Concat(#base);
}
private readonly IServiceProvider _serviceProvider;
}
And StockTicker looks like:
public class StockTicker : IStockTicker
{
private readonly HubContextAdapter<StockTickerHub, IStockTickerHubClient> _context;
public StockTicker(HubContextAdapter<StockTickerHub, IStockTickerHubClient> context)
{
_context = context;
}
}
When the StockTicker ticks and calls all clients to update the client method is not invoked and there is no network traffic.
SimpleInjector wants to instantiate the singletons after verification or after the first GetInstance call. This is too early for SignalR and the StockTicker and it will take an instance of GlobalHost.ConnectionManager before SimpleInjectorSignalRDependencyResolver is the resolver.
I chose to change the dependency on IConnectionManager to be Lazy<IConnectionManager> and the dependency on IStockTicker to be Lazy<IStockTicker> so that registration became like the following:
container = new Container();
container.Options.DefaultScopedLifestyle = new ExecutionContextScopeLifestyle();
container.RegisterSingleton(() => new SimpleInjectorSignalRDependencyResolver(_container));
container.RegisterSingleton(() =>
new HubConfiguration()
{
EnableDetailedErrors = true,
Resolver = _container.GetInstance<SimpleInjectorSignalRDependencyResolver>()
});
container.RegisterSingleton<IHubActivator, SimpleInjectorHubActivator>();
container.RegisterSingleton<IStockTicker,StockTicker>();
container.RegisterSingleton<Lazy<IStockTicker>>(() => new Lazy<IStockTicker>(() => container.GetInstace<IStockTicker>()) );
container.RegisterSingleton<HubContextAdapter<StockTickerHub, IStockTickerHubClient>>();
container.RegisterSingleton(() => new Lazy<IConnectionManager>(() => GlobalHost.ConnectionManager));
container.Verify();
I have used this link here as a reference;
https://unity.codeplex.com/discussions/446780
So as per the link I have added a UnityActionFilterProvider class;
public class UnityActionFilterProvider : ActionDescriptorFilterProvider, IFilterProvider
{
private readonly IUnityContainer container;
public UnityActionFilterProvider(IUnityContainer container)
{
this.container = container;
}
public new IEnumerable<FilterInfo> GetFilters(HttpConfiguration configuration, HttpActionDescriptor actionDescriptor)
{
var filters = base.GetFilters(configuration, actionDescriptor);
foreach (var filter in filters)
{
container.BuildUp(filter.Instance.GetType(), filter.Instance);
}
return filters;
}
}
and I then have added to my UnityConfig.cs
public static void RegisterFilterProviders(IUnityContainer UnityDependencyResolver)
{
var providers = GlobalConfiguration.Configuration.Services.GetFilterProviders().ToList();
GlobalConfiguration.Configuration.Services.Add(
typeof(IFilterProvider),
new UnityActionFilterProvider(UnityDependencyResolver));
var defaultprovider = providers.First(p => p is ActionDescriptorFilterProvider);
GlobalConfiguration.Configuration.Services.Remove(typeof(IFilterProvider), defaultprovider);
}
which I then call in my Startup.cs
public void Configuration(IAppBuilder app)
{
HttpConfiguration = new HttpConfiguration();
LoggingConfig.RegisterLogger();
ConfigureOAuth(app);
var unityContainer = UnityConfig.GetConfiguredContainer();
HttpConfiguration.DependencyResolver = new UnityDependencyResolver(unityContainer);
//for DI in the filters
UnityConfig.RegisterFilterProviders(unityContainer);
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(HttpConfiguration);
app.UseCors(CorsOptions.AllowAll);
app.UseWebApi(HttpConfiguration);
}
Finally, I have the following filter (Note: I have also tried this as an ActionFilter to see if this made a difference)
public class HasPermissionAttribute : AuthorizationFilterAttribute
{
[Dependency]
public UserPermissionService UserService { get; set; }
public override void OnAuthorization(HttpActionContext actionContext)
{
}
}
However, the UserService is null when this is being hit? Any ideas on what I am doing wrong here please?
Hopefully this will help someone, someday.
Basically it was all down to the fact I was using OAuth with the API.
Therefore I simply needed to ensure the configuration was passed across with the filters by editing the startup.cs as follows;
HttpConfiguration = new HttpConfiguration();
LoggingConfig.RegisterLogger();
ConfigureOAuth(app);
var unityContainer = UnityConfig.GetConfiguredContainer();
HttpConfiguration.DependencyResolver = new UnityDependencyResolver(unityContainer);
//for DI in the filters
UnityConfig.RegisterFilterProviders(unityContainer, HttpConfiguration);
where the registration was simply then ammended with;
public static void RegisterFilterProviders(IUnityContainer UnityDependencyResolver, HttpConfiguration configuration)
{
var providers = configuration.Services.GetFilterProviders().ToList();
configuration.Services.Add(
typeof(IFilterProvider),
new UnityActionFilterProvider(UnityDependencyResolver));
var defaultprovider = providers.First(p => p is ActionDescriptorFilterProvider);
configuration.Services.Remove(typeof(IFilterProvider), defaultprovider);
}