quartz net keep job backgroundjobserver alive c# autofac hangfire

c# - net - hangfire windows service



Hangfire dependencia dependencia de la vida Ăștil alcance (5)

Estoy reescribiendo toda esta pregunta porque me doy cuenta de la causa, pero todavía necesito una solución:

Tengo un trabajo recurrente en Hangfire que se ejecuta cada minuto y compruebo la base de datos, posiblemente actualiza algunas cosas y luego sale.

Yo inyecto mi dbcontext en la clase que contiene el método de trabajo. Registré este dbcontext para ser inyectado usando lo siguiente

builder.RegisterType<ApplicationDbContext>().As<ApplicationDbContext>().InstancePerLifetimeScope();

Sin embargo, parece que Hangfire no crea un alcance de por vida por separado cada vez que se ejecuta el trabajo, porque el constructor solo recibe una llamada, aunque el método del trabajo se llama cada minuto.

Esto me causa problemas. Si el usuario actualiza algunos valores en la base de datos (dbcontext se inyecta en otro lugar, y se usa para actualizar los valores), el contexto que aún se está usando, Hangfire comienza a devolver valores obsoletos que ya se han cambiado.


Actualmente, Hangfire utiliza una instancia compartida de JobActivator para cada trabajador, que utiliza el siguiente método para resolver una dependencia:

public override object ActivateJob(Type jobType)

Se planea agregar un JobActivationContext a este método para Milestone 2.0.0 .

Por ahora, no hay manera de decir para qué trabajo se resuelve una dependencia. La única forma de solucionar este problema sería utilizar el hecho de que los trabajos se ejecutan en serie en diferentes subprocesos (no sé AutoFac, así que uso Unity como ejemplo).

Podría crear un JobActivator que pueda almacenar ámbitos separados por hilo:

public class UnityJobActivator : JobActivator { [ThreadStatic] private static IUnityContainer childContainer; public UnityJobActivator(IUnityContainer container) { // Register dependencies container.RegisterType<MyService>(new HierarchicalLifetimeManager()); Container = container; } public IUnityContainer Container { get; set; } public override object ActivateJob(Type jobType) { return childContainer.Resolve(jobType); } public void CreateChildContainer() { childContainer = Container.CreateChildContainer(); } public void DisposeChildContainer() { childContainer.Dispose(); childContainer = null; } }

Use un JobFilter con implementación de IServerFilter para establecer este alcance para cada trabajo (hilo):

public class ChildContainerPerJobFilterAttribute : JobFilterAttribute, IServerFilter { public ChildContainerPerJobFilterAttribute(UnityJobActivator unityJobActivator) { UnityJobActivator = unityJobActivator; } public UnityJobActivator UnityJobActivator { get; set; } public void OnPerformed(PerformedContext filterContext) { UnityJobActivator.DisposeChildContainer(); } public void OnPerforming(PerformingContext filterContext) { UnityJobActivator.CreateChildContainer(); } }

Y finalmente configura tu DI:

UnityJobActivator unityJobActivator = new UnityJobActivator(new UnityContainer()); JobActivator.Current = unityJobActivator; GlobalJobFilters.Filters.Add(new ChildContainerPerJobFilterAttribute(unityJobActivator));



Para solucionar este problema, he creado una clase de JobContext desechable que tiene un ILifetimeScope que se eliminará cuando Hangfire complete el trabajo. El verdadero trabajo es invocado por la reflexión.

public class JobContext<T> : IDisposable { public ILifetimeScope Scope { get; set; } public void Execute(string methodName, params object[] args) { var instance = Scope.Resolve<T>(); var methodInfo = typeof(T).GetMethod(methodName); ConvertParameters(methodInfo, args); methodInfo.Invoke(instance, args); } private void ConvertParameters(MethodInfo targetMethod, object[] args) { var methodParams = targetMethod.GetParameters(); for (int i = 0; i < methodParams.Length && i < args.Length; i++) { if (args[i] == null) continue; if (!methodParams[i].ParameterType.IsInstanceOfType(args[i])) { // try convert args[i] = args[i].ConvertType(methodParams[i].ParameterType); } } } void IDisposable.Dispose() { if (Scope != null) Scope.Dispose(); Scope = null; } }

Hay un JobActivator que inspeccionará la acción y creará el LifetimeScope si es necesario.

public class ContainerJobActivator : JobActivator { private readonly IContainer _container; private static readonly string JobContextGenericTypeName = typeof(JobContext<>).ToString(); public ContainerJobActivator(IContainer container) { _container = container; } public override object ActivateJob(Type type) { if (type.IsGenericType && type.GetGenericTypeDefinition().ToString() == JobContextGenericTypeName) { var scope = _container.BeginLifetimeScope(); var context = Activator.CreateInstance(type); var propertyInfo = type.GetProperty("Scope"); propertyInfo.SetValue(context, scope); return context; } return _container.Resolve(type); } }

Para ayudar a crear trabajos, sin usar parámetros de cadena hay otra clase con algunas extensiones.

public static class JobHelper { public static object ConvertType(this object value, Type destinationType) { var sourceType = value.GetType(); TypeConverter converter = TypeDescriptor.GetConverter(sourceType); if (converter.CanConvertTo(destinationType)) { return converter.ConvertTo(value, destinationType); } converter = TypeDescriptor.GetConverter(destinationType); if (converter.CanConvertFrom(sourceType)) { return converter.ConvertFrom(value); } throw new Exception(string.Format("Cant convert value ''{0}'' or type {1} to destination type {2}", value, sourceType.Name, destinationType.Name)); } public static Job CreateJob<T>(Expression<Action<T>> expression, params object[] args) { MethodCallExpression outermostExpression = expression.Body as MethodCallExpression; var methodName = outermostExpression.Method.Name; return Job.FromExpression<JobContext<T>>(ctx => ctx.Execute(methodName, args)); } }

Así que para poner en cola un trabajo, por ejemplo, con la siguiente firma:

public class ResidentUploadService { public void Load(string fileName) { //... }

El código para crear el trabajo parece

var localFileName = "Somefile.txt"; var job = ContainerJobActivator .CreateJob<ResidentUploadService>(service => service.Load(localFileName), localFileName); var state = new EnqueuedState("queuename"); var client = new BackgroundJobClient(); client.Create(job,state);


Una solución es compatible fuera de la caja desde github 2.2.0.

En su situación, donde su dependencia se está registrando para el alcance de por vida, debe poder usar ámbitos no etiquetados al configurar hangfire.autofac. Desde el enlace:

GlobalConfiguration.Configuration.UseAutofacActivator(builder.Build(), false);


Edición: con Autofac, .NET 4.5 y Hangfire> = 1.5.0, use el paquete de github ( github ).

Trabajando con .NET 4.0 (Autofac 3.5.2 y Hangfire 1.1.1), configuramos la solución de Dresel con Autofac. La única diferencia está en el JobActivator:

using System; using Autofac; using Hangfire; namespace MyApp.DependencyInjection { public class ContainerJobActivator : JobActivator { [ThreadStatic] private static ILifetimeScope _jobScope; private readonly IContainer _container; public ContainerJobActivator(IContainer container) { _container = container; } public void BeginJobScope() { _jobScope = _container.BeginLifetimeScope(); } public void DisposeJobScope() { _jobScope.Dispose(); _jobScope = null; } public override object ActivateJob(Type type) { return _jobScope.Resolve(type); } } }