Como ya compartí en mi perfil de LinkedIn e Instagram, estos meses estoy enfocada en otras cosas más importantes 🥰 Sin embargo, ya estoy a ratitos trasteando, volviendo a este mundo que tanto me gusta. Mi entretenimiento estos días es Dapr, así que hoy quiero contarte cómo puedes empezar con ello, usando los ejemplos que he ido construyendo entre rato y rato, y que iré compartiendo contigo.
¿Qué es Dapr?
Antes de comenzar tenemos que entender qué es Dapr y por qué nos puede interesar usarlo. Dapr o Distributed Application Runtime lo que nos proporciona es una API con la que podemos abstraer nuestras aplicaciones de (casi) todos servicios con los que debe interactuar.

Este divide estas integraciones en diferentes bloques, dentro de los cuales tenemos componentes para las diferentes tecnologías. Los bloques son:
- Service to service invocation, cuando quieres llamar a otras APIs que también usan Dapr.
- State Management para el almacenamiento del estado de tipo clave/valor, como por ejemplo una caché.
- Publish and subscribe, donde utilizas el patrón de publicador suscriptor.
- Bindings, cuando quieres integrarte con servicios externos, más allá de Dapr.
- Actors, para el uso del patrón actor.
- Observability, relacionado con la monitorización, salud y trazabilidad de las aplicaciones.
- Secrets, para el tratamiento de información sensible como contraseñas o cadenas de conexión.
- Configuration, para la gestión de la configuración.
Vale, pero entonces esto… ¿cómo funciona? Como te decía al principio, Dapr es una API a la que nuestra aplicación llamará para interactuar con los diferentes servicios con los que debe integrarse. Para que esta comunicación sea eficiente se hace uso del patrón sidecar. Esto lo que quiere decir es que junto a tu aplicación se despliega un proceso, Dapr, uno muy pegadito al otro para evitar latencias elevadas, el cual puede usar para interactuar con otros servicios, como los anteriormente mencionados.
Por lo tanto, lo que evita es que tu aplicación tenga que implementar la integración con estos, instalar librerías específicas para interactuar con los servicios, etcétera minimizando así el acoplamiento con terceros, a la vez que la complejidad de tu código, ya que delega en Dapr este trabajo.

