Las ventajas que te ofrece Microsoft Azure y el mundo.NET

Azure Functions y Dependency Injection con Autofac: Juntos pero no revueltos

El mundo de Serverless mola mogollón ¿quién no lo ha probado alguna vez? ¿Te has visto en la tesitura de aplicar buenas prácticas como la inyección de dependencias en tus Azure Functions y has visto que se te complica la vida? Pues con este post verás que es muy sencillo aplicar este concepto.

Lo primero, cómo acceder a nuestra configuración vía inyección de dependencias.

En Asp .Net Core tenemos una cosa muy chula que es poder acceder a nuestra configuración e inyectarla por DI vía interfaz IConfiguration. Esto lo podemos realizar de una forma muy sencilla desde nuestra clase Startup.cs, ya que viene como parámetro nuestro amado IConfiguration.

  
public partial class Startup
{
	public IConfiguration Configuration { get; }
	private IHostingEnvironment Environment { get; }
		   
	public Startup(IConfiguration configuration, IHostingEnvironment environment)
	{
		Configuration = configuration;
		Environment = environment;
	}         
			 
 . . . . . . .
}

¿Cómo podemos hacer esto en una Azure Function? No vemos en ningún sitio que el método de inicialización tenga un IConfiguration como parámetro.
Lo primero, será crearnos un servicio para recuperar la variable de entorno ASPNETCORE_ENVIRONMENT.

Dicho servicio emplea la siguiente clase estática de constantes.

  
namespace MyFunctions.Common
{
    public static partial class Constants
    {
        public static class Environments
        {
            public static string Production => "Production";
        }

        public static class EnvironmentVariables
        {
            public static string AspnetCoreEnvironment => "ASPNETCORE_ENVIRONMENT";
        }
    }
}

Ahora toca codificar nuestro servicio de Environments

  
using System;
using static MyFunctions.Common.Constants;

namespace MyFunctions.Common
{
    public interface IEnvironmentService
    {
        string EnvironmentName { get; set; }
    }

    public class EnvironmentService : IEnvironmentService
    {
        public EnvironmentService()
        {
            EnvironmentName = Environment.GetEnvironmentVariable(EnvironmentVariables.AspnetCoreEnvironment) ?? Environments.Production;
        }

        public string EnvironmentName { get; set; }
    }
}

Una vez hecho esto, se procede a crear el servicio que nos devuelve ya la configuración (de regalo os pongo ejemplito de acceder a nuestros secretos vía Azure Key Vault).

  
using Microsoft.Extensions.Configuration;

namespace MyFunctions.Common
{
    public interface IConfigurationService
    {
        IConfiguration GetConfiguration();
    }

    public class ConfigurationService : IConfigurationService
    {
        public IEnvironmentService EnvService { get; }

        public ConfigurationService(IEnvironmentService envService)
        {
            EnvService = envService;
        }
         
        public IConfiguration GetConfiguration()
        {
            return Globals.GetConfiguration();
        }
    }
}

¿Y qué es eso de Globals? Pues lo que veréis a continuación.

  
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureKeyVault;
using System.IO;

namespace MyFunctions.Common
{
    public static class Globals
    {
        private static IConfigurationBuilder builder;
               
        public static IConfiguration GetConfiguration(ExecutionContext executionContext)
        {
            return GetConfiguration(executionContext.FunctionAppDirectory);
        }

        public static IConfiguration GetConfiguration()
        {
            return GetConfiguration(Directory.GetCurrentDirectory());
        }

        static IConfiguration GetConfiguration(string basePath)
        {
            if (builder == null)
            {
                builder = new ConfigurationBuilder()
                 .SetBasePath(basePath)
                 .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
                 .AddEnvironmentVariables();

                var config = builder.Build();
                var keyVaultEndpoint = config["KeyVaultUri"];
                
                // Aquí accedemos a nuestro Azure Key Vault, olé!
                if (!string.IsNullOrEmpty(keyVaultEndpoint))
                {
                    var azureServiceTokenProvider = new AzureServiceTokenProvider();
                    var keyVaultClient = new KeyVaultClient(
                        new KeyVaultClient.AuthenticationCallback(
                            azureServiceTokenProvider.KeyVaultTokenCallback));

                    builder.AddAzureKeyVault(
                        keyVaultEndpoint, keyVaultClient, new DefaultKeyVaultSecretManager());
                } 
            }

            return builder.Build();
        }
    }
}

Bien, ya casi lo tenemos. En las Azure Functions tenemos una clase WebJobsStartup.cs que es el punto de entrada de nuestro código Serverles.
NOTA: Descargad el paquete Nuget Willezone.Azure.WebJobs.Extensions.DependencyInjection para usar un método de extensión y poder aplicar la carga de la inyección de dependencias.

  
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MyFunctions;
using MyFunctions.Common;
using Willezone.Azure.WebJobs.Extensions.DependencyInjection;

[assembly: WebJobsStartup(typeof(Startup))]
namespace MyFunctions
{
    internal class Startup : IWebJobsStartup
    { 
        public void Configure(IWebJobsBuilder builder) =>
            builder.AddDependencyInjection(ConfigureServices);
 
        private static void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IEnvironmentService, EnvironmentService>();
            services.AddTransient<IConfigurationService, ConfigurationService>();

            var serviceProvider = services.BuildServiceProvider();
            var configurationtionService = serviceProvider.GetService<IConfigurationService>();
            var configuration = configurationtionService.GetConfiguration();

            // Quieres Redis?
            services.AddDistributedRedisCache(option =>
            {
                option.Configuration = configuration["RedisUri"];
            });
        }
    }    
}

Con esto y un bizcocho, ya tenemos nuestra configuración disponible en el servicio de DI de nuestras Azure Functions 🙂

Pero… ¿no ibas a hablar de Autofac?

Ahora viene lo bueno. Podemos usar nuestro contenedor de IoC/DI favorito (en mi caso Autofac) para gestionar las dependencias.

Como hemos visto en el punto anterior, podemos usar el método de extensión AddDependencyInjection utilizado en nuestra clase WebJobsStartup.cs para pasarle un IServiceProviderBuilder. El código resultante queda de la siguiente forma (muy elegante, sí señor):

  
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Hosting;
using MyFunctions;
using MyFunctions.Common.DI;
using Willezone.Azure.WebJobs.Extensions.DependencyInjection;

[assembly: WebJobsStartup(typeof(Startup))]
namespace MyFunctions
{
    internal class Startup : IWebJobsStartup
    { 
        public void Configure(IWebJobsBuilder builder) =>
            builder.AddDependencyInjection<AutofacServiceProviderBuilder>();     
    } 
}

Y la implementación de AutofacServiceProviderBuilder queda así:

  
using Autofac;
using Autofac.Extensions.DependencyInjection;
using AutoMapper;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.KeyVault.Models;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MyFunctions.Domain.Autofac;
using MyFunctions.Infrastructure.Autofac;
using MyFunctions.Services.Autofac;
using MyFunctions.Shared.Model.Infrastructure;
using MyFunctions.Shared.Utils.Mapper;
using System;
using System.Threading.Tasks;
using Willezone.Azure.WebJobs.Extensions.DependencyInjection;

namespace MyFunctions.Common.DI
{
    public class AutofacServiceProviderBuilder : IServiceProviderBuilder
    {
        public IConfiguration Configuration { get; set; }

        public AutofacServiceProviderBuilder(IConfiguration configuration) => Configuration = configuration;

        public IServiceProvider Build()
        {
            var services = new ServiceCollection();
            services.AddTransient<IEnvironmentService, EnvironmentService>();
            services.AddTransient<IConfigurationService, ConfigurationService>();

            var serviceProvider = services.BuildServiceProvider();
            var configurationtionService = serviceProvider.GetService<IConfigurationService>();
            Configuration = configurationtionService.GetConfiguration();

            // Nuestra configuración como servicio
            services.AddSingleton<IConfiguration>(Configuration);
            var appSettingsSection = Configuration.GetSection("AppSettings");
            services.Configure<AppSettings>(appSettingsSection);
 
	    // Quieres Redis? 
            services.AddDistributedRedisCache(option =>
            {
                option.Configuration = Configuration["RedisUri"];
            });

            // Quieres AutoMapper?            
            services.AddAutoMapper(new[] { typeof(MappingProfile) });
 
            // Aquí inyectamos los módulos Autofac
            var builder = new ContainerBuilder();

            builder.Populate(services);

            builder.RegisterModule<InfrastructureModules>();
            builder.RegisterModule<DomainModules>();
            builder.RegisterModule<ServiceModules>();
            builder.RegisterModule<ApplicationServiceModules>();
            builder.RegisterModule<DataModules>(); 

            return new AutofacServiceProvider(builder.Build());
        }
    }
}

Mola lo que has hecho. Ahora ¿cómo lo aplico en mis funciones?

Con el sistema de DI a punto, solo queda saber cómo hacer para resolver las dependencias en nuestras Azure Functions.
Sencillito, se emplea el atributo [Inject] en un parámetro de nuestra Function. Veámoslo con un patrón de orquestación para una Durable Function.

  
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace MyFunctions.Functions.ManageTransactions
{
    public static class ManageTransactionsOrchestatorClient
    {
        [FunctionName(nameof(ManageTransactionsOrchestatorClient))]
        public static async Task StartOrchestrationAsync([TimerTrigger("0 0 0 * * *", RunOnStartup = false)]TimerInfo myTimer,
            [OrchestrationClient] DurableOrchestrationClient orchestrationClient, ILogger log)
        {
            log.LogInformation($"{nameof(ManageTransactionsOrchestatorClient) } function executed at: {DateTime.Now}");
            await orchestrationClient.StartNewAsync(nameof(ManageTransactionsOrchestator), null);
        }
    }
}
  
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using MyFunctions.ApplicationServices.Abstract;
using MyFunctions.Domain.Abstract;
using MyFunctions.Shared.Model.Domain;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Willezone.Azure.WebJobs.Extensions.DependencyInjection;

namespace MyFunctions.Functions.ManageTransactions
{
    public static class ManageTransactionsOrchestator
    {
        [FunctionName(nameof(ManageTransactionsOrchestator))]
        public static async Task OrchestrateAsync([OrchestrationTrigger] DurableOrchestrationContext context, ILogger log)
        { 
            log.LogInformation($"{nameof(ManageTransactionsOrchestator)} function executed at: {DateTime.Now}");
            var retryOptions = new RetryOptions(TimeSpan.FromSeconds(5), 5);

            try
            {
                await context.CallActivityWithRetryAsync<bool>(nameof(SendNotification), retryOptions, null);
            }
            catch (FunctionFailedException ex)
            {
                log.LogError(ex, ex.Message);
                throw;
            }
        }

        [FunctionName(nameof(SendNotification))]
        public static async Task<bool> SendNotification([ActivityTrigger] DurableActivityContext context, ExecutionContext executionContext,
                                                        [Inject]INotificationHubService notificationHubService) // Inyectamos el servicio con [Inject]
        {
            return await notificationHubService.NotifyAsync("", 0, 3);
        }
    }
}

Conclusiones

En este artículo hemos visto cómo implementar DI en nuestras Azure Functions de una manera muy sencilla. Por ahora es una solución que estamos aplicando ya que desgraciadamente en las Functions V2 su sistema de DI out-of-the-box ha sufrido un pequeño revés y hasta que lo tengan arreglado, así es como podemos solventar el problema.

HAPPY CODING!

mm

Sobre Sergio Parra Guerra

Sergio Parra es Ingeniero Técnico en Informática de Sistemas por la UPSAM. Tiene a sus espaldas muchísimas certificaciones entre las cuales Microsoft Certified Professional y ex Microsoft MVP Visual Studio and Development Technologies. Actualmente es un magnífico Software & Cloud Architect en ENCAMINA.
Esta entrada ha sido publicada en Azure Functions. Enlace permanente.
Suscríbete a Piensa en Sofware desarrolla en Colores

Suscríbete a Piensa en Sofware desarrolla en Colores

Recibe todas las actualizaciones semanalmente de nuestro blog

You have Successfully Subscribed!

ENCAMINA, piensa en colores