
Writing Azure WebJobs with dependency injection in .NET Core
Take advantage of .NET Core native dependency injection in Azure WebJob
WebJobs are a great way to run recurrent background tasks that support your application. They come as a part of Azure App Service/Web App serverless model. Once you have your Web App in place in your Azure account, one of the sections in the Web App menu will be Web Jobs. You have option to setup WebJob to run as
- Scheduled by a CRON expression - automatically triggered when the CRON expression condition is met
- Continuos - as soon as one run is done, run a second one
- On demand - runs when you trigger it from a Azure dashboard or via Web Hook
If you decide to use one of first two options, you need to set your Web App to be "Always on". This means, your Web App application domain will always be loaded in the memory. There will be no optimization that shuts down your application if there is not traffic to your Web App when this option is on.
"Always on" option is only available in paid tier of Web App, so if you are running Azure Web App in a free tier, you wont be able to test first two WebJob triggering scenarios. No worries for that, for testing implementation of your WebJob code free tier is just enough.
At this point, running WebJob in Linux hosted WebApp is not supported in Azure. Make sure when you create WebApp in Azure App Services, you select plan that is Windows hosted
WebJob .NET Core code implementation
WebJobs are made to be simple scheduled task, something like Scheduled Tasks in Windows, so you can basically throw anything that would in general run on Windows like *.cmd, *.com, *.bat, *.exe, but as an addition , you can also use a *.zip archive which contains your entry point file with one of those executable Windows extensions. This means you can build a proper application with N-Tier architecture and also involve NuGet packages which your application will need to run properly.
First thing that probably comes to your mind is .NET Console application and you are definitely right, but Microsoft went one step further and introduced Azure WebJob Project template in Visual Studio 2019. Unfortunately, this project template is based on .NET Framework, so if you want to stick to .NET Core, this is not an option for you.
Of course, you can just create new .NET Core Console Application project and run that in Azure WebJob, but as you know, dependency injection setup, does not come as something already setup for you in a Console application project template.
Let's see how can we inject some basic stuff (configuration, environment, logging) use them in .NET Core Console Application WebJob project. as a first step, create new .NET Core Console Application project.
We are going to use IHostedService to host our logic in application and I'll add only IConfiguration, IHostingEnvironment and ILogger parameters in the constructor which will be injected from Program.cs class.
namespace SampleWebJob { public class ApplicationHostService : IHostedService { readonly ILogger<ApplicationHostService> _logger; readonly IConfiguration _configuration; readonly IHostingEnvironment _hostingEnvironment; public ApplicationHostService( ILogger<ApplicationHostService> logger, IConfiguration configuration, IHostingEnvironment hostingEnvironment ) { _logger = logger; _configuration = configuration; _hostingEnvironment = hostingEnvironment; } public async Task StartAsync(CancellationToken cancellationToken) { await Task.CompletedTask; //Do something _logger.LogWarning("Hello from console application"); } public async Task StopAsync(CancellationToken cancellationToken) { await Task.CompletedTask; } } }
Now we need to inject the dependencies to our host class from Program.cs class.
namespace SampleWebJob { class Program { static async Task Main(string[] args) { IHost host = new HostBuilder() .ConfigureHostConfiguration(configHost => { configHost.SetBasePath(Directory.GetCurrentDirectory()); configHost.AddEnvironmentVariables(prefix: "ASPNETCORE_"); configHost.AddCommandLine(args); }) .ConfigureAppConfiguration((hostContext, configApp) => { configApp.SetBasePath(Directory.GetCurrentDirectory()); configApp.AddEnvironmentVariables(prefix: "ASPNETCORE_"); configApp.AddJsonFile($"appsettings.json", true); configApp.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", true); configApp.AddCommandLine(args); }) .ConfigureServices((hostContext, services) => { services.AddLogging(); services.AddHostedService<ApplicationHostService>(); }) .ConfigureLogging((hostContext, configLogging) => { configLogging.AddSerilog(new LoggerConfiguration() .ReadFrom.Configuration(hostContext.Configuration) .CreateLogger()); configLogging.AddConsole(); configLogging.AddDebug(); }) .Build(); await host.RunAsync(); } } }
I involved Serilog for logging, but it is not really necessary since Azure already logs everything you output to the console
If we run this program, we will see the log message in the console. but application will still run and if you deploy your WebJob like this it will report that WebJob is still running even after your processing code finished. That is because host class is designed for a long running process and keeps runnig even after code in StartAsync is executed completely.
The easiest way to interrupt the process is to throw an exception, but since we still want to know when exceptions happen, we are going to declare our own exception and throw it from the host once the job is done. Then in Program.cs we will handle this exception type specifically.
namespace SampleWebJob { public class HostingStopException:Exception { } }
Now we simply throw this exception from RunAsync method when our WebJob processing is done. This will terminate the host instance and will casue application to crash so we do not have WebJob instance hanging once processing is done.
namespace SampleWebJob { public class ApplicationHostService : IHostedService { readonly ILogger<ApplicationHostService> _logger; readonly IConfiguration _configuration; readonly IHostingEnvironment _hostingEnvironment; public ApplicationHostService( ILogger<ApplicationHostService> logger, IConfiguration configuration, IHostingEnvironment hostingEnvironment ) { _logger = logger; _configuration = configuration; _hostingEnvironment = hostingEnvironment; } public async Task StartAsync(CancellationToken cancellationToken) { await Task.CompletedTask; //Do something _logger.LogWarning("Hello from console application"); //Throw exception to terminate the host throw new HostingStopException(); } public async Task StopAsync(CancellationToken cancellationToken) { await Task.CompletedTask; } } }
And now we handle it in Programs.cs while letting all other exceptions to bubble up. If we do not do this, we'll have in our WebJob log failure reported. Although we know that this exception is thrown by us, it becomes hard to distinguish what caused failure report. Since this is logically not a failure, we just use try/ctach block for HostingStopException exception type.
namespace SampleWebJob { class Program { static async Task Main(string[] args) { IHost host = new HostBuilder() .ConfigureHostConfiguration(configHost => { configHost.SetBasePath(Directory.GetCurrentDirectory()); configHost.AddEnvironmentVariables(prefix: "ASPNETCORE_"); configHost.AddCommandLine(args); }) .ConfigureAppConfiguration((hostContext, configApp) => { configApp.SetBasePath(Directory.GetCurrentDirectory()); configApp.AddEnvironmentVariables(prefix: "ASPNETCORE_"); configApp.AddJsonFile($"appsettings.json", true); configApp.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", true); configApp.AddCommandLine(args); }) .ConfigureServices((hostContext, services) => { services.AddLogging(); services.AddHostedService<ApplicationHostService>(); }) .ConfigureLogging((hostContext, configLogging) => { configLogging.AddSerilog(new LoggerConfiguration() .ReadFrom.Configuration(hostContext.Configuration) .CreateLogger()); configLogging.AddConsole(); configLogging.AddDebug(); }) .Build(); try { await host.RunAsync(); } catch (HostingStopException) { //Host terminated } } } }
Now when you run the the application, it will end right after the code inside RunAsync is executed. If you check the WebJob log, it will be reported that WebJob succeeded.
Alternatively you can end your WebJob instance by simply calling Environemnt.FailFast method (https://docs.microsoft.com/en-us/dotnet/api/system.environment.failfast?view=netcore-2.2) instead of throwing a custom exception
Deployment
The simplest way to set your code running as a WebJob in Azure AppService is just to simply wrap your published application with all binaries to a zip file and upload it thought Azure dashboard UI.Now, depending which target runtime you pick for publishing your WebJob it will slightly affect how your WebJob will start.
Since WebJob only runs on Windows based WebJob, it is completely fine to publish your code targeting win-x86 or win-x64, but if you are targeting Portable which is default, publish process will produce a dll file as an entry point assembly of your application. As such, dll cannot be just invoked, which means you need to wrap it with some other executable like cmd file.
Simply create run.cmd file as part of your project
dotnet SampleWebJob.dll
Also make sure that run.cmd is set to be copied to output folder on build/publish of the project
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp2.2</TargetFramework> <LangVersion>latest</LangVersion> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.2.4" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.2.0" /> <PackageReference Include="Serilog.Extensions.Logging" Version="2.0.4" /> <PackageReference Include="Serilog.Extensions.Logging.File" Version="1.1.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" /> </ItemGroup> <ItemGroup> <None Update="run.cmd"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> </ItemGroup> </Project>
Azure has a know issue with BOM encoding, so once you create your run.cmd file, make sure you do not have BOM encoding. The easiest way to do this is to open your run.cmd file with Notepad++ and in the top menu go to Encoding/Encode in UTF-8. This will ensure that BOM bytes are not included in the file
All the snippt code and project are available on GitHub from a public repository https://github.com/dejanstojanovic/Azure-WebJob-Core
References
- Get started with the Azure WebJobs SDK for event-driven background processing
- Run Background tasks with WebJobs in Azure App Service
- Develop and deploy WebJobs using Visual Studio - Azure App Service
- Azure WebJob project template targeting .NET Core
Disclaimer
Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.
Comments for this article