3

In Azure Functions you can use the nomenclature of %token% to have at runtime that token being replaced by an Environment Variable. e.g.

 public Task FunctionName(
     [ServiceBusTrigger("%MessageTopic%",
         "%MessageSubscription%",
         Connection = "ConnectionString")]
     ServiceBusReceivedMessage message,
     [DurableClient] DurableTaskClient durableOrchestrationClient)

Before Isolated Functions you could create an implementation of INameResolver to allow you to perform your own lookup and replacement of these tokens.

In Isolated Functions INameProvider no longer exists.

How can you perform this same functionality?

2 Answers 2

7

From what I can see the resolving of the %token% value from Environment variables is done in Func.exe / the Host in Azure Functions.

The Metadata that is needed for the runner to register with all the queue etc is sent across via a GRPC call on start up.

This metadata is generated at design time by a Code Generator and is in a class which implements IFunctionMetadataProvider.

I have been able to create a new class that implements decorate the actual code generated class that generates the metadata:

internal class ConfigurableFunctionMetadataProvider(IFunctionMetadataProvider baseProvider, IConfiguration configuration, ILogger<ConfigurableFunctionMetadataProvider> logger) : IFunctionMetadataProvider
{
  private static readonly Regex RegexExpression = new Regex("%([^%\"]*)%", RegexOptions.Compiled, TimeSpan.FromSeconds(5));
  public Task<ImmutableArray<IFunctionMetadata>> GetFunctionMetadataAsync(string directory) =>
    baseProvider.GetFunctionMetadataAsync(directory).ContinueWith(task =>
    {
      var functionMetadataArray = task.Result;
      foreach (var metadata in functionMetadataArray)
      {
        for (int i = 0; i < metadata.RawBindings.Count; i++)
        {
          Match matchResults = RegexExpression.Match(metadata.RawBindings[i]);
          while (matchResults.Success)
          {
            var replacement = configuration.GetValue<string>(matchResults.Groups[1].Value);
            if (replacement != null)
            {
              logger.LogInformation("Replacing token '{TokenName}' in '{FunctionName}' with '{Value}'", matchResults.Groups[0].Value, metadata.Name, replacement);
              metadata.RawBindings[i] = metadata.RawBindings[i].Replace(matchResults.Groups[0].Value, replacement);
            }

            matchResults = matchResults.NextMatch();
          }
        }
      }

      return functionMetadataArray;
    });
}

Then where you declare your dependency injection add the following line (using scrutor from nuget):

services.Decorate<IFunctionMetadataProvider, ConfigurableFunctionMetadataProvider>();

The code will also log out the fact it has made a substitution.

Sign up to request clarification or add additional context in comments.

2 Comments

I looked all over for this solution. Works flawlessly and after 3 days of dealing with this conversion to isolated functions and trying to use my dynamic queue names, I can safely say this is the only way to deal with having lost INameResolver. Thank you sir.
This solution is correct and requires more upvotes. It took me way too long to discover.
0

This approach does not require scrutor, but instead simply adds to the DI (IServiceCollection). It does rely on the current implementation detail where there are two IFunctionMetadataProvider implementations already registered.

I borrowed from https://github.com/Azure/azure-webjobs-sdk/issues/3070#issuecomment-2455260085 to create a slightly more polished version complete with extension methods to make it easy to add to the DI.

My code is here: https://gist.github.com/alasdaircs/78f941c6ce007a9f3ac6ce430abedaca

Reproduced here for posterity:

using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

using System.Collections.Immutable;
using System.Text.RegularExpressions;

namespace AcsSolutions.Extensions.Azure.Functions;

