I had the pleasure to speak at the Azure Community Conference this week, where I spoke about how Azure Web PubSub will change the way we think about real time apps.
Autor: Facundo La Rocca (Página 1 de 3)
Esta semana tuve el placer de participar como speaker en NetCoreConf, fué un excelente evento donde estuve hablando un poco sobre como hacer test unitario cuando utilizamos Entity Framework.
Como parte de la charla, discutimos también cómo utilizar SQLite como herramienta de soporte para tener tests que además de validar el correcto funcionamiento, nos garanticen consistencia.
Hola! En el post de hoy no vamos a hablar de código propiamente dicho, sino de una característica nueva que nos trae Visual Studio 2022 y que me resultó extremadamente útil: Eliminar de forma muy simple y segura todas las referencias a librerías u otros proyectos que no estén siendo utilizadas.
Todas las aplicaciones evolucionan, se expanden, crecen, mutan. Como parte de ese proceso evolutivo se suelen incluir nuevas funcionalidades, corregir defectos o refactorizar funcionalidades existentes. Y si, muchas veces agregamos nuevas referencias a librerías (vía NuGet) o a proyectos.
¿Y qué hay de malo con esto?
En principio no es tan grave realmente, a no alarmarse. Lo que suele suceder es que con el tiempo algunas referencias o librerías dejan de ser necesarias ya que no utilizamos las clases que nos proveen.
Visual Studio provee herramientas para limpiar el código cuando tenemos instrucciones del tipo using
sin utilizar, como vemos en el siguiente ejemplo:

usings
no necesariosAhora bien, si la librería FluentAssertions
ya no es necesaria a nivel proyecto, ¿Podemos desinstalarla o eliminarla automáticamente? ¿Tenemos algún mecanismo para detectar librerías que no son necesarias?
Durante mucho tiempo la respuesta a estas preguntas fue No, o al menos no en forma simple y segura.
Visual Studio 2022
Por suerte para nosotros, Visual Studio 2022 introdujo esta funcionalidad de forma nativa, lo que nos permite eliminar todas las referencias a proyectos o librerías no utilizadas con tan solo unos clics. Veamos cómo hacerlo.
Tenemos un proyecto muy simple con 3 librerías instaladas y una referencia a otro proyecto.

La única librería que estamos utilizando es Newtonsoft.Json
, con lo cual tanto FluentAssertions, Serilog
y el proyecto FacuTheRock.UnusedProject
pueden ser eliminados.
Haciendo clic derecho sobre el proyecto que queremos limpiar, en este caso FacuTheRock
, veremos disponible una nueva entrada de menú llamada Eliminar Referencias No Utilizadas (Remove Unused References).

Visual Studio nos mostrará un cuadro de diálogo muy completo listando todas las librerías y proyectos que no están siendo utilizadas y que podemos eliminar de forma segura. Además, dentro del mismo cuadro de diálogo podremos elegir si queremos eliminar la referencia o mantenerla y aplicar en forma masiva la acción seleccionada para cada proyecto.

Solo debemos seleccionar que acción tomar para cada item, aplicar los cambios y confirmar! Y eso es todo, con solo unos cuantos clics limpiamos nuestro proyecto de referencias a proyectos y librerías sin utilizar!
Visual Studio 2022 todavía se encuentra en modo de prueba (preview). Esto quiere decir que algunas funcionalidades pueden no funcionar correctamente en algunos escenarios.
Esta semana se llevo a cabo un evento increíble de la.cominidad de ASP.NET en Español donde tuve el placer (y honor) de hablar un poquito sobre Testing unitario.
En este caso vimos como escribir buenos tests para todas aquellos servicios (clases) que requieren un DbContext y para asegurarnos que nuestros tests se comporten siguiendo los mismos patrones que sigue ASP.NET.
Comenzamos escribiendo un test de integración contra una base de datos real, luego refactoeizamos el código para utilizar una base “en memoria” y finalmente utilizamos SQLite.
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:
- Implementar la interface
IFeatureFilter
- Registrar el filtro
- Agregar la configuración
- 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 FlagWeeklyOffers
utilizar el filtro cuyo alias esWeeklyOffersFilter
“. Tiene que ser el mismo que utilizaremos en la configuración en el archivoappsettings.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.
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.
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
:

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.
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:
- Activar o desactivar funcionalidades en momentos específicos.
- 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:
- Mostremos una nueva entrada del menú.
- Este disponible una pagina especial con la lista de productos
- 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
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, OfertasNavidenias
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.
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:
- Una entrada en el menú principal llamada
"Nueva Funcionalidad"
.

