Saltar al contenido principal

Blazor — C# en el browser 🟡

Blazor es el framework de Microsoft para construir UIs interactivas con C# en lugar de JavaScript. En entrevistas de roles .NET fullstack es un tema frecuente: no para que lo domines, sino para que expliques cuándo elegirías Blazor vs React y que entiendas sus modelos de rendering.


Los 4 modelos de Blazor

┌──────────────────────────────────────────────────────────────────────┐
│ MODELOS DE BLAZOR │
│ │
│ 1. Blazor Server 2. Blazor WebAssembly (WASM) │
│ ───────────────── ────────────────────────── │
│ C# corre en servidor C# corre en el browser │
│ UI sync via SignalR .NET runtime descargado al browser │
│ Sin descarga .NET Funciona offline │
│ │
│ 3. Blazor Hybrid 4. Blazor United / Auto (Net 8+) │
│ ───────────────── ───────────────────────────── │
│ MAUI + Blazor Combina todos los modelos │
│ Apps nativas con UI SSR + Server + WASM en 1 app │
│ Blazor Elige el modo por componente │
└──────────────────────────────────────────────────────────────────────┘

Comparativa de los 4 modelos

Blazor ServerBlazor WASMBlazor HybridBlazor United (Auto)
Dónde ejecuta el C#ServidorBrowser (WASM)App nativaServidor + Browser
Descarga inicialPequeñaGrande (~10MB)N/APequeña (luego más)
Latencia UIAlta (red por cada acción)Baja (local)Muy bajaDepende del modo
Funciona offline❌ No✅ Sí✅ SíParcial
Acceso a servidorDirecto (in-process)HTTP callsHTTP callsDirecto/HTTP
EscalabilidadLimitada (estado por conexión)ExcelenteN/ABuena
SEOBueno (HTML en servidor)Malo sin prerenderingN/AExcelente
Cuándo usarIntranet, apps internas, prototipadoApps públicas offline-firstDesktop/Mobile con .NET MAUIApps públicas complejas .NET 8+

Componentes Razor — sintaxis básica

@* ContadorProductos.razor *@
@page "/productos"
@inject IProductoService ProductoService

<h1>Productos</h1>

@if (cargando)
{
<p>Cargando...</p>
}
else if (error is not null)
{
<p class="error">@error</p>
}
else
{
<p>Total: @productos.Count productos</p>

<ul>
@foreach (var producto in productos)
{
<li>
<strong>@producto.Nombre</strong> — $@producto.Precio
<button @onclick="() => Eliminar(producto.Id)">Eliminar</button>
</li>
}
</ul>
}

<button @onclick="CargarProductos">Recargar</button>

@code {
private List<Producto> productos = [];
private bool cargando = true;
private string? error;

// Ciclo de vida: equivalente a useEffect([], []) en React
protected override async Task OnInitializedAsync()
{
await CargarProductos();
}

private async Task CargarProductos()
{
cargando = true;
error = null;

try
{
productos = await ProductoService.GetTodosAsync();
}
catch (Exception ex)
{
error = $"Error al cargar: {ex.Message}";
}
finally
{
cargando = false;
}
}

private async Task Eliminar(int id)
{
await ProductoService.EliminarAsync(id);
productos.RemoveAll(p => p.Id == id);
// StateHasChanged() se llama automáticamente después de event handlers
}
}

Parámetros — equivalente a props de React

@* TarjetaProducto.razor — componente hijo *@

@* Parámetros de entrada *@
<div class="tarjeta">
<h3>@Producto.Nombre</h3>
<p>$@Producto.Precio</p>

@if (MostrarBoton)
{
<button @onclick="() => OnEliminar.InvokeAsync(Producto.Id)">
Eliminar
</button>
}
</div>

@code {
// [Parameter] = prop de React
[Parameter, EditorRequired]
public Producto Producto { get; set; } = default!;

[Parameter]
public bool MostrarBoton { get; set; } = true;

// EventCallback = callback prop de React
[Parameter]
public EventCallback<int> OnEliminar { get; set; }

// [CascadingParameter] = Context de React (recibe valores de ancestros)
[CascadingParameter]
public Tema TemaApp { get; set; } = Tema.Claro;
}
@* Uso en el padre *@
<TarjetaProducto
Producto="@miProducto"
MostrarBoton="true"
OnEliminar="HandleEliminar" />

