Resolving Hangfire dependencies/HttpContext in .NET Core Startup - c#

I've installed and configured Hangfire in my .NET Core web application's Startup class as follows (with a lot of the non-Hangfire code removed):
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseHangfireServer();
//app.UseHangfireDashboard();
//RecurringJob.AddOrUpdate(() => DailyJob(), Cron.Daily);
}
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddOptions();
services.Configure<AppSettings>(Configuration);
services.AddSingleton<IConfiguration>(Configuration);
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IPrincipal>((sp) => sp.GetService<IHttpContextAccessor>().HttpContext.User);
services.AddScoped<IScheduledTaskService, ScheduledTaskService>();
services.AddHangfire(x => x.UseSqlServerStorage(connectionString));
this.ApplicationContainer = getWebAppContainer(services);
return new AutofacServiceProvider(this.ApplicationContainer);
}
}
public interface IScheduledTaskService
{
void OverduePlasmidOrdersTask();
}
public class ScheduledTaskService : IScheduledTaskService
{
public void DailyJob()
{
var container = getJobContainer();
using (var scope = container.BeginLifetimeScope())
{
IScheduledTaskManager scheduledTaskManager = scope.Resolve<IScheduledTaskManager>();
scheduledTaskManager.ProcessDailyJob();
}
}
private IContainer getJobContainer()
{
var builder = new ContainerBuilder();
builder.RegisterModule(new BusinessBindingsModule());
builder.RegisterModule(new DataAccessBindingsModule());
return builder.Build();
}
}
As you can see, I'm using Autofac for DI. I've set things up to inject a new container each time the Hangfire job executes.
Currently, I have UseHangfireDashboard() as well as the call to add my recurring job commented out and I'm receiving the following error on the line referencing IPrincipal:
System.NullReferenceException: 'Object reference not set to an instance of an object.'
I understand that Hangfire does not have an HttpContext. I'm not really sure why it's even firing that line of code for the Hangfire thread. I'm ultimately going to need to resolve a service account for my IPrincipal dependency.
How can I address my issue with Hangfire and HttpContext?

The main problem I'm having now is when I add UseHangfireServer, I
then need to resolve HttpContext too
Found here Using IoC containers
HttpContext is not available
Request information is not available during the instantiation of a
target type. If you register your dependencies in a request scope
(InstancePerHttpRequest in Autofac, InRequestScope in Ninject and so
on), an exception will be thrown during the job activation process.
So, the entire dependency graph should be available. Either register
additional services without using the request scope, or use separate
instance of container if your IoC container does not support
dependency registrations for multiple scopes.
resolving scoped dependencies in .net core would require a request which is not available during startup when registering and activating jobs. Therefore make sure that your service required for activation during startup are not registered using scoped lifetimes.
services.AddTransient<IScheduledTaskManager, ScheduledTaskManageImplementation>();
All that is left now is to configure the application to use that service with the recurring job,
public class Startup {
public IContainer ApplicationContainer { get; private set; }
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();
}
public void Configuration(IApplicationBuilder app) {
// app.AddLogger...
//add hangfire features
app.UseHangfireServer();
app.UseHangfireDashboard();
//Add the recurring job
RecurringJob.AddOrUpdate<IScheduledTaskManager>(task => task.ProcessDailyJob(), Cron.Daily);
//app.UseMvc...
//...other code
}
public IServiceProvider ConfigureServices(IServiceCollection services) {
// Adding custom services
services.AddTransient<IScheduledTaskManager, ScheduledTaskManageImplementation>();
//add other dependencies...
// add hangfire services
services.AddHangfire(x => x.UseSqlServerStorage("<connection string>"));
//configure Autofac
this.ApplicationContainer = getWebAppContainer(services);
//get service provider
return new AutofacServiceProvider(this.ApplicationContainer);
}
IContainer getWebAppContainer(IServiceCollection service) {
var builder = new ContainerBuilder();
builder.RegisterModule(new BusinessBindingsModule());
builder.RegisterModule(new DataAccessBindingsModule());
builder.Populate(services);
return builder.Build();
}
//...other code
}
References
Hangfire 1.6.0
Integrate HangFire With ASP.NET Core
Using IoC containers

Why is Hangfire trying to resolve the .NET Core Startup class?
Hangfire doesn't store lambda expressions in the database, it stores the type and method being called. Then when the scheduled task is due to run, it resolves the type from the container and calls the method.
In your case, the method is on Startup.
You can register Startup with Autofac if you want, but it's probably easiest to have a scheduled task service:
AddOrUpdate<IScheduledTaskService>(x => x.DailyTask(), Cron.Daily);