<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>
- Una acción llamada
NuevaFuncionalidad
en el controladorHome
que nos dirige una vista.
public class HomeController : Controller
{
public IActionResult Index() => View();
public IActionResult Privacy() => View();
public IActionResult NuevaFuncionalidad() =>
View();
}
- 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:
- Configuración e inyección de servicios
- Vistas
- 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.




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.
Esta semana tuve la oportunidad de participar del canal de Chris Reddington para hablar un poco de Azure AD B2C en la sección llamada “Tales from the Real World”, donde los participantes nos cuentan distintos ejemplos de servicios de Azure en la vida real.
En este caso estuvimos hablando de mi participación como organizador de vOpen.Tech y como gracias a Azure AD B2C fuimos capaces de sobreponernos a todos los desafíos que la pandemia de COVID nos generó en la organización de la edición 2020.
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 Startups, IStartupFilter
y IHostedServices
. Además vimos como ejecutar tareas desde el mismo Program.cs
utilizando el IServiceProvider
que configuramos para la aplicación.
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.
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.
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.
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.
}
}
- Si el ambiente es desarrollo, se configura un cache en memoria y se habilita la página de error.
- 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.
La semana pasada escribí un artículo sobre como inyectar dependencias en Azure Functions, debido a que la plantilla que utilizamos cuando creamos un proyecto nuevo no nos provee los mecanismos necesarios.
Ahora veremos como manejar configuraciones, de forma tal que podamos utilizar el archivo local.settingns.json
, variables de entorno y/o User Secrets.
Antes de comenzar
El framework de Azure Functions está preparado para leer las configuraciones que incluimos mediante el portal de Azure en la sección de Configuraciones.
Tomaremos el el concepto de Startup
del artículo sobre inyección de dependencias que mencioné anteriormente. Además tomaremos el código base y construiremos sobre él.
Contexto
Si bien cuando desplegamos nuestro código al servicio de Azure Functions las configuraciones configuradas desde el portal se leen automáticamente, puede que necesitemos personalizar el mecanismo con el cual esas configuraciones con leídas.
Por ejemplo, en nuestro ambiente local, usar User Secrets es recomendado. En ambientes pre productivos, podríamos necesitar leer un archivo adicional, como puede ser stage.settings.json
.
Requerimiento
El requerimiento de hoy es bastante simple, necesitamos poder leer configuraciones utilizando User Secrets, el archivo local.settings.json
y variables de entorno.
Implementación
Cuando creamos el Startup
y heredamos de la clase FunctionsStartup
, tendremos disponible un método virtual
llamado ConfigureAppConfiguration
. Es este método es que nos permitirá establecer como son leídas las configuraciones.
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
}
public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
var context = builder.GetContext();
builder.ConfigurationBuilder
.SetBasePath(context.ApplicationRootPath)
.AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
.AddUserSecrets(Assembly.GetExecutingAssembly(), optional: true)
.AddEnvironmentVariables();
}
}
SetBasePath
Siempre que sea necesario leer archivos locales, necesitamos establecer el directorio base. En aplicaciones web o APIs solemos utilizar el directorio actual, .SetBasePath(Directory.GetCurrentDirectory())
.
En el caso de Azure Functions necesitamos utilizar el contexto de ejecución, builder.GetContext().ApplicationRootPath
.
AddUserSecrets
Cuando instalamos la librería Microsoft.Extensions.Configuration.UserSecrets
tenemos que instalar la versión 3.1.XX. Por defecto, el Gestor de Paquetes (Package Manager) instalará la ultima versión, que a la fecha actual es 5.0.0, sin embargo el framework todavía no fue actualizado y sigue utilizando la versión de .Net Core 3.1.
Es inminente la actualización del framework de Azure Functions a la version .Net 5, pero al momento de escribir este artículo, todavía utiliza la versión 3.1.
Conclusión
En este breve post vimos cómo podemos configurar nuestra Azure Function para utilizar los distintos tipos de mecanismos para manejo de configuraciones utilizando todas las herramientas que le framework nos provee.
Con muy poco código, tenemos disponible variables de entorno, User Secrets y archivos json
.
A estas alturas, inyección de dependencias es uno de esos patrones de diseño que se ha convertido casi obligatorio, no sólo porque nos ayuda a escribir código mas testeable, sino también a escribir componentes desacoplados. No importa que tan compleja o simple sea una clase, si tiene alguna dependencia la inyectaremos mediante el constructor.
En este post veremos los pasos necesarios para poder comenzar a utilizar inyección de dependencias de la misma forma que lo hacemos en cualquier aplicación web.
Contexto
Cuando iniciamos un proyecto nuevo de Azure Functions, a diferencia de como sucede con proyectos de ASP.Net, no disponemos de ningún mecanismo listo para usar para inyectar dependencias.
A modo de ejemplo, pensemos una Azure Function llamada “Greeting” del tipo HttpTrigger que en base a un nombre devuelve un saludo.
Como parte de nuestra solución, implementamos un servicio muy simple de forma que nuestro código sea testeable.
public class GreetingService : IGreetingService
{
public string GetGreeting(string name) =>
string.IsNullOrEmpty(name)
? "Hi there!"
: $"Hi {name}!";
}
Luego, implementamos el código de la función de forma tal que lea el nombre recibido como query string
y obtenga el saludo correspondiente utilizando el servicio anterior.
public static class Greeting
{
[FunctionName("Greeting")]
public static IActionResult Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req)
{
var name = req.Query["name"];
var greetingService = new GreetingService();
var greeting = greetingService.GetGreeting(name);
return new OkObjectResult(greeting);
}
}
En la línea 8 podemos apreciar el problema: para poder utilizar el servicio necesitamos crear una instancia del mismo.
Requerimiento
Necesitamos implementar Inyección de Dependencias, no sólo porque las buenas prácticas así lo recomiendan, sino también porque queremos escribir Test Unitarios, y sabemos que de la forma que está escrito nuestro código es imposible.
Parte del requerimiento consiste en utilizar todas las herramientas que le framework nos provea, de forma que no tengamos que implementar nuestro propio set de clases o Contenedor para inyección de dependencias.
Implementación
Para poder completar nuestro requerimiento necesitamos dos cosas:
- Registrar y configurar nuestros servicios (generalmente cuando se inicia la aplicación)
- Inyectar las dependencias en forma automática, como lo hacemos en aplicaciones Web o APIs con ASP.Net.
Por suerte, el framework de Azure Functions nos provee de ambos mecanismos, solo tenemos que realizar las configuraciones necesarias.
Librerías
Comenzaremos instalando la librería Microsoft.Azure.Functions.Extensions, la cuál nos va facilitar los componentes que necesitamos.
dotnet add package Microsoft.Azure.Functions.Extensions
Startup
Azure Functions dispone de una clase abstracta diseñada para ser ejecutada al iniciar la aplicación. Esta clase se llama FunctionsStartup
y nos va a permitir realizar la registración de servicios que necesitamos.
Comenzaremos agregando una nueva clase al proyecto llamada Startup
y haremos que herede de la clase FunctionsStartup
.
using Demo;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
[assembly: FunctionsStartup(typeof(Startup))]
namespace Demo
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
}
}
}
El atributo de ensamblado (línea 4) es fundamental para que el framework pueda detectar los
startups
que deben ser invocados.
Configure
Al heredar de la clase abstracta FunctionsStartup
, tendremos que sobrescribir el método Configure
. Aquí haremos la registración de los servicios necesarios, igual que lo hacemos en cualquier aplicación ASP.Net. Siguiendo con el ejemplo, registraremos la clase GreetingService
.
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddSingleton<IGreetingService, GreetingService>();
}
Function
El último paso que nos queda es modificar la función para poder inyectar los servicios necesarios como lo haríamos con cualquier otra clase. Las modificaciones son:
- Modificar la clase y el método
Run
para que ya no seanstatic
. - Agregar un constructor parametrizado con los servicios que queremos recibir.
- Guardar los servicios en variables de instancia.
- Modificar el metodo en cuestion para utilizar las variables de instancia
public class Greeting
{
private readonly IGreetingService _greetingService;
public Greeting(IGreetingService greetingService) =>
_greetingService = greetingService;
[FunctionName("Greeting")]
public IActionResult Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req)
{
var name = req.Query["name"];
var greeting = _greetingService.GetGreeting(name);
return new OkObjectResult(greeting);
}
}
Conclusión
En este breve post vimos cómo podemos configurar nuestra Azure Function para utilizar Inyección de Dependencias utilizando todas las herramientas que le framework nos provee.
Si querés acceder al código completo, te dejo a continuación el repositorio en Github.
¿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:

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.

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.
En artículo anterior de la serie armamos nuestro Conector y configuramos Azure AD B2C para utilizarlo y así detectar y prevenir usuarios duplicados. Además creamos una Azure Function con código base para probar el conector.
En este post final completaremos la implementación escribiendo el código necesario para consultar Microsoft Graph API por usuarios con el mismo email ya sea como identidad o email adicional.
Preparación
Antes de comenzar con la implementación necesitamos recolectar las siguientes piezas de información y hacer algunas configuraciones:
- ID de Tenant o Directorio
- ID de Aplicación
- Generar un Secreto de Cliente
- Dar permisos para Microsoft Graph API
ID de Tenant y ID de Aplicación
Para obtener las primeras dos piezas de información que necesitamos debemos ir al registro de aplicaciones seleccionar la aplicación con la cuál vamos a trabajar. Cuándo realizamos la configuración inicial creamos un registro de aplicación llamado Sitio Web Increíble, ese es el que seleccionaremos en este caso.
Dela sección Información general del registro de aplicación podremos ver la información necesaria.

