I have created a new asp.net web api 2 project in visual studio 2015 using ASP.NET Web API 2.1 Custom Route Attributes.
I am using Swagger (SwashBuckle 5.0) for API documentation and wanted to have the documentation per version which i managed to get this document but swagger ui showing same xml comments from both versions.
The xml comments on api/view for version 1 and 2 there some different which does not appears on swagger ui.
Version 1
Version 2
public class SwaggerConfig
{
public static void Register()
{
var thisAssembly = typeof(SwaggerConfig).Assembly;
GlobalConfiguration.Configuration
.EnableSwagger(c =>
{
c.MultipleApiVersions(
(apiDesc, targetApiVersion) => ResolveVersionSupportByRouteConstraint(apiDesc, targetApiVersion),
(vc) =>
{
vc.Version("2", "Swagger API V2");
vc.Version("1", "Swagger API V1");
});
c.IncludeXmlComments(GetXmlCommentsPath());
})
.EnableSwaggerUi(c =>
{
c.EnableDiscoveryUrlSelector();
});
}
private static string GetXmlCommentsPath()
{
return String.Format(#"{0}\bin\ChartOfAccounts.Api.XML", AppDomain.CurrentDomain.BaseDirectory);
}
private static bool ResolveVersionSupportByRouteConstraint(ApiDescription apiDesc, string targetApiVersion)
{
var attr = apiDesc.ActionDescriptor.GetCustomAttributes<VersionedRoute>().FirstOrDefault();
if (attr == null)
{
return true;
}
int targetVersion;
if (int.TryParse(targetApiVersion, out targetVersion))
{
return attr.AllowedVersion == targetVersion;
};
return true;
}
}
Version 1 Controller
[VersionedRoute("api/view", 1)]
public class ViewController : ApiController
{
private IChartManager _chartManager;
public ViewController(IChartManager chartManager)
{
_chartManager = chartManager;
}
/// <summary>
/// Returns a single view
/// </summary>
/// <param name="Id">Used to identify view. {Only Guid Accepted} </param>
/// <returns></returns>
public async Task<HttpResponseMessage> GetAsync(Guid Id)
{
Chart chart = await _chartManager.GetChartByIdAsync(Id);
return Request.CreateResponse(HttpStatusCode.OK, chart);
}
}
Version 2 API Controller
[VersionedRoute("api/view", 2)]
public class Viewv2Controller : ApiController
{
private IChartManager _chartManager;
public Viewv2Controller(IChartManager chartManager)
{
_chartManager = chartManager;
}
/// <summary>
/// Returns a single view of the chart
/// </summary>
/// <param name="Id">Used to identify view</param>
/// <returns></returns>
public async Task<HttpResponseMessage> GetAsync(string Id)
{
Guid newGuidId;
if (Guid.TryParse(Id, out newGuidId))
{
Chart chart = await _chartManager.GetChartByIdAsync(newGuidId);
return Request.CreateResponse(HttpStatusCode.OK, chart);
}
return Request.CreateErrorResponse(HttpStatusCode.NotAcceptable, "Invalid Guid value for Parameter Id.");
}
}
Related
Describe the bug
After upgrading from .net core 2.2 to 3.1, integration tests are failing.
All tests are wrapped in TransactionScope so that all changes to db should be revered (scope.Complete() is not called).
When call to the data access layer is made through api (HttpClient) records are created in the database, but they should not be since the entire test is wrapped in TransactionScope.
To Reproduce
public class Entity
{
public int Id { get; set; }
public string Name { get; set; }
}
public class CustomDbContext : DbContext
{
private const string DefaultConnectionString = "Server=.;Initial Catalog=WebApi;Trusted_Connection=True;";
private readonly string _connectionString;
public CustomDbContext() : this(DefaultConnectionString)
{
}
public CustomDbContext(string connectionString)
{
_connectionString = connectionString;
}
public DbSet<Entity> Entities { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(_connectionString);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new EntityConfiguration());
}
public async Task Save<TModel>(TModel model)
{
using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
{
Update(model);
await SaveChangesAsync();
scope.Complete();
}
}
}
public class EntityService : IEntityService
{
private readonly CustomDbContext _db;
public EntityService(CustomDbContext db)
{
_db = db;
}
public async Task Save(Entity model) => await _db.Save(model);
}
[ApiController]
[Route("[controller]")]
public class EntityController : ControllerBase
{
private readonly IEntityService _service;
public EntityController(IEntityService service)
{
_service = service;
}
[HttpPost]
public async Task<IActionResult> Save(Entity model)
{
await _service.Save(model);
return Ok();
}
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddScoped<CustomDbContext>();
services.AddScoped<IEntityService, EntityService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
/// <summary>
/// Apply this attribute to your test method to automatically create a <see cref="TransactionScope"/>
/// that is rolled back when the test is finished.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class AutoRollbackAttribute : BeforeAfterTestAttribute
{
TransactionScope scope;
/// <summary>
/// Gets or sets whether transaction flow across thread continuations is enabled for TransactionScope.
/// By default transaction flow across thread continuations is enabled.
/// </summary>
public TransactionScopeAsyncFlowOption AsyncFlowOption { get; set; } = TransactionScopeAsyncFlowOption.Enabled;
/// <summary>
/// Gets or sets the isolation level of the transaction.
/// Default value is <see cref="IsolationLevel"/>.Unspecified.
/// </summary>
public IsolationLevel IsolationLevel { get; set; } = IsolationLevel.Unspecified;
/// <summary>
/// Gets or sets the scope option for the transaction.
/// Default value is <see cref="TransactionScopeOption"/>.Required.
/// </summary>
public TransactionScopeOption ScopeOption { get; set; } = TransactionScopeOption.Required;
/// <summary>
/// Gets or sets the timeout of the transaction, in milliseconds.
/// By default, the transaction will not timeout.
/// </summary>
public long TimeoutInMS { get; set; } = -1;
/// <summary>
/// Rolls back the transaction.
/// </summary>
public override void After(MethodInfo methodUnderTest)
{
scope.Dispose();
}
/// <summary>
/// Creates the transaction.
/// </summary>
public override void Before(MethodInfo methodUnderTest)
{
var options = new TransactionOptions { IsolationLevel = IsolationLevel };
if (TimeoutInMS > 0)
options.Timeout = TimeSpan.FromMilliseconds(TimeoutInMS);
scope = new TransactionScope(ScopeOption, options, AsyncFlowOption);
}
}
public class CustomWebApplicationFactory : WebApplicationFactory<Startup>
{
private const string TestDbConnectionString = "Server=.;Initial Catalog=WebApiTestDB_V3;Trusted_Connection=True;";
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
services.AddSingleton(_ => new CustomDbContext(TestDbConnectionString));
var sp = services.BuildServiceProvider();
var db = sp.GetRequiredService<CustomDbContext>();
db.Database.Migrate();
});
}
}
public class IntegrationTest : IClassFixture<CustomWebApplicationFactory>
{
protected readonly HttpClient _client;
protected readonly IServiceProvider _serviceProvider;
protected readonly CustomDbContext _db;
public IntegrationTest(CustomWebApplicationFactory factory)
{
_client = factory.CreateClient();
_serviceProvider = factory.Services.CreateScope().ServiceProvider;
_db = _serviceProvider.GetRequiredService<CustomDbContext>();
}
protected void DetachAll()
{
_db.ChangeTracker.Entries()
.ToList()
.ForEach(e => e.State = EntityState.Detached);
}
protected async Task<Entity> AddTestEntity()
{
var model = new Entity
{
Name = "test entity"
};
await _db.AddAsync(model);
await _db.SaveChangesAsync();
return model;
}
}
public static class HttpContentHelper
{
public static HttpContent GetJsonContent(object model) =>
new StringContent(JsonConvert.SerializeObject(model), Encoding.UTF8, "application/json");
}
[AutoRollback]
public class EntityIntegrationTest : IntegrationTest
{
private const string apiUrl = "/entity";
public EntityIntegrationTest(CustomWebApplicationFactory factory) : base(factory)
{
}
[Fact]
public async Task CanAdd()
{
// arrange
var model = new Entity
{
Name = "new entity"
};
var content = HttpContentHelper.GetJsonContent(model);
// act
var response = await _client.PostAsync(apiUrl, content);
// assert
response.EnsureSuccessStatusCode();
var result = await _db.Entities.FirstOrDefaultAsync();
Assert.Equal(model.Name, result.Name);
}
[Fact]
public async Task CanUpdate()
{
// arrange
var model = await AddTestEntity();
DetachAll(); // detach all entries because posting to api would create a new model, saving a new object with existing key throws entity already tracked exception
model.Name = "updated entity";
var content = HttpContentHelper.GetJsonContent(model);
// act
var response = await _client.PostAsync(apiUrl, content);
// assert
response.EnsureSuccessStatusCode();
var result = await _db.Entities.FirstOrDefaultAsync();
Assert.Equal(model.Id, result.Id);
Assert.Equal(model.Name, result.Name);
}
[Fact]
public async Task CannotInsertDuplicate()
{
// arrange
var entity = await AddTestEntity();
var model = new Entity
{
Name = entity.Name
};
var content = HttpContentHelper.GetJsonContent(model);
// act
var response = await _client.PostAsync(apiUrl, content);
// assert
var result = await response.Content.ReadAsStringAsync();
Assert.Contains("Cannot insert duplicate", result);
}
}
There are many files/classes involved so I've created a example repository
Example tests that are failing are in https://github.com/niksloter74/web-api-integration-test/tree/master/netcore3.1
Working example in .net core 2.2 https://github.com/niksloter74/web-api-integration-test/tree/master/netcore2.2
Direct test for service layer is working correctly
[AutoRollback]
public class EntityServiceTest : IntegrationTest
{
private readonly IEntityService service;
public EntityServiceTest(CustomWebApplicationFactory factory) : base(factory)
{
service = _serviceProvider.GetRequiredService<IEntityService>();
}
[Fact]
public async Task CanAdd()
{
// arrange
var model = new Entity
{
Name = "new entity"
};
// act
await service.Save(model);
// assert
var result = await _db.Entities.FirstOrDefaultAsync();
Assert.Equal(model.Name, result.Name);
}
[Fact]
public async Task CanUpdate()
{
// arrange
var model = await AddTestEntity();
model.Name = "updated entity";
// act
await service.Save(model);
// assert
var result = await _db.Entities.FirstOrDefaultAsync();
Assert.Equal(model.Id, result.Id);
Assert.Equal(model.Name, result.Name);
}
[Fact]
public async Task CannotInsertDuplicate()
{
// arrange
var entity = await AddTestEntity();
var model = new Entity
{
Name = entity.Name
};
// act
var ex = await Assert.ThrowsAnyAsync<Exception>(async () => await service.Save(model));
// assert
Assert.StartsWith("Cannot insert duplicate", ex.InnerException.Message);
}
}
This is by design but there’s a flag to get the old behavior back on TestServer called PreserveExecutionContext.
Here is an official discussion thread.
This line in IntegartionTest class fixed the problem
_factory.Server.PreserveExecutionContext = true;
I've also updated the repository
I'm writing a little Action Filter for an ASP.NET Core web API project. The filter is for testing the associated UI for error handling. It will throw an error if a specific verb and method is invoked. The filter isn't a problem. The problem is the appsettings.configuration.
Here's what I'm trying to do:
appsettings.development.json
"FaultTesting": {
"FaultRequests": false,
"SlowRequests": 0,
"FaultCalls": [
{
"Path": "/api/usercontext",
"Verbs": "get,put,delete"
},
{
"Path": "/api/cafeteriaaccounts",
"Verbs": "get"
}
]
}
These are my c# types to hold the configuration:
public class FaultTestingOptions
{
/// <summary>
/// If true, checks FaultCalls for a path and verb to match.
/// </summary>
public bool FaultRequests { get; set; }
/// <summary>
/// Number of milliseconds to delay the response.
/// </summary>
public int SlowRequests { get; set; }
public FaultCall[] FaultCalls { get; set; }
}
public class FaultCall
{
public string Path { get; set; }
public string Verbs { get; set; }
}
Add what I'm doing in startup:
services.AddMvc(config =>
{
...
FaultTestingFilter(Options.Create(GetFaultTestingOptions())));
...
});
private FaultTestingOptions GetFaultTestingOptions()
{
var options = new FaultTestingOptions
{
FaultRequests = Configuration["FaultTesting:FaultRequests"].ToBoolean(),
SlowRequests = Convert.ToInt32(Configuration["FaultTesting:SlowRequests"])
};
var calls = Configuration.GetSection("FaultTesting:FaultCalls")
.GetChildren()
.Select(x => x.Value)
.ToArray();
var fooie = Configuration["FaultTesting:FaultCalls"];
//options.FaultCalls = calls.Select(c => new FaultCall { Path = c, Verbs = c.Value });
return options;
}
"calls" is an array of two nulls, fooie is null.
What's the right approach here?
Better option is to bind TOption in ConfigServices method and then inject it to you filer. It work same as default model binder work, you did not need to manually read and set values.
ConfigureServices Method:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<FaultTestingOptions>(option => Configuration.GetSection("FaultTesting").Bind(option));
// Add framework services.
services.AddMvc();
}
Injecting in filter:
private readonly IOptions<FaultTestingOptions> config;
public FaultTestingFilter(IOptions<FaultTestingOptions> config)
{
this.config = config;
}
Accessing the properties.
var SlowRequests= config.Value.SlowRequests;
var FaultCalls= config.Value.FaultCalls;
I've got a controller with 3 action methods on it, two of which are regular OData calls and a third which is a function. Using Azure AD and ADAL to secure the WebAPI.
CustomAuthAttribute (IsAppAuthorizing Simply checks a web.config entry)
public class OpsmApiAuthorizeAttribute : AuthorizeAttribute
{
/// <summary>
/// Returns whether or not the user has authenticated with ADFS and whehter ornot we are configured to do authorization
/// </summary>
/// <param name="actionContext"></param>
/// <returns></returns>
protected override bool IsAuthorized(System.Web.Http.Controllers.HttpActionContext actionContext)
{
if (HttpContext.Current.IsAppAuthorizing())
return base.IsAuthorized(actionContext);
return true;
}
}
Startup.Auth.cs
public void ConfigureAuth(IAppBuilder app)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"],
},
MetadataAddress = ConfigurationManager.AppSettings["ida:MetadataEndpoint"],
});
}
Controller (ByUser is the OData function that does not get proper IPrincple information, other two methods work fine)
[OpsmApiAuthorizeAttribute]
public class ProjectsController : BaseController
{
/// <summary>
/// Get a Project Detail for a given project id
/// </summary>
/// <returns>json</returns>
[EnableQuery]
public IQueryable<OPSM.DataAccess.Database.OpsM.PRJ> Get([FromODataUri] string key)
{
...
}
/// <summary>
/// Get all Projects
/// </summary>
/// <returns>json</returns>
[EnableQuery]
public IQueryable<OPSM.DataAccess.Database.OpsM.PRJ> Get()
{
...
}
[HttpGet]
//[CacheOutput(ServerTimeSpan = 60 * 60)]
public IHttpActionResult ByUser([FromODataUri]string userId)
{
...
}
}
I´m developing an ASP.NET MVC Application, in which I use NHibernate and Ninject.
The Problem is caused by the following Controller:
public class ShoppingCartController : Controller
{
private readonly Data.Infrastructure.IShoppingCartRepository _shoppingCartRepository;
private readonly Data.Infrastructure.IShopItemRepository _shopItemRepository;
public ShoppingCartController(Data.Infrastructure.IShoppingCartRepository shoppingCartController,
Data.Infrastructure.IShopItemRepository shopItemRepository)
{
_shoppingCartRepository = shoppingCartController;
_shopItemRepository = shopItemRepository;
}
public ActionResult AddToShoppingCart(FormCollection formCollection)
{
var cartItem = new Data.Models.ShoppingCartItem();
cartItem.ChangeDate = DateTime.Now;
cartItem.ShopItem = _shopItemRepository.GetShopItem(SessionData.Data.Info, Convert.ToInt32(formCollection["shopItemId"]));
//IF I DONT´T CALL THE METHOD ABOVE, AddToCart works
_shoppingCartRepository.AddToCart(SessionData.Data.Info, cartItem);
//BUT IF I CALL THE GetShopItem METHOD I GET THE EXCEPTION HERE!
return RedirectToAction("Index", "Shop");
}
}
I know most of the Time this Exception is caused by wrong Mapping, but I´m pretty sure that my Mapping is right because the AddToCart-Method works if I don´t call GetShopItem...
So here is the Code of the ShopItemRepository:
public class ShopItemRepository : ReadOnlyRepository<ShopItem>, IShopItemRepository
{
public ShopItemRepository(IUnitOfWork uow) : base(uow)
{
}
public ShopItem GetShopItem(SessionParams param, int id)
{
return CurrentSession.QueryOver<ShopItem>()
.Where(x => x.ProcessId == param.ProcessId &&
x.CatalogueId == param.CatalogueId &&
x.Id == id)
.SingleOrDefault();
}
public IList<ShopItem> GetShopItems(SessionParams param)
{
return CurrentSession.GetNamedQuery("GetShopItems")
.SetParameter("requestor_id", param.RequestorId)
.SetParameter("recipient_id", param.RecipientId)
.SetParameter("process_id", param.ProcessId)
.SetParameter("catalogue_id", param.CatalogueId)
.List<ShopItem>();
}
}
And finally the Code of my UnitOfWork (basically it is just a Wrapper for the Session because I don´t want to reference NHibernate in my MVC Project)
public class UnitOfWork : IUnitOfWork, IDisposable
{
private NHibernate.ISession _currentSession;
public NHibernate.ISession CurrentSession
{
get
{
if(_currentSession == null)
{
_currentSession = SessionFactoryWrapper.SessionFactory.OpenSession();
}
return _currentSession;
}
}
public void Dispose()
{
if(_currentSession != null)
{
_currentSession.Close();
_currentSession.Dispose();
_currentSession = null;
}
GC.SuppressFinalize(this);
}
}
Addendum:
My NinjectWebCommon Class
public static class NinjectWebCommon
{
private static readonly Bootstrapper bootstrapper = new Bootstrapper();
/// <summary>
/// Starts the application
/// </summary>
public static void Start()
{
DynamicModuleUtility.RegisterModule(typeof(OnePerRequestHttpModule));
DynamicModuleUtility.RegisterModule(typeof(NinjectHttpModule));
bootstrapper.Initialize(CreateKernel);
}
/// <summary>
/// Stops the application.
/// </summary>
public static void Stop()
{
bootstrapper.ShutDown();
}
/// <summary>
/// Creates the kernel that will manage your application.
/// </summary>
/// <returns>The created kernel.</returns>
private static IKernel CreateKernel()
{
var kernel = new StandardKernel();
kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel);
kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>();
RegisterServices(kernel);
return kernel;
}
/// <summary>
/// Load your modules or register your services here!
/// </summary>
/// <param name="kernel">The kernel.</param>
private static void RegisterServices(IKernel kernel)
{
kernel.Bind<IUnitOfWork>().To<UnitOfWork>().InRequestScope();
kernel.Bind<Data.Infrastructure.ICatalogueRepository>().To<Data.Repositories.CatalogueRepository>();
kernel.Bind<Data.Infrastructure.ICategoryRepository>().To<Data.Repositories.CategoryRepository>();
kernel.Bind<Data.Infrastructure.IContactRepository>().To<Data.Repositories.ContactRepository>();
kernel.Bind<Data.Infrastructure.IProcessRepository>().To<Data.Repositories.ProcessRepository>();
kernel.Bind<Data.Infrastructure.IShopItemRepository>().To<Data.Repositories.ShopItemRepository>();
kernel.Bind<Data.Infrastructure.IShoppingCartRepository>().To<Data.Repositories.ShoppingCartRepository>();
}
}
IUnitOfWork is set to RequestScope so in the Case of ShoppingCartController, the two Repositories share the same UOW right?
Maybe this could cause the Problem?
Are you sure that this isn´t caused by wrong mapping? I had the same Issue and could resolve it by checking my mappings again!
I am trying to use the InSingletonScope for one of my service interface. However it is still creating a new instance of the object per web request. basically it behaves like an InRequestScope in my asp.net mvc 4 application.
I thought InSingletonScope is for the life time of the IIS process?
I register the following implementation for the interface in one of my NinjectModule. if I resolve it right away, repo1 and repo2 are actually the same instance. However, in my controller every request result in a new instance.
-------------------- Module registration
public class RepositoryModule : NinjectModule
{
#region Overrides of NinjectModule
public override void Load()
{
Bind<IFakeRepository>().To<FakeRepository>().InSingletonScope();
// following code onle execute the constructor once
var repo1String = Kernel.Get<IFakeRepository>().GetString();
var repo2String = Kernel.Get<IFakeRepository>().GetString();
}
#endregion
}
-------------------- Repository interface and implementation
public interface IFakeRepository
{
string GetString();
}
public class FakeRepository : IFakeRepository
{
public FakeRepository()
{
// every web request execute this constructor
Debug.Write("FakeRepository constructor called");
}
#region Implementation of IFackRepository
public string GetString()
{
return "dummy string";
}
#endregion
}
------------------ web api controller
public class TestRepoController : ApiController
{
public IFakeRepository FakeRepository { get; set; }
public TestRepoController(IFakeRepository fakeRepository)
{
FakeRepository = fakeRepository;
}
public string Get()
{
return FakeRepository.GetString();
}
}
----------------- web api route registration
config.Routes.MapHttpRoute(
name: "TestTakeRoutePost",
routeTemplate: "Fake",
defaults: new { controller = "TestRepo" }
);
----------------- NinjectWebCommon
[assembly: WebActivator.PreApplicationStartMethod(typeof(PNI.MediaServer.Application.App_Start.NinjectWebCommon), "Start")]
[assembly: WebActivator.ApplicationShutdownMethodAttribute(typeof(PNI.MediaServer.Application.App_Start.NinjectWebCommon), "Stop")]
public static class NinjectWebCommon
{
private static readonly Bootstrapper bootstrapper = new Bootstrapper();
/// <summary>
/// Starts the application
/// </summary>
public static void Start()
{
DynamicModuleUtility.RegisterModule(typeof(OnePerRequestHttpModule));
DynamicModuleUtility.RegisterModule(typeof(NinjectHttpModule));
bootstrapper.Initialize(CreateKernel);
}
/// <summary>
/// Stops the application.
/// </summary>
public static void Stop()
{
bootstrapper.ShutDown();
}
/// <summary>
/// Creates the kernel that will manage your application.
/// </summary>
/// <returns>The created kernel.</returns>
private static IKernel CreateKernel()
{
var kernel = new StandardKernel();
//load all Binds defined in the classes that inherit NinhectModule
kernel.Load(Assembly.GetExecutingAssembly());
kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel);
kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>();
// Set Web API Resolver
GlobalConfiguration.Configuration.DependencyResolver = new PniNinjectDependencyResolver(kernel);
return kernel;
}
}