/// <summary>
/// This class provides a configuration-bound implementation of <see cref="IFunctionMetadataProvider"/>.
/// It exists because the default <see cref="IFunctionMetadataProvider"/> does not resolve binding from the application's configuration
/// See <see href="https://learn.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-process-guide?tabs=ihostapplicationbuilder%2Cwindows#:~:text=Custom%20configuration%20sources,Configuration%20references%20features"/>
/// </summary>
internal partial class ConfigurationBoundFunctionMetadataProvider
    : IFunctionMetadataProvider
{
    [GeneratedRegex( "%(.+?)%" )]
    private static partial Regex SettingPlaceholderRegex();

    private readonly IServiceProvider _serviceProvider;
    private readonly IConfiguration _configuration;
    private readonly ILogger<ConfigurationBoundFunctionMetadataProvider> _logger;

    public ConfigurationBoundFunctionMetadataProvider(
        IServiceProvider serviceProvider,
        IConfiguration configuration,
        ILogger<ConfigurationBoundFunctionMetadataProvider> logger
    )
    {
        _serviceProvider = serviceProvider;
        _configuration = configuration;
        _logger = logger;
    }

    public async Task<ImmutableArray<IFunctionMetadata>> GetFunctionMetadataAsync( string directory )
    {
        var providers = _serviceProvider.GetServices<IFunctionMetadataProvider>().ToList();

        // There will already be two Singleton instances of IFunctionMetadataProvider registered:
        // 1. The default one provided by the Azure Functions SDK.
        // 2. The one generated by the Microsoft.Azure.Functions.Worker.Core.FunctionMetadata assembly.
        // We want to find the last one, but not accidentally find this one.
        var provider = providers.Last( x => x.GetType() != this.GetType() );
        // Get the function metadata from the last registered IFunctionMetadataProvider
        // This will still have the placeholders in the bindings, which we will replace with the actual values from the configuration.
        var list = await provider.GetFunctionMetadataAsync( directory );
        var result = new List<IFunctionMetadata>();

        var settingRegex = SettingPlaceholderRegex();
        foreach( var function in list )
        {
            for( var i = 0; i < function.RawBindings?.Count; i++ )
            {
                // The RawBindings property is a JSON string which may contain placeholders like %App:Jobs:FunctionName%
                var binding = function.RawBindings[i];
                var value = settingRegex.Replace(
                    binding,
                    m =>
                    {
                        var key = m.Groups[1].Value;
                        var value = _configuration[key];
                        if( value == null )
                        {
                            _logger.LogWarning(
                                "Placeholder '{Placeholder}' not found in configuration in {Function}",
                                key,
                                function.Name
                            );
                            value = key; // If the placeholder is not found, we keep the key as the value
                        }
                        else {
                            _logger.LogDebug(
                                "Replacing placeholder '{Placeholder}' with value '{Value}' from configuration in {Function}",
                                key,
                                value,
                                function.Name
                            );
                        }

                        return value;
                    }
                );

                // Update this RawBinding for the function with the placeholder replaced by the value from configuration
                function.RawBindings[i] = value;
            }

            // Add the function metadata to the result list
            result.Add( function );
        }

        // Return the result as an ImmutableArray
        return [.. result];
    }

}

public static class ConfigurationBoundFunctionMetadataProviderExtensionsr
{
    /// <summary>
    /// Adds a configuration-bound implementation of <see cref="IFunctionMetadataProvider"/> to the service collection.
    /// </summary>
    /// <remarks>This method registers <see cref="ConfigurationBoundFunctionMetadataProvider"/> as a singleton
    /// service for the <see cref="IFunctionMetadataProvider"/> interface. Use this method to enable function metadata
    /// retrieval based on configuration settings.</remarks>
    /// <param name="services">The <see cref="IServiceCollection"/> to which the provider will be added.</param>
    /// <returns>The updated <see cref="IServiceCollection"/> instance.</returns>
    public static IServiceCollection AddConfigurationBoundFunctionMetadataProvider(
        this IServiceCollection services
    )
        => services.AddSingleton<IFunctionMetadataProvider, ConfigurationBoundFunctionMetadataProvider>();

    /// <summary>
    /// Adds a configuration-bound implementation of <see cref="IFunctionMetadataProvider"/> to the service collection.
    /// </summary>
    /// <remarks>This method registers a singleton instance of <see
    /// cref="ConfigurationBoundFunctionMetadataProvider"/> as the implementation of <see
    /// cref="IFunctionMetadataProvider"/>. The provider uses the specified <paramref name="configuration"/> to retrieve
    /// and manage function metadata.</remarks>
    /// <param name="services">The <see cref="IServiceCollection"/> to which the provider will be added.</param>
    /// <param name="configuration">The <see cref="IConfiguration"/> instance used to bind function metadata.</param>
    /// <returns>The updated <see cref="IServiceCollection"/> instance.</returns>
    public static IServiceCollection AddConfigurationBoundFunctionMetadataProvider(
        this IServiceCollection services,
        IConfiguration configuration
    )
        => services.AddSingleton<IFunctionMetadataProvider, ConfigurationBoundFunctionMetadataProvider>(
            svc => new ConfigurationBoundFunctionMetadataProvider(
                svc,
                configuration,
                svc.GetRequiredService<ILogger<ConfigurationBoundFunctionMetadataProvider>>()
            )
        );
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.