Hablemos sobre .Net y Azure

Categoría: NetCore (Página 1 de 2)

Feature Flags personalizados

Hola a todos! Ya llevamos una serie de artículos dedicados a la implementación de Feature Flags. Entre otras cosas, vimos como utilizar algunos filtros condicionales que el framework nos provee y cómo aplicar middlewares y filtros MVC.

En el post de hoy veremos uno de los requerimientos más utilizados y sin los cuales los Feature Flags no serían tan poderosos: Feature Flags personalizados.

Contexto

Cuando analizamos los Feature Flags condicionales vimos como podemos aplicar algún tipo de lógica, que puede ser una ventana de tiempo (TimeWindowFilter) o un porcentaje de peticiones (PercentageFilter), para activar o desactivar funcionalidades. Claro quu si bien agregan valor, no son suficientes.

En ocasiones necesitamos tener un mayor control sobre como determinar si una funcionalidad se encuentra activa o no, incluso podemos necesitar consultar servicios externos o una base de datos.

Requerimiento

Como parte del desarrollo de nuestra estrategia de expansión de nuestro negocio eCommerce, queremos ofrecer una lista de productos especialmente seleccionado con un descuento especial promocional. Este descuento estará disponible solo los días viernes.

Implementación

Armar nuestro propio filtro requiere implementar la interface IFeatureFilter, la cuál define solo el método EvaluateAsync. Aquí podremos escribir nuestra lógica de negocio y retornar un valor booleano indicando si la funcionalidad se encuentra activa o no:

public interface IFeatureFilter : IFeatureFilterMetadata
{
    Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context);
}

Los pasos necesarios para completar nuestro filtro son:

  1. Implementar la interface IFeatureFilter
  2. Registrar el filtro
  3. Agregar la configuración
  4. Modificar la vista

IFeatureFilter

La idea de nuestro filtro es retornar true si el día de la semana es viernes, de lo contrario retornar false. Para hacerlo un poco más flexible, vamos a parametrizar el día de la semana de forma que podamos utilizarlo para otras promociones.

Crearemos una clase WeeklyOffersFeatureFilter e implementaremos la interface IFatureFilter. El código debería verse mas o menos así:

[FilterAlias("WeeklyOffersFilter")]
public class WeeklyOffersFeatureFilter : IFeatureFilter
{
    public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
    {
        var dayOfWeek = context.Parameters.GetValue<string>("DayOfWeek");
        var result = DateTime.Now.DayOfWeek.ToString() == dayOfWeek;

        return Task.FromResult(result);
    }
}

Analicemos los aspectos importantes de nuestro filtro:

  • [FilterAlias("WeeklyOffersFilter")]:
    Este atributo es necesario para indicarle al framework el nombre del filtro a utilizar. Es decir, “para el Feature Flag WeeklyOffers utilizar el filtro cuyo alias es WeeklyOffersFilter. Tiene que ser el mismo que utilizaremos en la configuración en el archivo appsettings.json.
  • context.Parameters.GetValue<string>("DayOfWeek"):
    Mediante el contexto podemos leer los parámetros establecidos en la configuración. En este caso estamos obteniendo el parámetro "DayOfWeek".
  • result:
    Simplemente comparamos el día de la semana actual contra el parámetro.

Registrar el filtro

Como siempre, solo tenemos que registrarlo utilizando el método de extensión AddFeatureFilter<>() en la clase Startup:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...

        services.AddFeatureManagement()
            .AddFeatureFilter<WeeklyOffersFeatureFilter>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // ...
    }
}

Configuración

Finalmente necesitamos generar una nueva configuración en el archivo appsettings.json:

{
  // ...
  "FeatureManagement": {
    "WeeklyOffers": {
      "EnabledFor": [
        {
          "Name": "WeeklyOffersFilter",
          "Parameters": {
            "DayOfWeek": "Friday"
          }
        }
      ]
    }
  }
}

Lo mas importante aqui es la linea 7, donde indicamos el nombre del filtro a utilizar, que tiene coincidir con el valor del atributo [FilterAlias("WeeklyOffersFilter")] que utilizamos cuando implementamos la interface.

Vista

El único paso que nos queda pendiente es modificar la vista, esto ya lo hemos hecho en post anteriores. En nuestro caso modificamos la vista del menú principal (_Layout.cshtml) para incluir una nueva entrada cuando nuestro Feature Flag WeeklyOffers se encuentre activa.

<feature name="WeeklyOffers">
  <li class="nav-item">
    <a asp-controller="Home" asp-action="NewFeature">
        Ofertas semanales
    </a>
  </li>
</feature>

Conclusión

Poder implementar filtros personalizados nos da mucha flexibilidad a la hora de habilitar funcionalidades. Por ejemplo, si nuestra funcionalidad depende de otro servicio que puede no estar funcionando o no estar activo aún, podríamos ejecutar un recuest a dicho servicio y devolver true si y solo si el request es exitoso.

En el próximo post veremos como combinar Feature Flags con Azure App Configuration para guardar nuestras configuraciones de forma centralizada y segura.

Feature Flags: Middlewares y filtros MVC

De a poco estamos descubriendo todo el potencial que los Feature Flags tienen parea ofrecernos, aprendimos los conceptos principales, como usar Feature Flags condiciones y como manejar Features Flags inactivas.

En esta ocasión veremos como aplicar aplicar filtros MVC y middlewares solo si determinada funcionalidad se encuentra activa.

Filtros MVC

Los filtros MVC nos permiten agregar funcionalidad en forma muy versátil y dinámica como parte del procesamiento de una petición o request. Una de las principales características es que podemos agregar funcionalidad sin necesariamente modificar un controlador o acción.

Los Feature Flags nos van a permitir aplicar filtros MVC solo si una funcionalidad determinada se encuentra activa.

Requerimiento

Tenemos un filtro MVC AddBetaHeaderFilter que agrega una cabecera especial a la petición indicando que la versión en ejecución es "Beta". De esta forma otros componentes pueden realizar modificaciones a la petición según la versión.

public class AddBetaHeaderFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(
        ActionExecutingContext context, 
        ActionExecutionDelegate next)
    {
        context.HttpContext
            .Request
            .Headers
            .Add("X-Version", "Beta");

        await next();
    }
}

Necesitamos aplicar este filtro solo y solo si el Feature Flag "Beta" se encuentra activo.

Implementación

Por suerte para nosotros solo necesitamos agregar el filtro a los filtros generales de MVC utilizando el helper AddForFeature<>. Para esto modificaremos la llamada a services.AddControllersWithViews() del método ConfigureServices de la clase Startup:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
        options.Filters.AddForFeature<AddBetaHeaderFilter>("Beta"));

    services.AddFeatureManagement();
    // More services...
}

Middlewares

Los middlewares son muy similares a los filtros MVC ya que nos permiten manipular las peticiones y las respuestas. A diferencia de los filtros, estos no nos brindan acceso al contexto de MVC.

También podemos aplicar middlewares al pipeline utilizando Feature Flags.

Requerimiento

Continuando con el ejemplo anterior, tenemos un middleware AddBetaHeaderMiddleware que agrega la misma cabecera “Beta” que agregamos con el filtro MVC de la sección anterior.

public class AddBetaHeaderMiddleware
{
    private readonly RequestDelegate _next;

    public AddBetaHeaderMiddleware(RequestDelegate next) =>
        _next = next;

    public async Task InvokeAsync(HttpContext context)
    {
        context
            .Request
            .Headers
            .Add("X-Version", "Beta");

        await _next(context);
    }
}

Necesitamos aplicar este middleware solo y solo si el Feature Flag "Beta" se encuentra activo.

Implementación

Para aplicar este filtro, tendremos que modificar el método Configure de la clase Startup, donde utilizaremos el método de extensión ya provisto por el framework UseMiddlewareForFeature<>().

public void Configure(
    IApplicationBuilder app,
    IWebHostEnvironment env)
{
    // More middlewares...

    app.UseMiddlewareForFeature<AddBetaHeaderMiddleware>("Beta");

    app.UseEndpoints(endpoints => {/**/});
}

El orden de los factores sí altera el producto

Tener en cuenta que los middlewares se ejecutan en el orden en el que se agregan. De modo tal, si en el ejemplo anterior registramos el middleware después de app.UseEndpoints(), el Feature Flag no funcionaría.

Conclusión

Paso a paso vamos descubriendo todo el potencial de los Feature Flags y como utilizarlos para lograr un desarrollo realmente ágil e incremental. En este post aplicamos middlewares y filtros.

En el próximo post iremos un poquito mas a fondo e implementaremos un Feature Flag personalizado.

Manejo de Features Flags inactivas

Como vimos en el post introductorio, cuando intentamos navegar hacia una página que no esta activa debido a un Feature Flag desactivado recibimos un mensaje de error 404 - Not Found o Página no encontrada.

En algunos escenarios ese error puede ser suficiente, pero en determinadas ocasiones, redireccionar al usuario a otra pagina o devolver algún otro código de error puede resultar una mejor opción.

Requerimiento

Nuestro sitio web tiene una vista con todos los productos que tienen un descuento especial por Navidad. Utilizamos Feature Flags condicionales para garantizar que esta funcionalidad este activa solo los días 24 y 25 de Diciembre.

Cuando un usuario accede a esta lista especial de descuentos fuera del periodo navideño, queremos redireccionar al usuario a otra vista para informarle que los descuentos ya no se encuentran disponibles e invitarlo a revisar otros posibles descuentos.

IDisabledFeaturesHandler

Para poder manejar en forma personalizada cómo respondemos ante una funcionalidad inactiva, tenemos la opción de implementar al interface IDisabledFeaturesHandler:

public interface IDisabledFeaturesHandler
{
    Task HandleDisabledFeatures(
        IEnumerable<string> features,
        ActionExecutingContext context);
}

Como podemos observar, la interfaz tiene solo un método con la lista de Feature Flags que se requirieron y el contexto de MVC actual. Veamos en detalle que significa cada uno de esos parámetros.

IEnumerable<string> features

El primer parámetro contiene la lista de todos los Feature Flags que fueron requeridos para poder acceder a la funcionalidad en cuestión. Alguno o algunos de ellos se encuentran inactivos y dispararon el manejador.

Recordemos que cuando una funcioalidad puede dependender de más de un Feature Flag, y además podemos requerir que todos los Feature Flags esten activos o al menos uno. En los siguiente ejemplo lo vemos en forma más detallada:

public class MyController : Controller
{
    // Cualquiera de los features es suficiente
    [FeatureGate(
        RequirementType.Any, 
        "FeatureFlag1", 
        "FeatureFlag2")]
    public IActionResult Funcionalidad1() => 
        View();

    // Ambos features deben estar activos
    [FeatureGate(
        RequirementType.All, 
        "FeatureFlag1", 
        "FeatureFlag2")]
    public IActionResult Funcionalidad2() => 
        View();
}

ActionExecutingContext context

Este parámetro corresponde al contexto de ejecución de la acción dentro de MVC (el mismo que tenemos disponibles en los Action Filters!!!) y nos brinda por un lado información sobre el controlador y acción que se intentaron ejecutar, por el otro lado nos permite manejar la respuesta que enviaremos al cliente.

Manos a la obra

Para completar nuestro requerimiento vamos a generar una nueva clase, la cual llamaremos DisabledFeaturesHandler, e implementaremos la interfaz IDisabledFeaturesHandler.

En el método HandleDisabledFeatures redireccionaremos al usuario a una vista llamada OtherDiscounts.cshtml donde le notificaremos que los descuentos ya no están disponibles.

