Azure WebJobs continue to be one of Azure’s most useful service offerings, with capabilities to run service bus, queue and timer triggered background jobs in the same App Service as the Web App hosting the website. With potential alternatives like Azure Functions, Logic Apps and Event Grid entering into the fray, the online forums are abuzz with constant discussions on choosing the right tool for the job. This particular blog post assumes that you have already taken that important decision and intend to harness the power of WebJobs for your needs. While there are many articles out there detailing the methods for creating, deploying and running WebJobs ; there are hardly any guidelines on how to structure your WebJob codebase to cut-short your development cycle and streamline the deployment process, especially if your use-case includes more than a couple WebJobs . If you are a developer who is tasked with creating a WebJob based solution, or if you are someone who is extending such a solution while striving to reduce the complexity, or if you are just plain curious, then this post is for you.

Design Decisions

Ok, first things first. Although this may very well be an unpopular opinion, I do believe that your WebJob project should not be a part of your ASP.NET solution, but instead be included in a separate WebJob specific solution. The rationale behind this idea is to focus all the build and deployment pipelining on this one solution and optimize it, so that it can be re-used by projects across applications that rely on WebJobs .

Within this WebJob specific solution, we need to de-couple the logic of initializing the WebJob using configuration properties and attaching associated trigger-methods from the core processing logic. We can achieve this objective by using the notion of WebJobProcessor projects, i.e the code that does the actual work and WebJobInitializer projects that do the bootstrapping.

Considering that most WebJobs end-up invoking some API that does the required operation, we need to de-couple the API client code from the WebJob code that uses them, thereby creating WebJobApiClient projects. Additionally, there are some common functionalities that can potentially be used across these projects like:

Telemetry client for logging information associated with an operation

Configuration Reader for reading the configuration using the preferred mechanism

Token Manager for fetching and managing OAuth tokens in case the APIs require authentication/authorization

To accomodate these functionalities, we need to create a WebJobShared project in our solution. Obviously, these WebJobs would need to be unit tested and quite possibly you would want web tests as well, hence we shall go ahead and create WebJobUnitTest and WebJobWebTest projects as well. Ok, we have made quite some additions so let’s see how our solution looks like right now. Bear in the mind that the below diagram is not a UML class diagram (althought it uses its components), but instead can be considered as a project/solution diagram showing the linkages and dependencies between the projects that are part of the same solution.

Dissecting the Design

Well that’s a lot of information in one image, so let’s analyze it piece by piece.

WebJob Initializers

As can be seen from the above image, WebJobInitializer is not a single project, but a group of projects. For each WebJob that we plan to deploy, there has to be a separate WebJob project that gets deployed. The WebJobInitializer projects correspond to these WebJob projects and hence we need a group of projects instead of just one. Currently, our framework hosts two webjobs, Order and Invoice processing, thus the projects WebJobInitialize.Order and WebJobInitialize.Invoice . Both these projects contain a single file, Program.cs which contains the Main() method that will be entry point when the WebJob begins executing after deployment. The only job of the Main() method is to invoke the RunAndBlock() method of the JostHostBase class, that’s part of the WebJobInitialize.Base project (also of type WebJob).

https://gist.github.com/Rajivts/9d62070b8b338a3fc6c29afedb5564be

As you would have already gathered from the code, the WebJob projects don’t do anything on their own but instead rely on the WebJobInitialize.Base project to provide the necessary bootstrapping logic. The JobHostBase class within this project, mimics the JostHost class provided by the WebJobs SDK by using its instance within the implementation. The functionality of JobHostBase can be cut down to the following points:

Read the relevant configuration for the target webjob (e.g from WebJobInitialize.Order ‘s config file) and instantiate a JobHostConfiguration object based on it

‘s config file) and instantiate a object based on it Depending on the type of WebJob required, append either ServiceBusConfiguration or TimerConfiguration to the JobHostConfiguration object

required, append either or to the object Create a Service Bus topic/queue triggered method and within it, call the ProcessMessage of the required processor. (e.g If we are initializing the Order WebJob, then the ProcessMessage of the OrderProcessor class is invoked)

of the required processor. (e.g If we are initializing the WebJob, then the of the class is invoked) Create a Timer Triggered method and within it, call the ExecuteAction of the requried processor. (e.g If we are initializing the Invoice WebJob, then the ExecuteAction of the InvoiceProcessor class is invoked)

https://gist.github.com/Rajivts/85dd091430b118cdfb1845a3c525d970

Apart from the above mentioned points, the code includes additional functionalities like custom retry logic for Service Bus message that distinguishes between fatal and transient errors and provides call-backs for successful and failed execution of the message. The comments will describe in detail what the code does so I shall limit my explanation.

WebJob Processor

From our analysis of JostHostBase class, we can conclude that each processor should atleast contain the following:

ProcessMessage() method for processing Service Bus messages

method for processing Service Bus messages ExecuteAction() method for processing Timer-Triggered logic

method for processing Timer-Triggered logic OnMessageFailure delegate that can be invoked when the service bus message processing finally fails

delegate that can be invoked when the service bus message processing finally fails OnMessageSuccessful delegate that can be invoked when the service bus message processing succeeds

