Hangfire icon indicating copy to clipboard operation
Hangfire copied to clipboard

[Question] Acquiring dependencies when using JobFilterAttribute with .NET CORE 2.0

Open praneethw opened this issue 8 years ago • 12 comments

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.
    }
}

praneethw avatar Sep 13 '17 13:09 praneethw

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
    }

pieceofsummer avatar Sep 13 '17 14:09 pieceofsummer

That sounds like a variant service-locator for me. Having access to the global context vs. ILifeTimeScopes is not the same.

hidegh avatar May 14 '18 13:05 hidegh

Can we have an update about this?

mrmokwa avatar Apr 26 '19 11:04 mrmokwa

@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:

  1. https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.2#dependency-injection
  2. 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.

hidegh avatar Apr 26 '19 16:04 hidegh

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);

mrmokwa avatar May 03 '19 20:05 mrmokwa

Is there any other workaround that doesn't require adding the filter globally? With my current design I am not able to add globally.

shorbachuk avatar Aug 09 '19 21:08 shorbachuk

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.

slangeder avatar Aug 12 '19 10:08 slangeder

Thanks @slangeder, this helped me a lot. I would think this would have been a little more straightforward.

shorbachuk avatar Aug 16 '19 04:08 shorbachuk

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."

eeskildsen avatar Sep 30 '19 19:09 eeskildsen

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())

mrmokwa avatar Oct 01 '19 11:10 mrmokwa

`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";
    }
}

} `

willardanuy avatar Aug 27 '20 17:08 willardanuy

@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?

odinserj avatar Sep 16 '22 02:09 odinserj