public class DisabledFeatureHandler : IDisabledFeaturesHandler
{
    public Task HandleDisabledFeatures(
        IEnumerable<string> features,
        ActionExecutingContext context)
    {
        var viewResult = new ViewResult
        {
            ViewName = "Views/Home/OtherDiscounts.cshtml"
        };

        context.Result = viewResult;

        return Task.CompletedTask;
    }
}

Por último, tenemos que incluir este manejador como parte de los servicios responsables del manejo de Features Flags. Esto lo haremos en ConfigureServices de la clase Startup:

public class Startup
{
    public void ConfigureServices(
        IServiceCollection services)
    {
        // ...
        services.AddFeatureManagement()
            .UseDisabledFeaturesHandler(
                new DisabledFeaturesHandler());
    }

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

A partir de este momento, cada vez que el usuario se dirija a la vista de descuentos navideños fuera del periodo correspondiente, será automáticamente redirigido a una vista particular en lugar de recibir un error 404 - NotFound:

Manejador de Feature Flags inactivos.

Conclusión

En este post vimos como con muy poquito codigo podemos manejar de forma elegante y simple como reaccionar en forma particular a alguna funcionalidad que se encuentra deshabilitada y tenemos la necesidad de responder de una manera diferente.

Todavía nos queda mucho por descubrir sobre manejo de sesión, implementación de filtros personalizados, middlewares y much más.

Feature Flags condicionales

En el post anterior te conté cómo gracias a la implementación de Feature Flags podemos agilizar nuestro desarrollo y realizar despliegues incrementales.

En esta oportunidad iremos un poco más profundo y veremos cómo podemos podemos activar o desactivar funcionalidad para ciertos rangos de tiempos o para un porcentaje determinado de usuarios.

Contexto

Si bien en muchos casos activar o desactivar una funcionalidad es suficiente, hay ciertos escenarios donde queremos tener un poco más de control sobre qué condiciones activan o desactivan determinado aspecto del sistema.

En ciertos escenarios, esas condiciones pueden ir variando con el tiempo, y por supuesto no queremos estar reconfigurando nuestras aplicaciones cada vez que alguna de las condiciones cambia.

Requerimientos

Los requerimientos que vamos a intentar satisfacer son los siguientes:

  1. Activar o desactivar funcionalidades en momentos específicos.
  2. Activar o desactivar funcionalidades para un determinado porcentaje de peticiones.

Activar o desactivar funcionalidades en momentos específicos

Un claro ejemplo de esta necesidad es habilitar alguna funcionalidad durante fechas particulares, como es el caso de algún descuento especial por Navidad o Black Friday.

Podríamos tener un caso en el cuál para Navidad deseamos habilitar una página especial con todos los productos que tienen un descuento especial del 25%, de forma tal que durante los días 24 y 25 de diciembre suceda lo siguiente:

  1. Mostremos una nueva entrada del menú.
  2. Este disponible una pagina especial con la lista de productos
  3. Aplicar un descuento del 25% a esos productos durante ese lapso de tiempo.

TimeWindowFilter

Para nuestro agrado, ya disponemos de un filtro llamado TimeWindowFilter que nos permite establecer fechas desde y hasta.

Lo primero que debemos hacer es agregar el filtro como parte de la configuración de servicios en Startup.ConfigureServices utilizando el método de extensión AddFeatureFilter<>():

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // More services
        services.AddFeatureManagement()
            .AddFeatureFilter<TimeWindowFilter>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // ...
    }
}

Luego, tendremos que generar una nueva configuración en el archivo appsettings.json al cual llamaremos OfertasNavidenias:

// appsettings.json
"FeatureManagement": {
  "OtroFeatureFlag": true,
  "OfertasNavidenias": {
    "EnabledFor": [
      {
        "Name": "Microsoft.TimeWindow",
        "Parameters": {
          "Start": "2021-12-24T00:00:00",
          "End": "2021-12-25T23:59:59"
        }
      }
    ]
  }
}

En este caso estamos indicando que el Feature Flag OfertasNavidenias se activa o desactiva para la lista de filtros incluida como parte del arreglo. En este caso estamos usando el filtro por defecto que nos provee el framework, Microsoft.TimeWindow. Los valores Start y End son las fechas/horas durante las cuales la funcionalidad está activa.

Ya con la configuración en su lugar, modificaremos la vista _Layout.cshtml para incluir la nueva entrada del menú solo cuando la funcionalidad "OfertasNavidenias" se encuentre habilitada. Para esto usamos el Tag Helper <feature>.

<header>
    <nav class="navbar">
        <ul class="navbar-nav">
            @* More menues... *@
            <feature name="OfertasNavidenias">
                <li class="nav-item">
                    <a asp-controller="OfertasNavidenias" asp-action="Index">Ofertas navideñas</a>
                </li>
            </feature>
        </ul>
    </nav>
</header>

Por cuestiones internas del framework, tuve que reemplazar la ñ cuando establecí el nombre del Feature Flag. Eso explica el error tipográfico en OfertasNavidenias.

Por el lado del controlador, necesitamos evitar que la acción del controlador Index sea accesible si la funcionalidad "OfertasNavidenias" para lo cual usamos el atributo [FeatureGate("OfertasNavidenias")].

Por el otro lado, necesitamos aplicar un descuento especial a cada producto. Para eso tenemos disponible el servicio IFeatureManager, el cual nos permite determinar si la funcionalidad se encuentra activa.

public class OfertasNavideniasController : Controller
{
    private readonly IFeatureManager _featureManager;

    public OfertasNavideniasController(IFeatureManager featureManager) =>
        _featureManager = featureManager;

    [FeatureGate("OfertasNavidenias")]
    public async Task<ActionResult> Index()
    {
        var isOfertasNavideniasEnabled = await _featureManager.IsEnabledAsync("OfertasNavidenias");

        if (isOfertasNavideniasEnabled)
        {
            var productosConDescuento = productos
                .Select(producto =>
                {
                    producto.Precio = producto.Precio * 0.75;

                    return producto;
                })
                .ToList();

            return View(productosConDescuento);
        }

        return View(productos);
    }
}

Pruebas

La forma de verificar o validar que nuestro Feature Flag junto con su configuración está funcionando correctamente es, como siempre, ejecutando la aplicación. Si nos encontramos dentro del rango de fechas indicadas entonces la entrada del menú estará disponible.

Activar o desactivar funcionalidades para un determinado porcentaje de peticiones

Cuando una nueva funcionalidad está lista para ser puesta en producción, es habitual activar dicha funcionalidad para un grupo reducido de usuarios, de forma que ante un eventual fallo la cantidad de usuarios afectados sea reducida.

Por ejemplo, podríamos habilitar una funcionalidad para el 5% de los usuarios de nuestro sitio web.

PercentageFilter

El framework ya nos provee un filtro llamado PercentageFilter que nos permite configurar el porcentaje de peticiones para los cuales una funcionalidad estará habilitada.

Para esto tenemos que agregar este nuevo filtro en Startup.ConfigureServices y agregar la sección correspondiente en appsettings.json.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // More services
        services.AddFeatureManagement()
            .AddFeatureFilter<TimeWindowFilter>()
            .AddFeatureFilter<PercentageFilter>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // ...
    }
}
// appsettings.json
"FeatureManagement": {
  "OtroFeatureFlag": true,
  "OfertasNavidenias": {...},
  "FuncionalidadBeta": {
    "EnabledFor": [
      {
        "Name": "Percentage",
        "Parameters": {
          "Value": 5
        }
      }
    ]
  }
}

En este caso la propiedad Value de la configuración corresponde al porcentaje de peticiones para los cuales esta funcionalidad estará habilitada.

Por último y como hicimos anteriormente, modificaremos la vista _Layout.cshtml para incluir la nueva entrada del menú "Beta":

<header>
    <nav class="navbar">
        <ul class="navbar-nav">
            @* More menues... *@
            <feature name="FuncionalidadBeta">
                <li class="nav-item">
                    <a asp-controller="FuncionalidadBeta" asp-action="Index">Funcionalidad Beta</a>
                </li>
            </feature>
        </ul>
    </nav>
</header>

Pruebas

Al ejecutar la aplicación, deberíamos notar que solo 5% de las veces que navegamos hacia el Home veremos disponible la entrada de menú Funcionalidad Beta.

Limitaciones

Esta implementación tiene como limitación que solo funciona por petición. Esto crea escenarios donde un usuario ve un resultado diferente en forma aleatoria, incluso si refresca el navegador.

Idealmente, cuando un usuario accede a la plataforma y la funcionalidad se encuentra activa, esta funcionalidad debería mantenerse activa durante el tiempo que dure la sesión del usuario.

En algunos casos, incluso queremos que la funcionalidad mantenga el mismo estado en sesiones diferentes.

Para resolver este inconveniente utilizaremos un servicio llamado ISessionManager o utilizando filtros personalizados. En futuros post veremos en detalle cómo implementar dichos conceptos.

Conclusión

Hoy profundizamos un poco más sobre Feature Flags y vimos cómo podemos utilizar algunos filtros condicionales provistos por el framework. Vimos como ejemplo cómo activar una funcionalidad para un porcentaje de peticiones y como activar una funcionalidad en cierto rango de fechas.

Todavía nos faltan muchas cosas, como manejo de funcionalidades desactivadas, manejo de sesión, filtros personalizados, etc. De a poco iremos profundizando y descubriendo todo el potencial que los Feature Flags nos ofrecen.

Desarrollo incremental y ágil con Features Flags

Hola a todos! Voy a comenzar el post con una pregunta: ¿Has tenido que revertir uno o más commits en producción por una nueva funcionalidad que trajo problemas?

Si la respuesta es No, siento un poco de envidia! Pero si tu respuesta es Si (como es mi caso), este post es para vos. Vamos a ver cómo podemos resolver ese problema utilizando Feature Flags.

Contexto

Seguramente has escuchado hablar alguna vez de la idea de desplegar código frecuentemente y en forma incremental, iteraciones cortas con retroalimentación (feedback) inmediato, fallar rápido (fail fast), etc.

Seguramente cuando tuviste que revertir ese commit conflictivo te hubiera gustado poder “apagar” esa funcionalidad sin revertir commits.

Quizás te gustaría desplegar una nueva funcionalidad pero no activarla, o activarla para un grupo reducido de usuarios.

Todas las respuestas conducen a Feature Flags!

Feature Flags

Feature Flags es una práctica que nos permite controlar total o parcialmente como se comporta una funcionalidad o grupo de funcionalidades mediante configuración de una forma estándar y simple.

Dicho de otro modo, los Features Flags nos van a permitir “encender” o “apagar” funcionalidades mediante configuración.

Antes de comenzar

Antes de comenzar a escribir código, veamos un poco como luce la solución actual., donde tenemos una aplicación Web MVC muy simple con la siguientes 3 características:

  1. Una entrada en el menú principal llamada "Nueva Funcionalidad".
Home
Home
<header>
    <nav class="...">
        <ul class="...">
            <li class="nav-item">
                <a asp-controller="Home" asp-action="Index">Home</a>
            </li>
            <li class="nav-item">
                <a asp-controller="Home" asp-action="Privacy">Privacy</a>
            </li>
            <li class="nav-item">
                <a asp-controller="Home" asp-action="NuevaFuncionalidad">Nueva Funcionalidad</a>
            </li>
        </ul>
    </nav>