Since each processor has to have these methods and properties, they have been abstracted out in the form of IBaseProcessor interface as below:

https://gist.github.com/Rajivts/88f7ebd06c263ec54cf0d3cef8b1a982

The detailed comments outline what these methods/properties represent and what is expected from their implementation.

WebJob Shared

As can be seen from the project diagram, the WebJobShared project exposes two contracts, namely ITokenManager and ITelemetryContext . As mentioned earlier, ITokenManager is responsible for handling OAuth related token processing. As such, this interface requires the implementor to provide the logic for just one functionality, GetOAuthTokenValue() . If you forsee an even more generic authentication/authorization scheme being used within your APIs, you can tweak the above contract accordingly. The ITelemetryContext is a contract purely for logging purposes and supports methods like LogException() , LogEvent() , LogTrace() and LogDependency() which are common requirements for telemetry and logging. Again, if your domain has certain specific logging requirements, the said contract can be modified accordingly.

The last remaining component of the WebJobShared project is the Configuration Reader, which is realized in terms of the ITenantConfigurationProvider contract. The concrete implementations of this contract have to provide the following methods/properties:

GetTenantValue() for getting the string based value of a configuration key

for getting the string based value of a configuration key GetTenantIntValue() for getting the int based value of a configuration key

for getting the int based value of a configuration key GetTenantBoolValue() for getting the boolean based value of a configuration key

for getting the boolean based value of a configuration key TenantCode , so that every look up for a key is done as TenantCode.Key and if that fails Common.Key and if that fails, simply Key

, so that every look up for a key is done as and if that fails and if that fails, simply TenantName so that any logging that gets performed can indicate exactly which tenant is logging the information

https://gist.github.com/Rajivts/815a5efefabfab95cefee9e2ff37b8d1

WebJob Api Client

For each API that the WebJob processors plan to use, there should exist a corresponding interface acting as the API client. If the APIs under consideration are RESTful in nature, you can use libraries like RestEase which will generate the contracts for you.

WebJob Unit Test

Throughout the solution we have depended on contracts/abstractions and refrained from using concrete implementations, hence writing unit test cases should be a pretty straight-forward job. Note that the project diagram shows a singular dependency of the unit test project on the WebJobProcessor . This is intentional since the unit test cases should only target the processor logic which will be the only piece of code with significant code changes with every new webjob.

WebJob Web Test

Writing Web Tests for webjobs is not a must-have but definitely a good-to-have. Periodic status updates and alert triggers if something goes wrong are the primary reasons for having such tests in place. This particular blog post provides all the required information for setting up and deploying the web test.

Deployment Strategy

There are multiple ways in which one can deploy WebJobs to an AppService, deploying directly from Visual Studio using Web Deploy, uploading zip file to the AppService using PowerShell, CICD using VSTS or Jenkins to name a few. While I can’t possibly describe all of them here, it should be apparent from our componentized architecture that we have saved ourselves a good bit of hassle in the build and deployment domain. Any build/deployment pipelining that you introduce in this framework will automatically be applicable to every webjob hosted within this project, albeit with a few minor tweaks. As an example, I will demonstrate how you can set up a build definition in VSTS to achieve seamless build & deployment for any of the webjobs added in our solution.

You might notice that I have included build, test and deploy in one single build definition. If required you can split up the components into their respective definitions. Let’s analyse each of the tasks defined above step-by-step to understand the working.

NuGet Restore

If your projects make use of NuGet packages, then as the first step we need to restore those packages.

Build Solution

Although the step is named Build Solution , we would just be building the WebJob project that we wish to deploy which will automatically build all the dependent projects.

Populate Configuration Parameters for Environment

The WebJob project to be deployed will contain a .config file which includes all the necessary configurations used by JostHostBase to bootstrap the webjob. However, these configurations will change from environment to environment and hence we have added a tokenizer task that will replace place-holders by their corresponding values from user defined build variables.

Build Unit Test Project/ Executing Unit Tests

Both are straight-forward tasks that behave as expected. We build the corresponding unit test project using the Build Unit Test Project step and then execute them using the Executing Unit Tests step.

Setting Correct WebJob Name

Recall that each WebJobInitialize project in-turn depends on JobHostBase which is a part of WebJobInitialize.Base project. This particular project is of type WebJob and hence will also generate an .exe on getting built. With multiple .exe in the deploy folder, we want a way to specify which one is the webjob to be run and which one is the bootstrapping project. We can do this by renaming the required .exe as run.exe by using a custom powershell script. This way we can make sure that Azure will always run the required .exe .

Deploy to Azure

The following two steps deal with restructuring the folder heirarchy of the files to be deployed and copying the files to Azure. I won’t describe these steps in detail, since this blog post already does that.

//TODO: Fix This

WebJobs also support other triggers (e.g EventHub, File) that are not yet part of this framework. However, the skeleton of the project is amenable to such changes

While writing the processor logic for the webjobs, one might have to rely on domain-specific business object that are part of ASP.NET solution. Unless those projects are also NuGet packages or something similar, the current model will require you to add a reference to those projects

Conclusion

I do hope I was able to convice you (if atleast a little bit) about the benefits of using such a modularized architecture for your webjob projects. Let me know your thoughts in the comments section.

Cheers!