azure-webjobs-sdk: Breaking change in ExtensionConfigContext

As noted here a lot of dependency injection stuff is build that relies on getting the JobHostConfiguration from the ExtensionConfigContext. With the new beta8 package the Config is gone. This is the old beta5 code:

public class ExtensionConfigContext : FluentConverterRules<Attribute, ExtensionConfigContext>
{
    public ExtensionConfigContext();

    public JobHostConfiguration Config { get; set; }

    public FluentBindingRule<TAttribute> AddBindingRule<TAttribute>() where TAttribute : Attribute; 
    [Obsolete("preview")]
    public Uri GetWebhookHandler();
    }

Now we are not able to build an Inject binding to inject certain dependencies into our functions. Like we did before:

public class InjectConfiguration : IExtensionConfigProvider
{
	public void Initialize(ExtensionConfigContext context)
	{
		var rule = context
					.AddBindingRule<InjectAttribute>()
					.Bind(new InjectBindingProvider());

		var registry = context.Config.GetService<IExtensionRegistry>();

		var filter = new ScopeCleanupFilter();
		registry.RegisterExtension(typeof(IFunctionInvocationFilter), filter);
		registry.RegisterExtension(typeof(IFunctionExceptionFilter), filter);
	}
}

Do you have any alternatives for dependency injection? And if so please show an example.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 7
  • Comments: 36 (1 by maintainers)

Most upvoted comments

<!--
  When doing a Pack, MS extensions metadata generator calls  _GenerateFunctionsExtensionsMetadataPostPublish...
  but _GenerateFunctionsExtensionsMetadataPostPublish actually executes before the function dll itself (i.e. AzureFunctionApp.dll) has been copied
  to the publish dir by _FunctionsPostPublish. This target copies the AzureFunctionApp.dll to the publish dir much earlier so that the extensions metadata
  generator finds it when it searches dlls for extensions (i.e. IWebJobsStartup implementations).
  -->
  <Target Name="CopyTargetPathToEarlyPublish" BeforeTargets="Publish">
    <Copy SourceFiles="$(TargetPath)" DestinationFiles="$(FunctionsTargetPath)" />
  </Target>

@DibranMulder I think you have seen the new extension model, and the DI changes.

With those changes to create a extension now you first need to create a startup class, using [assembly:WebJobsStartup] and implementing IWebJobsStartup interface, there you can add your own services to the builder via builder.Services and register your extensions config provider:

[assembly: WebJobsStartup(typeof(WebJobsExtensionStartup ), "A Web Jobs Extension Sample")]
namespace ExtensionSample
{
    public class WebJobsExtensionStartup : IWebJobsStartup
    {
        public void Configure(IWebJobsBuilder builder)
        {
             builder.Services.AddSingleton<InjectBindingProvider>();
             //Registering an extension
             builder.AddExtension<InjectConfiguration>(); //AddExtension returns a builder that allows extending the configuration model
        }
    } 
}

Then in your IExtensionConfigProvider you can inject any dependencies via constructor injections, for example, binding, bindingproviders, or any custom dependency.

However you can’t request an IExtensionRegistry inside an IExtensionConfigProvider due to a circular dependency issue, breaking the host with a StackOverflowException #1872.

Looking at the latest Filter tests you can register your filter with DI in the Configure method of the Startup class, as an IFunctionFilter with the Singleton lifetime and the Web Jobs framework will figure out if it is a Pre, Post or Exception filter:

public class WebJobsExtensionStartup : IWebJobsStartup
    {
        public void Configure(IWebJobsBuilder builder)
        {
             builder.Services.AddSingleton<InjectBindingProvider>();

             //Registering a filter
             builder.Services.AddSingleton<IFunctionFilter, ScopeCleanupFilter>();

             //Registering an extension
             builder.AddExtension<InjectConfiguration>(); //AddExtension returns a builder that allows extending the configuration model
 
        }
    } 

To get the host to load the extension, it must be registered in bin/extensions.json file, in JavaScript or Java via func extensions command, and in C# the SDK 1.0.19 does it automatically at build time for the current function project or any dependency (ProjectReference or PackageReference) in the current project.

I have adapted your samples to use the built-in IServiceProvider, also configuring the services into the framework’s own container instance, and creating scopes before function calls.

FYI

I found this reference in case somebody is interested, organic DI still work in progress.

Azure Functions Live Stream from 27th September 2018 (from 27th minute - till around 37th minute) https://youtu.be/7mAzMYOP9NY?t=27m19s

Example from the video (Note again, organic DI not yet available).

No static methods 😄.

image

@DibranMulder you don’t need to build the service collection, you can add your services, then they will be injected in your extension config provider, including the current IServiceProvider:

[assembly: WebJobsStartup(typeof(WebJobsExtensionStartup), "A Web Jobs Extension Sample")]
namespace Company.Bla
{
    public class WebJobsExtensionStartup : IWebJobsStartup
    {
        public void Configure(IWebJobsBuilder builder)
        {
            IConfigurationRoot config = new ConfigurationBuilder()
                .SetBasePath(Environment.CurrentDirectory)
                .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables()
                .Build();

            var connectionString = config.GetConnectionString("SqlConnectionString");

            builder.Services.AddDbContext<EfContext>(options => options.UseSqlServer(connectionString));

            builder.Services.AddSingleton<InjectBindingProvider>();
            builder.AddExtension<InjectConfiguration>();
        }
    }
}