</header>
  1. Una acción llamada NuevaFuncionalidad en el controlador Home que nos dirige una vista.
public class HomeController : Controller
{
    public IActionResult Index() => View();

    public IActionResult Privacy() => View();

    public IActionResult NuevaFuncionalidad() =>
        View();
}
  1. Una vista llamada NuevaFuncionalidad.
@{
    ViewData["Title"] = "Nueva Funcionalidad";
}

<div class="text-center">
    <h1 class="display-4">Nueva Funcionalidad</h1>
</div>

Requerimientos

¿Qué pasaría si esta nueva funcionalidad tiene una falla y necesitamos desactivarla? ¿Cómo podemos incorporar esta nueva funcionalidad si todavía esta incompleta?

El requerimiento que vamos a atacar es simple: necesitamos poder desactivar o activar esta nueva funcionalidad en forma simple y rápida, sin necesidad de crear nuevos commits y de una forma estandarizada.

Manos a la obra

Para poder comenzar a utilizar Features Flags vamos a necesitar instalar una librería llamada Microsoft.FeatureManagement.AspNetCore.

dotnet add package Microsoft.FeatureManagement.AspNetCore

Luego tendremos que implementar 3 aspectos:

  1. Configuración e inyección de servicios
  2. Vistas
  3. Controladores

Configuración e inyección de servicios

Comenzaremos por configurar los servicios necesarios mediante Inyección de Dependencias.

// ...
using Microsoft.FeatureManagement;

namespace FacuTheRock.FeaturesFlags.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            // ...
            services.AddFeatureManagement();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // ...
        }
    }
}

Una vez configurados los servicios, debemos proceder con las configuraciones necesarias. Por suerte para nosotros, solo necesitamos agregar una sección llamada FeatureManagement dentro del archivo appsettings.json.

En esta sección listaremos todas las funcionalidades e indicaremos si esta activa o inactiva. Agregaremos una nueva funcionalidad a la lista a la cual llamaremos NuevaFuncionalidad cuyo valor dejaremos en false o inactivo.

{
  // ...
  "FeatureManagement": {
    "NuevaFuncionalidad": false,
    "Funcionalidad1": true,
    // ...
  }
}

Vistas

Ahora necesitamos mostrar y ocultar la opción del menú Nueva Funcionalidad en función del Feature Flag que configuramos anteriormente. Para eso tenemos disponible un tag helper llamado feature al cual le indicaremos el nombre del Feature Flag que queremos observar.

Es importante incluir en el código de la vista la referencia a Microsoft.FeatureManagement.AspNetCore:

@*_Layout.cshtml*@

@addTagHelper *, Microsoft.FeatureManagement.AspNetCore

<header>
    <nav class="...">
        <ul class="...">
            <li class="nav-item">
                <a asp-controller="Home" asp-action="Index">Home</a>
            </li>
            <li class="nav-item">
                <a asp-controller="Home" asp-action="Privacy">Privacy</a>
            </li>
            <feature name="NuevaFuncionalidad">
                <li class="nav-item">
                    <a asp-controller="Home" asp-action="NuevaFuncionalidad">Nueva Funcionalidad</a>
                </li>
            </feature>
        </ul>
    </nav>
</header>

El tag helper se ocupa de verificar si la funcionalidad se encuentra activa, y de ser así renderiza los elementos contenidos dentro el.

En este caso y debido a la mediante configuramos indicamos que la funcionalidad se encuentra inactiva, la opción de menú correspondiente no estará disponible.

Controlador

Si bien ya no vemos la opción de menú disponible, todavía es posible acceder al controlador si utilizamos la URL directamente, por ejemplo http://localhost:5000/Home/NuevaFuncionalidad. En el caso de que el Feature Flag este desactivado, el controlador no debería ser accesible y debería retornar respuesta un error 404 - NotFound.

Por suerte para nosotros, solo basta con agregar el atributo FeatureGate con el nombre de la funcionalidad a verificar:

public class HomeController : Controller
{
    public IActionResult Index() => View();

    public IActionResult Privacy() => View();

    [FeatureGate("NuevaFuncionalidad")]
    public IActionResult NuevaFuncionalidad() =>
        View();
}

Testing

Lo único que nos queda por hacer es jugar con la configuración de nuestro Feature Flag y ver como impacta tanto a nivel de Vista como a nivel de controlador.

Menu - Feature Flag true
Menu – Feature Flag true
Menu - Feature Flag false
Menu – Feature Flag false
Controlador - Feature Flag true
Controlador – Feature Flag true
Controlador - Feature Flag false
Controlador – Feature Flag false

Conclusión

Hoy vimos como gracias a la implementación de Features Flags podemos no solo activar o desactivar funcionalidades sin escribir código adicional y solo mediante configuración, sino que además nos permite desplegar funcionalidades parciales y activarlas en el momento que consideremos conveniente.

Pero esto es solo el comienzo, en los próximos artículos veremos como podemos verificar sistemas externos o nuestra propia base de datos para validar los flags, como activar filtros MVC, middlewares y mucho mas.

Startupeando en .Net (Latino.NET OnLine)

Esta semana tuve el placer de volver a dar la charla “Startupeando en .Net 5” para la comunidad de El Camino Dev, pero esta vez para la comunidad de Latino.Net OnLine!

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

Startupeando en .Net – Latino.NET OnLine

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.

User Secrets: protegiendo configuraciones sensibles .Net 5

¿Estás usando appsetings.json o appsettings.development.json para guardar información sensible? ¿Has “commiteado” alguna vez cadenas de conexión, secretos privados o llaves de aplicación por accidente?

Si la respuesta a las preguntas anteriores es afirmativa y nunca habías escuchado hablar de User Secrets, entonces este artículo te puede interesar.

Contexto

En la mayoría de nuestras aplicaciones necesitamos interactuar con algún tipo de API, servicio o base de datos que requiere la utilización de credenciales ya sea en forma de cadena de conexión (connection string), llaves de API (API Keys) o secretos (secrets).

Cómo programadores, no nos corresponde ocuparnos de la gestión de esas credenciales en ambientes productivos, pero sí nos corresponde manejarlas y protegerlas correctamente en nuestros ambientes locales.

Si bien hay diferentes formas de manejar credenciales (variables de entorno para producción por ejemplo), es muy frecuente la utilización de archivos appsettings.json y/o appsettings.development.json.

Problema

Para ejecutar nuestra aplicación en forma local, solemos utilizar el archivo appsettings.development.json para colocar aquellas configuraciones que dependen del ambiente, como por ejemplo las cadenas de conexión a base de datos. Por ejemplo:

{
  "ConnectionStrings": {
    "Database": "Server=dev.server;Database=Products;User Id=dev;Password=Passw0rd;"
  },
  "PricesEndpoint": "https://dev.server.com/api/prices?key=xkihf57845addg"
}

El problema con este método es que al momento de subir nuestros cambios al repositorio es muy fácil olvidarnos de este archivo e incluirlo como parte de la implementación.

.gitignore como alternativa

Un posible solución al problema anterior es incluir el archivos appsettings.development.json en el archivo .gitignore de forma que git ignore los cambios. En escenarios muy simples esto puede funcionar sin problemas.

Sin embargo, es común que haya ciertas configuraciones de facto que se van agregando o modificando con el tiempo, y que luego cada programador puede ir modificando en forma local según sea el caso. Por ejemplo:

{
  // Default for development environment.
  "UseCache":  false
}

Esto puede resultar bastante tedioso y confuso de manejar si el archivo appsettings.development.json se encuentra ignorado.

Solución: User Secrets

El Secret Manager es una funcionalidad desarrollada específicamente para manejar información sensible en ambientes locales de desarrollo de una forma más segura, y se ocupa de la gestión de los User Secrets.

Este guarda las piezas de información sensible dentro de un archivo que se encuentra en un directorio totalmente diferente al directorio del proyecto, de forma que nuestro sistema de control de cambios no lo vea como parte de las modificaciones.

¡Jamás volverás a “commitear” cadenas de conexión o secretos!

Activar User Secrets

Secret Manager funciona por proyecto, es decir, lo tenemos que activar en todos los proyectos donde lo necesitamos.

Podemos hacerlo mediante la línea de comandos utilizando .NET Cli ejecutando el siguiente comando en la carpeta del proyecto:

dotnet user-secrets init

O también podemos hacerlos desde Visual Studio simplemente haciendo clic derecho sobre el proyecto y luego seleccionando la opción Administrar secretos de usuario:

Administrar secretos de usuario desde Visual Studio
Administrar secretos de usuario desde Visual Studio

Al activar los secretos de usuario, se genera una entrada en el archivo del proyecto (.csproj) llamada UserSecretsId con un guid:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <UserSecretsId>0dc8fc18-564f-4c6e-9602-65fae16ab231</UserSecretsId>
  </PropertyGroup>
</Project>

Ese guid corresponde al nombre de un directorio creado %APPDATA%\Microsoft\UserSecrets\ (Windows) o ~/.microsoft/usersecrets/ (Linux / MacOS), dentro del directorio encontraremos un archivo en formato JSON llamado secrets.json con las configuraciones que hayamos agregado.

Directorio de secretos de usuario
Directorio de secretos de usuario

Agregar User Secrets

Podemos agregar secretos de usuario mediante el cliente .NET Cli utilizando el siguiente comando:

dotnet user-secrets set "ConnectionStrings:Database" "Server=..."

dotnet user-secrets set "PricesEndpoint" "https://dev..."

En el primer comando los “:” (dos puntos) se utilizan para indicar los niveles de objetos de un JSON

También podemos hacerlo desde Visual Studio mediante la opción Administrar secretos de usuario como vimos anteriormente. La ventaja de esta alternativa es que Visual Studio nos permite manejar los secretos directamente como un archivo JSON, exactamente igual al archivo appsettings.json:

{
  "ConnectionStrings": {
    "Database": "Server=..."
  },
  "PricesEndpoint": "https://dev..."
}

Leer User Secrets

Los secretos de usuario se leen como cualquier otra configuración una vez que fueron cargados apropiadamente. Por ejemplo, podemos acceder a la configuración PricesEndpoint mediante la interfaz IConfiguration:

public class Startup
{
    public Startup(IConfiguration configuration) =>
        Configuration = configuration;

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        var pricesEndpoint = Configuration["PricesEndpoint"];
        // O Configuration.GetConnectionString("Database")        
        var connectionString = Configuration["ConnectionStrings:Database"];
        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
    }
}

Para que esto funcione es necesario cargar primero los secretos de usuario. Si estamos utilizando Host.CreateDefaultBuilder(args) en la clase Program.cs, entonces no es necesario hacer nada mas ya que los secretos de usuario se cargar en forma automática.

