I have a Quartz job that is setup during Application_Start
in the Global.asax.cs
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
Logger.log("About to Setup Retry Job");
JobScheduler.Start();
}
This calls the Start
method which then schedules the job.
The job runs every 20 seconds and throws an exception. Here is my Job.
public class RetryTempJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
try
{
Logger.log("Executing Job");
new ProcessOrder().retryFailedOrders();
//Logger.log("Done Executing Syspro Job");
await Console.Error.WriteLineAsync("Done Executing Syspro Job");
}
catch (Exception se)
{
await Console.Error.WriteLineAsync("" + se.InnerException);
}
}
}
An exception is thrown at this line Logger.log("Executing Job");
. This is a `Static method that opens a log file and write to it. This method works everywhere else in my site.
Here is the Exception Message: {"Object reference not set to an instance of an object."}
The InnerException is NULL. Here is the stack:
DarwinsShoppingCart.dll!DarwinsShoppingCart.SharedClasses.JobScheduler.RetrySyspro.Execute(Quartz.IJobExecutionContext context) Line 69 C#
Quartz.dll!Quartz.Core.JobRunShell.Run(System.Threading.CancellationToken cancellationToken) Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start<Quartz.Core.JobRunShell.<Run>d__9>(ref Quartz.Core.JobRunShell.<Run>d__9 stateMachine) Unknown
Quartz.dll!Quartz.Core.JobRunShell.Run(System.Threading.CancellationToken cancellationToken) Unknown
Quartz.dll!Quartz.Core.QuartzSchedulerThread.Run.AnonymousMethod__0() Unknown
mscorlib.dll!System.Threading.Tasks.Task<System.Threading.Tasks.Task>.InnerInvoke() Unknown
mscorlib.dll!System.Threading.Tasks.Task.Execute() Unknown
mscorlib.dll!System.Threading.Tasks.Task.ExecutionContextCallback(object obj) Unknown
mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Unknown
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Unknown
mscorlib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot) Unknown
mscorlib.dll!System.Threading.Tasks.Task.ExecuteEntry(bool bPreventDoubleExecution) Unknown
mscorlib.dll!System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() Unknown
mscorlib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch() Unknown
mscorlib.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() Unknown
Here is my Logger class code
public static void log(string strLog)
{
StreamWriter log;
FileStream fileStream = null;
DirectoryInfo logDirInfo = null;
FileInfo logFileInfo;
string username = Environment.UserName;
string logFilePath = HttpContext.Current.Server.MapPath("~/log/Log.txt");
logFileInfo = new FileInfo(logFilePath);
logDirInfo = new DirectoryInfo(logFileInfo.DirectoryName);
double fileSize = ConvertBytesToMegabytes(logFileInfo.Length);
if (fileSize > 30)
{
string FileDate = DateTime.Now.ToString().Replace("/", "-").Replace(":", "-");
string oldfilepath = HttpContext.Current.Server.MapPath("~/log/log-" + FileDate + ".txt");
File.Move(logFileInfo.FullName, oldfilepath);
}
if (!logFileInfo.Exists)
{
fileStream = logFileInfo.Create();
}
else
{
fileStream = new FileStream(logFilePath, FileMode.Append);
}
log = new StreamWriter(fileStream);
log.WriteLine(DateTime.Now.ToString("MM-dd HH:mm:ss") + " " + username + " " + strLog);
log.Close();
}
Given that the logger can be called within a request and is also possible to be invoked outside of a request. If a job is started and there is no request HttpContext
will be null
Consider decoupling the logger from implementation concerns like the HttpContext
and abstract the process of mapping the path and taking advantage of dependency injections
public interface IPathProvider {
string MapPath(string path);
}
You should also avoid the static logger code smell as it makes the code difficult to maintain and test in isolation.
public interface ILogger {
void log(string message);
//...
}
public class Logger : ILogger {
private readonly IPathProvider pathProvider;
public Logger(IPathProvider pathProvider) {
this.pathProvider = pathProvider;
}
public void log(string strLog) {
FileStream fileStream = null;
string username = Environment.UserName;
string logFilePath = pathProvider.MapPath("~/log/Log.txt");
var logFileInfo = new FileInfo(logFilePath);
var logDirInfo = new DirectoryInfo(logFileInfo.DirectoryName);
double fileSize = ConvertBytesToMegabytes(logFileInfo.Length);
if (fileSize > 30) {
string FileDate = DateTime.Now.ToString().Replace("/", "-").Replace(":", "-");
string oldfilepath = pathProvider.MapPath("~/log/log-" + FileDate + ".txt");
File.Move(logFileInfo.FullName, oldfilepath);
}
if (!logFileInfo.Exists) {
fileStream = logFileInfo.Create();
} else {
fileStream = new FileStream(logFilePath, FileMode.Append);
}
using(fileStream) {
using (var log = new StreamWriter(fileStream)) {
log.WriteLine(DateTime.Now.ToString("MM-dd HH:mm:ss") + " " + username + " " + strLog);
log.Close();
}
}
}
}
and have the job explicitly depend on the abstraction.
public class RetryTempJob : IJob {
private readonly ILogger logger;
public RetryTempJob(ILogger logger) {
this.logger = logger;
}
public async Task Execute(IJobExecutionContext context) {
try {
logger.log("Executing Job");
new ProcessOrder().retryFailedOrders();
//logger.log("Done Executing Syspro Job");
await Console.Error.WriteLineAsync("Done Executing Syspro Job");
} catch (Exception se) {
await Console.Error.WriteLineAsync("" + se.InnerException);
}
}
}
There is more that can be abstracted out here but that is a little outside of the scope of the example.
Now that the design issues have been addressed, we can look at the path provider implementation to take care of the HttpContext
situation.
Server.MapPath()
requires anHttpContext
whileHostingEnvironment.MapPath
does not.
Reference What is the difference between Server.MapPath and HostingEnvironment.MapPath?
The implementation can try checking the context for null but Server.MapPath()
eventually calls HostingEnvironment.MapPath()
so it would be better to just use HostingEnvironment.MapPath()
public class PathProvider : IPathProvider {
public string MapPath(string path) {
return HostingEnvironment.MapPath(path);
}
}
What is left now is to configure the scheduler to allow dependency injection and for you to decide which DI framework you want to use.
Create a job factory that inherits from the default Quartz.NET job factory SimpleJobFactory
. This new job factory is going to take an IServiceProvider
in its constructor, and override the default NewJob()
method provided by SimpleJobFactory
.
class MyDefaultJobFactory : SimpleJobFactory {
private readonly IServiceProvider container;
public MyDefaultJobFactory(IServiceProvider container) {
this.container = container;
}
public override IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) {
IJobDetail jobDetail = bundle.JobDetail;
Type jobType = jobDetail.JobType;
try {
// this will inject any dependencies that the job requires
return (IJob) this.container.GetService(jobType);
} catch (Exception e) {
var errorMessage = string.Format("Problem instantiating job '{0}'.", jobType.FullName);
throw new SchedulerException(errorMessage, e);
}
}
}
There are many DI frameworks to choose from, but for this example I an using the .Net Core Dependency Injection Extension, which, because of the modular nature of .Net Core, allows for it to be dropped easily into your project.
Finally, configure the service collection on application start
protected void Application_Start() {
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
var services = new ServiceCollection();
var serviceProvider = ConfigureService(services);
var logger = serviceProvider.GetService<ILogger>();
logger.log("About to Setup Retry Job");
var jobScheduler = serviceProvider.GetRequiredService<IScheduler>();
//...add jobs as needed
jobScheduler.ScheduleJob(.....);
//and start scheduler
jobScheduler.Start();
}
private IServiceProvider ConfigureService(IServiceCollection services) {
//Add job dependencies
services.AddSingleton<ILogger, Logger>();
services.AddSingleton<IPathProvider, PathProvider>();
//add scheduler
services.AddSingleton<IScheduler>(sp => {
var scheduler = new StdSchedulerFactory().GetScheduler();
scheduler.JobFactory = new MyDefaultJobFactory(sp);
return scheduler;
});
//...add any other dependencies
//build and return service provider
return services.BuildServiceProvider();
}
As I said before you can use any other DI/IoC container you want. The refactored code is now flexible enough for you to swap one for the other by simply wrapping it in a IServiceProvider
derived class and giving it to the job factory.
And by cleaning the code from concerns that make the code smell, you can manage it easier.
There is no HttpContext
when using a Quarz Job. It runs i a separate thread. So on a website with a Quarz Job I use HostingEnvironment
So insead of
HttpContext.Current.Server.MapPath("~/log/Log.txt")
Use
using System.Web.Hosting;
HostingEnvironment.MapPath("~/log/Log.txt");
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With