diff --git a/src/Microsoft.Azure.WebJobs.Host/Executors/JobHostContextFactory.cs b/src/Microsoft.Azure.WebJobs.Host/Executors/JobHostContextFactory.cs index 3b1686078..cb1f037b0 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Executors/JobHostContextFactory.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Executors/JobHostContextFactory.cs @@ -52,6 +52,7 @@ internal class JobHostContextFactory : IJobHostContextFactory private readonly IDrainModeManager _drainModeManager; private readonly IApplicationLifetime _applicationLifetime; private readonly ITargetScalerManager _targetScalerManager; + private readonly IEnumerable _listenerDecorators; public JobHostContextFactory( IDashboardLoggingSetup dashboardLoggingSetup, @@ -75,7 +76,8 @@ public JobHostContextFactory( IScaleMonitorManager monitorManager, IDrainModeManager drainModeManager, IApplicationLifetime applicationLifetime, - ITargetScalerManager targetScalerManager) + ITargetScalerManager targetScalerManager, + IEnumerable listenerDecorators) { _dashboardLoggingSetup = dashboardLoggingSetup; _functionExecutor = functionExecutor; @@ -99,6 +101,7 @@ public JobHostContextFactory( _drainModeManager = drainModeManager; _applicationLifetime = applicationLifetime; _targetScalerManager = targetScalerManager; + _listenerDecorators = listenerDecorators; } public async Task Create(JobHost host, CancellationToken shutdownToken, CancellationToken cancellationToken) @@ -127,8 +130,7 @@ public async Task Create(JobHost host, CancellationToken shutdow // they are started). host.OnHostInitialized(); }; - IListenerFactory functionsListenerFactory = new HostListenerFactory(functions.ReadAll(), _singletonManager, _activator, _nameResolver, _loggerFactory, - _monitorManager, _targetScalerManager, listenersCreatedCallback, _jobHostOptions.Value.AllowPartialHostStartup, _drainModeManager); + IListenerFactory functionsListenerFactory = new HostListenerFactory(functions.ReadAll(), _loggerFactory, _monitorManager, _targetScalerManager, _listenerDecorators, listenersCreatedCallback, _drainModeManager); string hostId = await _hostIdProvider.GetHostIdAsync(cancellationToken); bool dashboardLoggingEnabled = _dashboardLoggingSetup.Setup(functions, functionsListenerFactory, out IFunctionExecutor hostCallExecutor, diff --git a/src/Microsoft.Azure.WebJobs.Host/Hosting/WebJobsServiceCollectionExtensions.cs b/src/Microsoft.Azure.WebJobs.Host/Hosting/WebJobsServiceCollectionExtensions.cs index 0064c92cb..726ef971d 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Hosting/WebJobsServiceCollectionExtensions.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Hosting/WebJobsServiceCollectionExtensions.cs @@ -128,6 +128,8 @@ public static IWebJobsBuilder AddWebJobs(this IServiceCollection services, Actio services.TryAddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); + AddListenerDecorators(services); + services.ConfigureOptions(); services.ConfigureOptions(); services.AddOptions() @@ -210,5 +212,13 @@ private static void AddOptionsLogging(this IServiceCollection services) services.AddSingleton(); services.AddSingleton, LoggerFilterOptionsFormatter>(); } + + private static void AddListenerDecorators(IServiceCollection services) + { + // Order is important for these platform decorator registrations! + // They will be applied in this order after any user registered decorators. + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + } } } diff --git a/src/Microsoft.Azure.WebJobs.Host/Listeners/FunctionListenerDecorator.cs b/src/Microsoft.Azure.WebJobs.Host/Listeners/FunctionListenerDecorator.cs new file mode 100644 index 000000000..79fcdc1da --- /dev/null +++ b/src/Microsoft.Azure.WebJobs.Host/Listeners/FunctionListenerDecorator.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.Azure.WebJobs.Host.Listeners +{ + internal class FunctionListenerDecorator : IListenerDecorator + { + private readonly ILoggerFactory _loggerFactory; + private readonly IOptions _jobHostOptions; + + public FunctionListenerDecorator(ILoggerFactory loggerFactory, IOptions jobHostOptions) + { + _loggerFactory = loggerFactory; + _jobHostOptions = jobHostOptions; + } + + public IListener Decorate(ListenerDecoratorContext context) + { + // wrap the listener with a function listener to handle exceptions + bool allowPartialHostStartup = _jobHostOptions.Value.AllowPartialHostStartup; + return new FunctionListener(context.Listener, context.FunctionDefinition.Descriptor, _loggerFactory, allowPartialHostStartup); + } + } +} diff --git a/src/Microsoft.Azure.WebJobs.Host/Listeners/HostListenerFactory.cs b/src/Microsoft.Azure.WebJobs.Host/Listeners/HostListenerFactory.cs index ce5fe838f..a2542a2cf 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Listeners/HostListenerFactory.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Listeners/HostListenerFactory.cs @@ -14,7 +14,6 @@ using Microsoft.Azure.WebJobs.Logging; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace Microsoft.Azure.WebJobs.Host.Listeners { @@ -23,31 +22,24 @@ internal class HostListenerFactory : IListenerFactory private static readonly MethodInfo JobActivatorCreateMethod = typeof(IJobActivator).GetMethod("CreateInstance", BindingFlags.Public | BindingFlags.Instance).GetGenericMethodDefinition(); private const string IsDisabledFunctionName = "IsDisabled"; private readonly IEnumerable _functionDefinitions; - private readonly SingletonManager _singletonManager; - private readonly IJobActivator _activator; - private readonly INameResolver _nameResolver; private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; - private readonly bool _allowPartialHostStartup; private readonly Action _listenersCreatedCallback; private readonly IScaleMonitorManager _monitorManager; private readonly ITargetScalerManager _targetScalerManager; private readonly IDrainModeManager _drainModeManager; + private readonly IEnumerable _listenerDecorators; - public HostListenerFactory(IEnumerable functionDefinitions, SingletonManager singletonManager, IJobActivator activator, - INameResolver nameResolver, ILoggerFactory loggerFactory, IScaleMonitorManager monitorManager, ITargetScalerManager targetScalerManager, Action listenersCreatedCallback, bool allowPartialHostStartup = false, IDrainModeManager drainModeManager = null) + public HostListenerFactory(IEnumerable functionDefinitions, ILoggerFactory loggerFactory, IScaleMonitorManager monitorManager, ITargetScalerManager targetScalerManager, IEnumerable listenerDecorators, Action listenersCreatedCallback, IDrainModeManager drainModeManager = null) { _functionDefinitions = functionDefinitions; - _singletonManager = singletonManager; - _activator = activator; - _nameResolver = nameResolver; _loggerFactory = loggerFactory; _logger = _loggerFactory?.CreateLogger(LogCategories.Startup); - _allowPartialHostStartup = allowPartialHostStartup; _monitorManager = monitorManager; _targetScalerManager = targetScalerManager; _listenersCreatedCallback = listenersCreatedCallback; _drainModeManager = drainModeManager; + _listenerDecorators = listenerDecorators; } public async Task CreateAsync(CancellationToken cancellationToken) @@ -59,31 +51,22 @@ public async Task CreateAsync(CancellationToken cancellationToken) // Determine if the function is disabled if (functionDefinition.Descriptor.IsDisabled) { - string msg = string.Format("Function '{0}' is disabled", functionDefinition.Descriptor.ShortName); - _logger?.LogInformation(msg); + _logger?.LogInformation($"Function '{functionDefinition.Descriptor.ShortName}' is disabled"); continue; } + // Create the listener IListenerFactory listenerFactory = functionDefinition.ListenerFactory; if (listenerFactory == null) { continue; } - IListener listener = await listenerFactory.CreateAsync(cancellationToken); - RegisterScaleMonitor(listener, _monitorManager); - RegisterTargetScaler(listener, _targetScalerManager); - - // if the listener is a Singleton, wrap it with our SingletonListener - SingletonAttribute singletonAttribute = SingletonManager.GetListenerSingletonOrNull(listener.GetType(), functionDefinition.Descriptor); - if (singletonAttribute != null) - { - listener = new SingletonListener(functionDefinition.Descriptor, singletonAttribute, _singletonManager, listener, _loggerFactory); - } + RegisterScalers(listener); + + listener = ApplyDecorators(listener, functionDefinition); - // wrap the listener with a function listener to handle exceptions - listener = new FunctionListener(listener, functionDefinition.Descriptor, _loggerFactory, _allowPartialHostStartup); listeners.Add(listener); } @@ -91,9 +74,16 @@ public async Task CreateAsync(CancellationToken cancellationToken) var compositeListener = new CompositeListener(listeners); _drainModeManager?.RegisterListener(compositeListener); + return compositeListener; } + internal void RegisterScalers(IListener listener) + { + RegisterScaleMonitor(listener, _monitorManager); + RegisterTargetScaler(listener, _targetScalerManager); + } + /// /// Check to see if the specified listener is an and if so /// register it with the . @@ -229,5 +219,42 @@ internal static bool IsDisabledByProvider(Type providerType, MethodInfo jobFunct return (bool)methodInfo.Invoke(instance, new object[] { jobFunction }); } } + + /// + /// Applies any user registered decorators, followed by any platform decorators. + /// See . + /// + /// The listener to apply decorators to. + /// The function the the listener is for. + /// The resulting listener with decorators applied. + private IListener ApplyDecorators(IListener listener, IFunctionDefinition functionDefinition) + { + Type rootListenerType = listener.GetType(); + var platformDecorators = _listenerDecorators.Where(p => p.GetType().Assembly == typeof(HostListenerFactory).Assembly); + var userDecorators = _listenerDecorators.Except(platformDecorators); + + listener = ApplyDecorators(userDecorators, listener, functionDefinition, rootListenerType, writeLog: true); + + // Order is important - platform decorators must be applied AFTER any user decorators, in order. + listener = ApplyDecorators(platformDecorators, listener, functionDefinition, rootListenerType); + + return listener; + } + + private IListener ApplyDecorators(IEnumerable decorators, IListener listener, IFunctionDefinition functionDefinition, Type rootListenerType, bool writeLog = false) + { + foreach (var decorator in decorators) + { + var context = new ListenerDecoratorContext(functionDefinition, rootListenerType, listener); + listener = decorator.Decorate(context); + + if (writeLog) + { + _logger.LogDebug($"Applying IListenerDecorator '{decorator.GetType().FullName}'"); + } + } + + return listener; + } } } diff --git a/src/Microsoft.Azure.WebJobs.Host/Listeners/IListenerDecorator.cs b/src/Microsoft.Azure.WebJobs.Host/Listeners/IListenerDecorator.cs new file mode 100644 index 000000000..32b05d32a --- /dev/null +++ b/src/Microsoft.Azure.WebJobs.Host/Listeners/IListenerDecorator.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace Microsoft.Azure.WebJobs.Host.Listeners +{ + /// + /// Custom decorator interface called during listener creation to + /// allow function listeners to be customized. + /// + public interface IListenerDecorator + { + /// + /// Creates a listener. + /// + /// The listener context. + /// The listener to use. This may be a new wrapped listener, or the original + /// listener. + IListener Decorate(ListenerDecoratorContext context); + } +} \ No newline at end of file diff --git a/src/Microsoft.Azure.WebJobs.Host/Listeners/ListenerDecoratorContext.cs b/src/Microsoft.Azure.WebJobs.Host/Listeners/ListenerDecoratorContext.cs new file mode 100644 index 000000000..6da3b2017 --- /dev/null +++ b/src/Microsoft.Azure.WebJobs.Host/Listeners/ListenerDecoratorContext.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.Azure.WebJobs.Host.Indexers; +using System; + +namespace Microsoft.Azure.WebJobs.Host.Listeners +{ + /// + /// Context class for . + /// + public class ListenerDecoratorContext + { + /// + /// Constructs an instance. + /// + /// The function the specified listener is for. + /// Gets the type of the root listener. + /// The listener to decorate. + public ListenerDecoratorContext(IFunctionDefinition functionDefinition, Type rootListenerType, IListener listener) + { + FunctionDefinition = functionDefinition; + ListenerType = rootListenerType; + Listener = listener; + } + + /// + /// Gets the the specified listener is for. + /// + public IFunctionDefinition FunctionDefinition { get; } + + /// + /// Gets the listener to decorate. + /// + public IListener Listener { get; } + + /// + /// Gets the type of the root listener. + /// + public Type ListenerType { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.Azure.WebJobs.Host/Listeners/SingletonListenerDecorator.cs b/src/Microsoft.Azure.WebJobs.Host/Listeners/SingletonListenerDecorator.cs new file mode 100644 index 000000000..76a0b6a2f --- /dev/null +++ b/src/Microsoft.Azure.WebJobs.Host/Listeners/SingletonListenerDecorator.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.Azure.WebJobs.Host.Listeners +{ + internal class SingletonListenerDecorator : IListenerDecorator + { + private readonly SingletonManager _singletonManager; + private readonly ILoggerFactory _loggerFactory; + + public SingletonListenerDecorator(SingletonManager singletonManager, ILoggerFactory loggerFactory) + { + _singletonManager = singletonManager; + _loggerFactory = loggerFactory; + } + + public IListener Decorate(ListenerDecoratorContext context) + { + var functionDescriptor = context.FunctionDefinition.Descriptor; + + // if the listener is a Singleton, wrap it with our SingletonListener + IListener listener = context.Listener; + SingletonAttribute singletonAttribute = SingletonManager.GetListenerSingletonOrNull(context.ListenerType, functionDescriptor); + if (singletonAttribute != null) + { + listener = new SingletonListener(functionDescriptor, singletonAttribute, _singletonManager, listener, _loggerFactory); + } + + return listener; + } + } +} diff --git a/test/Microsoft.Azure.WebJobs.Host.FunctionalTests/HostListenerFactoryTests.cs b/test/Microsoft.Azure.WebJobs.Host.FunctionalTests/HostListenerFactoryTests.cs index 7a467c5a9..dc0b55cd8 100644 --- a/test/Microsoft.Azure.WebJobs.Host.FunctionalTests/HostListenerFactoryTests.cs +++ b/test/Microsoft.Azure.WebJobs.Host.FunctionalTests/HostListenerFactoryTests.cs @@ -13,10 +13,12 @@ using Microsoft.Azure.WebJobs.Host.Protocols; using Microsoft.Azure.WebJobs.Host.Scale; using Microsoft.Azure.WebJobs.Host.TestCommon; +using Microsoft.Azure.WebJobs.Logging; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Moq; using Xunit; @@ -25,17 +27,52 @@ namespace Microsoft.Azure.WebJobs.Host.UnitTests.Listeners public class HostListenerFactoryTests { private readonly IJobActivator _jobActivator; - private IConfiguration _configuration = new ConfigurationBuilder().Build(); + private readonly IHost _testHost; + private IConfiguration _configuration; public HostListenerFactoryTests() { - var hostBuilder = new HostBuilder(); - var host = hostBuilder.Build(); - var serviceProvider = (IServiceProvider)host.Services.GetService(typeof(IServiceProvider)); - _jobActivator = new DefaultJobActivator(serviceProvider); - DisableProvider_Static.Method = null; DisableProvider_Instance.Method = null; + + _testHost = CreateTestJobHost(); + _jobActivator = _testHost.Services.GetService(); + _configuration = _testHost.Services.GetService(); + } + + [Fact] + public async Task CreateAsync_AppliesListenerDecorators() + { + HostListenerFactory factory = await CreateHostListenerFactoryAsync(); + IListener listener = await factory.CreateAsync(CancellationToken.None); + + // access the root inner listener + var innerListeners = ((IEnumerable)listener).ToArray(); + var innerListener = innerListeners[0]; + + // expect the first two outer listeners to be our platform listeners + FunctionListener functionListener = (FunctionListener)innerListener; + var innerListenerField = typeof(FunctionListener).GetField("_listener", BindingFlags.NonPublic | BindingFlags.Instance); + innerListener = (SingletonListener)innerListenerField.GetValue(functionListener); + + innerListenerField = typeof(SingletonListener).GetField("_innerListener", BindingFlags.NonPublic | BindingFlags.Instance); + innerListener = (IListener)innerListenerField.GetValue(innerListener); + + TestListenerDecorator.DecoratorListener decoratorListener = (TestListenerDecorator.DecoratorListener)innerListener; + + // verify all decorators were consulted, resulting in a nested stack of listeners + Assert.Equal("C", decoratorListener.Tag); + decoratorListener = (TestListenerDecorator.DecoratorListener)decoratorListener.InnerListener; + Assert.Equal("B", decoratorListener.Tag); + decoratorListener = (TestListenerDecorator.DecoratorListener)decoratorListener.InnerListener; + Assert.Equal("A", decoratorListener.Tag); + + var logProvider = _testHost.GetTestLoggerProvider(); + var logs = logProvider.GetAllLogMessages().Where(p => p.Category == LogCategories.Startup && p.FormattedMessage.Contains(nameof(IListenerDecorator))).ToArray(); + Assert.Equal(3, logs.Length); + Assert.Equal($"Applying IListenerDecorator '{typeof(TestListenerDecoratorA).FullName}'", logs[0].FormattedMessage); + Assert.Equal($"Applying IListenerDecorator '{typeof(TestListenerDecoratorB).FullName}'", logs[1].FormattedMessage); + Assert.Equal($"Applying IListenerDecorator '{typeof(TestListenerDecoratorC).FullName}'", logs[2].FormattedMessage); } [Fact] @@ -61,7 +98,8 @@ public async Task CreateAsync_RegistersScaleMonitors() var monitorManager = new ScaleMonitorManager(); var targetScaleManager = new TargetScalerManager(); var drainModeManagerMock = new Mock(); - HostListenerFactory factory = new HostListenerFactory(functions, singletonManager, _jobActivator, null, loggerFactory, monitorManager, targetScaleManager, () => { }, false, drainModeManagerMock.Object); + var listenerDecorators = _testHost.Services.GetServices(); + HostListenerFactory factory = new HostListenerFactory(functions, loggerFactory, monitorManager, targetScaleManager, listenerDecorators, () => { }, drainModeManagerMock.Object); IListener listener = await factory.CreateAsync(CancellationToken.None); var innerListeners = ((IEnumerable)listener).ToArray(); @@ -94,7 +132,8 @@ public async Task CreateAsync_RegistersTargetScalers() var monitorManager = new ScaleMonitorManager(); var targetScaleManager = new TargetScalerManager(); var drainModeManagerMock = new Mock(); - HostListenerFactory factory = new HostListenerFactory(functions, singletonManager, _jobActivator, null, loggerFactory, monitorManager, targetScaleManager, () => { }, false, drainModeManagerMock.Object); + var listenerDecorators = _testHost.Services.GetServices(); + HostListenerFactory factory = new HostListenerFactory(functions, loggerFactory, monitorManager, targetScaleManager, listenerDecorators, () => { }, drainModeManagerMock.Object); IListener listener = await factory.CreateAsync(CancellationToken.None); var innerListeners = ((IEnumerable)listener).ToArray(); @@ -204,7 +243,8 @@ public async Task CreateAsync_SkipsDisabledFunctions(Type jobType, string method var monitorManagerMock = new Mock(MockBehavior.Strict); var targetScalerManagerMock = new Mock(MockBehavior.Strict); var drainModeManagerMock = new Mock(); - HostListenerFactory factory = new HostListenerFactory(functions, singletonManager, _jobActivator, null, loggerFactory, monitorManagerMock.Object, targetScalerManagerMock.Object, () => { }, false, drainModeManagerMock.Object); + var listenerDecorators = _testHost.Services.GetServices(); + HostListenerFactory factory = new HostListenerFactory(functions, loggerFactory, monitorManagerMock.Object, targetScalerManagerMock.Object, listenerDecorators, () => { }, drainModeManagerMock.Object); IListener listener = await factory.CreateAsync(CancellationToken.None); @@ -271,7 +311,71 @@ public void IsDisabledBySetting_BindsSettingName(string settingName, bool disabl Assert.Equal(result, disabled); } - public static class Functions1 + private async Task CreateHostListenerFactoryAsync() + { + var indexProvider = _testHost.Services.GetService(); + var index = await indexProvider.GetAsync(CancellationToken.None); + List functions = new List(); + functions.Add(index.LookupByName("TestJob")); + + // create the listener + ILoggerFactory loggerFactory = _testHost.Services.GetService(); + var monitorManager = _testHost.Services.GetService(); + var targetScaleManager = _testHost.Services.GetService(); + var drainModeManager = _testHost.Services.GetService(); + var listenerDecorators = _testHost.Services.GetServices(); + HostListenerFactory factory = new HostListenerFactory(functions, loggerFactory, monitorManager, targetScaleManager, listenerDecorators, () => { }, drainModeManager); + + return factory; + } + + private IHost CreateTestJobHost(Action extraConfig = null) + { + var hostBuilder = new HostBuilder() + .ConfigureDefaultTestHost(b => + { + // For Queue and Blob Triggers + b.AddAzureStorageBlobs(); + b.AddAzureStorageQueues(); + + b.AddAzureStorageCoreServices(); + }) + .ConfigureLogging((context, b) => + { + b.SetMinimumLevel(LogLevel.Debug); + }) + .ConfigureServices(services => + { + // add some custom decorators that we expect to be applied in order, before any platform + // decorators + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + }); + + extraConfig?.Invoke(hostBuilder); // test hook gets final say to replace. + + IHost host = hostBuilder.Build(); + + return host; + } + + private class TestJobActivator : IJobActivator + { + private int _hostId; + + public TestJobActivator(int hostId) + { + _hostId = hostId; + } + + public T CreateInstance() + { + return (T)Activator.CreateInstance(typeof(T), _hostId); + } + } + + public class Functions1 { public static void DisabledAtParameterLevel( [Disable("DisableSettingTrue")] @@ -338,6 +442,7 @@ public static void DisabledByAppSetting_FunctionNameAttributeTest_Linux( { } + [Singleton(Mode = SingletonMode.Listener)] public static void TestJob( [QueueTrigger("test")] string message) { @@ -386,6 +491,95 @@ public static void IsDisabled(MethodInfo method) } } + public class TestListenerDecoratorA : TestListenerDecorator + { + public TestListenerDecoratorA() : base("A") + { + } + } + + public class TestListenerDecoratorB : TestListenerDecorator + { + public TestListenerDecoratorB() : base("B") + { + } + } + + public class TestListenerDecoratorC : TestListenerDecorator + { + public TestListenerDecoratorC() : base("C") + { + } + } + + + public abstract class TestListenerDecorator : IListenerDecorator + { + private string _tag; + + public TestListenerDecorator(string tag) + { + _tag = tag; + } + + public IListener Decorate(ListenerDecoratorContext context) + { + return new DecoratorListener(context.Listener, _tag); + } + + public class DecoratorListener : IListener + { + public DecoratorListener(IListener innerListener, string tag) + { + InnerListener = innerListener; + Tag = tag; + } + + internal string Tag { get; } + + internal IListener InnerListener { get; set; } + + public void Cancel() + { + } + + public void Dispose() + { + } + + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } + } + + public class TestListener : IListener + { + public void Cancel() + { + } + + public void Dispose() + { + } + + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } + public class TestListener_Monitor : IListener, IScaleMonitor { public ScaleMonitorDescriptor Descriptor => throw new NotImplementedException(); diff --git a/test/Microsoft.Azure.WebJobs.Host.UnitTests/PublicSurfaceTests.cs b/test/Microsoft.Azure.WebJobs.Host.UnitTests/PublicSurfaceTests.cs index 2538177ee..abb1f69fc 100644 --- a/test/Microsoft.Azure.WebJobs.Host.UnitTests/PublicSurfaceTests.cs +++ b/test/Microsoft.Azure.WebJobs.Host.UnitTests/PublicSurfaceTests.cs @@ -310,7 +310,9 @@ public void WebJobs_Host_VerifyPublicSurfaceArea() "IScaleStatusProvider", "ScaleOptions", "TriggerMetadata", - "AggregateScaleStatus" + "AggregateScaleStatus", + "IListenerDecorator", + "ListenerDecoratorContext" }; TestHelpers.AssertPublicTypes(expected, assembly);