Hangfire, .Net Core and Entity Framework: concurrency exception - c#

I am developing a .Net core application with Hangfire and facing the below exception
A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.
I have used Hangfire for scheduling the jobs with 1 hour interval. I am facing the above issue when the new process/job gets started before the earlier job has finished its process.
How can we implement multiple Hangfire processes/jobs(multiple workers) to work (in parallel) to accomplish the task. (Resolved now, by using the default AspNetCoreJobActivator)
var scopeFactory = serviceProvider.GetService<IServiceScopeFactory>();
if (scopeFactory != null)
GlobalConfiguration.Configuration.UseActivator(new AspNetCoreJobActivator(scopeFactory));
Now, I am getting the following exception in CreateOrderData.cs:-
/*System.InvalidOperationException: An exception has been raised that
is likely due to a transient failure. If you are connecting to a SQL
Azure database consider using SqlAzureExecutionStrategy. --->
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred
while updating the entries. See the inner exception for details. --->
System.Data.SqlClient.SqlException: Transaction (Process ID 103) was
deadlocked on lock resources with another process and has been chosen
as the deadlock victim. Rerun the transaction. */
I am scheduling the hangfire cron job as below:-
RecurringJob.AddOrUpdate<IS2SScheduledJobs>(x => x.ProcessInputXML(), Cron.MinuteInterval(1));
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
string hangFireConnection = Configuration["ConnectionStrings:HangFire"];
GlobalConfiguration.Configuration.UseSqlServerStorage(hangFireConnection);
var config = new AutoMapper.MapperConfiguration(cfg =>
{
cfg.AddProfile(new AutoMapperProfileConfiguration());
);
var mapper = config.CreateMapper();
services.AddSingleton(mapper);
services.AddScoped<IHangFireJob, HangFireJob>();
services.AddScoped<IScheduledJobs, ScheduledJobs>();
services.AddScoped<BusinessLogic>();
services.AddHangfire(opt =>
opt.UseSqlServerStorage(Configuration["ConnectionStrings:HangFire"]));
services.AddEntityFrameworkSqlServer().AddDbContext<ABCContext>(options =>
options.UseSqlServer(Configuration["ConnectionStrings:ABC"]));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IServiceProvider serviceProvider)
{
GlobalConfiguration.Configuration.UseActivator(new HangFireActivator(serviceProvider));
//hangFireJob.Jobs();
// add NLog to ASP.NET Core
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
loggerFactory.AddNLog();
// app.UseCors("AllowSpecificOrigin");
foreach (DatabaseTarget target in LogManager.Configuration.AllTargets.Where(t => t is DatabaseTarget))
{
target.ConnectionString = Configuration.GetConnectionString("Logging");
}
LogManager.ReconfigExistingLoggers();
}
Hangfire.cs
public class HangFireJob : IHangFireJob
{
private ABCContext _abcContext;
private IScheduledJobs scheduledJobs;
public HangFireJob(ABCContext abcContext, IScheduledJobs scheduledJobs)
{
_abcContext = abcContext;
this.scheduledJobs = scheduledJobs;
}
public void Jobs()
{
RecurringJob.AddOrUpdate<IScheduledJobs>(x => x.ProcessInputXML(), Cron.HourInterval(1));
}
}
ScheduledJobs.cs
public class S2SScheduledJobs : IS2SScheduledJobs
{
private BusinessLogic _businessLogic;
public ScheduledJobs(BusinessLogic businessLogic)
{
_businessLogic = businessLogic;
}
public async Task<string> ProcessInputXML()
{
await _businessLogic.ProcessXML();
}
}
BusinessLogic.cs
public class BusinessLogic
{
private ABCContext _abcContext;
public BusinessLogic(ABCContext abcContext) : base(abcContext)
{
_abcContext = abcContext;
}
public async Task ProcessXML()
{
var batchRepository = new BatchRepository(_abcContext);
var unprocessedBatchRecords = await BatchRepository.GetUnprocessedBatch();
foreach (var batchRecord in unprocessedBatchRecords)
{
try
{
int orderId = await LoadDataToOrderTable(batchRecord.BatchId);
await UpdateBatchProcessedStatus(batchRecord.BatchId);
if (orderId > 0)
{
await CreateOrderData(orderId);
}
}
catch(Exception ex)
{
}
}
}
CreateOrderData.cs
public async Task<int> CreateOrderData(int orderId)
{
try
{
await OrderRepo.InsertOrder(order);
await _abcContext.SaveChangesAsync();
}
catch(Exception ex)
{
/*System.InvalidOperationException: An exception has been raised that is likely due to a transient failure. If you are connecting to a SQL Azure database consider using SqlAzureExecutionStrategy. ---> Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details. ---> System.Data.SqlClient.SqlException: Transaction (Process ID 103) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction. */
}
}
InsertOrder.cs
public async Task InsertOrder(Order o)
{
// creation of large number of entites(more than 50) to be inserted in the database
woRepo.Insert(p);
poRepo.Insert(y);
//and many more like above
Insert(order);
}
Insert.cs
public virtual void Insert(TEntity entity)
{
entity.ObjectState = ObjectState.Added;
if (entity is IXYZEntity xyzEntity)
{
xyzEntity.CreatedDate = DateTime.Now;
xyzEntity.UpdatedDate = xyzEntity.CreatedDate;
xyzEntity.CreatedBy = _context.UserName ?? string.Empty;
xyzEntity.UpdatedBy = _context.UserName ?? string.Empty;
}
else if (entity is IxyzEntityNull xyzEntityNull)
{
xyzEntityNull.CreatedDate = DateTime.Now;
xyzEntityNull.UpdatedDate = xyzEntityNull.CreatedDate;
xyzEntityNull.CreatedBy = _context.UserName;
xyzEntityNull.UpdatedBy = _context.UserName;
}
_dbSet.Add(entity);
_context.SyncObjectState(entity);
}
LoadDataToOrder.cs
public async Task<int> LoadDataToOrder(int batchId)
{
// using (var unitOfWork = new UnitOfWork(_abcContext))
// {
var orderRepo = new OrderRepository(_abcContext);
Entities.Order order = new Entities.Order();
order.Guid = Guid.NewGuid();
order.BatchId = batchId;
order.VendorId = null;
orderRepo.Insert(order);
//unitOfWork.SaveChanges();
await _abcContext.SaveChangesAsync();
return order.OrderId;
//
}
}
HangfireActivator.cs
public class HangFireActivator : Hangfire.JobActivator
{
private readonly IServiceProvider _serviceProvider;
public HangFireActivator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public override object ActivateJob(Type type)
{
return _serviceProvider.GetService(type);
}
}
Please advise.
Thanks.