public class Program
{
    public static void Main(string[] args) =>
        CreateHostBuilder(args).Build().Run();

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

En el caso de que no estemos utilizando Host.CreateDefaultBuilder(args) o querramos cambiar el comportamiento, tenemos que utilizar el método de extensión AddUserSecrets().

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((hostContext, builder) =>
        {
            builder.SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile("appsettings.development.json", optional: true, reloadOnChange: true)
                .AddUserSecrets<Program>(optional: true, reloadOnChange: true)
                .AddEnvironmentVariables();
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

El orden en el que se incluyen las distintas fuentes de configuración es importante, ya que las ultimas sobrescriben a las primeras.

Debido a que los User Secrets sólo se usan en el ambiente local de cada desarrollador, lo recomendable es incluirlos sólo y sólo si el ambiente es Desarrollo:

Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((hostContext, builder) =>
    {
        var environment = hostContext.HostingEnvironment;
        // ...
        if(environment.IsDevelopment())
        {
            builder.AddUserSecrets<Program>(optional: true, reloadOnChange: true);
        }
        // ...
    })
    // ...

Conclusión

En este artículo aprendimos que incluso en ambientes locales de desarrollo debemos manejar información sensible, como cadenas de conexión y secretos, con el mismo cuidado que lo hacemos en producción.

Además aprendimos cómo User Secrets puede ayudarnos a evitar dolores de cabeza en la gestión de información sensible en nuestros ambientes de desarrollo locales de una forma muy simple y práctica.

Controllers y CancellationTokens en .Net 5

Hola! Quiero comenzar el post con la siguiente pregunta:
Si haces una solicitud a tu Web Api desde un cliente REST (Postman por ejemplo) y luego la cancelas antes de que termine, ¿Qué sucede?

En pocas palabras, podría no pasar nada o podríamos estar desperdiciando recursos sin necesidad. Todo depende de cómo estés escribiendo los controladores.

En los próximos párrafos intentaré responder esa pregunta, y mediante ejemplos, veremos como solucionarlo.

Caso de uso

Analicemos el siguiente controlador MVC de una aplicación Web API regular. Es un controlador muy simple que simula una operación costosa. En este caso es un simple Task.Delay() de 5 y 10 segundos respectivamente.

[HttpGet]
public async Task<IActionResult> Get()
{
    _logger.LogInformation("1- Ready to start");

    await Task.Delay(TimeSpan.FromSeconds(5));

    _logger.LogInformation("2- Half of the work done");

    await Task.Delay(TimeSpan.FromSeconds(10));

    _logger.LogInformation("3- We finished!!!");

    return Ok();
}

Y ahora lo probaremos desde Postman. Para esto ejecutamos la solución y luego hacemos una petición a nuestra API. La idea es esperar los primeros 5 segundos y luego cancelar la petición desde Postman. La petición completa toma algo más de 15 segundos.

Cancelación de la solicitud
Cancelación de la solicitud

Observación

En la imagen anterior vemos sobre la izquierda Postman y sobre la derecha la consola de Visual Studio mostrando los logs de la API.

Algo muy interesante es que a pesar de haber cancelado la petición luego de los 5 segundos los logs siguen apareciendo en la consola. Como podemos apreciar, Postman efectivamente cancela la solicitud, pero la API parece ignorar la cancelación.

Si efectivamente estuviéramos realizando una operación costosa, cómo puede ser acceso a base de datos o comunicación con otros servicios, habríamos desperdiciados recursos procesando parte de una solicitud que ya no tiene sentido debido a que el cliente desistió de la misma.

Aspectos técnicos

Esta situación donde una tarea es cancelada o abortada es bastante frecuente en distintos tipos de aplicaciones (ya sean web, desktop o nativas) y el framework nos provee mecanismos para hacer la tarea mucho mas simple.

A su vez, Asp.Net esta preparado para aprovechar esos mecanismos y detectar cuando el cliente canceló la solicitud o la conexión ha sido interrumpida, es nuestra responsabilidad hacer uso de ellos.

CancellationTokens

Cada vez que una acción es ejecutada el framework inyectará una instancia de un CancellationToken enlazada al ciclo de vida de la petición. De esta forma podremos detectar si la la petición ha sido cancelada.

Podemos lanzar un excepción del tipo TaskCancelledException como en el siguiente ejemplo:

[HttpGet]
public IActionResult Get(CancellationToken token)
{
    token.ThrowIfCancellationRequested();

    return Ok();
}

O bien podemos validar si se ha solicitado su cancelación:

[HttpGet]
public IActionResult Get(CancellationToken token)
{
    if(token.IsCancellationRequested)
    {
        return BadRequest("Solicitud cancelada.");
    }

    return Ok();
}

Pasando CancellationTokens en la cadena de ejecución

En general, es común que los métodos que impliquen algún tipo de tarea asincrónica (I/O en general como comunicación con base de datos, manejos de streams, network, etc.) acepten un CancellationToken como último parámetro.

[HttpGet]
public async Task<IActionResult> Get(CancellationToken token)
{
    await Task.Delay(TimeSpan.FromSeconds(5), token);

    return Ok();
}

Modificando el controlador

Cómo vimos en el ejemplo anterior, el cambio necesario para manejar correctamente la cancelación de la solicitud solo requiere agregar un parámetro adicional CancellationToken a la acción del controlador y pasar ese token al método Task.Delay().

[HttpGet]
public async Task<IActionResult> Get(CancellationToken token)
{
    _logger.LogInformation("1- Ready to start");

    await Task.Delay(TimeSpan.FromSeconds(5), token);

    _logger.LogInformation("2- Half of the work done");

    await Task.Delay(TimeSpan.FromSeconds(10), token);

    _logger.LogInformation("3- We finished!!!");

    return Ok();
}

Esta vez el comportamiento será diferente:

Cancelación de la solicitud - Error
Cancelación de la solicitud – Error

Dos cosas interesantes podemos destacar:

  1. Por un lado la tarea efectivamente fue cancelada y la ejecución de la petición no continuo.
  2. Se produjo una excepción

TaskCancelledException

Cuando un CancellationToken recibe la notificación de cancelación arroja una excepción del tipo TaskCancelledException. Es nuestra responsabilidad manejar en forma correcta esta situación.

Es importante mencionar que la forma en la que manejamos las cancelaciones es completamente arbitraria y depende del tipo de software y reglas de negocio con las que estemos trabajando.

A fines prácticos para nuestro ejemplo, voy a utilizar un simple try/catch.

[HttpGet]
public async Task<IActionResult> Get(CancellationToken token)
{
    try
    {
        _logger.LogInformation("1- Ready to start");

        await Task.Delay(TimeSpan.FromSeconds(5), token);

        _logger.LogInformation("2- Half of the work done");

        await Task.Delay(TimeSpan.FromSeconds(10), token);

        _logger.LogInformation("3- We finished!!!");

        return Ok();
    }
    catch (TaskCanceledException)
    {
        _logger.LogInformation("Task was cancelled");

        return BadRequest();
    }
}
Cancelación de la solicitud - Éxito
Cancelación de la solicitud – Éxito

Consideraciones

Un aspecto muy importante a tener en cuenta es qué tipo de consecuencias puede generar la cancelación de la solicitud en términos de consistencia, ya que podríamos potencialmente truncar una operación en la mitad.

Por ejemplo, si estamos trabajando con Entity Framework Core es conveniente agrupar las operaciones dentro de una transacción y pasar el CancellationToken. CommitAsync() y RollbackAsync() también aceptan un CancellationToken.

using (var transaction = await _dbContext.Database.BeginTransactionAsync(token))
{
    // Modifying operations here

    // Commit if successful
    await transaction.CommitAsync(token);
    // Rollback otherwise
    await transaction.RollbackAsync(token);
}

Conclusión

Por un lado podemos concluir que es importante contemplar la idea de que la petición puede ser abortada o cancelada, y podríamos estar malgastando recursos innecesarios.

Por el otro lado y para suerte nuestra, el framework nos provee todos los mecanismos necesarios para que la tarea se fácil y sencilla mediante la utilización de CancellationTokens.

Versionando APIs con .NET 5 (Net-Baires)

Uno de los grandes desafíos a la hora de diseñar y desarrollar REST APIs es cómo encarar aquellos cambios que afectan a los clientes, conocidos como breaking changes.

Para evitar los breaking changes es indispensable aplicar una estrategia de versionado clara y efectiva.

En esta sesión cuento todo lo que necesitamos saber para versionar nuestras APIs de forma eficiente y efectiva utilizando todo el poder que .NET 5 nos otorga.

El código completo de los ejemplos, como siempre disponible en mi GitHub.

Unit-Testeando HttpClient con .NET 5

Ya desde las primeras versiones, .NET Core utiliza inyección de dependencias desde sus cimientos, y provee un completo mecanismo de Inversión de Control (IoC) para hacernos la vida más simple.

Podemos registrar cualquier clase y delegar en el framework la tarea de generar instancias e inyectar los objetos, y la clase HttpClient no es la excepción. Ahora, cómo inyectamos HttpClient fakes?

Ahora veremos todo lo necesario para poder escribir buenos test unitarios con excelentes HttpClientfakeados“!

Un fake es un objeto usamos para simular un comportamiento específico. Es uno de los conceptos de las pruebas unitarias.

Contexto

Hemos estado trabajando en un nuevo servicio que realiza una petición HTTP como parte de las reglas de negocio.

public class StockService
{
    private readonly HttpClient _httpClient;

    public StockService(HttpClient httpClient) =>
        _httpClient = httpClient;

    public async Task<string> IsAvailableAsync(bool check)
    {
        if (check == false)
            return "N";

        var result = await _httpClient.GetAsync("stock");

        // To keep it simple, we ignore other results
        return result.StatusCode switch
        {
            HttpStatusCode.OK => "S",
            HttpStatusCode.BadRequest => "E"
        };
    }
}

A su vez, registramos el HttpClient en Startup.ConfigureServices() donde además configuramos la dirección base del servidor.

public void ConfigureServices(IServiceCollection services)
{
    // Other registrations...
    services.AddHttpClient<StockService>(client =>
    {
        // For demo only. Use settings instead
        client.BaseAddress = new Uri("https://api.facutherock.net");
    });
}

Necesitamos asegurarnos de que este servicio se comporte acorde a la especificación.

Requerimientos

Queremos escribir buenos test unitarios para asegurarnos que el servicio StockService.IsAvailableAsync() cumpla con los siguientes requerimientos:

  1. Retornar "N" si el parámetro check es igual a false.
  2. Si el parámetro check es igual a true y el resultado de la petición es exitosa (200 – Ok) devolver "S".
  3. Si el parámetro check es igual a true pero el resultado de la petición es de error (400 – BadRequest) devolver "E".

Si bien los requerimientos son muy simples, nos van a servir para demostrar los aspectos que necesitamos.

Desafío

A primera vista podríamos intentar utilizar un HttpClient directamente, cómo en el siguiente ejemplo:

[Fact]
public async Task BadTest()
{
    // Arrange
    var httpClient = new HttpClient();
    var sut = new StockService(httpClient);

    // Act
    var result = await sut.IsAvailableAsync(true);

    // Assert
    Assert.Equal("S", result);
}

Sin embargo, ese test fallaría ya que intentaría hacer una petición a un servidor inválido. Incluso si no fallase, implicaría una petición real, lo cuál no es recomendable para un test unitario.

El desafío consiste en crear una versión del HttpClient que simule hacer la petición, pero que realmente no lo haga.

Aspectos técnicos

La clase HttpClient realiza las peticiones utilizando un HttpMessageHandler de base, el cuál expone un método llamado SendAsync.

La manera en la que podemos simular determinados comportamientos es sobrescribiendo el método anterior. Para esto necesitamos generar nuestra propia subclase.

Podremos inyectar nuestra subclase utilizando una de las sobrecargas que la clase HttpClient tiene aceptando una instancia de HttpMessageHandler.

Tiempo de codear

Vamos a empezar a escribir los test necesarios para asegurar el buen funcionamiento de nuestro servicio.

La estrategia consiste en ir generando subclases de HttpMessageHandler que respondan siempre de la misma forma y cuya respuesta sea controlada por nosotros.

Requerimiento 1

El test para este requerimiento es el más simple de implementar, ya que en este caso no necesitamos utilizar el HttpClient para la petición.

Podemos utilizar directamente una nueva instancia de HttpClient:

[Fact]
public async Task FirstRequirementTest()
{
    // Arrange
    var httpClient = new HttpClient();
    var sut = new StockService(httpClient);

    // Act
    var result = await sut.IsAvailableAsync(false);

    // Assert
    Assert.Equal("N", result);
}

Requerimiento 2

Este test es un poco más complejo, ya que necesitamos que la instancia de HttpClient devuelva un mensaje de retorno exitoso.

Para esto, crearemos una subclase de HttpMessageHandler y sobrescribiremos el método SendAsync para devolver siempre un resultado exitoso.

public class OkHttpMessageHandler : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var okResult = new HttpResponseMessage
        {
            StatusCode = System.Net.HttpStatusCode.OK
        };

        return Task.FromResult(okResult);
    }
}

Finalmente crearemos una instancia de un HttpClient y completaremos el test.

[Fact]
public async Task SecondRequirementTest()
{
    // Arrange
    var okHttpMessageHandler = new OkHttpMessageHandler();
    using var httpClient = new HttpClient(okHttpMessageHandler)
    {
        BaseAddress = new Uri("http://fake.server")
    };
    var sut = new StockService(httpClient);

    // Act
    var result = await sut.IsAvailableAsync(true);

    // Assert
    Assert.Equal("S", result);
}

Requerimiento 3

Este test es exactamente al anterior, solo que la respuesta del HttpClient de ser de error.

public class BadRequestHttpMessageHandler : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var badRequestResult = new HttpResponseMessage
        {
            StatusCode = System.Net.HttpStatusCode.BadRequest
        };