I'm not sure of the type for jobmanager off the top of my head, but you can resolve the dependency from the container using a scope. You'll want to resolve from the scope in a using statement to prevent memory leaks. See the Autofac Docs
// not sure what type "jobManager" is
TYPE jobManager;
using(var scope = ApplicationContainer.BeginLifetimeScope())
{
jobManager = scope.Resolve<TYPE>();
}
RecurringJob.AddOrUpdate( ... );

Related

What is the best way to handle Websharper Remoting Singletons while using Simpleinjector with potentially Scoped Dependencies?

I'm trying to Convert an application I have working under a Basic Websharper Setup into an ASP.NET Core Application, Using Simple Injector for DI, but Cannot get the Configuration quite right. I think I'm missing a piece of abstraction Due to Websharper's Remoting Abstraction. How should I get the best of both worlds?
I'm using Websharper with the ASPNETCORE Nuget Package, SimpleInjector with the ASPNETCORE Nuget Package, and my own custom libraries.
I've gotten things to work as singletons with the code below, but I'd much rather be able to use proper scoping...
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSitelet<OddJobSitelet>()
//.AddWebSharperRemoting<OddJobRemoting>() //This will not work, results in a torn lifestyle related error
.AddAuthentication("WebSharper")
.AddCookie("WebSharper", options => { });
IntegrateSimpleInjector(services);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
InitializeContainer(app);
if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); }
WebSharper.Core.Remoting.AddHandler(typeof(OddJobRemoting), container.GetService<OddJobRemoting>()); //This only takes an instance, which appears to preclude scoping.
app.UseAuthentication()
.UseStaticFiles()
.UseWebSharper()
.Run(context =>
{
context.Response.StatusCode = 404;
return context.Response.WriteAsync("Page not found");
});
}
public static void Main(string[] args)
{
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build()
.Run();
}
private Container container = new Container();
private void IntegrateSimpleInjector(IServiceCollection services)
{
/*services.Add(new ServiceDescriptor(typeof(OddJobRemoting), (aspnetCore) => container.GetService<OddJobRemoting>(), //Tried this, did not help since Websharper Remoting doesn't appear to use ServiceCollection this way*/
ServiceLifetime.Scoped));
container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.EnableSimpleInjectorCrossWiring(container);
services.UseSimpleInjectorAspNetRequestScoping(container);
}
private void InitializeContainer(IApplicationBuilder app)
{
//If I use anything other than Lifestyle.Singleton for either the remoting Handler or my provider, It results in a torn lifestyle.
container.Register<IJobSearchProvider>(() =>
{
return new SQLiteJobQueueManager(new SQLiteJobQueueDataConnectionFactory(TempDevInfo.ConnString),
TempDevInfo.TableConfigurations["console"], new NullOnMissingTypeJobTypeResolver());
}, Lifestyle.Singleton);
container.Register<OddJobRemoting>(Lifestyle.Singleton);
// Allow Simple Injector to resolve services from ASP.NET Core.
container.AutoCrossWireAspNetComponents(app);
}
}
As Requested:
If I change the lifestyles from singleton, I will get the following Exception:
The OddJobRemoting is registered as 'Async Scoped' lifestyle, but the instance is requested outside the context of an active (Async Scoped) scope.
This happens on the line:
WebSharper.Core.Remoting.AddHandler(typeof(OddJobRemoting), container.GetService<OddJobRemoting>());
To note, I'm having to do this because Websharper's AddHandler method requires an instance and appears to treat such as singletons internally.

ASP.NET Core does not replace IoC with StructureMap