Following solutions worked for the 2 problems:
Implementation of multiple Hangfire processes/jobs(multiple workers) to work (in parallel).
Answer: This got resolved when I used the built-in AspNetCoreJobActivator instead that's available out of the box, i.e. removed the HangfireActivator class and removed the call to the UseActivator method.
var scopeFactory = serviceProvider.GetService<IServiceScopeFactory>();
if (scopeFactory != null)
GlobalConfiguration.Configuration.UseActivator(new AspNetCoreJobActivator(scopeFactory));
SqlAzureExecutionStrategy Exception in CreateOrder.cs (transaction was deadlocked)
Answer: Resolved this issue by retrying the query automatically when deadlock occurs.
Thanks odinserj for the suggestions.

Related

Login failed for user '<token-identified principal>'. Token is expired

I have a webjob hosted within Azure which is scheduled to run continuous mode type. In this webjob I have some database operations which are managed using EntityFrameworkCore. I am also using userassigned ManagedIdentity to get the token and pass it to the AccessToken property of the connection. The token lifetime of the userassigned ManagedIdentity is 24 hours. For the first time the webjob runs successfully without any issues, but on the subsequent iterations post 24 hours it breaks , since the token by that time has expired.
Program.cs
public static void Main()
{
var builder = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json", optional: false, reloadOnChange: true).AddEnvironmentVariables();
IConfiguration configuration = builder.Build();
IHost host = new HostBuilder().ConfigureWebJobs(webJobConfiguration =>
{
webJobConfiguration.AddAzureStorage();
webJobConfiguration.AddAzureStorageCoreServices();
webJobConfiguration.AddTimers();
}).ConfigureServices(serviceCollection =>
{
serviceCollection.AddTransient<ImportFunctions>();
serviceCollection.AddSingleton<IConfiguration>(configuration);
serviceCollection.AddSingleton(new ConfigManager(configuration));
serviceCollection.AddTransient(typeof(IDBAuthTokenService), typeof(AzureSqlAuthTokenService));
serviceCollection.AddDbContext<AppDbContext>(options => options.UseSqlServer(GetConnectionString()));
}).Build();
using (host)
{
host.Run();
}
string GetConnectionString()
{
string connection = configuration["connectionString"];
bool readFromKeyVault = bool.Parse(configuration["ReadFromKeyVault"] ?? "false");
if (readFromKeyVault)
{
connection = GetKeyVaultClient().GetSecretValue("appconnectionstring");
}
return connection;
}
KeyVaultAccessClient GetKeyVaultClient()
{
string keyVaultURL = configuration["KeyVaultURL"];
string userAssignedClientId = configuration["MsiConfiguration:UserAssignedClientId"];
return new KeyVaultAccessClient(keyVaultURL, userAssignedClientId);
}
}
IDBAuthTokenService.cs
public interface IDBAuthTokenService
{
Task<string> GetTokenAsync();
}
AzureSqlAuthTokenService.cs
public class AzureSqlAuthTokenService : IDBAuthTokenService
{
public AzureSqlAuthTokenService()
{
}
public async Task<string> GetTokenAsync()
{
var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions{ManagedIdentityClientId = ConfigManager.Get("UserAssignedClientId")});
var tokenRequestContext = new TokenRequestContext(new[]{ConfigManager.Get("AzureSQLResourceId")});
var token = await credential.GetTokenAsync(tokenRequestContext, default);
return token.Token;
}
}
AppDbContext.cs
public partial class AppDbContext : DbContext
{
public AppDbContext()
{
}
public AppDbContext(IDBAuthTokenService tokenService, DbContextOptions<AppDbContext> options) : base(options)
{
var connection = this.Database.GetDbConnection() as SqlConnection;
connection.AccessToken = tokenService.GetTokenAsync().Result;
}
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
}
In this implementation I tried to leverage polly nuget package to implement the retry logic as mentioned below:
ImportFunctions.cs
public class ImportFunctions
{
private IEmailEID _emailEID;
private IConfiguration _config;
private readonly IDBAuthTokenService _tokenService;
private AsyncRetryPolicy retryPolicy;
public ImportFunctions(IEmailEID emailEID, IConfiguration config, IDBAuthTokenService tokenService)
{
_emailEID = emailEID;
_config = config;
_tokenService = tokenService;
#region RetryDetails
int MAX_RETRIES = 3;
retryPolicy = Policy.Handle<Exception>(ex => ex.Message.Trim().Length > 0).WaitAndRetryAsync(retryCount: MAX_RETRIES, sleepDurationProvider: (attemptCount) => TimeSpan.FromSeconds(attemptCount * 2), onRetry: (exception, sleepDuration, attemptNumber, context) =>
{
Console.WriteLine($"Exception details: {exception.Message}");
ReInitializeAppDbContext();
});
#endregion
}
public void DailyTrigger([TimerTrigger(typeof(DailyJobSchedule))] TimerInfo timerInfo)
{
try
{
if (!bool.Parse(_config["disableEmails"]))
{
//Code here runs every day
Console.WriteLine("Daily Job triggered at :" + DateTime.Now);
_emailEID.TestMethod1();
_emailEID.TestMethod2();
Console.WriteLine("Daily Job ended at :" + DateTime.Now);
}
}
catch (Exception ex)
{
Console.WriteLine("Exception :" + ex.Message);
}
}
private void ReInitializeAppDbContext()
{
var contextOptions = new DbContextOptionsBuilder<AppDbContext>().UseSqlServer(GetConnectionString()).Options;
using var context = new AppDbContext(_tokenService, contextOptions);
}
}
Any database call that happens after 24 hours fails since the token has expired by that time and in that case I am trying to reintialize the dbcontext with a new token as mentioned in the method : ReInitializeAppDbContext()
But still I see the error : Login failed for user ''. Token is expired for all the executions post 24 hours duration.
Can anyone help me to resolve this issue?
I would suggest taking advantage of the recent versions of the Microsoft.Data.SqlClient NuGet package which takes care of the token acquisition/caching/renewal process.
See this blog post I wrote about it: https://mderriey.com/2021/07/23/new-easy-way-to-use-aad-auth-with-azure-sql/
If you cannot use it or want to get the token yourself, I’d suggest adding logging to the code to ensure that then token renewal process does happen as you expect it to.
Before the newer versions of Microsoft.Data.SqlClient came along, I had success using code from this blog post: https://mderriey.com/2020/09/12/resolve-ef-core-interceptors-with-dependency-injection/