La forma de comunicarse con este proceso es a través de HTTP o gRPC, pero tienes SDKs para los lenguajes más comunes que facilita el trabajo.
¿Cómo empiezo?
Existen diferentes formas de trabajar con Dapr: en tu máquina local, en un entorno contenerizado, en Kubernetes, la nube, etcétera. Mi idea es ir viéndolas en diferentes artículos. Hoy, para empezar, lo más fácil es instalar el CLI de Dapr en tu sistema operativo. En mi caso, para MacOS, puedo usar Homebrew:
brew install dapr/tap/dapr-cli
En el momento que escribí este artículo la última versión es la 1.9.0. Una vez instalado, lo siguiente que necesitas es inicializar su despliegue en tu máquina local. Este por defecto se apoya en Docker, por lo que también debes tenerlo instalado:
dapr init
Lo que hace está inicialización es generar una serie de contenedores:
- dapr_placement: se trata de un servicio que se utiliza cuando hacemos uso del bloque Actors, que veremos más adelante.
- dapr_redis: ejecuta una instancia local de Redis.
- dapr_zipkin: crea una instancia local de Zipkin.
Estos servicios se inicializan para tener un arranque con un mínimo de servicios con los que interactuar en tu entorno de desarrollo. Si no quieres que los genere puedes iniciar Dapr con el parámetro –slim, pero en este primer artículo vamos a usarlos para que veas como funciona todo esto.
Por otro lado, en la ruta ~/.dapr/components (o %UserProfile%\.dapr\components en Windows) podemos encontrar los componentes que por defecto vienen configurados, que son statestore y pubsub. Gracias a ello puedes empezar a usar alguno de los bloques mencionados más arriba sin tener que configurar nada, como veremos a continuación.
Aplicación de ejemplo
Ahora que ya tienes el entorno local preparado, vamos a ver cómo funciona una aplicación con Dapr. Para ello, he usado una API en .NET Core llamada tour-of-heroes-api, que es la que uso en mis clases de Lemoncode, y que devuelve información de heroes.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using tour_of_heroes_api.Models;
using tour_of_heroes_api.Modesl;
namespace tour_of_heroes_api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class HeroController : ControllerBase
{
private readonly HeroContext _context;
public HeroController(HeroContext context)
{
_context = context;
}
// GET: api/Hero
[HttpGet]
public async Task<ActionResult<IEnumerable<Hero>>> GetHeroes()
{
return await _context.Heroes.ToListAsync();
}
// GET: api/Hero/5
[HttpGet("{id}")]
public async Task<ActionResult<Hero>> GetHero(int id)
{
var hero = await _context.Heroes.FindAsync(id);
if (hero == null)
{
return NotFound();
}
return hero;
}
// PUT: api/Hero/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPut("{id}")]
public async Task<IActionResult> PutHero(int id, Hero hero)
{
if (id != hero.Id)
{
return BadRequest();
}
_context.Entry(hero).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!HeroExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/Hero
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost]
public async Task<ActionResult<Hero>> PostHero(Hero hero)
{
_context.Heroes.Add(hero);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetHero), new { id = hero.Id }, hero);
}
// DELETE: api/Hero/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteHero(int id)
{
var hero = await _context.Heroes.FindAsync(id);
if (hero == null)
{
return NotFound();
}
_context.Heroes.Remove(hero);
await _context.SaveChangesAsync();
return NoContent();
}
private bool HeroExists(int id)
{
return _context.Heroes.Any(e => e.Id == id);
}
}
}
Esta utiliza Entity Framework para la comunicación con la base de datos, un SQL Server dockerizado, donde se almacenan los héroes. Lo que he hecho ha sido crearme un nuevo branch, llamado dapr, donde he probado cómo puedo mejorar mi código.
Mi primer objetivo ha sido almacenar el resultado de GetHeroes en caché, ya que en la versión inicial siempre llama a base de datos para recuperarlos. Si no usara Dapr tendría que elegir de antemano qué caché quiero utilizar y posteriormente buscar cuál es la librería disponible para esa tecnología e implementar la lógica en mi código. Con Dapr la integración quedaría de la siguiente manera:
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using tour_of_heroes_api.Models;
using Dapr.Client;
using System.Collections.Generic;
using Newtonsoft.Json;
using Microsoft.Extensions.Logging;
namespace tour_of_heroes_api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class HeroController : ControllerBase
{
private readonly HeroContext _context;
private DaprClient _daprClient;
const string DAPR_STORE_NAME = "statestore";
ILogger<HeroController> _logger;
public HeroController(
HeroContext context,
DaprClient client,
ILogger<HeroController> logger
)
{
_context = context;
_daprClient = client;
_logger = logger;
}
// GET: api/Hero
[HttpGet]
public async Task<IEnumerable<Hero>> GetHeroes()
{
// return await _context.Heroes.ToListAsync();
_logger.LogInformation($"Getting heroes...");
var heroes = await _daprClient.GetStateAsync<List<Hero>>(DAPR_STORE_NAME, "heroes");
if (heroes == null)
{
_logger.LogInformation($"Not heroes in cache. Updating...");
heroes = await UpdateCache();
}
return heroes;
}
private async Task<List<Hero>> UpdateCache()
{
var heroes = await _context.Heroes.ToListAsync();
await _daprClient.SaveStateAsync<List<Hero>>(DAPR_STORE_NAME, "heroes", heroes);
return heroes;
}
Como puedes ver, he comentado la llamada a base de datos, he añadido algunos logs y he hecho uso de una instancia de DaprClient para la cual he tenido que añadir el paquete de nuget Dapr.AspNetCore. En el método GetHeroes intento recuperar el valor usando el método GetStateAsync, el cuál hace alusión al bloque State Management. Si te das cuenta, mi aplicación no sabe si lo que hay por detrás es un Redis, o un CosmosDB, Firebase, etcétera sino que le pide a Dapr simplemente «devuélveme el estado con esta clave: heroes». Si es la primera vez te devolverá null y entonces pasará al siguiente método, UpdateCache, donde he trasladado la llamada a la base de datos y el resultado lo persisto en el state store, sea cuál sea en este momento y en este entorno.
¿Cómo sabe Dapr con qué tecnología debe interactuar?
Como te comenté, cuando hacemos dapr init nuestro entorno local se prepara para funcionar con Dapr. Ademas de los contenedores que ejecuta, entre ellos un Redis, también genera unos archivos con la configuración para un state store y un servicio de publicacón suscripción. En este caso nos interesa el primero, y podemos ver dicha configuración en el archivo ~/.dapr/components/statestore.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
- name: actorStateStore
value: "true"
El primer valor importante es el de name, statestore, que coincide con el nombre de la constante DAPR_STORE_NAME de nuestro código. Así sabe Dapr, de los archivos de la configuración, con qué componente queremos interactuar. En el apartado spec se determina la tecnología a usar a través de type, en este caso state.redis, y dependiendo del tipo la metadata será una u otra. En este caso los valores se refieren al contenedor que dapr init creo con una instancia de Redis local.
¿Cómo ejecuto mi aplicación junto con Dapr?
Ahora que ya tienes todo listo lo último que queda es ejecutar tu código, junto con Dapr, para ver que todo funciona como se espera. Para ello, en local, tienes dos opciones: puedes ejecutar el siguiente comando desde el terminal:
dapr run --app-id tour-of-heroes-api --app-port 5222 -- dotnet run
O bien, si usas Visual Studio Code puedes configurar los archivos launch.json y task.json para incluir lo necesario para ejecutar esto mismo pulsando F5:
launch.json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/bin/Debug/net6.0/tour-of-heroes-api.dll",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Launch (web) with Dapr",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "dapr-debug",
"program": "${workspaceFolder}/bin/Debug/net6.0/tour-of-heroes-api.dll",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DAPR_HTTP_PORT": "3502",
"DAPR_GRPC_PORT": "50002"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
},
"postDebugTask": "daprd-down"
}
]
}
Para generar el segundo apartado, una vez que tienes generado la depuración sin Dapr, puede ayudarte la extensión de Dapr para VS Code, aunque todavía está en preview. Esta toma los valores de la configuraión sin Dapr y le añade una pre tarea (dapr-debug) y una post tarea (dapr-down).
task.json
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/tour-of-heroes-api.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/tour-of-heroes-api.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/tour-of-heroes-api.csproj"
],
"problemMatcher": "$msCompile"
},
{
"label": "clean",
"command": "dotnet",
"type": "process",
"args": [
"clean",
"${workspaceFolder}",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish-release",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}",
"--configuration",
"Release",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile",
"dependsOn": "clean"
},
{
//https://docs.dapr.io/developing-applications/ides/vscode/vscode-how-to-debug-multiple-dapr-apps/#daprd-parameter-table
"appId": "tour-of-heroes-api",
"appPort": 5222,
"label": "dapr-debug",
"type": "dapr",
"dependsOn": "build",
"httpPort": 3502,
"grpcPort": 50002
},
{
"appId": "tour-of-heroes-api",
"label": "daprd-down",
"type": "daprd-down"
}
]
}
Para este ejemplo, en la tarea dapr-debug he incluido algunos parámetros para que veas que es posible indicar diferentes valores como en la linea de comandos, así como el puerto de escucha de la aplicación, definido en este caso en el appsettings.json, los puertos que se han definido para la escucha de Dapr tanto por HTTP como por GRPC, etcétera.
Ahora, si ejecutas el código, de cualquiera de las dos maneras, creas un contenedor con SQL Server, y cargas algunos heroes, con la ayuda de este archivo y la extensión de VS Code llamada REST Client, comprobarás el funcionamiento de Dapr sin salir de tu local 😃
¡Saludos!