My application is based on ASP.NET Core 2.1 and .NET Core 2.1 (downgraded from 2.2) generic host as Windows Service. So, IHostBuilder is launched first with other services and frameworks and then (if role permits) web service gets launched on top using IWebHostBuilder with all that WebHost.CreateDefaultBuilder(args).UseStartup<Startup>().StartAsync(). Secondary WebHost is another story; it is initialized and works, but I haven't checked yet if IoC replacement has the same trouble as generic host.
For now, generic host initialization:
new HostBuilder().ConfigureServices((hostContext, services) =>
{
services.AddHostedService<LifetimeService>(); // Gets launched when host is up
var container = ContainerBuilder.BuildBaseContainer(services, new WorkingPath());
services.AddSingleton<IContainer>(container);
services.AddStructureMap(); // Has no effect
});
IContainer initialization:
public static Container BuildBaseContainer(IServiceCollection services, IWorkingPath workingPath)
{
var container = new Container();
container.Configure(config =>
{
config.Scan(scan =>
{
workingPath.OwnLoadedAssemblies.Where(asm => !asm.IsDynamic).ForEach(scan.Assembly);
scan.LookForRegistries();
scan.AddAllTypesOf<IPlatformService>();
});
config.For<IContainer>().Use(() => container);
config.Populate(services);
});
container.AssertConfigurationIsValid();
return container;
}
And the trouble is here, in the constructor of that registered hosted service (or anywhere else)
public LifetimeService(IEnumerable<IPlatformService> services,
IServiceProvider sp, IContainer c)
{
var inCollection = services.Any();
var inContainer = c.TryGetInstance<IPlatformService>() != default;
var inProvider = sp.GetRequiredService<IPlatformService>() != default;
}
ps: IServiceProvider and IContainer are for demonstration purposes only, I only need 'services'
When LifetimeService is initialized during container.AssertConfigurationIsValid() I get
inCollection is true
inContainer is true
inProvider is true
IServiceProvider is StructureMapServiceProvider
Actual LifetimeService execution shows that
inCollection is false
inContainer is true
inProvider is false
IServiceProvider is ServiceProviderEngineScope
I don't plan to pass IServiceProvider or IContainer into constructors, but it seems that dependencies are resolved using IServiceProvider, not IContainer, and I get nulls. Silly thing like sp.GetRequiredService<IContainer>().TryGetInstance<IPlatformService>() does work.
There been some happy-path examples using WebHost and Startup classes where injection ought to be working properly. Doesn't seem relevant for generic host ...which might replace WebHost one day, but is little known and not widely used. Well, could be due to .NET Core version downgrade too, but quite unlikely. I've also tried replacing IServiceProvider and IServiceScopeFactory from IContainer during ConfigureServices() without luck. My idea is to replace or forward internal container to StructureMap. I might be misunderstanding how that should work...
Has anyone successfully tried to 'marry' generic host and external IoC?
I've solved the puzzle! Finally, according to a too much simplified example (https://github.com/aspnet/Hosting/blob/master/samples/GenericHostSample/ProgramFullControl.cs), I had to change HostBuilder initialization to
new HostBuilder()
.UseServiceProviderFactory(new StructureMapContainerFactory(workingPath))
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<LifetimeService>();
});
and introduce provider factory itself
public class StructureMapContainerFactory : IServiceProviderFactory<IContainer>
{
private readonly IWorkingPath workingPath;
// pass any dependencies to your factory
public StructureMapContainerFactory(IWorkingPath workingPath)
{
this.workingPath = workingPath;
}
public IContainer CreateBuilder(IServiceCollection services)
{
services.AddStructureMap();
return ContainerBuilder.BuildBaseContainer(services, workingPath);
}
public IServiceProvider CreateServiceProvider(IContainer containerBuilder)
{
return containerBuilder.GetInstance<IServiceProvider>();
}
}
Now internal container is substituted with StructureMap and resolved IServiceProvider in LifetimeService is of type StructureMapServiceProvider.

Unable to resolve service for type IOptions[DataAccessConfiguration] in non-ASP.NET Core app