Generar secreto de cliente
Desde el mismo registro de aplicación podremos generar un secreto de cliente. En la sección Certificados y Secretos creamos un Nuevo secreto de cliente. Sólo necesitamos completar la descripción (la cuál es usada como referencia) y seleccionar una expiración.

Al finalizar veremos el secreto de cliente en la lista de secretos, necesitamos copiar el valor. Es importante tener en cuenta que una vez que abandonemos esta página ya no podremos a volver a ver el valor.

Permisos para Microsoft Graph API
Desde la sección Permisos de API en el registro de aplicación, agregamos un nuevo permiso de Microsoft Graph. Dentro de la categoría Permisos de Aplicación agregamos el permiso User.Read.All como se muestra a continuación:
Es muy importante conceder consentimiento permisos de administrador para nuestro tenant, de lo contrario los permisos no serán efectivos.
Implementación
Si bien Microsoft Graph expone una API REST muy bien documentada, el equipo de Microsoft creó una librería para facilitarnos la tarea y que se encuentra disponible vía paquetes de NuGet. Comenzaremos agregando al proyecto de Azure Function los siguientes paquetes de NuGet:
Microsoft.Graph
Microsoft.Graph.Auth
Microsoft.Identity.Client
Luego, a partir del ID de Aplicación, el ID de Tenant y el Secreto de Cliente que obtuvimos anteriormente, vamos a generar una instancia de GraphServiceClient
:
// Usings
using Microsoft.Graph;
using Microsoft.Graph.Auth;
using Microsoft.Identity.Client;
// Código
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create("<< ID de Aplicación >>")
.WithTenantId("<< ID de Tenant >>")
.WithClientSecret("<< Secreto de Cliente >>")
.Build();
var authProvider = new ClientCredentialProvider(confidentialClientApplication);
var graphClient = new GraphServiceClient(authProvider);
Buscando usuarios por email
Estamos buscando encontrar usuarios con el mismo email que el usuario actual está intentando usar, con lo cuál buscamos usuario cuya identidad sea el mail o usuarios que tengan el email como parte de sus emails adicionales. Esto requiere dos peticiones diferentes.
- Búsqueda por identidad
var usersByIdentity = (await _graphClient.Users
.Request()
.Filter($"identities/any(c:c/issuerAssignedId eq '{email}' and c/issuer eq '{"<< ID de Tenant >>"}')")
.GetAsync())
.ToArray();
- Usuarios por otros emails:
var usersByOtherEmails = (await _graphClient.Users
.Request()
.Filter($"otherMails/any(c:c eq '{email}') and UserType eq 'Member'")
.GetAsync())
.ToArray();
Si alguna de las dos peticiones anteriores contiene algún resultado, eso quiere decir entonces que hemos encontrado otro usuario con el mismo email que el usuario actual y demos detener el flujo de usuario. De lo contrario podemos continuar con la ejecución del flujo de usuario.
Quiero hacer mención al los usuarios de Stackoverflow boehlefeld y LuisEduardox de quién tomé las dos consultas necesarias para interactuar con Microsoft Graph a partir de las respuestas aquí y aquí mencionadas.
Como vimos en el post anterior, detenemos la ejecución del flujo de usuario retornando como respuesta "action" : "ShowBLockPage"
.
if (usersByIdentity.Any() || usersByOtherEmails.Any())
{
return new OkObjectResult(
new
{
version = "1.0.0",
action = "ShowBlockPage",
userMessage = $"Email {req.email} is duplicated."
});
}
return new OkObjectResult(
new
{
version = "1.0.0",
action = "Continue"
});
Solución completa
Juntando todas las piezas, nuestra Azure Function se ve de la siguiente forma:
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create("<< ID de Aplicación >>")
.WithTenantId("<< ID de Tenant >>")
.WithClientSecret("<< Secreto de Cliente >>")
.Build();
var authProvider = new ClientCredentialProvider(confidentialClientApplication);
var graphClient = new GraphServiceClient(authProvider);
var usersByIdentity = (await _graphClient.Users
.Request()
.Filter($"identities/any(c:c/issuerAssignedId eq '{email}' and c/issuer eq '{"<< ID de Tenant >>"}')")
.GetAsync())
.ToArray();
var usersByOtherEmails = (await _graphClient.Users
.Request()
.Filter($"otherMails/any(c:c eq '{email}') and UserType eq 'Member'")
.GetAsync())
.ToArray();
if (usersByIdentity.Any() || usersByOtherEmails.Any())
{
return new OkObjectResult(
new
{
version = "1.0.0",
action = "ShowBlockPage",
userMessage = $"Email {req.email} is duplicated."
});
}
return new OkObjectResult(
new
{
version = "1.0.0",
action = "Continue"
});
Sólo nos resta desplegar la nueva versión de nuestra Azure Function y probar.
Conclusión
En este post nos enfocamos en la parte final de la solución la cual implicó la interacción con Microsoft Graph para detectar usuarios con el mismo email que el usuario que se está intentando crear.
Una solución un poco más elegante pero mucho más compleja es la unificación de identidades, para lo que necesitamos utilizar Políticas Personalizadas de Azure AD B2C. Dejaremos esa para más adelante.
Para ver la implementación final en detalle te recomiendo revisar el repositorio en GitHub.
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.

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:

Dos cosas interesantes podemos destacar:
- Por un lado la tarea efectivamente fue cancelada y la ejecución de la petición no continuo.
- 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();
}
}

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
.