        return Task.FromResult(badRequestResult);
    }
}

Y el test queda de la siguiente forma:

[Fact]
public async Task ThirdRequirementTest()
{
    // Arrange
    var badRequestHttpMessageHandler = new BadRequestHttpMessageHandler();
    using var httpClient = new HttpClient(badRequestHttpMessageHandler)
    {
        BaseAddress = new Uri("http://fake.server")
    };
    var sut = new StockService(httpClient);

    // Act
    var result = await sut.IsAvailableAsync(true);

    // Assert
    Assert.Equal("E", result);
}

Conclusión

Para escribir buenos test unitarios en casos donde se utiliza la clase HttpClient, es necesario tener control total de las respuesta que el mismo retorna.

En este post vimos como utilizar HttpMessageHandler para simular el comportamiento deseado y asegurar un correcto funcionamiento.

Código más testeable con ISystemClock

Te propongo examinar juntos el siguiente fragmento de código:

public class BirthDayCalculator
{
    public int CalculateNextBirthDayInDays(
        int month,
        int day)
    {
        var today = DateTime.UtcNow;
        var birthDay = new DateTime(today.Year, month, day);

        if (birthDay < today)
            birthDay = birthDay.AddYears(1);

        return (int)(birthDay -  today).TotalDays;
    }
}

A primera vista se ve bien, dados un mes y un día, el método CalculateNextBirthDayInDays() calcula y retorna la cantidad de días restantes para el próximo cumpleaños.

Sin embargo, algo no huele del todo bien. Ese método no soporta test unitarios.

Problema

Dos de los requesitos estrictamente necesarios de un test unitario, es que debe tener absoluto control del contexto de ejecución y debe ser repetible.

La solución que vimos en la sección anterior, funciona, pero depende de la propiedad estática DateTime.UtcNow. Debido a que no podemos controlar los valores que dicha propiedad puede tomar, no tenemos forma de escribir casos de prueba controlados y repetibles.

Alternativa poco elegante

Una alternativa simple al problema planteado es que el método CalculateNextBirthDayInDays() reciba un parámetro adicional representando la fecha y hora actual. Algo más o menos así:

public class BirthDayCalculator
{
    public int CalculateNextBirthDayInDays(
        int month,
        int day,
        DateTime today)
    {
        var birthDay = new DateTime(today.Year, month, day);

        if (birthDay < today)
            birthDay = birthDay.AddYears(1);

        return (int)(birthDay -  today).TotalDays;
    }
}

Esta modificación, poco elegante pero efectiva, nos permite escribir pruebas como la siguiente:

[Fact]
public void CalculateNumberOfDays()
{
    // Arrange
    var sut = new BirthDayCalculator();

    // Act
    var actualDays = sut.CalculateNextBirthDayInDays(
        month: 3,
        day: 13, 
        today: DateTime.UtcNow);

    // Assert
    Assert.Equal(290, actualDays);
}

Esta implementación tiene los siguientes puntos en contra:

  • No tenemos una forma común o servicio para obtener la fecha actual en forma testeable y centralizada, generando casi con seguridad código repetido.
  • Cada vez que es necesario realizar cálculos con fechas, necesitamos agregar un parámetro adicional. Incluso en aquellos casos donde se haga uso de del método CalculateNextBirthDayInDays().
public int ConsumingCode(DateTime today)
{
    int month = 3, day = 13;
    var calculator = new BirthDayCalculator();

    return calculator.CalculateNextBirthDayInDays(
        month,
        day,
        today);
}

ISystemClock al rescate

El equipo de Microsoft ha creado una bonita interfaz junto con una implementación estándar que se encuentra disponible en el paquete Microsoft.AspNetCore.Authentication llamada ISystemClock y SystemClock respectivamente.

Esta interfaz nos permite solucionar los dos puntos anteriores. Por un lado, no necesitamos agregar parámetros extras a todos los métodos, por el otro, nos permite utilizar inyección de dependencias para centralizar una única implementación.

namespace Microsoft.AspNetCore.Authentication
{
    public interface ISystemClock
    {
        DateTimeOffset UtcNow { get; }
    }
}

¿Cómo la usamos?

Vamos a modificar la clase BirthDayCalculator para recibir por parámetro en el constructor mediante inyección de dependencias la interfaz ISystemClock.

Luego simplemente reemplazamos la llamada a DateTime.UtcNow por _systemClock.UtcNow.

public class BirthDayCalculator
{
    private readonly ISystemClock _systemClock;

    public BirthDayCalculator(ISystemClock systemClock)
    {
        _systemClock = systemClock;
    }

    public int CalculateNextBirthDayInDays(
        int month,
        int day)
    {
        var today = _systemClock.UtcNow;
        var birthDay = new DateTime(today.Year, month, day);

        if (birthDay < today)
            birthDay = birthDay.AddYears(1);

        return (int)(birthDay - today).TotalDays;
    }
}

¿Cómo lo testeamos?

Primero generaremos una clase fake, la cuál llamaremos FakeSystemClock, que implemente la interfaz y que reciba mediante un parámetro en el constructor la fecha y hora a retornar.

public class FakeSystemClock : ISystemClock
{
    private readonly DateTime _desiredDay;

    public FakeSystemClock(DateTime desiredDay)
    {
        _desiredDay = desiredDay;
    }

    public DateTimeOffset UtcNow => _desiredDay;
}

Finalmente, modificamos el test para generar una nueva instancia de FakeSystemClock y pasamos esa instancia a nuestro BirthDayCalculator.

[Fact]
public void CalculateNumberOfDays()
{
    // Arrange
    var desiredDay = new DateTime(2021, 5, 26, 10, 0, 0);
    var fakeSystemClock = new FakeSystemClock(desiredDay);
    var sut = new BirthDayCalculator(fakeSystemClock);

    // Act
    var actualDays = sut.CalculateNextBirthDayInDays(
        month: 3,
        day: 13);

    // Assert
    Assert.Equal(290, actualDays);
}

Si estuviésemos utilizando librerías como Moq o FakeItEasy, no necesitaríamos crear esta clase fake.

ISystemClock en Asp.Net Core

Si estamos en el contexto de aplicaciones Asp.Net Core, podemos utilizar la implementación estándar provista como parte del paquete.

Sólo necesitamos registrarla en nuestro contenedor IoC.

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<ISystemClock, SystemClock>();

    // More services
    services.AddControllers();
}

Conclusión

De una forma muy simple y con poco refactoring, podemos eliminar las dependencias a DateTime.UtcNow y hacer nuestro código testeable gracias a ISystemClock.

Podrás encontrar el código completo de ejemplo en mi GitHub.

Manejo de excepciones sin excepciones con .NET 5

Uno de los aspectos más importantes en el desarrollo de aplicaciones es el manejo de excepciones. Esto es verdadero independientemente del tipo de aplicación o plataforma.

Es habitual que incluso diseñemos reglas de negocio en forma de excepciones, por ejemplo para validar parámetros de entrada o ejecutar acciones no permitidas sobre algunas entidades de dominio.

Algo negativo o falla de diseño que tiene esta forma de manejar excepciones es que no es para nada declarativa. C# no provée ningún mecanismo para hacer explícito qué excepciones puede arrojar un método.

En esta sesión veremos una alternativa para solucionar esa falla de diseño apoyándonos en una excelente librería llamada OneOf.

Manejo de excepciones sin excepciones

Como siempre, podrán encontrar el código completo de todos los ejemplos en mi GitHub.

Integrando Web MVC + Web API .Net 5 con Azure AD B2C

Cuándo trabajamos en ambientes de microservicios, o al menos con distintas APIs, que están protegidos utilizando Azure AD B2C, es necesario que el Front End pueda interactuar con las APIs.

Hoy veremos los pasos necesarios para integrar los tres componentes y tener así nuestro ecosistema de servicios integrados y protegidos con Azure AD B2C.

IMPORTANTE: para poder implementar los pasos aquí mencionados es necesario contar con una configuración básica en Azure AD B2C. Podrás encontrarlos en el siguiente paso a paso.

Antes de Comenzar

En este tutorial vamos a utilizar .Net 5 como marco o framework de desarrollo, con lo cual es un requisito excluyente tener instalado .Net 5 SDK. Si aún no lo tienes, lo puedes descargar aquí, también encontrarás un instructivo paso a paso en caso de que tengas dudas.

Por otro lado, vamos a utilizar Visual Studio 2019 Community Edition, sin embargo este no es un requerimiento excluyente ya que podemos utilizar cualquier editor de texto o ambiente de desarrollo como Visual Studio Code o Notepad++. Puedes descargar Visual Studio 2019 aquí.

Contexto

En los últimos años ha proliferado mucho el desarrollo de aplicaciones compuestas por módulos o APIs independientes del Front End, favoreciendo así la escalabilidad y adaptabilidad a los cambios.

Por supuesto que esto genera un nuevo desafío: Integrar Front Ends y APIs de forma fácil y simple.

Requerimiento

Por un lado tenemos una aplicación Web MVC .Net 5 integrada con Azure AD B2C, por el otro lado tenemos una aplicación Web API .Net 5 que sólo acepta peticiones de usuarios que hayan sido autenticados por Azure AD B2C.

Necesitamos realizar los cambios necesarios para que nuestra aplicación MVC pueda comunicarse con nuestra API.

Solución

Nuestra API solo acepta peticiones de usuarios autenticados por Azure AD B2C, para esto utiliza JWT tokens. Estos tokens contienen la información suficiente para asegurar que se trata de un usuario legítimo.

La API espera recibir con cada petición una cabecera o header de autenticación incluyendo un JWT token emitido por Azure AD B2C.

La solución consiste en configurar los mecanismos necesarios para que la aplicación MVC pueda generar JWT tokens en nombre del usuario y así hacer las peticiones a la API en forma correcta.

Paso 1: Preparar la aplicación Web MVC