All of our business services were previously set up to use Dependency Injection with IOptions because they were being consumed by ASP.NET Core apps, like so:
NotificationDataAccess.cs:
public class NotificationDataAccess : BaseDataAccess, INotificationDac<Notification>
{
public NotificationDataAccess(IOptions<DataAccessConfiguration> options, IClaimsAccessor claimsAccessor) :
base(options, claimsAccessor)
{
}
}
NotificationBusinessService.cs:
public class NotificationBusinessServices : INotificationServices<Notification>
{
private readonly INotificationDac<Notification> _notificationDataAccess;
public NotificationBusinessServices(
INotificationDac<Notification> notifcationDataAccess)
{
_notificationDataAccess = notifcationDataAccess;
}
}
Now I'm left with the unenviable task of trying to figure out how to leverage the same pattern from a windows service, which doesn't benefit from the built-in ASP.NET Core features for handling DI. When the service starts up, I execute the following code:
// Set up configuration, services, and logging.
IServiceCollection services = new ServiceCollection();
var startup = new Startup();
startup.ConfigureServices(services);
IServiceProvider serviceProvider = services.BuildServiceProvider();
var configuration = serviceProvider.GetService<IConfigurationRoot>();
var notificationService = serviceProvider.GetService<INotificationServices<Notification>>();// TODO: This errors!
processor = new Processor(configuration, notificationService);
And here is the Startup.cs code, which is supposed to configure the services:
public class Startup
{
IConfigurationRoot Configuration { get; }
public Startup()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Path.Combine(AppContext.BaseDirectory))
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IConfigurationRoot>(Configuration);
//services.AddMvc();
// Add application services.
Listings.Business.Configuration.Instance = new BusinessLayerConfiguration();
services.Configure<DataAccessConfiguration>(options => Configuration.GetSection("Data").Bind(options));
services.AddScoped(typeof(INotificationDac<Notification>), typeof(NotificationDataAccess));
services.AddScoped(typeof(INotificationServices<Notification>), typeof(NotificationBusinessServices));
}
}
Unfortunately, when I run the windows service it throws an exception when trying to get the notificationService:
var notificationService = serviceProvider.GetService<INotificationServices<Notification>>();
The exception is:
System.InvalidOperationException: 'Unable to resolve service for type
'Microsoft.Extensions.Options.IOptions`1[Rpr.Listings.DataAccess.DataAccessConfiguration]'
while attempting to activate
'Rpr.Listings.DataAccess.NotificationDataAccess'.'
I was hoping my "services.Configure" code would resolve this, but alas no. Clearly I need to register IOptions in my Startup.cs, however I have no idea how to do so. Is this something that usually happens out of the box with ASP.NET MVC? Does "services.AddMvc();" normally register this binding correctly? I can call that, but would need to import a ton of ASP.NET MVC packages into my windows service, which I'm reluctant to do.
Please let me know how to register the IOptions binding correctly, thanks!
It turns out that all I was missing was:
services.AddOptions();
Once I added that, the IOptions binding was registered correctly!
In case it helps anyone, I had this issue in a console app and it was caused by creating the service provider
IServiceProvider serviceProvider = services.BuildServiceProvider();
before I'd registered my config
services.Configure<DataAccessConfiguration>(Configuration.GetSection("Data"));

asp net core requesting service in Configure method returns null

I'm trying to get some of my services in the configure method so I can seed my database easier with them.
So I've injected IServiceProvider in my Configure method but every time I do : var service = serviceProvider.GetService<UserService>(); it returns null...
Here's my code :
public class 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);
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(this.Configuration.GetConnectionString("DbConnectionString")));
// Add framework services.
services.AddIdentity<IdentityUser, IdentityRole>().AddEntityFrameworkStores<IdentityDbContext>().AddDefaultTokenProviders();
string connection = this.Configuration.GetConnectionString("DbConnectionString");
services.AddEntityFramework();
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connection));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.Configure<AppSettings>(this.Configuration.GetSection("AppSettings"));
services.AddScoped<IsAuthorized>();
services.AddSingleton<UserManager<ApplicationUser>>();
services.AddTransient<IUsersService, UserService>();
services.AddMvc(config => { config.Filters.Add(typeof(SingletonUsers)); });
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IServiceProvider services)
{
loggerFactory.AddConsole(this.Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseMvc();
// This returns the context.
var context = services.GetService<ApplicationDbContext>();
// This returns null.
var userService = services.GetService<UserService>();
// Can't work without the UserService
MyDbInitializer.Initialize(context, userService);
}
}
as you registered UserService using
services.AddTransient<IUsersService, UserService>();
instead of
var userService = services.GetService<UserService>();
you need to ask for interface type:
var userService = services.GetService<IUserService>();
As an alternative to injecting IServiceProvider, you can just request the services as parameters to Configure:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
ApplicationDbContext context,
IUserService userService)
{
}
IServiceProvider is not registered with the Di/IoC, because the IServiceProvider is the IoC/DI (or a wrapper around it, when using 3rd party DI).
Once a IServiceProvider is created, it's dependency configuration can't be changed anymore (this applies to the out of the box IoC).
When you need a dependency in Configure you should pass this dependency as the method parameter (method injection) and get the instance. If you still need to access the IServiceProvider, you can do so by calling app.ApplicationServices.
But be aware, when you use app.ApplicationServices, you are resolving from the application-wide container. Every scoped or transient service resolved this way, will stay active until the application ends. This is because during application startup there is no scoped container, it is created during a request.
This means, you have to create a scope within Configure method, instantiate the services you need, call them and then dispose the scoped context before you leave it.
// Example of EF DbContext seeding I use in my application
using (var scope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
using (var context = scope.ServiceProvider.GetRequiredService<MyDbContext>())
{
if(context.Database.EnsureCreated())
{
context.SeedAsync().Wait();
}
}
}
This makes sure that all services are clearly disposed and you have no memory leaks. This can have severe impact if/when you initialize DbContext without creating a scoped container, like turning DbContext into a singleton (because it will be resolved from parent container) or causing memory leaks because services resolved in application scope remain active for the lifetime of the application.