public class InjectConfiguration : IExtensionConfigProvider
       {
            private readonly InjectBindingProvider _InjectBindingProvider;

            public InjectConfiguration(InjectBindingProvider injectBindingProvider)
            {
                _InjectBindingProvider = injectBindingProvider;
             }

            public void Initialize(ExtensionConfigContext context)
            {
                context
                    .AddBindingRule<InjectAttribute>()
                    .Bind(_InjectBindingProvider);
            }
       }

That way you can mix and match your app services with those coming from the framework. The azure function host controls now the lifetime of the root service provider, and builds it before calling any IExtensionConfigProvider.

I also had to only use the IWebJobsBuilder.Services as the injection point to add the extension for the binding -- adding my services in to that didn't work

@ryanspletzer I didn’t notice that behavior, maybe it was caused by the early building of ServiceProvider. however I did saw some subtle differences in behavior between the Webjobs SDK, the ASP.NET Service Provider and the current DI implementation in WebJobs.WebScript (aka functions), I suspect that what is causing the differences is the extraneus choice of using DryIOC behind the scenes in the functions hosts. Those differences forced me to rollback to a separate service collection for application services and only use the built in one for framework things (like filters), I will open a new issue soon. Issue opened: https://github.com/Azure/azure-functions-host/issues/3399

Update: I see that @cResults already published how to inject dependencies into IExtensiongConfigProvider.

public class InjectBindingProvider : IBindingProvider
{
	public static readonly ConcurrentDictionary<Guid, IServiceScope> Scopes =
		new ConcurrentDictionary<Guid, IServiceScope>();

	private IServiceProvider _serviceProvider;

	public InjectBindingProvider(IServiceProvider serviceProvider)
	{
		_serviceProvider = serviceProvider;
	}

	public Task<IBinding> TryCreateAsync(BindingProviderContext context)
	{
		IBinding binding = new InjectBinding(_serviceProvider, context.Parameter.ParameterType);
		return Task.FromResult(binding);
	}
}

internal class InjectBinding : IBinding
{
	private readonly Type _type;
	private readonly IServiceProvider _serviceProvider;

	internal InjectBinding(IServiceProvider serviceProvider, Type type)
	{
		_type = type;
		_serviceProvider = serviceProvider;
	}

	public bool FromAttribute => true;

	public Task<IValueProvider> BindAsync(object value, ValueBindingContext context) =>
		Task.FromResult((IValueProvider)new InjectValueProvider(value));

	public async Task<IValueProvider> BindAsync(BindingContext context)
	{
		await Task.Yield();
		var scope = InjectBindingProvider.Scopes.GetOrAdd(context.FunctionInstanceId, (_) => _serviceProvider.CreateScope());
		var value = scope.ServiceProvider.GetRequiredService(_type);
		return await BindAsync(value, context.ValueContext);
	}

	public ParameterDescriptor ToParameterDescriptor() => new ParameterDescriptor();

	private class InjectValueProvider : IValueProvider
	{
		private readonly object _value;

		public InjectValueProvider(object value) => _value = value;

		public Type Type => _value.GetType();

		public Task<object> GetValueAsync() => Task.FromResult(_value);

		public string ToInvokeString() => _value.ToString();
	}
}

@ryanspletzer I was able to get a logger into the servicecollection. i had a number of iterations that LOOKED a lot better, but this one works for now.

This appears to be working. I say appears because my test requires an ILogger, which I’m still tyring how to create in the current environment. Anyway, when InjectConfiguration.Initialize is called, _InjectBindingProvider was populated with the instance registered in the start up.

        public class InjectConfiguration : IExtensionConfigProvider
       {
            private readonly InjectBindingProvider _InjectBindingProvider;

            public InjectConfiguration(InjectBindingProvider injectBindingProvider)
            {
                _InjectBindingProvider = injectBindingProvider;
             }

            public void Initialize(ExtensionConfigContext context)
            {
                context
                    .AddBindingRule<InjectAttribute>()
                    .Bind(_InjectBindingProvider);
            }
       }

@ryanspletzer mine is based on @BorisWilHems as well. Then followed the File/Folder layout from https://github.com/Azure/azure-webjobs-sdk-extensions

Thanks got it to work! The code below might be interesting for people reading this thread. Do you see any bad practices or things you should do different?

[assembly: WebJobsStartup(typeof(WebJobsExtensionStartup), "A Web Jobs Extension Sample")]
namespace Company.Bla
{
    public class WebJobsExtensionStartup : IWebJobsStartup
    {
        public void Configure(IWebJobsBuilder builder)
        {
            IConfigurationRoot config = new ConfigurationBuilder()
                .SetBasePath(Environment.CurrentDirectory)
                .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables()
                .Build();

            var connectionString = config.GetConnectionString("SqlConnectionString");

            builder.Services.AddDbContext<EfContext>(options => options.UseSqlServer(connectionString));

            ServiceProvider serviceProvider = builder.Services.BuildServiceProvider(true);

            builder.Services.AddSingleton(new InjectBindingProvider(serviceProvider));
            builder.AddExtension<InjectConfiguration>();
        }
    }
}

Yes it works with AzFunc Here is my webjobs DI extension https://github.com/espray/azure-webjobs-sdk-extensions

@chris31389 I also had issues with DI and implement simple DI base on @ielcoro answer : https://github.com/ArtemTereshkovich/DependencyInjectionAzureFunction