Hangfire
Hangfire copied to clipboard
[Question] Acquiring dependencies when using JobFilterAttribute with .NET CORE 2.0
How is it that I can extend JobFilterAttribute from my own class and be able to access dependencies from within the class?
What I need to do is access configuration settings to acquire web service information to make a web service all form during state changes.
Is this possible to be done with .NET CORE 2.0
public class StateFilter : JobFilterAttribute, IApplyStateFilter
{
public void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
// NOTE: Access dependencies to make web service call
}
public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
// Not implemented intentionaly.
}
}
I've made a PR #670 for that like a year ago, but unfortunately it was never merged :(
The only workaround I currently see is to add a job filter in Configure method, passing it IServiceProvider as an argument to resolve the required services manually:
public class MyFilter : IApplyStateFilter
{
private readonly IServiceProvider _services;
public MyFilter(IServiceProvider services)
{
_services = services;
}
public void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
var svc = _services.GetRequiredService<MyService>();
// do something with a service
}
public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
// Not implemented intentionaly.
}
}
Startup.cs:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider services)
{
GlobalJobFilters.Filters.Add(new MyFilter(services));
// other initialization
}
That sounds like a variant service-locator for me. Having access to the global context vs. ILifeTimeScopes is not the same.
Can we have an update about this?
@MRMokwa Hope this below helps (haven't tested it with JobAttribute, but have similar scenario with TypeFilter where I used DI to inject a logger/service into)
With MVC6 adding filter by type and using one of 3 approaches could be a solution here as well, so just create a new, derived attribute from JobFilter, implement IFilterFactory for it and when registerng, use type, so:
services
.AddMvc(opts => {
opts.Filters.Add(typeof(MyFilterWithDi));
see:
- https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.2#dependency-injection
- https://andrewlock.net/exploring-middleware-as-mvc-filters-in-asp-net-core-1-1/#the-middlewarefilterattribute
I've used TypeFilterAttribute in the past, but as it's not an interface, you can't use it, furthermore the IFilterFactory seems to be easier to impl. and should work the same.
Plz. leave a feedback or samples for others if you succeed.
I ended up doing something similar as @pieceofsummer said with IServiceProvider.
public void Configure(IHubContext<MyHubClass> hub) {
GlobalJobFilters.Filters.Add(new CustomHangfireFilter(hub));
}
I also used JobStorage to get any aditional information that I needed in OnStateApplied, (username in my case) to notify my client with SignalR.
var username = JobStorage.Current.GetConnection().GetJobParameter(context.BackgroundJob.Id, paramName);
Is there any other workaround that doesn't require adding the filter globally? With my current design I am not able to add globally.
I use the following code to inject dependencies in my JobFilters.
Startup.cs - Configure Method
Hangfire.Common.JobFilterProviders.Providers.Add(new TypeJobFilterProvider(app.ApplicationServices));
public class TypeJobFilterProvider : IJobFilterProvider
{
private readonly IServiceProvider serviceProvider;
public TypeJobFilterProvider(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
public IEnumerable<JobFilter> GetFilters(Job job)
{
var typeFilters = GetTypeJobFilterAttributes(job.Type)
.Select(typeFilter => Tuple.Create(typeFilter.CreateInstance(serviceProvider), typeFilter.Order))
.Select(jobFilter => new JobFilter(jobFilter.Item1, JobFilterScope.Type, jobFilter.Item2));
var methodFilters = GetTypeJobFilterAttributes(job.Method)
.Select(typeFilter => Tuple.Create(typeFilter.CreateInstance(serviceProvider), typeFilter.Order))
.Select(jobFilter => new JobFilter(jobFilter.Item1, JobFilterScope.Method, jobFilter.Item2));
return typeFilters.Concat(methodFilters);
}
private static IEnumerable<TypeJobFilterAttribute> GetTypeJobFilterAttributes(MemberInfo memberInfo)
{
return memberInfo
.GetCustomAttributes(typeof(TypeJobFilterAttribute), inherit: true)
.Cast<TypeJobFilterAttribute>();
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class TypeJobFilterAttribute : Attribute
{
private ObjectFactory factory;
private int order = JobFilter.DefaultOrder;
public TypeJobFilterAttribute(Type type)
{
ImplementationType = type ?? throw new ArgumentNullException(nameof(type));
}
public object[] Arguments { get; set; }
public int Order
{
get => order;
set
{
if (value < JobFilter.DefaultOrder)
{
throw new ArgumentOutOfRangeException(nameof(value), "The Order value should be greater or equal to '-1'");
}
order = value;
}
}
public Type ImplementationType { get; }
public object CreateInstance(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
{
throw new ArgumentNullException(nameof(serviceProvider));
}
if (factory == null)
{
var argumentTypes = Arguments?.Select(a => a.GetType())?.ToArray();
factory = ActivatorUtilities.CreateFactory(ImplementationType, argumentTypes ?? Type.EmptyTypes);
}
return factory(serviceProvider, Arguments);
}
}
public class ExampleAttribute : TypeJobFilterAttribute
{
private ExampleAttribute(ExampleFilterSettings settings) : base(typeof(ExampleFilter))
{
Arguments = new[] { settings };
}
public ExampleAttribute(params string[] mySetting)
: this(new ExampleFilterSettings { MySetting = mySetting })
{
}
}
public class ExampleFilter : IServerFilter
{
private readonly ExampleFilterSettings settings;
public ExampleFilter (
IDependencyToInject dependency,
ExampleFilterSettings settings)
{
}
public void OnPerforming(PerformingContext context)
{
// Execute code before task was performed
}
public void OnPerformed(PerformedContext context)
{
// Execute code after task was performed
}
}
UPD (@odinserj): Added order support for the TypeJobFilterAttribute class.
Thanks @slangeder, this helped me a lot. I would think this would have been a little more straightforward.
I ended up doing something similar as @pieceofsummer said with
IServiceProvider.public void Configure(IHubContext<MyHubClass> hub) { GlobalJobFilters.Filters.Add(new CustomHangfireFilter(hub)); }
@MRMokwa Are you doing anything special to stop hub from being disposed? When I do this I get "Cannot access a disposed object."
Unfortunatelly no. Take a look at my code below and see if that helps.
public class CustomHangfireFilter : JobFilterAttribute, IApplyStateFilter
{
private readonly IHubContext<TarefaHub> _hubContext;
public CustomHangfireFilter(IHubContext<TarefaHub> hubContext)
{
_hubContext = hubContext;
}
public async void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
var paramName = nameof(CustomHangfireStorageParams.Username);
var username = JobStorage.Current.GetConnection().GetJobParameter(context.BackgroundJob.Id, paramName);
if (username != null)
{
await _hubContext.Clients.User(username).SendAsync("StatusUpdate", new
{
context.BackgroundJob.Id,
Status = context.NewState.Name
});
}
}
public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
}
}
Hangfire.AspNetCore - 1.7.3 Hangfire.PostgreSql - 1.6.0
Edit: [Important] You must close the connection when using GetConnection. using(var conection = JobStorage.Current.GetConnection())
`using Hangfire.Common; using Hangfire.States; using IntegraServicos.Domain.Enums; using System; using System.Linq;
namespace IntegraServicos.Helper.HangFire { public class SelectQueueAttribute : JobFilterAttribute, IElectStateFilter {
public void OnStateElection(ElectStateContext context)
{
var enqueuedState = context.CandidateState as EnqueuedState;
if (enqueuedState != null)
{
enqueuedState.Queue = DetermineQueue(context.BackgroundJob.Job);
}
}
String DetermineQueue(Job job)
{
var query = String.Empty;
try
{
var args = job.Args.ToList();
if (args.Count > 1)
{
query = QueueServiceName.Services.Where(x => x.Contains(args[0].ToString().Replace("-", "_").ToLower())).FirstOrDefault().ToLower();
}
else
{
query = QueueServiceName.Services.Where(x => x.Contains(args.Cast<ServiceCallModel>().ToList().FirstOrDefault().ServiceKey.Replace("-", "_").ToLower())).FirstOrDefault().ToLower();
}
}
catch (Exception)
{
Console.WriteLine("Nenhum fila encontrada no argumento, sera enviado para a default");
}
return query ?? "default";
}
}
} `
@slangeder thanks for showing a great method of supporting dependency injection in filters! May I use your code in Hangfire.NetCore / Hangfire.AspNetCore perhaps with some changes and co-author you in a commit?