0

I am having an existing webjob(V3.0) in .net core that has a function that is invoked by manual trigger, essentially by a webhook. I want to add another function to the same webjob that should be invoked on a Timer trigger every 20 mins. Is it possible to have both these in the same webjob. If it is possible what should the host configuration that I need to do. I tried going through Microsoft's documentation but there is barely any documentation with respect to the host configuration part with multiple triggers

  • What do you mean `invoked by manual trigger, essentially by a webhook`, you mean one webjob invoke with the REST api and one with timer trigger? – George Chen Oct 22 '19 at 08:16
  • @GeorgeChen yes exactly. I need a single web job where in One function is triggered on a web hook by a REST Api and another one on a timer trigger. I am using WebJobs 3.0 – Phaneendra Satyavolu Oct 23 '19 at 14:54

1 Answers1

0

Yes but your function would need to be triggered by something in Azure Storage like a Queue. This code is probably more then you might need. All of my services implement a custom interface IServiceInvoker. My CTOR asks for an

IEnumerable<IServiceInvoker>

which gets all of the services. Then I either use a constant or a passed in value to determine what service to run. Since I ONLY want ONE function to ever be running I am using the Singleton attribute passing in String.Empty. I also have the following settings on my Queues

b.AddAzureStorage(a =>
{
    a.BatchSize = 1;
    a.NewBatchThreshold = 1;
    a.MaxDequeueCount = 1;
    a.MaxPollingInterval = TimeSpan.FromSeconds(60);
    a.VisibilityTimeout = TimeSpan.FromSeconds(60);
});

Finally I found that during testing I sometimes needed to turn off on or more functions hence the class ServiceConfigurationProvider.

Code sample follows I removed quite a bit of code so YMMV

        public static async Task Main(string[] args)
        {
            await CreateHostBuilder(args).Build().RunAsync();
        }

more code

    public class Functions
    {

        /// <summary>
        /// This scopes the singleton attribute to each individual function rather than the entire host
        /// </summary>
        const String SCOPESINGLETONTOFUNCTION = "";
        readonly ILogger<Functions> _logger;
        readonly Dictionary<String, IServiceInvoker> _services;
        readonly IConfiguration _configuration;

        private Functions()
        {
            _services = new Dictionary<string, IServiceInvoker>();

        }

        public Functions(IEnumerable<IServiceInvoker> services, ILoggerFactory loggerFactory, IServiceProvider serviceProvider, IConfiguration configuration) : this()
        {

            _logger = loggerFactory.CreateLogger<Functions>();
            foreach (var service in services)
            {
                _services.Add(service.ServiceIdentifier, service);
            }
            _configuration = configuration;
        }

        [Disable(typeof(ServiceConfigurationProvider))]
        [Singleton(SCOPESINGLETONTOFUNCTION)]
        public async Task TimerTriggerFunction([TimerTrigger("%TimerTriggerFunctionExpression%")]TimerInfo myTimer, CancellationToken cancellationToken)
        {
            try
            {
                if (_services.TryGetValue("ServiceName", out IServiceInvoker serviceToInvoke))
                {
                    await serviceToInvoke.InvokeServiceAsync(null, cancellationToken, false);
                }
            }
            catch (Exception ex)
            {
                _logger?.LogError(ex, $"Unhandled exception occurred in method:'{nameof(TimerTriggerFunction)}'");
            }
        }

        [Disable(typeof(ServiceConfigurationProvider))]
        [Singleton(SCOPESINGLETONTOFUNCTION)]
        public async Task ServiceInvokerQueueFunction([QueueTrigger("%ServiceInvokerQueueName%", Connection = "AzureWebJobsStorage")] ServiceInvokerMessage serviceInvokerMessage, CancellationToken cancellationToken)
        {
            if (serviceInvokerMessage is null || String.IsNullOrEmpty(serviceInvokerMessage.ServiceIdentifier))
            {
                _logger?.LogError("The queue message received in the ServiceInvokerQueueFunction could not be serialized into a ServiceInvokerMessage instance.");
            }
            else
            {

                Boolean serviceExists = _services.TryGetValue(serviceInvokerMessage.ServiceIdentifier, out IServiceInvoker serviceToInvoke);
                if (serviceExists)
                {
                    try
                    {
                        await serviceToInvoke.InvokeServiceAsync(null, cancellationToken, true);
                    }
                    catch (Exception exception)
                    {
                        _logger?.LogError(exception, $"Unhandled exception occurred in method:'{nameof(ServiceInvokerQueueFunction)}' for service:'{serviceInvokerMessage.ServiceIdentifier}'");
                    }
                }
            }
        }

        [Disable(typeof(ServiceConfigurationProvider))]
        [Singleton(SCOPESINGLETONTOFUNCTION)]
        public async Task RecordQueueFunction([QueueTrigger("%RecordQueueName%", Connection = "RecordConnectString")] string message, CancellationToken cancellationToken)
        {
            {
                _logger?.LogInformation(message);
                try
                {
                    if (_services.TryGetValue("ServiceName", out IServiceInvoker serviceToInvoke))
                    {
                        await serviceToInvoke.InvokeServiceAsync(message, cancellationToken, false);
                    }
                }
                catch (Exception ex)
                {
                    _logger?.LogError(ex, $"Unhandled exception occurred in method:'{nameof(RecordQueueFunction)}'");
                    throw;
                }
            }
        }
    }
public class ServiceConfigurationProvider
{
    readonly IConfiguration _configuration;
    public ServiceConfigurationProvider(IConfiguration configuration)
    {
        _configuration = configuration;
    }
    public bool IsDisabled(MethodInfo method)
    {
        Boolean returnValue = false;
        String resultConfiguration = _configuration[$"{method.Name}Disable"];
        if (!String.IsNullOrEmpty(resultConfiguration))
        {
            Boolean.TryParse(resultConfiguration, out returnValue);
        }
        return returnValue;
    }
}
bmukes
  • 119
  • 2
  • 9
  • Thanks for the reply. But the problem is that I have a single web job that already has an existing function that is invoked manually on a REST Api ( by a web hook) and the same is decoreated with NoAutomaticTrigger attribute and I am now trying to add another function that is triggered on a schedule. Your code looks good but it would be great if there is a way to identify that a particular service\function needs to be invoked on a web hook trigger – Phaneendra Satyavolu Oct 23 '19 at 15:02
  • You mean that I need to modify the REST api to add some dummy value to the queue so that the corresponding web job function can be triggered on that particular queue trigger. Is my understanding correct or am I missing something – Phaneendra Satyavolu Oct 23 '19 at 15:11
  • I understand now. You are following the manually triggered example at [SDK Link](https://learn.microsoft.com/en-us/azure/app-service/webjobs-sdk-how-to#version-3x-1). To do what you want you have to have a continuous web job and trigger the manual portion using an Azure storage queue. Change your program.cs so that you do not specifically call a function and remove the call to 'await host.StopAsync();' then change your manual function to be triggered from a storage queue. Finally add in your new schedued function. – bmukes Oct 24 '19 at 15:05