Azure Function Middleware: How to return a custom HTTP response?

I am exploring Azure Function running on .net 5 and I found out about the new middleware capabilities.
I have built a dummy middleware like this one:
public sealed class ExceptionLoggingMiddleware : IFunctionsWorkerMiddleware
{
private readonly ILogger<ExceptionLoggingMiddleware> m_logger;
public ExceptionLoggingMiddleware(ILogger<ExceptionLoggingMiddleware> logger)
{
m_logger = logger;
}
public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
try
{
await next(context);
}
catch (Exception unhandledException)
{
m_logger.LogCritical(unhandledException, "Unhandled exception caught: {UnhandledException}", unhandledException.Message);
}
}
}
In my use case, the Azure Function is an HTTP triggered function:
public sealed class StorageAccountsFunction
{
private readonly ILogger<StorageAccountsFunction> m_logger;
public StorageAccountsFunction
(
ILogger<StorageAccountsFunction> logger
)
{
m_logger = logger;
}
[Function("v1-post-storage-account")]
public async Task<HttpResponseData> CreateAsync
(
[HttpTrigger(AuthorizationLevel.Anonymous, "POST", Route = "v1/storage-accounts")]
HttpRequestData httpRequestData,
FunctionContext context
)
{
m_logger.LogInformation("Processing a request to create a new storage account");
throw new Exception("Oh no! Oh well..");
}
}
In my Function App running in-process on .net core 3.1, each Function had the responsibility of catching the unhandled exception (via a base class) and returned the appropriate HTTP status code.
I would like to have that logic sit in a middleware instead to have it centralized and avoid any future mistakes.
Question
The exception is caught by the middleware properly. However, I do not see how I can alter the response and return something more appropriate, instead of a 500 Internal Server Error that I get right now?
According to this issue, there is currently no official implementation regarding this, but they also mention a "hacky workaround" until the proper functionality is implemented directly into Azure functions
We created an extension method for FunctionContext:
internal static class FunctionUtilities
{
internal static HttpRequestData GetHttpRequestData(this FunctionContext context)
{
var keyValuePair = context.Features.SingleOrDefault(f => f.Key.Name == "IFunctionBindingsFeature");
var functionBindingsFeature = keyValuePair.Value;
var type = functionBindingsFeature.GetType();
var inputData = type.GetProperties().Single(p => p.Name == "InputData").GetValue(functionBindingsFeature) as IReadOnlyDictionary<string, object>;
return inputData?.Values.SingleOrDefault(o => o is HttpRequestData) as HttpRequestData;
}
internal static void InvokeResult(this FunctionContext context, HttpResponseData response)
{
var keyValuePair = context.Features.SingleOrDefault(f => f.Key.Name == "IFunctionBindingsFeature");
var functionBindingsFeature = keyValuePair.Value;
var type = functionBindingsFeature.GetType();
var result = type.GetProperties().Single(p => p.Name == "InvocationResult");
result.SetValue(functionBindingsFeature, response);
}
}
The usage in the middleware looks like this:
public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
try
{
await next(context);
}
catch (Exception ex)
{
if (ex.InnerException is *NameOfExceptionYouNeed* e)
{
var req = context.GetHttpRequestData();
var res = await req.ErrorResponseAsync(e.Message);
context.InvokeResult(res);
return;
}
throw;
}
}
This is natively supported now as of version 1.8.0 of Microsoft.Azure.Functions.Worker.
The FunctionContextHttpRequestExtensions class was introduced so now you can just
using Microsoft.Azure.Functions.Worker;
public class MyMiddleware : IFunctionsWorkerMiddleware
{
public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
// To access the RequestData
var req = await context.GetHttpRequestDataAsync();
// To set the ResponseData
var res = req!.CreateResponse();
await res.WriteStringAsync("Please login first", HttpStatusCode.Unauthorized);
context.GetInvocationResult().Value = res;
}
}
This code works for me. It is based on the example here: https://github.com/Azure/azure-functions-dotnet-worker/blob/main/samples/CustomMiddleware/ExceptionHandlingMiddleware.cs
public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
// Simple example which always fails. Use the following in an error condition
var httpReqData = await context.GetHttpRequestDataAsync();
if (httpReqData != null)
{
var newHttpResponse = httpReqData.CreateResponse(HttpStatusCode.InternalServerError);
await newHttpResponse.WriteAsJsonAsync(new { ResponseStatus = "Invocation failed!" }, newHttpResponse.StatusCode);
context.GetInvocationResult().Value = newHttpResponse;
}
}