@code {
private async Task HandleEliminar(int id)
{
await ProductoService.EliminarAsync(id);
}
}

Ciclo de vida de un componente

Equivalencias con React:

Blazor React (hooks)
───────────────────────────────────────────────
SetParametersAsync() ← (antes del render con props nuevas)
OnInitialized() ← useEffect(() => {}, []) síncrono
OnInitializedAsync() ← useEffect(() => { fetch... }, [])
OnParametersSet() ← useEffect(() => {}, [prop])
OnParametersSetAsync() ← useEffect(() => { fetch... }, [prop])
ShouldRender() ← React.memo / shouldComponentUpdate
OnAfterRender() ← useEffect(() => { DOM ops }, [])
Dispose() / IAsyncDisposable ← cleanup de useEffect
@implements IAsyncDisposable

@code {
private Timer? _timer;

// Solo en primera renderización (como useEffect con [])
protected override void OnInitialized()
{
Console.WriteLine("Componente montado");
}

// Async — para fetch inicial de datos
protected override async Task OnInitializedAsync()
{
datos = await Service.GetDatosAsync();
}

// Corre cada vez que cambian los parámetros (como useEffect con [prop])
protected override async Task OnParametersSetAsync()
{
if (ProductoId != _ultimoProductoId)
{
_ultimoProductoId = ProductoId;
producto = await Service.GetProductoAsync(ProductoId);
}
}

// Después de que el DOM se actualizó (como useEffect después del paint)
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Interop con JS para DOM manipulation
await JS.InvokeVoidAsync("initChart", chartElementRef);
}
}

// Optimización: evitar re-renders innecesarios
protected override bool ShouldRender()
{
return _datosHanCambiado; // solo re-renderizar si es necesario
}

// Cleanup (como el return de useEffect)
public async ValueTask DisposeAsync()
{
_timer?.Dispose();
await JS.InvokeVoidAsync("destroyChart", chartElementRef);
}
}

Data binding

@* One-way binding (de C# al HTML) *@
<p>@mensaje</p>
<input value="@nombre" /> @* Solo lee — no actualiza nombre al tipear *@

@* Two-way binding — equivalente a value + onChange en React *@
<input @bind="nombre" />
<input @bind="nombre" @bind:event="oninput" /> @* Actualiza en cada tecla *@

<select @bind="categoriaSeleccionada">
@foreach (var cat in categorias)
{
<option value="@cat.Id">@cat.Nombre</option>
}
</select>

@code {
private string nombre = "";
private int categoriaSeleccionada;
private string mensaje => $"Hola, {nombre}";
}

Formularios y validación

@* FormularioProducto.razor *@
<EditForm Model="@dto" OnValidSubmit="GuardarAsync">
<DataAnnotationsValidator />
<ValidationSummary />

<div>
<label for="nombre">Nombre</label>
<InputText id="nombre" @bind-Value="dto.Nombre" />
<ValidationMessage For="() => dto.Nombre" />
</div>

<div>
<label for="precio">Precio</label>
<InputNumber id="precio" @bind-Value="dto.Precio" />
<ValidationMessage For="() => dto.Precio" />
</div>

<div>
<label for="categoria">Categoría</label>
<InputSelect id="categoria" @bind-Value="dto.CategoriaId">
<option value="">-- Seleccioná --</option>
@foreach (var cat in categorias)
{
<option value="@cat.Id">@cat.Nombre</option>
}
</InputSelect>
</div>

<button type="submit" disabled="@guardando">
@(guardando ? "Guardando..." : "Guardar")
</button>
</EditForm>

@code {
private CrearProductoDto dto = new();
private List<Categoria> categorias = [];
private bool guardando;

protected override async Task OnInitializedAsync()
{
categorias = await CategoriaService.GetTodasAsync();
}

private async Task GuardarAsync()
{
guardando = true;
try
{
await ProductoService.CrearAsync(dto);
NavigationManager.NavigateTo("/productos");
}
finally
{
guardando = false;
}
}
}
// DTO con Data Annotations — mismas que en la API
public class CrearProductoDto
{
[Required(ErrorMessage = "El nombre es requerido")]
[StringLength(100, MinimumLength = 3)]
public string Nombre { get; set; } = string.Empty;

[Range(0.01, double.MaxValue, ErrorMessage = "El precio debe ser positivo")]
public decimal Precio { get; set; }

[Required]
public int CategoriaId { get; set; }
}

JavaScript Interop

Blazor puede llamar JavaScript (y viceversa) cuando necesitás APIs del browser que no están disponibles en .NET.

@inject IJSRuntime JS

@code {
private ElementReference canvasRef;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Llamar función JS desde C#
await JS.InvokeVoidAsync("inicializarGrafico", canvasRef, datos);
}
}

