I have an interesting project I set out to create today-
I wanted to build a simple page, which would allow our NOC to trigger certain workflows, automatically (assuming they have proper permission)
My requirements:
- Jobs should be easily added to the list, just by specifying an Attribute, and Implementing an interface (to ensure the proper method exists)
- Jobs needs to be kicked off on my back-end servers and not on the web-front end.
- Everything needs to be properly logged.
- The front-end needs visual-feedback a job has been executed.
Here is the web page to trigger jobs.
As you can see, the above page is very simple.
In the above image, if a job fails, it will turn red, with direction to call the developers (me).
How it works (Short Version)
At startup, Reflection is used to build a dictionary containing types which implement IRemoteJob interface, and the NocJob attribute.
A webpage displays the values. Permissions are implemented at the controller level.
When a job is triggered, the Type’s full name is passed back to the method. The method will compare the provided name, against the dictionary of valid workflows. If the workflow is found, and verification passes, Hangfire.Io is then used to execute the workflow on my backend- automation services away from the web front end.
Since- we cannot execute a string containing the name of a type… We utilize Ninject’s kernel to resolve a instance of the class. We then Invoke the execute method, which is defined by the IRemoteJob interface, on the class instance.
Error handling is handed by Hangfire.
In short- that is it.
How it works (Long Version)
RuntimeTypeResolver.cs // IRunTimeTypeResolver
public class RuntimeTypeResolver : IRuntimeTypeResolver { private IKernel kernel; public RuntimeTypeResolver(IKernel Kernel) { this.kernel = Kernel; } public object GetInstanceOfType(Type type) { return kernel.Get(type); } }
This class, resolves a class instance from the Ninject kernel and returns it.
JobHelper.cs
//Note - Singleton class. public class JobHelper { //IRunTimeTypeResolver passes a Type into a dependancy injection kernel. The kernel will return a instance of the Type. private IRuntimeTypeResolver rtr; public JobHelper(IJobScheduler JOB, IRuntimeTypeResolver RTR) { this.rtr = RTR; //Note- Dictionary only needs to be populated at startup. //Create a dictionary containing all eligible classes to be executed ad-hoc. RemoteJobs = this.GetType() //Types are filtered to this current assembly. .Assembly .GetTypes() //Type/Class implements IRemoteJob interface, and is NOT an interface or abstract class. .Where(x => (typeof(IRemoteJob).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract)) //Class has the NocJobAttribute, which contains display information. .Where(x => Attribute.IsDefined(x, typeof(NocJobAttribute))) //Cast the types, and create a dictionary. .ToDictionary(o => (NocJobAttribute)Attribute.GetCustomAttribute(o, typeof(NocJobAttribute)), o => o); } //A read-only dictionary containing the information. public IReadOnlyDictionary<NocJobAttribute, Type> RemoteJobs { get; } /// <summary> /// This is intended as an entry point for background jobs. /// </summary> /// <param name="JobName"></param> public void EnqueueRemoteJob(string JobName) { //Get the Type from the list. Type Rec = RemoteJobs .First(o => o.Value.FullName.Equals(JobName, StringComparison.OrdinalIgnoreCase)) .Value; //Get the Execute method. It should exist, because all items in this list, implement IRemoteJob. MethodInfo ExecuteMethod = Rec.GetRuntimeMethod("Execute", new Type[] { }); //Get a instance of the type, from the RuntimeTypeResolver class. object Instance = rtr.GetInstanceOfType(Rec); //Invoke the Execute Method on the class instance. ExecuteMethod.Invoke(Instance, null); } }
The primary purpose of this job, is to both hold a list of “valid” workflows which can be executed, and to expose a method which the Hangfire.io can trigger with the Type’s name, to invoke the workflow.
JobController.cs – Trigger_NOC_Job method
[HttpPost] [CheckPermissionAttribute(Permission.Trigger_AutomationJobs)] public ActionResult Trigger_NOC_Job(string JobType) { log.Component = "Trigger"; var L = new JobTriggerLog { JobName = new string[] { JobType }, Time = DateTime.Now, UserID = User.Identity.Name, UserName = User.GetFullName() }; try { //now the FUN part... if (!jobHelper.RemoteJobs.Any(o => o.Value.FullName.Equals(JobType, StringComparison.OrdinalIgnoreCase))) { L.Error = new Exception("Job was not located.").GetUsefulDetails(false); log.Error(L); return new HttpStatusCodeResult(System.Net.HttpStatusCode.NotFound, "Job was not found."); } //Get the Type from the list. Type Rec = jobHelper .RemoteJobs .First(o => o.Value.FullName.Equals(JobType, StringComparison.OrdinalIgnoreCase)) .Value; //Sanity check to double-check the Execute method exists, without required parameters. if (Rec.GetRuntimeMethod("Execute", new Type[] { }) == null) throw new NullReferenceException("Execute method was not found on job."); //Use IJobScheduler to enqueue the job on the backend automation services. job.Enqueue<JobHelper>(o => o.EnqueueRemoteJob(JobType)); log.Success(L); return Json(true); } catch (Exception ex) { L.Error = ex.GetUsefulDetails(); log.Error(L); return new HttpStatusCodeResult(System.Net.HttpStatusCode.InternalServerError, $"Something went wrong. {ex.Message}"); } finally { log.SendLogSession(); } }
This is the api which is called when a user would like to trigger a workflow.
Why is this interesting?
Creating a method for each action is easy to do. Creating a single method which can dynamically invoke class instances at runtime, while resolving all of their dependencies, and following your current error handling strategies- isn’t quite so easy.
This process- allows me as the developer, to quickly, and easily add new workflows / jobs to the NOC visible page. The NOC can easily trigger the jobs…. and the process of creating the class instances, resolving dependencies…. is all auto-magically handled by the dependency injection framework.
As well, I have not posted anything in a while, and I felt this was interesting enough to share.
Be easy on the potential mistakes I made! This is the first draft of this workflow/class…