How to configure DI services available to the Startup class constructor

When I create the webhost for an ASP.NET Core application I can specify the Startup class but not an instance.
The constructor of your own Startup class can take parameter which are provided through DI. I know how to register services for DI within ConfigureServices but as that is a member of that class these services are not available for the constructor of my startup class.
How do I register services which will be available as constructor parameter of the Startup class?
The reason is that I have to supply an object instance which must be created outside/before the webhost is created and I do not want to pass it in a global-like style.
Code to create the IWebHost:
this.host = new WebHostBuilder()
.UseConfiguration(config)
.UseKestrel()
.UseIISIntegration()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<WebStartup>()
log.Debug("Run webhost");
this.host.Start();
Constructor of WebStartup:
public WebStartup(IHostingEnvironment env, MyClass myClass)
{
var config = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddEnvironmentVariables()
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true)
.Build();
...
}
So specifically, how to I register MyClass in this example (which obviously must be done before WebStartup is instanciated by the IWebHost)?
Although Steven's concerns are valid and you should take note of them, it is technically possible to configure the DI container that is used to resolve your Startup class.
ASP.NET hosting uses dependency injection to wire up an instance of your Startup class and also let us add our own services to that container using the ConfigureServices extension method on IWebHostBuilder:
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.ConfigureServices(services => services.AddSingleton<IMyService, MyService>())
.UseStartup<Startup>()
.Build();
host.Run();
and:
public Startup(IMyService myService)
{
myService.Test();
}
In fact, all that UseStartup<WebStartup> does is adding it as a service implementation of IStartup to the hosting DI container (see this).
Please note that instances of your services will be resolved again in the application container as a new instance of the IServiceProvider will be built. The registration of the services will, however, be passed to the application IServiceCollection in your Startup class.
This is a 'chicken or the egg' problem: You can't let the DI container resolve an object graph before it is configured.
Your problem however should not exist, because just as you should strictly separate the registration phase of the container from the resolve-phase (as ASP.NET Core enforces upon you), the same way should you separate the registration phase from the phase before that where you load configuration values.
What this means is that classes that you require during the registration phase of the container should not be resolved by the container. because that could lead to common problems that are hard to track down.
Instead you should create the class by hand. For instance:
public class Startup
{
private static MyDependency dependency;
public Startup(IHostingEnvironment env)
{
dependency = new MyDependency();
var instance = new MyClass(dependency);
var builder = new ConfigurationBuilder()
.SetBasePath(instance.ContentRootPath)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// Register the dependency if it is required by other components.
services.AddSingleton<MyDependency>(dependency);
// Add framework services.
services.AddMvc();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}
// etc.
}
You have an instance of MyClass called myClass and you need to inject that instance into your Startup class. I had a similar requirement and after some experimentation I came up with the following.
First, we inject the myClass instance before we call UseStartup:
var host = new WebHostBuilder()
// .... other code
.ConfigureServices(services => services.AddSingleton(myClass)) // Inject myClass instance
.UseStartup<WebStartup>()
Now we need to get hold of the object in Startup (WebStartup in your case). I wasn't able to find a way to access it from the constructor, but that shouldn't matter as it can be accessed from within the startup class' ConfigureServices() and, if necessary, saved to a field or property from there if it need to be made available to Startup.Configure() which is called later. Here is what I came up with:
// This goes into Startup.cs/WebStartup.cs
public virtual void ConfigureServices(IServiceCollection services)
{
var myClass = services.Single(s => s.ServiceType == typeof(MyClass)).ImplementationInstance as MyClass;
if (myClass == null)
throw new InvalidOperationException(nameof(MyClass) + " instance is null");
// Now do all the things
}
I suspect that there may be something in the Framework to retrieve the injected instance more easily, but if not - or until I find it - I can confirm that the above works perfectly!

Categories