private async Task CopiarAlPortapapeles(string texto)
{
await JS.InvokeVoidAsync("navigator.clipboard.writeText", texto);
}

private async Task<string> LeerDelLocalStorage(string key)
{
// InvokeAsync retorna un valor desde JS
return await JS.InvokeAsync<string>("localStorage.getItem", key);
}
}
// wwwroot/js/interop.js
window.inicializarGrafico = (canvasElement, datos) => {
const ctx = canvasElement.getContext('2d');
new Chart(ctx, { type: 'bar', data: datos });
};

// Llamar C# desde JavaScript
// En componentes Blazor Server con DotNet.invokeMethodAsync
window.notificarCambio = () => {
DotNet.invokeMethodAsync('MiApp', 'RecibirNotificacion');
};

State Management

// Patrón 1: Service como state container (para estado global simple)
// Se registra como Scoped en Blazor Server, Singleton en WASM

public class CarritoState
{
private readonly List<ItemCarrito> _items = [];

public IReadOnlyList<ItemCarrito> Items => _items.AsReadOnly();
public int Total => _items.Sum(i => i.Cantidad);

// Notificar a los componentes que el estado cambió
public event Action? OnChange;

public void Agregar(Producto producto)
{
var existente = _items.FirstOrDefault(i => i.ProductoId == producto.Id);
if (existente != null)
existente.Cantidad++;
else
_items.Add(new ItemCarrito { ProductoId = producto.Id, Nombre = producto.Nombre });

OnChange?.Invoke(); // Dispara re-render en los componentes suscritos
}

public void Vaciar()
{
_items.Clear();
OnChange?.Invoke();
}
}
@* Componente que usa el state *@
@inject CarritoState Carrito
@implements IDisposable

<span>Carrito: @Carrito.Total items</span>

@code {
protected override void OnInitialized()
{
// Suscribirse a cambios del state
Carrito.OnChange += StateHasChanged;
}

public void Dispose()
{
// Desuscribirse al desmontar (evitar memory leaks)
Carrito.OnChange -= StateHasChanged;
}
}
// Patrón 2: CascadingValue — pasar estado a componentes descendientes
// Equivalente al Context.Provider de React
@* En el layout raíz *@
<CascadingValue Value="@tema" Name="TemaApp">
@Body
</CascadingValue>

@* En cualquier componente descendiente *@
@code {
[CascadingParameter(Name = "TemaApp")]
public Tema TemaApp { get; set; } = Tema.Claro;
}

Autenticación y autorización

@* Verificar autenticación en componentes *@
<AuthorizeView>
<Authorized>
<p>Bienvenido, @context.User.Identity?.Name</p>
<NavLink href="/dashboard">Dashboard</NavLink>
</Authorized>
<NotAuthorized>
<p>Por favor <a href="/login">iniciá sesión</a></p>
</NotAuthorized>
</AuthorizeView>

@* Restringir una página completa *@
@page "/admin"
@attribute [Authorize(Roles = "admin")]

<h1>Panel de administración</h1>
// Program.cs — configurar auth en Blazor WASM
builder.Services
.AddOidcAuthentication(options =>
{
builder.Configuration.Bind("Auth", options.ProviderOptions);
options.ProviderOptions.DefaultScopes.Add("openid");
options.ProviderOptions.DefaultScopes.Add("profile");
options.ProviderOptions.DefaultScopes.Add("api");
});

// Program.cs — configurar auth en Blazor Server
builder.Services
.AddAuthentication(options => { /* JWT o Cookie */ })
.AddJwtBearer(options => { /* config */ });

Blazor vs React — la comparativa clave para entrevistas

¿Cuándo elegirías Blazor sobre React?