How to fix 'A second operation started on this context before a previous operation completed...' when working with dependency injection?

when reading data from the database I get this error:
A second operation started on this context before a previous operation
completed. Any instance members are not guaranteed to be thread safe.
I have the following ApplicationContext.cs:
public class ApplicationContext : Microsoft.EntityFrameworkCore.DbContext
{
public ApplicationContext(DbContextOptions<ApplicationContext> options)
: base(options)
{ }
public DbSet<MyClass> MyClasses{ get; set; }
}
The following ApplicationContextFactory.cs
public class ApplicationContextFactory : IDesignTimeDbContextFactory<ApplicationContext>
{
public ApplicationContext CreateDbContext(string[] args)
{
var builder = new DbContextOptionsBuilder<ApplicationContext>();
var connection = "myConnectionString";
builder.UseSqlServer(connection);
return new ApplicationContext(builder.Options);
}
}
The following ServiceLoader.cs (where I declare the DI):
public static class ServiceLoader
{
public static void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IRepository, Repository>();
var connection = "myConnectionString";
services.AddDbContext<ApplicationContext>(options => options.UseSqlServer(connection));
}
}
and finally, the following Repository, where the exception is thrown:
public class Repository : IRepository
{
private ApplicationContext _db;
public Repository (ApplicationContext db)
{
_db = db;
}
public List<MyClass> Get()
{
_db.MyClasses.ToList();
}
}
I have also tried to declare the Repository as Transient instead of Singleton, but a similar error is thrown
'An attempt was made to use the context while it is being configured. A DbContext instance cannot be used inside OnConfiguring since it is still being configured at this point. This can happen if a second operation is started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.'
Any idea on how to fix this? Thanks!
In my case I found the following information helpful:
https://learn.microsoft.com/en-us/ef/core/miscellaneous/configuring-dbcontext
And changed the lifetime scope of my Db Context to transient using the overloaded AddDbContext method in startup:
services.AddDbContext<MyAppDbContext>(options => {
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection"));
}, ServiceLifetime.Transient);
You can wrap an async Task around your Get() function and await your results:
public async Task<List<MyClass>> Get()
{
return await _db.MyClasses.ToListAsync();
}
I wrote a solution, which uses a queue. It is still single threaded, but you can call it from different threads.
public class ThreadSafeDataContext
{
private Thread databaseThread;
private Queue<PendingQuery> pendingQueries = new Queue<PendingQuery>();
private DatabaseContext db = new DatabaseContext();
private bool running = true;
public ThreadSafeDataContext()
{
databaseThread = new Thread(new ThreadStart(DoWork));
databaseThread.Start();
}
public void StopService()
{
running = false;
}
private void DoWork()
{
while(running)
{
if (pendingQueries.Count > 0)
{
// Get and run query
PendingQuery query = pendingQueries.Dequeue();
query.result = query.action(db);
query.isFinished = true;
}
else
{
Thread.Sleep(1); // Waiting for queries
}
}
}
public T1 Query<T1>(Func<DatabaseContext, T1> action)
{
Func<DatabaseContext, object> a = (DatabaseContext db) => action(db);
PendingQuery query = new PendingQuery(a);
pendingQueries.Enqueue(query);
while (!query.isFinished) {
Thread.Sleep(1); // Wait until query is finished
}
return (T1)query.result;
}
}
class PendingQuery
{
public Func<DatabaseContext, object> action;
public bool isFinished;
public object result;
public PendingQuery(Func<DatabaseContext, object> action)
{
this.action = action;
}
}
Then you can just run a query from different threads by using:
TeamMembers teamMembers = threadSafeDb.Query((DatabaseContext c) => c.team.ToArray())