Lo primero que haremos es preparar nuestra aplicación Web MVC para mostrar los resultados obtenidos de la API, esto implica crear un Modelo de Vista o View Model, un Controlador y una Vista..

Modelo de Vista o View Model

Para poder mostrar el resultado obtenido de la API necesitamos un View Model, para lo cuál crearemos una nueva clase llamada WeatherForecastViewModel.cs bajo el directorio Models:

public class WeatherForecastViewModel
{
    public DateTime Date { get; set; }

    public int TemperatureC { get; set; }

    public int TemperatureF { get; set; }

    public string Summary { get; set; }
}

El View Model que acabamos de crear se corresponde a la plantilla que utiliza Visual Studio por defecto.

Controlador

Por el momento el controlador tendrá la tarea de comunicarse con la API y devolver los resultados a la vista. Los pasos son los siguientes:

  1. Hacer la petición a la API
  2. Leer y deserializar el resultado
  3. Devolver el resultado a la vista.

Crearemos un nuevo Controlador llamado WeatherController dentro del directorio Controllers. Este controlador tendrá una sola acción:

public class WeatherController : Controller
{
    private readonly HttpClient _httpClient;

    public WeatherController(HttpClient httpClient) =>
        _httpClient = httpClient;

    public async Task<IActionResult> Index()
    {
        var response = await _httpClient.GetAsync("https://localhost:5002/weatherforecast");
        var content = await response.Content.ReadAsStringAsync();
        var values = JsonConvert.DeserializeObject<IEnumerable<WeatherForecastViewModel>>(content);

        return View(values);
    }
}

Debido a que la API está protegida, este controlador todavía no va a funcionar. Modificaremos en los próximos pasos el controlador para poder realizar la interacción con la API en forma exitosa.

Además de incluir algunos usings que serán sugeridos por Visual Studio, es necesario instalar un paquete adicional para la deserialización: Newtonsoft.Json.

Vista

Para mostrar los resultados crearemos una nueva vista llamada Index.cshtml, pero antes debemos crear un nuevo directorio llamado Weather dentro del directorio Views.

Directorio de vistas
Directorio de vistas

El código de la vista estará compuesto simplemente por una tabla mostrando los resultados obtenidos en el controlador.

@model IEnumerable<WeatherForecastViewModel>
@{
    ViewData["Title"] = "Weather";
}

<h1>Weather</h1>
<table class="table">
    <thead>
        <tr>
            <th>@Html.DisplayNameFor(model => model.Date)</th>
            <th>@Html.DisplayNameFor(model => model.TemperatureC)</th>
            <th>@Html.DisplayNameFor(model => model.TemperatureF)</th>
            <th>@Html.DisplayNameFor(model => model.Summary)</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>@Html.DisplayFor(modelItem => item.Date)</td>
                <td>@Html.DisplayFor(modelItem => item.TemperatureC)</td>
                <td>@Html.DisplayFor(modelItem => item.TemperatureF)</td>
                <td>@Html.DisplayFor(modelItem => item.Summary)</td>
            </tr>
        }
    </tbody>
</table>

Como último paso debemos agregar un acceso directo a esta nueva vista en la cabecera general.

Para esto agregaremos uno nuevo elemento a la sección Header de la vista compartida llamada _Layout.cshtml que se encuentra dentro del directorio Views/Shared:

<ul class="navbar-nav flex-grow-1">
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-controller="Weather" asp-action="Index">Weather</a>
    </li>
</ul>
<partial name="_LoginPartial" />

En este punto ya tenemos la vista preparada, si ejecutamos el proyecto MVC deberíamos ver una nueva entrada en la cabecera. Sin embargo, si intentamos navegar hasta esta nueva vista, obtendremos un error.

Este error se debe a que todavía, nuestra aplicación no puede generar el token necesario para interactuar con la API.

Pantalla de bienvenida
Pantalla de bienvenida

Paso 2: Configuración en Azure AD B2C

Hasta el momento configuramos en nuestro tenant de Azure AD B2C un registro de aplicación para la aplicación MVC y otro registro de aplicación para la API.

Además, cuando registramos la API creamos un ámbito o scope llamado Lector que se ve asi:

https://linkedinazureadb2c.onmicrosoft.com/posts/Lector

Bien, ahora necesitamos configurar nuestra aplicación MVC en Azure AD B2C para que pueda solicitar JWT tokens incluyendo el scope de la API. Para esto necesitamos realizar lo siguiente:

  1. Generar un secreto de cliente o client secret
  2. Asignar permisos para utilizar el scope

Generar secreto de cliente

Para generar un secreto tenemos que ir al registro de aplicación correspondiente a la aplicación MVC, y luego en sección Certificados y secretos crear un nuevo secreto de cliente.

Certificados y secretos
Certificados y secretos

Sólo necesitamos darle una descripción, podemos dejar el campo Expiración en su valor por defecto que es de 6 meses. Una vez creado, podremos obtener el valor creado por Azure desde la de secretos utilizando el botón para copiar.

El valor del secreto solo se puede ver y/o copiar por única vez al momento de su creación. Es importante tomar nota del mismo o tendremos que crear uno nuevo.

Asignar permisos para utilizar el scope

Desde el registro de aplicación MVC iremos a la sección Permisos de API y agregaremos un nuevo permiso.

Agregar permiso de API
Agregar permiso de API

Luego navegamos a la pestaña Mis API y seleccionamos del listado la API que habíamos creado para proteger nuestra Web API .Net 5.

Finalmente seleccionamos el permiso Lector que creamos cuando creamos el scope y agregamos el permiso.

Como último paso para terminar de dar los permisos necesarios es conceder consentimiento de administrador. Desde la misma sección de Permisos de API veremos el nuevo permiso pero en estado No concedido.

Concedemos el permiso y terminamos. El icono pasara de naranja a verde como se muestra en la imagen con los otros permisos.

Conceder consentimiento de administrador
Conceder consentimiento de administrador

Paso 3: Configurar aplicación Web MVC

Habiendo realizado todas las configuraciones pertinentes en torno a Azure AD B2C, podemos proceder con la configuración de la aplicación MVC.

Comenzaremos agregando el secreto de cliente que configuramos en el paso anterior al archivo appsettings.json, dentro de la sección "AzureAdB2C".

"AzureAdB2C": {
  "Instance": "https://linkedinazureadb2c.b2clogin.com",
  "ClientId": "00000000-0000-0000-0000-000000000000",
  "Domain": "linkedinazureadb2c.onmicrosoft.com",
  "SignUpSignInPolicyId": "B2C_1_InicioSesionConRegistro",
  "ClientSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

Finalmente debemos configurar los servicios necesarios para poder generar los JWT tokens necesarios y así poder interactuar con la API.

Para esto, en el método ConfigureServices de la clase Startup.cs utilizaremos los métodos de extensión EnableTokenAcquisitionToCallDownstreamApi() indicando la lista de scopes habilitados y AddInMemoryTokenCaches().

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews()
        .AddMicrosoftIdentityUI();

    services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAdB2C")
        .EnableTokenAcquisitionToCallDownstreamApi(new string[] { "https://linkedinazureadb2c.onmicrosoft.com/posts/Lector" })
        .AddInMemoryTokenCaches();

    services.AddRazorPages();
}

Paso 4: Generar JWT tokens

En el paso anterior configuramos los servicios necesarios para poder generar los JWT tokens que necesitamos incluir en las peticiones a nuestra API.

Uno de esos servicios es ITokenAcquisition, el cuál nos habilita una serie de métodos para obtener de forma muy sencilla distintos tipos de JWT tokens.

Comenzaremos recibiendo en el constructor del controlador mediante inyección de dependencias el servicio y guardándolo en una variable local. Tendremos que agregar la referencia a Microsoft.Identity.Web en la lista de usings.

private readonly HttpClient _httpClient;
private readonly ITokenAcquisition _tokenAcquisition;

public WeatherController(
    ITokenAcquisition tokenAcquisition,
    HttpClient httpClient)
{
    _httpClient = httpClient;
    _tokenAcquisition = tokenAcquisition;
}

Luego modificaremos la acción Index() del controlador. Utilizaremos el método GetAccessTokenForUserAsync() del servicio ITokenAcquisition para obtener un JWT token en nombre del usuario actual indicando la lista de scopes que queremos incluir.

Por último, solo necesitamos incluir el token como nuevo header de autenticación en la petición a la API.

public async Task<IActionResult> Index()
{
    var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { "https://linkedinazureadb2c.onmicrosoft.com/posts/Lector" });
    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

    var response = await _httpClient.GetAsync("https://localhost:5002/weatherforecast");
    var content = await response.Content.ReadAsStringAsync();
    var values = JsonConvert.DeserializeObject<IEnumerable<WeatherForecastViewModel>>(content);

    return View(values);
}

Paso 5: Pruebas

Si llegamos a este punto y no hemos olvidado ningún paso ni configuración, estamos listos para compilar y ejecutar la solución.

Luego de iniciar sesión, podemos verificar que todo funcione correctamente haciendo clic en la nueva acción disponible en la cabecera. Deberíamos ver el resultado devuelto por la API en forma de tabla.

Resultado en forma de tabla
Resultado en forma de tabla

Si intentamos acceder a la vista sin haber iniciado sesión obtendremos una excpción del tipo MsalUiRequiredException.

Es nuestra responsabilidad detectar este escenario y redireccionar al usuario a la pantalla de inicio de sesión.

Conclusión

Con este post hemos completado una integración completa de una aplicación Web MVC, una Web API y Azure AD B2C, y vimos que es muy simple e intuitivo la forma en que los tres componentes se integran.

Gracias a las librerías Microsoft.Identity.Web y Microsoft.Identity.Web.UI podemos escribir muy poco código y reducir así el esfuerzo y tiempo necesarios.

Podrás encontrar el código completo en mi GitHub.

Protegiendo Web API .Net 5 con Azure AD B2C

Uno de los requerimientos más comunes cuándo utilizamos Azure AD B2C es que sólo usuarios autenticados de nuestro tenant puedan acceder a nuestras APIs.

En este artículo veremos como configurar Azure AD B2C y como integrar una Web API .Net 5 para aceptar peticiones de usuarios autenticados.

IMPORTANTE: para poder implementar los pasos aquí mencionados es necesario contar con una configuración básica en Azure AD B2C. Podrás encontrarlos en el siguiente paso a paso.

Antes de Comenzar

En este tutorial vamos a utilizar .Net 5 como marco o framework de desarrollo, con lo cual es un requisito excluyente tener instalado .Net 5 SDK. Si aún no lo tienes, lo puedes descargar aquí, también encontrarás un instructivo paso a paso en caso de que tengas dudas.

Por otro lado, vamos a utilizar Visual Studio 2019 Community Edition, sin embargo este no es un requerimiento excluyente ya que podemos utilizar cualquier editor de texto o ambiente de desarrollo como Visual Studio Code o Notepad++. Puedes descargar Visual Studio 2019 aquí.

Contexto

Muy frecuentemente, encontramos como parte de nuestras aplicaciones componentes del tipo API REST para exponer funcionalidades específicas.

Suele ser habitual que queramos proteger dichas APIs de accesos a usuarios no autorizados o que no forman parte de nuestra aplicación.

Requerimiento

Tenemos una aplicación Web API .Net 5 la cuál queremos integrar con Azure AD B2C, de forma que solo usuarios autenticados puedan acceder.

Esta aplicación todavía no implementa ningún mecanismo de autenticación, con lo cual veremos también los pasos necesarios para configurar autenticación.

