Job Filter as a Service#670
Conversation
|
There is the hidden |
|
Yep, I know about Also a DI container (for scoped services) might be disposed and another one created (so the provider instance in global providers will be using a dead DI container, and the number of instances will keep growing). You'll need some non trivial scope tracking logic to handle this, or to limit the provider to singleton services only (don't know which one is worse). |
|
This would be extremely useful. +1 |
|
@pieceofsummer and @odinserj , |
|
Please consider this, it would be very useful. |
|
I know this PR was created a long time ago, but wanting to implement it in our code, I just want to confirm one thing. As the Because the One way of making this work is to make all dependencies to be scoped, which include the Am I missing something? |
|
I believe the example class set as a singleton was only because The PR would probably need some refactoring to match it with the recent code base changes before you can use it though. |
@pieceofsummer, thanks for the quick answer! I agree for the needed refactor to match the recent code, though the discussion around scoped dependencies will apply again. Yes, you can easily register your filter as scoped if it needs scoped dependencies. What I want to say is that won't be enough, as you would need to register the That's why I would register those classes as scoped as part of this PR as well. Or think about some other solution. Otherwise, if people want scoped dependencies, they would need to read the Hangfire source code, figure this thing out and register these classes as scoped manually before calling Does this make sense to you? is what I'm saying correct? |
|
Starting from Hangfire 1.7.0 it's possible to use the services.AddHangfire((provider, configuration) => configuration
.UseServiceProviderJobFilterProvider(provider)
.UseXXX()
.UseZZZ()
.UseYYYStorage());So the public class ServiceProviderJobFilterProvider : IJobFilterProvider
{
private readonly IServiceProvider _services;
public ServiceProviderJobFilterProvider([NotNull] IServiceProvider services)
{
_services = services ?? throw new ArgumentNullException(nameof(services));
}
public IEnumerable<JobFilter> GetFilters(Job job)
{
return _services.GetServices<IJobFilter>()
.Select(x => new JobFilter(x, JobFilterScope.Global, null));
}
}
public static class ServiceProviderJobFilterProviderExtensions
{
public static IGlobalConfiguration UseServiceProviderJobFilterProvider(
[NotNull] this IGlobalConfiguration configuration,
[NotNull] IServiceProvider services)
{
JobFilterProviders.Providers.Add(new ServiceProviderJobFilterProvider(services));
return configuration;
}
} |
|
@odinserj, it's a nice wrap up for filters using transient and singleton dependencies, but it looks like it won't work correctly with scoped services. The way I have it working now is to manually register the services.AddScoped<IJobFilterProvider>(x => new AspNetCoreJobFilterProviders(x));
services.AddScoped<IBackgroundJobClient>(x => new BackgroundJobClient(
x.GetService<JobStorage>(),
x.GetService<IJobFilterProvider>()
));
services.AddHangfire((provider, configuration) => configuration.UseXXXStorage());I'm fine with this working for Client filters so far, as we have an additional issue with using DI in Server filters (a known one). As the Please have these two things in mind, as merging this and saying that Hangfire now supports filters through DI would probably not be ideal without proper support for scoped dependencies. |
|
@miroslavgrozdanovski I guess this is the reason why the latest version uses factories to construct You can technically create a custom In my project I'm doing exactly this but for a different reason, to allow hot stop/restart server with new options when the relevant configuration file changes. Totally doable, but requires some extra work :) |
|
@pieceofsummer, yeah, there a lot of options if you start overriding the classes, though that path will always be undocumented and fragile from future changes, as you are messing with the internals. It may be worth it for more custom requirements like yours, but it would be good if having proper DI (supporting scoped as well) in job filters is the default Hangfire behavior. |
|
@miroslavgrozdanovski please give an example of a custom filter that will not work with the |
Sure @odinserj , below is an example that just tries to pass on a value stored in a scoped class (let's say the ID of the logged in user). public class UserFilter : IJobFilter, IClientFilter
{
public bool AllowMultiple => false;
public int Order => 1;
private readonly IScopedService _scopedService;
public UserFilter(IScopedService scopedService)
{
_scopedService = scopedService;
}
public void OnCreating(CreatingContext filterContext)
{
int userId = _scopedService.UserId;
// Store it as a Job Parameter
}
public void OnCreated(CreatedContext filterContext)
{
}
}This is the rest of the code that sets the UserId and tries to enqueue a job. [ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
private readonly IBackgroundJobClient _backgroundJobClient;
private readonly IScopedService _scopedService;
public TestController(
IBackgroundJobClient backgroundJobClient,
IScopedService scopedService)
{
_backgroundJobClient = backgroundJobClient;
_scopedService = scopedService;
}
[HttpGet]
public void TestHangfire()
{
_scopedService.UserId = 123;
_backgroundJobClient.Enqueue<TestJob>(x => x.TestMethod());
}
}Using your proposed solution, the filter provider will use the root container. If my I would have mentioned these exceptions earlier, but I only got them while trying to reproduce the issue on a new clean project that uses the basic DI container from .NET Core. Previously, I was using Lamar on my work project, so this exception wasn't occurring. But it still wasn't working, as the UserId was always 0 in the filter. My solution of making the |
This is why I don't want to provide an out-of-box solution that's based on overridden @miroslavgrozdanovski, thanks for the detailed description. Actually there's the [ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
private readonly IBackgroundJobClient _backgroundJobClient;
public TestController(IBackgroundJobClient backgroundJobClient)
{
_backgroundJobClient = backgroundJobClient;
}
[HttpGet]
public void TestHangfire()
{
HttpContext.Items.Add("UserId", 12345);
_backgroundJobClient.Enqueue<TestJob>(x => x.TestMethod());
}
}To access the public class UserFilter : IClientFilter
{
private readonly IHttpContextAccessor _accessor;
public UserFilter(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
public void OnCreating(CreatingContext filterContext)
{
int userId = (int)_accessor.HttpContext.Items["UserId"];
// Store it as a Job Parameter
}
public void OnCreated(CreatedContext filterContext)
{
}
}The we should register the services.AddHttpContextAccessor();
services.AddHangfire((provider, config) => config
.UseXXXStorage()
.UseFilter(new UserFilter(provider.GetRequiredService<IHttpContextAccessor>())));And we can use a solution that will not be broken with future releases, because it's expected. |
The current implementation of job filters suggests two possible ways of using them:
GlobalJobFilters.Filterscollection.Both these ways appear to be virtually useless on .NET Core, because none of them provides any relevant information about current execution context. Since there's no
HttpContext.Currentand the likes available anymore (because of non-async nature, and the ugly looks too, I suppose :) ), so accessing anything beyond job filter'sfilterContextargument becomes quite troublesome.The following PR provides a way to store job filters in a DI container, and to interact with other services.
For example, you might want to capture username (role, IP address, cookie, etc.) of the user who initiated the background job:
Now register it in
Startup.cs:And... voila! All registered services implementing
IJobFilterwill be treated as job filters.P.S. The only thing is, you cannot use
BackgroundJob/RecurringJobstatic methods anymore, because they use a private (non-DI) instance of client class, hence they will ignore the new lookup method.You'll need to inject a proper instance of
IBackgroundJobClient/IRecurringJobManagerand use its methods instead. It is not actually a bad thing, since it employs a typical .NET Core pattern instead of a dubious static wrapper, but the static wrapper still shouldn't produce "partial success" results (failing with exception would be better).P.P.S.
BackgroundJobandRecurringJobappear to use different ways of storing a local instance. Feels likeBackgroundJobwas designed with possibility to switch implementations in mind (even though it's not actually used), whileRecurringJobnever bothered to.