Skip to content

Instantly share code, notes, and snippets.

@eugenpodaru
Last active November 1, 2020 13:45
namespace Devlight.Azure.Functions.Extensions.Startup
{
using System;
public sealed class StartupInfo
{
public string HostName { get; private set; }
public string SiteName { get; private set; }
public string Name { get; private set; }
public string Version { get; private set; }
internal StartupInfo()
{
}
internal static StartupInfo Create(string name, string version) => new StartupInfo
{
Name = name,
Version = version,
HostName = Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME"),
SiteName = Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME")
};
}
}
namespace Devlight.Azure.Functions.Extensions.Startup
{
using System;
using Microsoft.Azure.WebJobs.Description;
[AttributeUsage(AttributeTargets.Parameter)]
[Binding]
public sealed class StartupTriggerAttribute : Attribute
{
}
}
namespace Devlight.Azure.Functions.Extensions.Startup.Bindings
{
using Listeners;
using Microsoft.Azure.WebJobs.Host.Bindings;
using Microsoft.Azure.WebJobs.Host.Listeners;
using Microsoft.Azure.WebJobs.Host.Protocols;
using Microsoft.Azure.WebJobs.Host.Triggers;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
internal sealed class StartupTriggerBinding : ITriggerBinding
{
private readonly ParameterInfo _parameter;
private readonly IOptions<StartupTriggerOptions> _options;
public Type TriggerValueType => typeof(StartupInfo);
public IReadOnlyDictionary<string, Type> BindingDataContract => new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
public StartupTriggerBinding(ParameterInfo parameter, IOptions<StartupTriggerOptions> options)
{
_parameter = parameter ?? throw new ArgumentNullException(nameof(parameter));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
public Task<ITriggerData> BindAsync(object value, ValueBindingContext context)
{
var startupInfo = value as StartupInfo ?? StartupInfo.Create(_options.Value?.Title, _options.Value?.Version);
var valueProvider = new ValueProvider(startupInfo);
var bindingData = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
return Task.FromResult<ITriggerData>(new TriggerData(valueProvider, bindingData));
}
public Task<IListener> CreateListenerAsync(ListenerFactoryContext context)
{
return context == null
? throw new ArgumentNullException(nameof(context))
: Task.FromResult<IListener>(new StartupTriggerListener(context.Executor, _options));
}
public ParameterDescriptor ToParameterDescriptor() => new StartupTriggerParameterDescriptor
{
Name = _parameter.Name
};
private class ValueProvider : IValueProvider
{
private readonly object _value;
public ValueProvider(object value) => _value = value;
public Type Type => typeof(StartupInfo);
public Task<object> GetValueAsync() => Task.FromResult(_value);
public string ToInvokeString() => string.Empty;
}
private class StartupTriggerParameterDescriptor : TriggerParameterDescriptor
{
public override string GetTriggerReason(IDictionary<string, string> _) => "Trigger fired when host started";
}
}
}
namespace Devlight.Azure.Functions.Extensions.Startup.Bindings
{
using System;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Host.Triggers;
using Microsoft.Extensions.Options;
internal sealed class StartupTriggerBindingProvider : ITriggerBindingProvider
{
private readonly IOptions<StartupTriggerOptions> _options;
public StartupTriggerBindingProvider(IOptions<StartupTriggerOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
public Task<ITriggerBinding> TryCreateAsync(TriggerBindingProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var parameter = context.Parameter;
var attribute = parameter.GetCustomAttribute<StartupTriggerAttribute>(inherit: false);
if (attribute == null)
{
return Task.FromResult<ITriggerBinding>(null);
}
if (parameter.ParameterType != typeof(StartupInfo))
{
throw new InvalidOperationException(
string.Format("Can't bind StartupTriggerAttribute to type '{0}'.", parameter.ParameterType));
}
return Task.FromResult<ITriggerBinding>(new StartupTriggerBinding(parameter, _options));
}
}
}
namespace Devlight.Azure.Functions.Extensions.Startup
{
using Bindings;
using Microsoft.Azure.WebJobs.Description;
using Microsoft.Azure.WebJobs.Host.Config;
using Microsoft.Extensions.Options;
using System;
[Extension("Startup Trigger")]
public sealed class StartupTriggerExtensionConfigProvider : IExtensionConfigProvider
{
private readonly IOptions<StartupTriggerOptions> _options;
public StartupTriggerExtensionConfigProvider(IOptions<StartupTriggerOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
public void Initialize(ExtensionConfigContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.AddBindingRule<StartupTriggerAttribute>().BindToTrigger(new StartupTriggerBindingProvider(_options));
}
}
}
using Devlight.Azure.Functions.Extensions.Startup;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Hosting;
[assembly: WebJobsStartup(typeof(StartupTriggerExtensionRegistration), "Startup Trigger")]
namespace Devlight.Azure.Functions.Extensions.Startup
{
public sealed class StartupTriggerExtensionRegistration : IWebJobsStartup
{
public void Configure(IWebJobsBuilder builder) => builder.AddExtension<StartupTriggerExtensionConfigProvider>();
}
}
namespace Devlight.Azure.Functions.Extensions.Startup.Listeners
{
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Host.Executors;
using Microsoft.Azure.WebJobs.Host.Listeners;
using Microsoft.Extensions.Options;
[Singleton(Mode = SingletonMode.Listener)]
internal sealed class StartupTriggerListener : IListener
{
private readonly ITriggeredFunctionExecutor _executor;
private readonly IOptions<StartupTriggerOptions> _options;
private readonly CancellationTokenSource _cancellationTokenSource;
private bool _disposed;
public StartupTriggerListener(ITriggeredFunctionExecutor executor, IOptions<StartupTriggerOptions> options)
{
_executor = executor ?? throw new ArgumentNullException(nameof(executor));
_options = options ?? throw new ArgumentNullException(nameof(options));
_cancellationTokenSource = new CancellationTokenSource();
}
public async Task StartAsync(CancellationToken cancellationToken)
{
ThrowIfDisposed();
await InvokeJobFunction();
}
public Task StopAsync(CancellationToken cancellationToken)
{
ThrowIfDisposed();
_cancellationTokenSource.Cancel();
return Task.FromResult(true);
}
public void Cancel()
{
ThrowIfDisposed();
_cancellationTokenSource.Cancel();
}
public void Dispose()
{
if (!_disposed)
{
_cancellationTokenSource.Cancel();
_cancellationTokenSource.Dispose();
_disposed = true;
}
}
/// <summary>
/// Invokes the job function.
/// </summary>
internal async Task InvokeJobFunction()
{
var token = _cancellationTokenSource.Token;
var startupInfo = StartupInfo.Create(_options.Value?.Title, _options.Value?.Version);
var input = new TriggeredFunctionData
{
TriggerValue = startupInfo
};
try
{
var result = await _executor.TryExecuteAsync(input, token);
if (!result.Succeeded)
{
token.ThrowIfCancellationRequested();
}
}
catch
{
// We don't want any function errors to stop the execution
// schedule. Errors will be logged to Dashboard already.
}
}
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(null);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment