Hablemos sobre .Net y Azure

Mes: julio 2021

La pesadilla del HttpContext

Hola!!! En este post quiero hablar un poquito sobre uno de los componentes principales de MVC, particularmente de los controladores: el HttpContext.

Todos los desarroladores tarde o temprano nos encontramos con la necesidad de manipular el HttpContext, ya sea para validar si el usuario esta autenticado o para obtener algún claim. Analicemos el siguiente ejemplo:

[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
    [HttpGet]
    public IActionResult Greeting()
    {
        if(HttpContext.User.Identity.IsAuthenticated == false)
        {
            return Ok("Hello there!!!");
        }

        var username = HttpContext.User.FindFirst("username")?.Value;
        return Ok($"Hello {username}!!!");
    }
}

Dentro del Controlador tenemos disponible el HttpContext al cuál podemos acceder directamente sin ningún tipo de problemas.

Sin embargo, sabemos que lo aconsejable es utilizar una capa de servicios para desacoplar el Controlador de la capa de negocio. Es cuando introducimos esta capa de servicios donde comienza “La pesadilla del HttpContext”.

Con este ejemplo se verá mucho más claro:

[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
    private readonly GreetingService _greetingService;

    public UsersController(GreetingService greetingService) =>
        _greetingService = greetingService;

    [HttpGet]
    public IActionResult Greeting()
    {
        var greeting = _greetingService.Greeting(HttpContext);
        return Ok(greeting);
    }
}

public class GreetingService
{
    public string Greeting(HttpContext httpContext)
    {
        if (httpContext.User.Identity.IsAuthenticated == false)
        {
            return "Hello there!!!";
        }

        var username = httpContext.User.FindFirst("username")?.Value;
        return $"Hello {username}!!!";
    }
}

¿Es más claro el problema ahora? Al introducir la capa de servicios, nos vemos obligados a pasar como parámetro el HttpContext. Si tuvieramos otro servicio, entonces tendríamos una cadena de métodos a los cuáles deberíamos pasar el contexto como parámetro. Algo así:

public class GreetingService
{
    public string Greeting(HttpContext httpContext)
    {
        var isAuthenticated = authService.Validate(httpContext);
        if (isAuthenticated == false)
        {
            return "Hello there!!!";
        }

        var username = httpContext.User.FindFirst("username")?.Value;
        return $"Hello {username}!!!";
    }
}

Claro que podríamos pasar como parámetro sólo las variables isAuthenticated y username, pero eso no resolvería el problema de fondo.

¿Y si usaramos inyección de dependencias?

Pienso, todos los servicios pueden ser inyectados si utilizamos el contenedor IoC y sabemos que por cada request se genera un scope (de ahí viene services.AddScoped<>()).

Además sabemos que para cada request se genera un HttpContext al cuál podemos acceder desde el Controlador, desde los filtros de MVC y también lo tenemos disponible en los middlewares.

Entonces, ¿No podríamos recibirlo mediante inyección de dependencias como cualquier otro servicio?

La respuesta es no, no podemos, pero casi…

IHttpContextAccessor

Para resolver el problema de “La pesadilla del HttpContext” tenemos disponible la interfaz IHttpContextAccessor y una implementación default HttpContextAccessor.

Esto nos va a permitir acceder al HttpContext de forma fácil y sencilla sin tener que escribir código de más. Algo así:

public class GreetingService
{
    private readonly IHttpContextAccessor contextAccessor;

    public GreetingService(IHttpContextAccessor contextAccessor) =>
        this.contextAccessor = contextAccessor;

    public string Greeting()
    {
        var httpContext = contextAccessor.HttpContext;
        if (httpContext.User.Identity.IsAuthenticated == false)
        {
            return "Hello there!!!";
        }

        var username = httpContext.User.FindFirst("username")?.Value;
        return $"Hello {username}!!!";
    }
}

Super importante, como con cualquier otro servicio, necesitamos registrarlo apropiadamente. Claro, el framework es nuestro mejor amigo, y siempre nos extiende una mano:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // More services...
        services.AddHttpContextAccessor();
        // More services...
    }

    public void Configure(IApplicationBuilder app)
    {
        // ...
    }
}

Conclusión

Con esta pequeña refactorización evitamos tener que pasar por parámetro el HttpContext, potencialmente en cadena, y así generar código mucho más limpio y entendible.

Startupeando en .Net (El Camino Dev)

Hace algunos días tuve el honor y agrado de participar como speaker para el canal de YouTube de El Camino Dev donde estuve hablando sobre un tema muy interesante pero que no es muy habitual: alternativas disponibles para ejecutar tareas como parte del inicio de una aplicación MVC.

Algunos de los temas que mencioné fueron Múltiples Startups, IStartupFilter y IHostedServices. Además vimos como ejecutar tareas desde el mismo Program.cs utilizando el IServiceProvider que configuramos para la aplicación.

Startupeando – El Camino Dev

Inyección de servicios por ambiente en ASP.Net

Hola de nuevo! Aquí estamos, intentando descubrir algunas funcionalidades “ocultas” de .Net. Hace unos días escribí un artículo sobre como utilizar múltiples Startups en tus aplicaciones MVC.

Siguiendo en la misma línea, hoy veremos otra forma de ordenar la configuración de servicios y del pipeline, esta vez sin la necesidad de tener clases Startup.cs separadas.

Situación actual

Como es habitual, realizamos la configuración de todos los servicios y la configuración del pipeline de nuestra aplicación en la clase Startup.cs. Aquí vemos un ejemplo simple pero muy frecuente:

public class Startup
{
    private readonly IWebHostEnvironment env;

    public Startup(IWebHostEnvironment env) =>
        this.env = env;

    public void ConfigureServices(IServiceCollection services)
    {
        if(env.IsDevelopment())
            services.AddDistributedMemoryCache();
        else
            services.AddDistributedRedisCache(options => { });

        // More services here...
    }

    public void Configure(IApplicationBuilder app)
    {
        if (env.IsDevelopment()) 
            app.UseDeveloperExceptionPage();

        // UseRouting(), UseAuthorization(), ...
    }
}

En el caso de que el ambiente sea Development, configuramos un caché distribuido en memoria, caso contrario utilizamos Redis. De la misma forma, sólo mostramos la página de excepciones si el ambiente es Development.

Requerimiento

Queremos hacer nuestro Startup.cs mas limpios y declarativos, eliminando así la necesidad de tener que validar si el ambiente es Development o no. Idealmente, queremos apoyarnos en el framework sin tener que escribir nuestra propia implementación.

Otra funcionalidad oculta

Sabemos que nuestra clase Startup.cs debe contener dos métodos, ConfigureServices para registrar los servicios necesarias y Configure para configurar el pipeline. Estos métodos son llamados por el runtime en tiempo de ejecución en el momento adecuado.

Sin embargo una funcionalidad no muy conocida es que podemos tener distintos métodos con el nombre Configure{Ambiente}Services y Configure{Ambiente}. Luego el runtime ejecutará los que correspondan al ambiente actual.

Si no existe un método que corresponda al ambiente actual, ejecutará los métodos por defecto ConfigureServices y Configure.

Implementación

El cambio es mu simple, crearemos dos nuevos métodos, ConfigureDevelopmentServices y ConfigureDevelopment, y configuraremos todos los servicios correspondientes al ambiente de desarrollo específicamente:

public class Startup
{
    // Default
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDistributedRedisCache(options => { });

        // More services...
    }

    // Development
    public void ConfigureDevelopmentServices(IServiceCollection services)
    {
        services.AddDistributedMemoryCache();

        // More services...
    }

    //Default
    public void Configure(IApplicationBuilder app)
    {
        // UseRouting(), UseAuthorization(), ...
    }

    // Development
    public void ConfigureDevelopment(IApplicationBuilder app)
    {
        app.UseDeveloperExceptionPage();

        // UseRouting(), UseAuthorization(), ...
    }
}

Para probarlo, solo debemos establecer algunos breakpoints y ejecutar la aplicación en el ambiente de desarrollo. El framework hará el resto!

Un poco de refactorización

Podemos tener una solución mucho más elegante si creamos un método ConfigureBaseServices conde pongamos todos los servicios comunes, de forma que no tengamos que repetir código (algo que odiamos!):

// Default
public void ConfigureServices(IServiceCollection services)
{
    services.AddDistributedRedisCache(options => { });
    ConfigureBaseServices(services);
}

// Development
public void ConfigureDevelopmentServices(IServiceCollection services)
{
    services.AddDistributedMemoryCache();
    ConfigureBaseServices(services);
}

// Base
private void ConfigureBaseServices(IServiceCollection services)
{
    // Common services...
}

Conclusión

En este post vimos cómo aplicando una pequeña refactorización en la clase Startup.cs podemos sacar provecho de una funcionalidad del framework poco conocida que nos permite configurar los servicios y el pipeline según el ambiente en el que se ejecute la aplicación.

Startups múltiples en .Net 5

Hola! Aquí estamos de nuevo escribiendo sobre .Net. En esta oportunidad vamos a hablar de una funcionalidad que existe desde .Net Core 2.1, pero que sin embargo no es para nada conocida: los Startups múltiples.

Veamos el siguiente código. Corresponde a un Startup.cs de una aplicación Web API muy pequeña, que registra y/o configura servicios en función del ambiente.

public class Startup
{
    private readonly IWebHostEnvironment env;

    public Startup(IWebHostEnvironment env) =>
        this.env = env;

    public void ConfigureServices(
        IServiceCollection services)
    {
        if(env.IsDevelopment())
            services.AddDistributedMemoryCache();
        else
            services.AddDistributedRedisCache(options => { });
        
        // More services...
    }

    public void Configure(IApplicationBuilder app)
    {
        if (env.IsDevelopment())
            app.UseDeveloperExceptionPage();

        // UseRouting, UseAuthorization, etc.
    }
}
  1. Si el ambiente es desarrollo, se configura un cache en memoria y se habilita la página de error.
  2. Si el ambiente NO es desarrollo, se utiliza como cache Redis y no se habilita la página de error.

Funcionar, funciona… Pero ese if validando el ambiente hace ruido. Además de odiar los ifs, mientras más crece la aplicación más crecen esas validaciones, y menos me gusta el código.

La funcionalidad oculta

Como mencionamos al comienzo del post, ya desde la versión 2.1 de .Net Core tenemos disponible una funcionalidad muy poco conocida llamada Startups Múltiples.

¿En qué consiste?

Si creamos una clase Startup con el nombre del ambiente como sufijo y lo registramos, el framework automáticamente ejecutará el Startup.cs que corresponda al ambiente.

Por ejemplo, podemos crear dos Startups, Startup.cs y StartupDevelopment.cs:

public class Startup
{
    public void ConfigureServices(
        IServiceCollection services)
    {
        services.AddDistributedRedisCache(options => { });
        // More services...
    }

    public void Configure(IApplicationBuilder app)
    {
        // UseRouting, UseAuthorization, etc.
    }
}
public class StartupDevelopment
{
    public void ConfigureServices(
        IServiceCollection services)
    {
        services.AddDistributedMemoryCache();
        // More services...
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseDeveloperExceptionPage();
        // UseRouting, UseAuthorization, etc.
    }
}

Finalmente, en la clase Program.cs dentro del método CreateHostBuilder sólo tenemos que registrar el emsamblado que contiene nuestros Startups.cs y sentarnos a disfrutar de las bondades de .Net.

public static IHostBuilder CreateHostBuilder(string[] args)
{
    var assemblyName = typeof(Startup).Assembly.FullName;

    return Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup(assemblyName);
        });
}

En tiempo de ejecución se seleccionará el Startup cuyo sufijo corresponda con el nombre del ambiente de ejecución. Por ejemplo, si el ambiente es Development, se ejecutará la clase StartupDevelopment. Si no existe el startup correspondiente, se ejecutará la clase por defecto Startup.

¿Es esto siempre necesario?

Bueno, el ejemplo anterior es trivialmente simple como para justificar esta funcionalidad. Esta idea está pensada para aquellos casos donde el startup tiene muchos componentes que dependen del ambiente.

Sobre todo en desarrollo, donde es frecuente que mostremos errores con su stack, cambiemos los niveles de logging, deshabilitemos caches o los usemos en memoria, etc.

Conclusión

Acabamos de explorar una funcionalidad muy poco conocida dentro de .Net llamada Startups Multiples que nos permite tener distintas clases Startup.cs según el ambiente de ejecución. Esto nos permite tener clases más compactas y eliminar la necesidad de tener que validar constantemente el ambiente para inyectar o configurar servicios.

Gestión de configuraciones en Azure Functions

La semana pasada escribí un artículo sobre como inyectar dependencias en Azure Functions, debido a que la plantilla que utilizamos cuando creamos un proyecto nuevo no nos provee los mecanismos necesarios.

Ahora veremos como manejar configuraciones, de forma tal que podamos utilizar el archivo local.settingns.json, variables de entorno y/o User Secrets.

Antes de comenzar

El framework de Azure Functions está preparado para leer las configuraciones que incluimos mediante el portal de Azure en la sección de Configuraciones.

Tomaremos el el concepto de Startup del artículo sobre inyección de dependencias que mencioné anteriormente. Además tomaremos el código base y construiremos sobre él.

Contexto

Si bien cuando desplegamos nuestro código al servicio de Azure Functions las configuraciones configuradas desde el portal se leen automáticamente, puede que necesitemos personalizar el mecanismo con el cual esas configuraciones con leídas.

Por ejemplo, en nuestro ambiente local, usar User Secrets es recomendado. En ambientes pre productivos, podríamos necesitar leer un archivo adicional, como puede ser stage.settings.json.

Requerimiento

El requerimiento de hoy es bastante simple, necesitamos poder leer configuraciones utilizando User Secrets, el archivo local.settings.json y variables de entorno.

Implementación

Cuando creamos el Startup y heredamos de la clase FunctionsStartup, tendremos disponible un método virtual llamado ConfigureAppConfiguration. Es este método es que nos permitirá establecer como son leídas las configuraciones.

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
    }

    public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
    {
        var context = builder.GetContext();

        builder.ConfigurationBuilder
            .SetBasePath(context.ApplicationRootPath)
            .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
            .AddUserSecrets(Assembly.GetExecutingAssembly(), optional: true)
            .AddEnvironmentVariables();
    }
}

SetBasePath

Siempre que sea necesario leer archivos locales, necesitamos establecer el directorio base. En aplicaciones web o APIs solemos utilizar el directorio actual, .SetBasePath(Directory.GetCurrentDirectory()).

En el caso de Azure Functions necesitamos utilizar el contexto de ejecución, builder.GetContext().ApplicationRootPath.

AddUserSecrets

Cuando instalamos la librería Microsoft.Extensions.Configuration.UserSecrets tenemos que instalar la versión 3.1.XX. Por defecto, el Gestor de Paquetes (Package Manager) instalará la ultima versión, que a la fecha actual es 5.0.0, sin embargo el framework todavía no fue actualizado y sigue utilizando la versión de .Net Core 3.1.

Es inminente la actualización del framework de Azure Functions a la version .Net 5, pero al momento de escribir este artículo, todavía utiliza la versión 3.1.

Conclusión

En este breve post vimos cómo podemos configurar nuestra Azure Function para utilizar los distintos tipos de mecanismos para manejo de configuraciones utilizando todas las herramientas que le framework nos provee.

Con muy poco código, tenemos disponible variables de entorno, User Secrets y archivos json.

© 2021 Facu The Rock

Tema por Anders NorenArriba ↑