Microsoft Identity Web

Microsoft ha estado mejorando y simplificando la forma en la que se integran aplicaciones web con Azure AD, no solo soportando estándares como OAuth2 y OpenID Connect, sino también reduciendo la cantidad de código necesario para implementar la solución.

Para nuestra integración utilizaremos la librería Microsoft.Identity.Web. Estas librerías están construidas sobre la Biblioteca de Autehticación de Microsoft o MSAL.

  • Microsoft.Identity.Web
    Esta librería proporciona los mecanismos necesarios para interactuar con Azure AD de una forma simple, segura y estándar. Por ejemplo, los manejadores para las URL de redirección para el inicio de sesión y el cierre de sesión.

Paso 1: Solución, proyecto y librerías

Comenzaremos creando una solución y un proyecto Web API. Luego instalaremos la librería mencionada en la sección anterior y para validar que todo esté funcionando correctamente compilaremos y ejecutaremos la solución.

# Crear solución y projecto API
dotnet new sln --name AzureAdB2C
dotnet new webapi --name AzureAdB2C.API --output AzureAdB2C.API
dotnet sln add .\AzureAdB2C.API\AzureAdB2C.API.csproj

# Instalar la librería necesaria
dotnet add .\AzureAdB2C.API\AzureAdB2C.API.csproj package Microsoft.Identity.Web

# Restaurar y compilar
dotnet restore
dotnet build

# Ejecutar
dotnet run --project .\AzureAdB2C.API\AzureAdB2C.API.csproj

Deberíamos ver algo similar al siguiente resultado desde la consola, donde se proporcionará la URL local para poder acceder a la documentación y corroborar que esta funcionando.

Dotnet run desde la línea de comandos
Dotnet run desde la línea de comandos

Por defecto, .Net 5 incluye Swagger como mecanismo de documentación de APIs. Para acceder al mismo, usaremos la URL provista en la línea de comandos (en este caso https://localhost:5001) y agregaremos como parte de la ruta /swagger.

https://localhost:5001/swagger

Documentación con Swagger
Documentación con Swagger

Paso 2: Configuración de servicios y pipeline

Vamos a configurar los servicios necesarios para la integración con Azure AD B2C. Además configuraremos el pipeline para utilizar Autenticación.

Para este realizaremos los cambios detallados a continuación en la clase Startup.cs del proyecto AzureAdB2C.API.

Como paso preliminar agregaremos el siguiente using a la clase Startup.cs:

using Microsoft.Identity.Web;
using Microsoft.AspNetCore.Authentication.JwtBearer;

ConfigureServices

En el método ConfigureServices configuraremos los servicios necesarios para la integración con Azure AD B2C.

Microsoft.Identity.Web incluye un método de extensión muy conveniente para configurar la integración llamado AddMicrosoftIdentityWebApi().

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddMicrosoftIdentityWebApi(options =>
            {
                Configuration.Bind("AzureAdB2C", options);

                options.TokenValidationParameters.NameClaimType = "name";
            },
            options => { Configuration.Bind("AzureAdB2C", options); });

    services.AddControllers();
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "AzureAdB2C.API", Version = "v1" });
    });
}

El método AddMicrosoftIdentityWebApi() nos permite utilizar la configuración de nuestra aplicación mediante el objeto IConfiguration indicando el nombre de la sección a utilizar, en este caso "AzureAdB2C". Agregaremos esta sección de configuración mas adelante en archivo appsettings.json.

Configure

El único requisito a nivel de pipeline, es incluir el middleware de autenticación con el método UseAuthentication(). Debido a que estamos trabajando con el pipeline, es muy importante respetar el orden en el que llamamos al método.

Tiene que ser después de UseRouting() y antes de UseAuthorization().

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

Paso 3: Registrar la API en Azure AD B2C

Para que nuestra API pueda interactuar con Azure AD B2C es necesario generar un registro de aplicación previamente.

Para poder registrar una nueva aplicación, debemos navegar a la sección de Registros de aplicaciones situada sobre el panel de la izquierda y luego ir a Nuevo Registro.

Registro de aplicaciones
Registro de aplicaciones

En formulario de registro solo vamos a completar el campo Nombre y dejaremos el resto de los campos con sus valores por defecto. En este caso elegí como nombre “API”.

Nuevo registro de aplicación
Nuevo registro de aplicación

Ya finalizado el registro de aplicación para la API, en la sección de Información general vamos a tomar nota del Id de aplicación, ya que lo vamos a necesitar para la configuración de la API.

Id de aplicación o Client Id
Id de aplicación o Client Id

Paso 4: Exponer un ámbito o scope

Parte del requerimiento que estamos implementando implica que solo usuario autorizados puedan acceder a la API. A su vez los usuarios tienen acceso a determinadas APIs o recursos.

Esos recursos (o grupos de recursos) se representan a través de lo que se denomina ámbito o scope.

Cada API o recurso expone la lista de scopes que maneja de forma que puedan ser asignados a los usuarios en función de los roles que asignados.

A modo de ejemplo y para clarificar un poco más que es un scope, una API para gestión de Posts de un Blog, podría exponer la siguiente lista:

  • Lector: este scope habilita el acceso al los posts recomendados que un usuario puede tener en base a sus etiquetas preferidas.
  • Escritor: este scope habilita el acceso en modo edición a los posts que un usuario que ha escrito.
  • Moderador: este scope habilita la eliminación de posts que no cumplen con las reglas del blog

Exponer un ámbito o scope

Para exponer un scope vamos a ir al registro de aplicación que creamos en el paso anterior para nuestra API y luego a la sección Exponer una API. Finalmente vamos a clicar la opción Agregar un ámbito.

Exponer una API
Exponer una API

Establecer URI de Identificador

La primera vez que agregamos un scope vamos a tener que definir la URI con la cuál se identificará a todos los scopes expuestos.

URI de identitificación
URI de identitificación

Azure AD B2C sugerirá un valor aleatorio, sin embargo es recomendable asignar un valor que tenga sentido y represente la API.

Siguiendo con el ejemplo, posts es un buen nombre.

De esta forma, los scopes Lector, Escritor y Moderador se verían así:

  • https://linkedinazureb2c.onmicrosoft.com/posts/Lector
  • https://linkedinazureb2c.onmicrosoft.com/posts/Escritor
  • https://linkedinazureb2c.onmicrosoft.com/posts/Moderador

Agregar Scope

Una vez establecida la URI, continuamos con la creación del scope.

Agregar scope
Agregar scope

Paso 5: Configuración de la aplicación Web API

En este punto solo nos falta recolectar algunas piezas de información y configurar la aplicación Web API para que se integre correctamente con Azure AD B2C. La información que necesitamos es la siguiente:

  • Instancia
  • Dominio
  • Id de Cliente
  • Id de flujo de usuario

El Id de Cliente lo obtuvimos en el Paso 3 cuando registramos la API, para obtener la Instancia, Dominio y Id del flujo de Usuario te dejo esta breve sección del artículo que escribí sobre integración con aplicaciones Web MVC.

En el archivo de configuración appsettings.json agregaremos una nueva sección llamada “AzureAdB2C” con las cuatro piezas de información del paso anterior.

"AzureAdB2C": {
  "Instance": "https://linkedinazureadb2c.b2clogin.com",
  "ClientId": "f4ad0000-0000-0000-0000-000000004aec",
  "Domain": "linkedinazureadb2c.onmicrosoft.com",
  "SignUpSignInPolicyId": "B2C_1_InicioSesionConRegistro"
}

Conclusión

En este post vimos cómo proteger una Web API con Azure AD B2C de forma que sólo usuario autenticados mediante un flujo de usuario de Azure AD B2C puedan acceder a la API.

Sin embargo, todavía no hemos podido probar la integración ya que para eso necesitamos poder autenticar un usuario.

En el próximo artículo compartiré como integrar una aplicación Web MVC con aplicaciones Web API utilizando Azure AD B2C como mecanismo de autenticación.

Podrás encontrar el código completo en mi GitHub.

Integración de Azure AD B2C con MVC .Net 5

Hace unas semanas escribí una serie de artículos donde compartí paso a paso como dar los primeros pasos con Azure AD B2C y cómo configurar el servicio de forma correcta.

En este artículo veremos como realizar la integración de Azure AD B2C con aplicaciones Web MVC .Net 5 de forma fácil y rápida.

IMPORTANTE: trabajaremos sobre la base construida en los artículos anteriores, con lo cual es recomendable tener claros los conceptos y pasos allí detallados.

Antes de Comenzar

En este tutorial vamos a utilizar .Net 5 como marco o framework de desarrollo, con lo cual es un requisito excluyente tener instalado .Net 5 SDK. Si aún no lo tienes, lo puedes descargar aquí, también encontrarás un instructivo paso a paso en caso de que tengas dudas.

Por otro lado, vamos a utilizar Visual Studio 2019 Community Edition, sin embargo este no es un requerimiento excluyente ya que podemos utilizar cualquier editor de texto o ambiente de desarrollo como Visual Studio Code o Notepad++. Puedes descargar Visual Studio 2019 aquí.

Contexto

En la actualidad, casi cualquier aplicación requiere que ciertas partes o componentes estén restringidas a usuarios autenticados de la aplicación.

Habitualmente la autenticación se realiza presentando al usuario un formulario de Inicio de sesión donde debe ingresar el nombre de usuario o email y una contraseña.

Es aquí donde entra Azure AD B2C y los flujos de usuario.

Requerimiento

Tenemos una aplicación Web MVC .Net 5 la cual queremos integrar con nuestro Azure AD B2C. Esta aplicación todavía no implementa ningún tipo de autenticación.

La idea principal es que no sólo aprendamos a integrar Azure AD B2C si no que además veamos los requerimientos mínimos para comenzar a utilizar Autenticación.

Microsoft Identity Web

Microsoft ha estado mejorando y simplificando la forma en la que se integran aplicaciones web con Azure AD, no solo soportando estándares como OAuth2 y OpenID Connect, sino también reduciendo la cantidad de código necesario para implementar la solución.

Para nuestra integración utilizaremos las librerías Microsoft.Identity.Web y Microsoft.Identity.Web.UI. Estas librerías están construidas sobre la Biblioteca de Autehticación de Microsoft o MSAL.

  • Microsoft.Identity.Web
    Esta librería proporciona los mecanismos necesarios para interactuar con Azure AD de una forma simple, segura y estándar. Por ejemplo, los manejadores para las URL de redirección para el inicio de sesión y el cierre de sesión.
  • Microsoft.Identity.Web.UI
    Esta librería proporsiona los componentes visuales (controladores y vistas) necesarias para la integración, como pantallas de consentimiento, cierre de sessión, etc.

Paso 1: Solución, proyecto y librerías

Comenzaremos creando una solución y un proyecto Web MVC. Luego instalaremos las dos librerías mencionadas en la sección anterior y para validar que todo esté funcionando correctamente compilaremos y ejecutaremos la solución.

# Crear solución y projecto MVC
dotnet new sln --name AzureAdB2C
dotnet new mvc --name AzureAdB2C.MVC --output AzureAdB2C.MVC
dotnet sln add .\AzureAdB2C.MVC\AzureAdB2C.MVC.csproj

# Instalar las librerías necesarias
dotnet add .\AzureAdB2C.MVC\AzureAdB2C.MVC.csproj package Microsoft.Identity.Web
dotnet add .\AzureAdB2C.MVC\AzureAdB2C.MVC.csproj package Microsoft.Identity.Web.UI