Hangfire dependency injection with .NET Core

How can I use .NET Core's default dependency injection in Hangfire?
I am new to Hangfire and searching for an example which works with ASP.NET Core.
See full example on GitHub https://github.com/gonzigonz/HangfireCore-Example.
Live site at http://hangfirecore.azurewebsites.net/
Make sure you have the Core version of Hangfire:
dotnet add package Hangfire.AspNetCore
Configure your IoC by defining a JobActivator. Below is the config for use with the default asp.net core container service:
public class HangfireActivator : Hangfire.JobActivator
{
private readonly IServiceProvider _serviceProvider;
public HangfireActivator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public override object ActivateJob(Type type)
{
return _serviceProvider.GetService(type);
}
}
Next register hangfire as a service in the Startup.ConfigureServices method:
services.AddHangfire(opt =>
opt.UseSqlServerStorage("Your Hangfire Connection string"));
Configure hangfire in the Startup.Configure method. In relationship to your question, the key is to configure hangfire to use the new HangfireActivator we just defined above. To do so you will have to provide hangfire with the IServiceProvider and this can be achieved by just adding it to the list of parameters for the Configure method. At runtime, DI will providing this service for you:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
IServiceProvider serviceProvider)
{
...
// Configure hangfire to use the new JobActivator we defined.
GlobalConfiguration.Configuration
.UseActivator(new HangfireActivator(serviceProvider));
// The rest of the hangfire config as usual.
app.UseHangfireServer();
app.UseHangfireDashboard();
}
When you enqueue a job, use the registered type which usually is your interface. Don't use a concrete type unless you registered it that way. You must use the type registered with your IoC else Hangfire won't find it.
For Example say you've registered the following services:
services.AddScoped<DbManager>();
services.AddScoped<IMyService, MyService>();
Then you could enqueue DbManager with an instantiated version of the class:
BackgroundJob.Enqueue(() => dbManager.DoSomething());
However you could not do the same with MyService. Enqueuing with an instantiated version would fail because DI would fail as only the interface is registered. In this case you would enqueue like this:
BackgroundJob.Enqueue<IMyService>( ms => ms.DoSomething());
DoritoBandito's answer is incomplete or deprecated.
public class EmailSender {
public EmailSender(IDbContext dbContext, IEmailService emailService)
{
_dbContext = dbContext;
_emailService = emailService;
}
}
Register services:
services.AddTransient<IDbContext, TestDbContext>();
services.AddTransient<IEmailService, EmailService>();
Enqueue:
BackgroundJob.Enqueue<EmailSender>(x => x.Send(13, "Hello!"));
Source:
http://docs.hangfire.io/en/latest/background-methods/passing-dependencies.html
Note: if you want a full sample, see my blog post on this.
All of the answers in this thread are wrong/incomplete/outdated. Here's an example with ASP.NET Core 3.1 and Hangfire.AspnetCore 1.7.
Client:
//...
using Hangfire;
// ...
public class Startup
{
// ...
public void ConfigureServices(IServiceCollection services)
{
//...
services.AddHangfire(config =>
{
// configure hangfire per your requirements
});
}
}
public class SomeController : ControllerBase
{
private readonly IBackgroundJobClient _backgroundJobClient;
public SomeController(IBackgroundJobClient backgroundJobClient)
{
_backgroundJobClient = backgroundJobClient;
}
[HttpPost("some-route")]
public IActionResult Schedule([FromBody] SomeModel model)
{
_backgroundJobClient.Schedule<SomeClass>(s => s.Execute(model));
}
}
Server (same or different application):
{
//...
services.AddScoped<ISomeDependency, SomeDependency>();
services.AddHangfire(hangfireConfiguration =>
{
// configure hangfire with the same backing storage as your client
});
services.AddHangfireServer();
}
public interface ISomeDependency { }
public class SomeDependency : ISomeDependency { }
public class SomeClass
{
private readonly ISomeDependency _someDependency;
public SomeClass(ISomeDependency someDependency)
{
_someDependency = someDependency;
}
// the function scheduled in SomeController
public void Execute(SomeModel someModel)
{
}
}
As far as I am aware, you can use .net cores dependency injection the same as you would for any other service.
You can use a service which contains the jobs to be executed, which can be executed like so
var jobId = BackgroundJob.Enqueue(x => x.SomeTask(passParamIfYouWish));
Here is an example of the Job Service class
public class JobService : IJobService
{
private IClientService _clientService;
private INodeServices _nodeServices;
//Constructor
public JobService(IClientService clientService, INodeServices nodeServices)
{
_clientService = clientService;
_nodeServices = nodeServices;
}
//Some task to execute
public async Task SomeTask(Guid subject)
{
// Do some job here
Client client = _clientService.FindUserBySubject(subject);
}
}
And in your projects Startup.cs you can add a dependency as normal
services.AddTransient< IClientService, ClientService>();
Not sure this answers your question or not
Currently, Hangfire is deeply integrated with Asp.Net Core. Install Hangfire.AspNetCore to set up the dashboard and DI integration automatically. Then, you just need to define your dependencies using ASP.NET core as always.
If you are trying to quickly set up Hangfire with ASP.NET Core (tested in ASP.NET Core 2.2) you can also use Hangfire.MemoryStorage. All the configuration can be performed in Startup.cs:
using Hangfire;
using Hangfire.MemoryStorage;
public void ConfigureServices(IServiceCollection services)
{
services.AddHangfire(opt => opt.UseMemoryStorage());
JobStorage.Current = new MemoryStorage();
}
protected void StartHangFireJobs(IApplicationBuilder app, IServiceProvider serviceProvider)
{
app.UseHangfireServer();
app.UseHangfireDashboard();
//TODO: move cron expressions to appsettings.json
RecurringJob.AddOrUpdate<SomeJobService>(
x => x.DoWork(),
"* * * * *");
RecurringJob.AddOrUpdate<OtherJobService>(
x => x.DoWork(),
"0 */2 * * *");
}
public void Configure(IApplicationBuilder app, IServiceProvider serviceProvider)
{
StartHangFireJobs(app, serviceProvider)
}
Of course, everything is store in memory and it is lost once the application pool is recycled, but it is a quick way to see that everything works as expected with minimal configuration.
To switch to SQL Server database persistence, you should install Hangfire.SqlServer package and simply configure it instead of the memory storage:
services.AddHangfire(opt => opt.UseSqlServerStorage(Configuration.GetConnectionString("Default")));
I had to start HangFire in main function. This is how I solved it:
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
using (var serviceScope = host.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
try
{
var liveDataHelper = services.GetRequiredService<ILiveDataHelper>();
var justInitHangfire = services.GetRequiredService<IBackgroundJobClient>();
//This was causing an exception (HangFire is not initialized)
RecurringJob.AddOrUpdate(() => liveDataHelper.RePopulateAllConfigDataAsync(), Cron.Daily());
// Use the context here
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "Can't start " + nameof(LiveDataHelper));
}
}
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
Actually there is an easy way for dependency injection based job registration.
You just need to use the following code in your Startup:
public class Startup {
public void Configure(IApplicationBuilder app)
{
var factory = app.ApplicationServices
.GetService<IServiceScopeFactory>();
GlobalConfiguration.Configuration.UseActivator(
new Hangfire.AspNetCore.AspNetCoreJobActivator(factory));
}
}
However i personally wanted a job self registration including on demand jobs (recurring jobs which are never executed, except by manual trigger on hangfire dashboard), which was a little more complex then just that. I was (for example) facing issues with the job service activation, which is why i decided to share most of my implementation code.
//I wanted an interface to declare my jobs, including the job Id.
public interface IBackgroundJob {
string Id { get; set; }
void Invoke();
}
//I wanted to retrieve the jobs by id. Heres my extension method for that:
public static IBackgroundJob GetJob(
this IServiceProvider provider,
string jobId) => provider
.GetServices<IBackgroundJob>()
.SingleOrDefault(j => j.Id == jobId);
//Now i needed an invoker for these jobs.
//The invoker is basically an example of a dependency injected hangfire job.
internal class JobInvoker {
public JobInvoker(IServiceScopeFactory factory) {
Factory = factory;
}
public IServiceScopeFactory Factory { get; }
public void Invoke(string jobId)
{
//hangfire jobs should always be executed within their own scope.
//The default AspNetCoreJobActivator should technically already do that.
//Lets just say i have trust issues.
using (var scope = Factory.CreateScope())
{
scope.ServiceProvider
.GetJob(jobId)?
.Invoke();
}
}
//Now i needed to tell hangfire to use these jobs.
//Reminder: The serviceProvider is in IApplicationBuilder.ApplicationServices
public static void RegisterJobs(IServiceProvider serviceProvider) {
var factory = serviceProvider.GetService();
GlobalConfiguration.Configuration.UseActivator(new Hangfire.AspNetCore.AspNetCoreJobActivator(factory));
var manager = serviceProvider.GetService<IRecurringJobManager>();
var config = serviceProvider.GetService<IConfiguration>();
var jobs = serviceProvider.GetServices<IBackgroundJob>();
foreach (var job in jobs) {
var jobConfig = config.GetJobConfig(job.Id);
var schedule = jobConfig?.Schedule; //this is a cron expression
if (String.IsNullOrWhiteSpace(schedule))
schedule = Cron.Never(); //this is an on demand job only!
manager.AddOrUpdate(
recurringJobId: job.Id,
job: GetJob(job.Id),
cronExpression: schedule);
}
//and last but not least...
//My Method for creating the hangfire job with injected job id
private static Job GetJob(string jobId)
{
var type = typeof(JobInvoker);
var method = type.GetMethod("Invoke");
return new Job(
type: type,
method: method,
args: jobId);
}
Using the above code i was able to create hangfire job services with full dependency injection support. Hope it helps someone.
Use the below code for Hangfire configuration
using eForms.Core;
using Hangfire;
using Hangfire.SqlServer;
using System;
using System.ComponentModel;
using System.Web.Hosting;
namespace eForms.AdminPanel.Jobs
{
public class JobManager : IJobManager, IRegisteredObject
{
public static readonly JobManager Instance = new JobManager();
//private static readonly TimeSpan ZeroTimespan = new TimeSpan(0, 0, 10);
private static readonly object _lockObject = new Object();
private bool _started;
private BackgroundJobServer _backgroundJobServer;
private JobManager()
{
}
public int Schedule(JobInfo whatToDo)
{
int result = 0;
if (!whatToDo.IsRecurring)
{
if (whatToDo.Delay == TimeSpan.Zero)
int.TryParse(BackgroundJob.Enqueue(() => Run(whatToDo.JobId, whatToDo.JobType.AssemblyQualifiedName)), out result);
else
int.TryParse(BackgroundJob.Schedule(() => Run(whatToDo.JobId, whatToDo.JobType.AssemblyQualifiedName), whatToDo.Delay), out result);
}
else
{
RecurringJob.AddOrUpdate(whatToDo.JobType.Name, () => RunRecurring(whatToDo.JobType.AssemblyQualifiedName), Cron.MinuteInterval(whatToDo.Delay.TotalMinutes.AsInt()));
}
return result;
}
[DisplayName("Id: {0}, Type: {1}")]
[HangFireYearlyExpirationTime]
public static void Run(int jobId, string jobType)
{
try
{
Type runnerType;
if (!jobType.ToType(out runnerType)) throw new Exception("Provided job has undefined type");
var runner = runnerType.CreateInstance<JobRunner>();
runner.Run(jobId);
}
catch (Exception ex)
{
throw new JobException($"Error while executing Job Id: {jobId}, Type: {jobType}", ex);
}
}
[DisplayName("{0}")]
[HangFireMinutelyExpirationTime]
public static void RunRecurring(string jobType)
{
try
{
Type runnerType;
if (!jobType.ToType(out runnerType)) throw new Exception("Provided job has undefined type");
var runner = runnerType.CreateInstance<JobRunner>();
runner.Run(0);
}
catch (Exception ex)
{
throw new JobException($"Error while executing Recurring Type: {jobType}", ex);
}
}
public void Start()
{
lock (_lockObject)
{
if (_started) return;
if (!AppConfigSettings.EnableHangFire) return;
_started = true;
HostingEnvironment.RegisterObject(this);
GlobalConfiguration.Configuration
.UseSqlServerStorage("SqlDbConnection", new SqlServerStorageOptions { PrepareSchemaIfNecessary = false })
//.UseFilter(new HangFireLogFailureAttribute())
.UseLog4NetLogProvider();
//Add infinity Expiration job filter
//GlobalJobFilters.Filters.Add(new HangFireProlongExpirationTimeAttribute());
//Hangfire comes with a retry policy that is automatically set to 10 retry and backs off over several mins
//We in the following remove this attribute and add our own custom one which adds significant backoff time
//custom logic to determine how much to back off and what to to in the case of fails
// The trick here is we can't just remove the filter as you'd expect using remove
// we first have to find it then save the Instance then remove it
try
{
object automaticRetryAttribute = null;
//Search hangfire automatic retry
foreach (var filter in GlobalJobFilters.Filters)
{
if (filter.Instance is Hangfire.AutomaticRetryAttribute)
{
// found it
automaticRetryAttribute = filter.Instance;
System.Diagnostics.Trace.TraceError("Found hangfire automatic retry");
}
}
//Remove default hangefire automaticRetryAttribute
if (automaticRetryAttribute != null)
GlobalJobFilters.Filters.Remove(automaticRetryAttribute);
//Add custom retry job filter
GlobalJobFilters.Filters.Add(new HangFireCustomAutoRetryJobFilterAttribute());
}
catch (Exception) { }
_backgroundJobServer = new BackgroundJobServer(new BackgroundJobServerOptions
{
HeartbeatInterval = new System.TimeSpan(0, 1, 0),
ServerCheckInterval = new System.TimeSpan(0, 1, 0),
SchedulePollingInterval = new System.TimeSpan(0, 1, 0)
});
}
}
public void Stop()
{
lock (_lockObject)
{
if (_backgroundJobServer != null)
{
_backgroundJobServer.Dispose();
}
HostingEnvironment.UnregisterObject(this);
}
}
void IRegisteredObject.Stop(bool immediate)
{
Stop();
}
}
}
Admin Job Manager
public class Global : System.Web.HttpApplication
{
void Application_Start(object sender, EventArgs e)
{
if (Core.AppConfigSettings.EnableHangFire)
{
JobManager.Instance.Start();
new SchedulePendingSmsNotifications().Schedule(new Core.JobInfo() { JobId = 0, JobType = typeof(SchedulePendingSmsNotifications), Delay = TimeSpan.FromMinutes(1), IsRecurring = true });
}
}
protected void Application_End(object sender, EventArgs e)
{
if (Core.AppConfigSettings.EnableHangFire)
{
JobManager.Instance.Stop();
}
}
}

How to run scheduled tasks in Orchard?

I have to run a automated job every 5 hours.
I found this post on how to create scheduled tasks, using IScheduledTaskHandler and IScheduledTaskManager.
Scheduled tasks using Orchard CMS
I copied the same code, I added my service call inside the Process function. It compiles fine. But I am not sure if I have to 'start' this scheduled task, like a windows service start. Does it get picked up automatically after I build the solution? When does the clock starts ticking if I want to run this job in 5 hours? And if I want to stop/pause, how can I do that?
Thanks.
EDIT:
I am getting an exception if I try to enable the custom module with task handler.
Exception Details: System.ArgumentNullException: Value cannot be null. Parameter name: source
Line 241: var shellContext = _shellContexts.FirstOrDefault(c => c.Settings.Name == settings.Name);
Source File: \orchard-1.4\src\Orchard\Environment\DefaultOrchardHost.cs Line: 241
The _shellContexts is coming up as null. If I remove the task handler class from the project/module, everything works fine.
Here is the task handler code.
public class ScheduledTaskHandler : IScheduledTaskHandler
{
private const string TaskType = "MyTaskUniqueID";
private readonly IScheduledTaskManager _taskManager;
private readonly IMyService _myService;
public ILogger Logger { get; set; }
public ScheduledTaskHandler(IScheduledTaskManager taskManager, IMyService myService)
{
_myService = myService;
_taskManager = taskManager;
Logger = NullLogger.Instance;
try
{
DateTime firstDate = new DateTime().AddMinutes(5);
ScheduleNextTask(firstDate);
}
catch (Exception e)
{
this.Logger.Error(e, e.Message);
}
}
public void Process(ScheduledTaskContext context)
{
if (context.Task.TaskType == TaskType)
{
try
{
_myService.RunJob();
}
catch (Exception e)
{
this.Logger.Error(e, e.Message);
}
finally
{
DateTime nextTaskDate = new DateTime().AddHours(5);
ScheduleNextTask(nextTaskDate);
}
}
}
private void ScheduleNextTask(DateTime date)
{
if (date > DateTime.UtcNow)
{
var tasks = this._taskManager.GetTasks(TaskType);
if (tasks == null || tasks.Count() == 0)
this._taskManager.CreateTask(TaskType, date, null);
}
}
}
Clock starts ticking automatically when you start the app - you cannot stop/pause it.
Scheduler runs in 1 minute intervals - it checks if there are tasks that should be ran now and runs them. Tasks are being stored in database - corresponding record is always deleted just before task execution is about to start (to ensure that a given task will run only once).
If you need a recurrent job to be ran, you need to create a new task just after the previous one has finished (like in the example you linked to).
if somebody is interested I've simply removed the starter code from ScheduledTaskHandler.
The following I put on some controller constructor
private void ScheduleStartTask() {
var tasks = _scheduledTaskManager.GetTasks(TaskType);
if (tasks == null || tasks.Count() == 0) {
var date = _clock.UtcNow.AddSeconds(5);
_scheduledTaskManager.CreateTask(TaskType, date, null);
}
}
and on the handler
public void Process(ScheduledTaskContext context) {
if (context.Task.TaskType == TaskType) {
try {
var x = "kuku";
} catch (Exception e) {
this.Logger.Error(e, e.Message);
} finally {
this.ScheduleNextTask();
}
}
}
private void ScheduleNextTask() {
var date = _clock.UtcNow.AddSeconds(5);
_taskManager.DeleteTasks(null, a => a.TaskType == TaskType);
_taskManager.CreateTask(TaskType, date, null);
}
Use ILoggerFactory Instead of ILogger and then Get Logger Instance from it.
public ScheduledTaskHandler(IScheduledTaskManager taskManager,ILoggerFactory LoggerFactory, IMyService myService)
{
_myService = myService;
_taskManager = taskManager;
Logger = NLoggerFactory.CreateLogger(typeof(ScheduledTaskHandler));;
try
{
DateTime firstDate = new DateTime().AddMinutes(5);
ScheduleNextTask(firstDate);
}
catch (Exception e)
{
this.Logger.Error(e, e.Message);
}
}

Categories

Resources