<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Cristóbal</title>
    <description>The latest articles on DEV Community by Cristóbal (@kiddtobal).</description>
    <link>https://dev.to/kiddtobal</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4063208%2Ff16101a4-b456-4efe-86e0-c0db990ae941.png</url>
      <title>DEV Community: Cristóbal</title>
      <link>https://dev.to/kiddtobal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kiddtobal"/>
    <language>en</language>
    <item>
      <title>Construí un sistema de cotizaciones real con .NET 10 y Blazor Server — y esto fue lo que aprendí</title>
      <dc:creator>Cristóbal</dc:creator>
      <pubDate>Tue, 04 Aug 2026 23:30:37 +0000</pubDate>
      <link>https://dev.to/kiddtobal/construi-un-sistema-de-cotizaciones-real-con-net-10-y-blazor-server-y-esto-fue-lo-que-aprendi-4il3</link>
      <guid>https://dev.to/kiddtobal/construi-un-sistema-de-cotizaciones-real-con-net-10-y-blazor-server-y-esto-fue-lo-que-aprendi-4il3</guid>
      <description>&lt;p&gt;Hace unas semanas empecé un proyecto personal con un objetivo simple: automatizar el proceso de cotización de una vidriería que hasta entonces armaba cada presupuesto a mano. Cálculos repetidos, formato distinto cada vez, cero historial ordenado.&lt;/p&gt;

&lt;p&gt;Terminó siendo bastante más de lo que imaginé al principio: un sistema completo de gestión de presupuestos, con catálogo de productos, múltiples precios por proveedor, cálculo de márgenes, exportación a PDF con el formato real que ya usaba el negocio, y deployment como servicio en red local.&lt;/p&gt;

&lt;p&gt;Stack:&lt;/p&gt;

&lt;p&gt;.NET 10 + Blazor Server — interactividad renderizada en servidor, sin necesidad de una API separada&lt;br&gt;
Entity Framework Core + SQLite&lt;br&gt;
MudBlazor para la interfaz&lt;br&gt;
QuestPDF para la generación del documento final&lt;/p&gt;

&lt;p&gt;No fue un tutorial seguido al pie de la letra. Fue ir resolviendo, un problema real a la vez. Estos son tres momentos que me parecieron interesantes de compartir.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pensar bien las reglas de borrado, no solo el modelo&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cuando modelás relaciones en Entity Framework Core, es fácil enfocarse solo en qué campo referencia a qué otro, y dejar OnDelete en su comportamiento por defecto. Pero cada relación tiene una pregunta de negocio detrás: ¿qué tiene que pasar si borro esto?&lt;/p&gt;

&lt;p&gt;En este proyecto, un Cliente con presupuestos asociados no puede borrarse — hay que evitar perder historial comercial por accidente. En cambio, un ItemPresupuesto no tiene sentido de existir sin su Presupuesto padre, así que si el presupuesto se borra, sus ítems se van con él.&lt;/p&gt;

&lt;p&gt;Bloque 1 — Relaciones y reglas de borrado (VidrieriaContext.cs)&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
entity.HasOne(p =&amp;gt; p.Cliente)&lt;br&gt;
    .WithMany(c =&amp;gt; c.Presupuestos)&lt;br&gt;
    .HasForeignKey(p =&amp;gt; p.ClienteId)&lt;br&gt;
    .OnDelete(DeleteBehavior.Restrict);&lt;/p&gt;

&lt;p&gt;entity.HasOne(p =&amp;gt; p.Cotizador)&lt;br&gt;
    .WithMany()&lt;br&gt;
    .HasForeignKey(p =&amp;gt; p.CotizadorId)&lt;br&gt;
    .OnDelete(DeleteBehavior.Restrict);&lt;br&gt;
csharp&lt;br&gt;
entity.HasOne(i =&amp;gt; i.Presupuesto)&lt;br&gt;
    .WithMany(p =&amp;gt; p.Items)&lt;br&gt;
    .HasForeignKey(i =&amp;gt; i.PresupuestoId)&lt;br&gt;
    .OnDelete(DeleteBehavior.Cascade);&lt;/p&gt;

&lt;p&gt;Misma herramienta (OnDelete), dos decisiones opuestas — y ambas correctas, según qué representa cada relación en el negocio.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Un selector de catálogo que escala sin volverse ilegible&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El sistema tiene categorías con subcategorías, productos dentro de cada una, y cada producto con varios precios de referencia según el proveedor. Con pocos datos cargados, un árbol simple funciona bien — pero apenas crece el catálogo, navegar a fuerza de clicks se vuelve tedioso y aumenta el riesgo de elegir el ítem equivocado.&lt;/p&gt;

&lt;p&gt;La solución fue un componente con búsqueda que filtra el árbol completo en memoria y auto-expande las ramas que coinciden:&lt;/p&gt;

&lt;p&gt;Bloque 2 — Filtrado recursivo (SelectorProductoCatalogo.razor)&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
private bool CategoriaVisible(Categoria categoria) =&amp;gt;&lt;br&gt;
    !HayBusqueda&lt;br&gt;
    || CategoriaNombreCoincide(categoria)&lt;br&gt;
    || productosPorCategoria[categoria.Id].Any(ProductoCoincide)&lt;br&gt;
    || categoriasPorPadre[categoria.Id].Any(CategoriaVisible);&lt;/p&gt;

&lt;p&gt;Una categoría es visible si su nombre coincide, si tiene algún producto que coincide, o si alguna de sus subcategorías es visible (recursivamente). Con eso alcanza para que, al escribir en el buscador, el árbol se reduzca solo a las ramas relevantes — sin tocar la base de datos en cada tecla, todo resuelto sobre datos ya cargados en memoria con ILookup.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El bug que no tiraba ningún error&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Este fue el más difícil de diagnosticar. Un botón dejó de responder — sin excepciones, sin nada en la consola del navegador, sin nada en el servidor. El circuito de Blazor estaba vivo, otros botones funcionaban.&lt;/p&gt;

&lt;p&gt;La causa: unos componentes globales (proveedores de notificaciones y diálogos) vivían en el layout raíz de la aplicación, fuera del árbol de cualquier página con render mode interactivo. Quedaban renderizados una sola vez, en modo estático, y nunca se conectaban al circuito real.&lt;/p&gt;

&lt;p&gt;Bloque 3 — El fix del render mode que dejaba botones "muertos"&lt;/p&gt;

&lt;p&gt;razor&lt;/p&gt;



&lt;p&gt;&lt;br&gt;
&lt;br&gt;
razor&lt;/p&gt;





&lt;p&gt;La lección que me llevo: en Blazor Web App, el render mode no se hereda entre componentes "hermanos" en el árbol — solo se propaga hacia los descendientes de quien lo declara. Un detalle chico, pero que sin entenderlo puede hacer perder horas.&lt;/p&gt;

&lt;p&gt;Cierre&lt;/p&gt;

&lt;p&gt;Ninguno de estos problemas apareció en un tutorial. Aparecieron usando el sistema de verdad, con datos reales, con un usuario real dándome feedback. Esa fue la parte más valiosa del proyecto: no solo escribir código que funcione, sino entender por qué algo no funcionaba antes de corregirlo.&lt;/p&gt;

&lt;p&gt;Si te interesa la arquitectura, algún detalle técnico puntual, o simplemente charlar sobre el proyecto, los comentarios están abiertos. 👇&lt;/p&gt;

</description>
      <category>backend</category>
      <category>dotnet</category>
      <category>programming</category>
      <category>software</category>
    </item>
  </channel>
</rss>