# Restaurar y compilar
dotnet restore
dotnet build

# Ejecutar
dotnet run --project .\AzureAdB2C.MVC\AzureAdB2C.MVC.csproj

Deberíamos ver algo similar al siguiente resultado desde la consola, donde se proporcionará la URL local para poder acceder al sitio web y corroborar que esta funcionando.

Dotnet run desde la línea de comandos
Dotnet run desde la línea de comandos
Home aplicación web MVC
Home aplicación web MVC

Paso 2: Configuración de servicios y pipeline

Vamos a configurar los servicios necesarios para la integración con Azure AD B2C. Además configuraremos el pipeline para utilizar Autenticación.

Para este realizaremos los cambios detallados a continuación en la clase Startup.cs del proyecto AzureAdB2C.MVC.

Como paso preliminar agregaremos los siguientes usings a la clase Startup.cs:

using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;

ConfigureServices

Por un lado tenemos que configurar los servicios necesarios relacionados a la interfaz de usuario, como por ejemplo las vistas de consentimiento y cierre de sesión.

Uno de los cambios interesantes en la nueva librería MSAL es que incluye todos los controladores y vistas necesarias en forma predeterminada. Esto quiere decir que si no tenemos requerimientos especiales, no necesitamos crear ningún controlador ni vista adicional.

Necesitamos incluir los metodos de extension AddMicrosoftIdentityUI() para la parte de la interfaz de usuario y AddMicrosoftIdentityWebAppAuthentication() para la integración con Azure AD B2C.

Además incluiremos los servicios necesarios para trabajar con Razor Pages, ya que son requeridos por AddMicrosoftIdentityUI()

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews()
        .AddMicrosoftIdentityUI();

    services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAdB2C");

    services.AddRazorPages();
}

El método AddMicrosoftIdentityWebAppAuthentication() requiere como parámetro inicial el objeto de configuración IConfiguration y como segundo parámetro el nombre de la sección de configuración a utilizar, en este caso "AzureAdB2C". Agregaremos esta sección de configuración mas adelante en archivo appsettings.json.

Configure

El único requisito a nivel de pipeline, es incluir el middleware de autenticación con el método UseAuthentication(). Debido a que estamos trabajando con el pipeline, es muy importante respetar el orden en el que llamamos al método.

Tiene que ser después de UseRouting() y antes de UseAuthorization().

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

Por último, incluiremos el pipeline para utilizar Razor Pages.

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");

    endpoints.MapRazorPages();
});

Paso 3: Vistas para inicio y cierre de sesión

Ya que partimos de una aplicación Web MVC base, el sitio todavía no ofrece las acciones para inicio y cierre de sesión correspondientes. Esta es una tarea que tendremos que hacer nosotros.:

La implementación consiste en

  • Agregar una vista parcial incluyendo las acciones para inicio y cierre de sesión, habitualmente llamada _LoginPartial.cshtml.
  • Modificar la vista parcial _Layout.cshtml para incluir la vista del punto anterior.

_LoginPartial.cshtml

Esta vista se encargara de mostrar la acción para inicio de sesión en el caso de que el usuario no haya iniciado sesión aún, o mostrar el nombre del usuario junto con la acción de cerrar sesión en caso contrario.

@using Microsoft.Identity.Web
@if (User.Identity.IsAuthenticated)
{
    <ul class="nav navbar-nav navbar-right">
        <li class="navbar-text">Hello @User.GetDisplayName()!</li>
        <li><a asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignOut">Sign out</a></li>
    </ul>
}
else
{
    <ul class="nav navbar-nav navbar-right">
        <li><a asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn">Sign in</a></li>
    </ul>
}

Algo destacable son las acciones para inicio de sesión (Sign in) y cierre de sesión (Sign out). Ambas utilizan el controlador Account dentro del área MicrosoftIdentity.

Este controlador forma parte de la librería Microsoft.Identity.Web.UI. Este uno de los mejores cambios que esta librería nos ofrece, ya que nos evita tener que crear controladores y vistas.

_Layout.cshtml

Esta vista contiene, entre otras cosas, la cabecera (header) con la barra de navegación (navbar) o menú. Agregaremos como parte de la cabecera la vista parcial que creamos en la sección anterior.

<header>
    <nav class="navbar navbar-expand-sm ...">
        <div class="container">
            <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">AzureAdB2C.MVC</a>
            <button class="navbar-toggler">...            </button>
            <div class="navbar-collapse collapse ...">
                <ul class="navbar-nav flex-grow-1">...</ul>
                <partial name="_LoginPartial" />
            </div>
        </div>
    </nav>
</header>

Si ejecutamos la aplicación ahora veremos disponible la acción Sign in en la barra de menú sobre la parte de arriba a la derecha.

Home aplicación web MVC con Inicio de Sesión
Home aplicación web MVC con Inicio de Sesión

Paso 4: Agregar URLs de redirección en Azure AD B2C

Ya con nuestra aplicación funcionando, configuraremos la URL de redirección hacia nuestro sitio web local para el inicio de sesión y para el cierre de sesión. Como vimos anteriormente, Microsoft.Identity.Web incluye ambos manejadores.

  • El manejador de inicio de sesión responde a la URL /signin-oidc, que en nuestro ambiente de desarrollo local sería https:localhost:5001/signin-oidc.
  • El manejador de cierre de sesión responde a la URL /signout-oidc, que en nuestro ambiente de desarrollo local sería https:localhost:5001/signout-oidc.

Cuando realizamos la configuración inicial del tenant, registramos una nueva aplicación llamada Sitio Web Increíble donde agregamos una URL de redirección de prueba.

Para agregar la URL de redirección, desde el registro de aplicación Sitio Web Increíble en la sección Autenticación, tendremos la lista de URLs. Haciendo clic en Agregar un URI podremos agregar el nuestro.

Más abajo, agregaremos también la URL de cierre de sesión del canal frontal. Importante siempre guardar los cambios.

Configuración URL de retorno Azure AD B2C
Configuración URL de retorno Azure AD B2C

Paso 5: Recolectar información de Azure AD B2C

Para finalizar la integración necesitamos disponer de los siguientes datos:

  • Instancia
  • Dominio
  • Id de Cliente
  • Id del flujo de Inicio de Sesión y Registro

Instancia y Dominio

Si bien el nombre de la instancia y el nombre del dominio son diferentes, en ambas requerimos el subdominio. El subdominio lo podemos obtener desde el panel de Información General del tenant a partir del Nombre de Dominio.

Panel de información general Azure AD B2C
Panel de información general Azure AD B2C

Ya con el nombre de dominio completo, vamos a extraer la primer parte, y ese será el subdominio que usaremos para construir el nombre de la instancia. En el ejemplo anterior el nombre de dominio es linkedinazureadb2c.onmicrosoft.com, nos quedaremos con linkedinazureadb2c.

El nombre de instancia responde al siguiente formato https://{subdominio}.b2clogin.com, donde reemplazaremos {subdominio} por linkedinazureadb2c.

Estos son los valores que necesitamos:

  • Instancia: https://linkedinazureadb2c.b2clogin.com
  • Dominio: linkedinazureadb2c.onmicrosoft.com

Id de Cliente

El Id de Cliente o Client Id es el Id de registro de aplicación (Sitio Web Increíble). Y lo podemos obtener desde la lista de registros de aplicaciones bajo la columna Id de aplicación (cliente).

Registros de aplicaciones Azure AD B2C
Registros de aplicaciones Azure AD B2C

Id del flujo de Inicio de Sesión y Registro

El último dato que necesitamos es el id del flujo de usuario para inicio de sesión y registro, el cual obtendremos desde la sección de flujos de usuarios bajo la columna Nombre.

Flujos de usuario Azure AD B2C
Flujos de usuario Azure AD B2C

Paso 6: Configuración de la aplicación Web MVC

En este punto solo nos falta tomar la información que recolectamos anteriormente y configurar la aplicación Web MVC para que se integre correctamente con Azure AD B2C.

En el archivo de configuración appsettings.json agregaremos una nueva sección llamada “AzureAdB2C” con las cuatro piezas de información del paso anterior.

"AzureAdB2C": {
  "Instance": "https://linkedinazureadb2c.b2clogin.com",
  "ClientId": "f4ad0000-0000-0000-0000-000000004aec",
  "Domain": "linkedinazureadb2c.onmicrosoft.com",
  "SignUpSignInPolicyId": "B2C_1_InicioSesionConRegistro"
}

Paso 7: Pruebas

Si realizamos todos los pasos correctamente, al clicar en la acción Sign In seremos redireccionados a Azure AD B2C para iniciar sesión. Una vez el usuario ha sido autenticado exitosamente, veremos el nombre del usuario junto con la acción Sign Out.

Home aplicación web MVC con Cierre de Sesión
Home aplicación web MVC con Cierre de Sesión

Si ahora clicamos en la acción Sign out seremos redireccionados primero a Azure AD B2C, donde limpiaran las cookies de sesión y finalmente a la página de confirmación de cierre de sesión.

Home aplicación web MVC confirmación de Cierre de Sesión
Home aplicación web MVC confirmación de Cierre de Sesión

El redireccionamiento hacia Azure AD B2C y posteriormente hacia la página de confirmación de cierre de sesión suele ser muy rápido, en términos prácticos es imperceptible.

Conclusión

En este post vimos cómo integrar Azure AD B2C con aplicaciones Web MVC .Net 5 en forma fácil y rápida, incluyendo inicio y cierre de sesión.

Podrás encontrar el código completo en mi GitHub.

Azure AD B2C + .Net 5 – Global Azure 2021 Latam

Como suele suceder todos los años, a mediados de Abril se celebra uno de los eventos más importantes a nivel global de la comunidad de Azure: el Global Azure.

Durante 3 días, comunidades de todo el mundo organizan en forma local charlas y workshops totalmente abiertos y gratuitos, donde los participantes pueden aprender y conocer más sobre el mundo Azure de la mano de distintos Líderes y expertos.

Este año tuve el placer (y por partida doble) de participar en la edición Latinoamérica con un colega y amigo experto en la materia, Jorge Levy.

Hemos compartido nuestra experiencia sobre el poder y simpleza de Azure AD B2C y cuál fácil es integrarlo con aplicaciones Web MVC .Net 5 y Web API .Net 5.

Configuración e Implementación de Azure AD B2C con .Net 5

En la actualidad, casi cualquier aplicación requiere que los usuarios puedan autenticarse, sin embargo, implementar un mecanismo de autenticación seguro y confiable requiere mucho conocimiento y tiempo, incluso teniendo experiencia previa.

En esta sesión, te cuento como gracias a este espectacular servicio B2C o Business-To-Clients, podemos ahorrar innumerables horas de desarrollo en implementación y despliegue de soluciones de Autenticación.

Veremos como construir y desplegar, sin necesidad de escribir código y en forma segura, confiable y escalable, un sistema de Inicio de Sesión y Registro de Usuario, incluyendo integración con Redes Sociales y proveedores externos.

Además haremos una integración punta a punta con una aplicación Web MVC .Net 5, Web API .Net 5 e integraremos nuestra Web MVC para poder consumir nuestra Web API en forma rápida y sencilla.

https://www.youtube.com/watch?v=rrJeS2N0X-I&t=14370s

Si quieres ver el código completo de los ejemplos que utilizamos durante la sesión, te dejo el repo de GitHub.

« Entradas anteriores

© 2021 Facu The Rock

Tema por Anders NorenArriba ↑