✅ Equipo es .NET, nadie sabe JS/TS profundamente
✅ App interna / intranet sin SEO crítico
✅ Necesitás compartir lógica entre API y UI (mismos DTOs, validaciones)
✅ Blazor Server para prototipos rápidos (latencia no importa)
✅ Blazor WASM para apps offline-first (campo, sin red)
✅ Blazor Hybrid para desktop/mobile con MAUI

✅ Cuándo elegirías React sobre Blazor:
✅ SEO crítico (blogs, e-commerce público)
✅ Equipo mixto o front-end specialists
✅ Ecosistema npm necesario (librerías de charts, UI, etc.)
✅ Performance de UI de alta frecuencia (juegos, real-time intenso)
✅ PWA o app móvil standalone (sin MAUI)
✅ Posibilidad de que el backend cambie de .NET a otro stack
BlazorReact
LenguajeC#JavaScript / TypeScript
EcosistemaNuGet (más chico)npm (enorme)
Bundle inicialWASM: ~10MB / Server: ~pequeño~200-500KB (SPA)
SEOBueno (Server/United)Requiere Next.js para SSR
Curva de aprendizajeBaja para devs .NETMedia-alta para devs .NET
PerformanceBuena (mejora con AOT)Excelente
ToolingVisual Studio / RiderVS Code + toda la toolchain JS
Jobs/mercadoNicho (.NET shops)Masivo

Preguntas frecuentes de entrevista 🎯

1. ¿Qué es Blazor y cuáles son sus modelos de rendering?

Blazor es el framework de Microsoft para construir UIs web con C#. Tiene 4 modelos: Server (C# en el servidor, UI via SignalR — baja latencia de startup pero red en cada acción), WebAssembly (C# compila a WASM y corre en el browser — offline-first, sin servidor), Hybrid (WASM dentro de MAUI para apps nativas) y United/Auto (.NET 8+, combina todos — elige SSR, Server o WASM por componente).

2. ¿Cuándo recomendarías Blazor en vez de React?

Cuando el equipo es 100% .NET y no hay expertise en JavaScript, cuando es una app interna donde el SEO no importa y la latencia de Blazor Server es aceptable, cuando se quiere compartir DTOs y validaciones entre API y UI (mismo proyecto), o cuando se necesita una app offline-first en entorno controlado (Blazor WASM). Para apps públicas con SEO, ecosistema npm, o equipos mixtos, React/Next.js es la mejor opción.

3. ¿Qué es el JS Interop y cuándo lo usarías?

IJSRuntime permite llamar funciones JavaScript desde C# y viceversa. Lo usaría cuando Blazor no tiene acceso nativo a una API del browser (Clipboard, Canvas, WebSockets de browser, librerías JS de terceros como Chart.js). En Blazor WASM es sincrónico; en Blazor Server es siempre asíncrono porque cruza la conexión SignalR.

4. ¿Cuál es la diferencia entre OnInitializedAsync y OnParametersSetAsync?

OnInitializedAsync corre una sola vez cuando el componente se monta por primera vez — equivalente a useEffect(() => {}, []). OnParametersSetAsync corre cada vez que los parámetros del componente cambian (incluyendo la primera vez) — equivalente a useEffect(() => {}, [parametro]). Para fetch inicial de datos se usa OnInitializedAsync; para reaccionar a cambios de props se usa OnParametersSetAsync.

5. ¿Cómo manejarías el state global en Blazor?

Dos opciones principales: (1) Service como state container registrado en DI — el componente se suscribe al evento OnChange del service y llama StateHasChanged() cuando recibe la notificación. (2) CascadingValue/CascadingParameter — equivalente al Context de React, para pasar estado a componentes descendientes sin prop drilling. Para apps más complejas hay librerías como Fluxor (flux pattern) o Blazored.

6. ¿Qué problema tiene Blazor Server en producción a escala?

Cada usuario tiene una conexión SignalR activa con el servidor, y el estado de la UI se mantiene en memoria del servidor. Esto limita la escalabilidad horizontal — no podés balancear libremente entre instancias sin sticky sessions o backplane de SignalR (Redis, Azure SignalR Service). Con miles de usuarios simultáneos, la memoria del servidor crece linealmente. Blazor WASM no tiene este problema porque todo corre en el cliente.